| 1 | /* |
|---|
| 2 | Template - Copyright 2005 Six Apart |
|---|
| 3 | $Id$ |
|---|
| 4 | |
|---|
| 5 | Copyright (c) 2005, Six Apart, Ltd. |
|---|
| 6 | All rights reserved. |
|---|
| 7 | |
|---|
| 8 | Redistribution and use in source and binary forms, with or without |
|---|
| 9 | modification, are permitted provided that the following conditions are |
|---|
| 10 | met: |
|---|
| 11 | |
|---|
| 12 | * Redistributions of source code must retain the above copyright |
|---|
| 13 | notice, this list of conditions and the following disclaimer. |
|---|
| 14 | |
|---|
| 15 | * Redistributions in binary form must reproduce the above |
|---|
| 16 | copyright notice, this list of conditions and the following disclaimer |
|---|
| 17 | in the documentation and/or other materials provided with the |
|---|
| 18 | distribution. |
|---|
| 19 | |
|---|
| 20 | * Neither the name of "Six Apart" nor the names of its |
|---|
| 21 | contributors may be used to endorse or promote products derived from |
|---|
| 22 | this software without specific prior written permission. |
|---|
| 23 | |
|---|
| 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|---|
| 25 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|---|
| 26 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|---|
| 27 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
|---|
| 28 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|---|
| 29 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|---|
| 30 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
|---|
| 31 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
|---|
| 32 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|---|
| 33 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|---|
| 34 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|---|
| 35 | |
|---|
| 36 | */ |
|---|
| 37 | |
|---|
| 38 | |
|---|
| 39 | /* core template object */ |
|---|
| 40 | |
|---|
| 41 | Template = new Class( Object, { |
|---|
| 42 | beginToken: "[#", |
|---|
| 43 | endToken: "#]", |
|---|
| 44 | |
|---|
| 45 | |
|---|
| 46 | init: function( source ) { |
|---|
| 47 | if( source ) |
|---|
| 48 | this.compile( source ); |
|---|
| 49 | }, |
|---|
| 50 | |
|---|
| 51 | |
|---|
| 52 | compile: function( source ) { |
|---|
| 53 | var statements = [ |
|---|
| 54 | "context.open();", |
|---|
| 55 | "with( context.vars ) { " |
|---|
| 56 | ]; |
|---|
| 57 | |
|---|
| 58 | var start = 0, end = -this.endToken.length; |
|---|
| 59 | while( start < source.length ) { |
|---|
| 60 | end += this.endToken.length; |
|---|
| 61 | |
|---|
| 62 | /* plaintext */ |
|---|
| 63 | start = source.indexOf( this.beginToken, end ); |
|---|
| 64 | |
|---|
| 65 | if( start < 0 ) |
|---|
| 66 | start = source.length; |
|---|
| 67 | if( start > end ) |
|---|
| 68 | statements.push( "context.write( ", '"' + source.substring( end, start ).escapeJS() + '"', " );" ); |
|---|
| 69 | |
|---|
| 70 | start += this.beginToken.length; |
|---|
| 71 | |
|---|
| 72 | // code |
|---|
| 73 | if( start >= source.length ) |
|---|
| 74 | break; |
|---|
| 75 | |
|---|
| 76 | end = source.indexOf( this.endToken, start ); |
|---|
| 77 | |
|---|
| 78 | if( end < 0 ) |
|---|
| 79 | throw "Template parsing error: Unable to find matching end token (" + this.endToken + ")."; |
|---|
| 80 | |
|---|
| 81 | var length = ( end - start ); |
|---|
| 82 | |
|---|
| 83 | /* empty tag */ |
|---|
| 84 | if( length <= 0 ) |
|---|
| 85 | continue; |
|---|
| 86 | |
|---|
| 87 | /* comment */ |
|---|
| 88 | else if( length >= 4 && |
|---|
| 89 | source.charAt( start ) == "-" && |
|---|
| 90 | source.charAt( start + 1 ) == "-" && |
|---|
| 91 | source.charAt( end - 1 ) == "-" && |
|---|
| 92 | source.charAt( end - 2 ) == "-" ) |
|---|
| 93 | continue; |
|---|
| 94 | |
|---|
| 95 | /* write */ |
|---|
| 96 | else if( source.charAt( start ) == "=" ) |
|---|
| 97 | statements.push( "context.write( ", source.substring( start + 1, end ), " );" ); |
|---|
| 98 | |
|---|
| 99 | else if( source.charAt( start ) == "*" ) { |
|---|
| 100 | // commands that effect flow |
|---|
| 101 | |
|---|
| 102 | var cmd = source.substring( start + 1, end ).match( /^\s*(\w+)/ ); |
|---|
| 103 | if ( cmd ) { |
|---|
| 104 | cmd = cmd[ 1 ]; |
|---|
| 105 | |
|---|
| 106 | switch ( cmd ) { |
|---|
| 107 | case "return": |
|---|
| 108 | statements.push( "return context.close();" ); |
|---|
| 109 | } |
|---|
| 110 | } |
|---|
| 111 | |
|---|
| 112 | /* filters */ |
|---|
| 113 | } else if( source.charAt( start ) == "|" ) { |
|---|
| 114 | start += 1; |
|---|
| 115 | |
|---|
| 116 | /* find the first whitespace */ |
|---|
| 117 | var afterfilters = source.substring( start, end ).search(/\s/); |
|---|
| 118 | |
|---|
| 119 | var filters = []; |
|---|
| 120 | var params = []; |
|---|
| 121 | if (afterfilters > 0) { |
|---|
| 122 | /* pipes or commas must seperate filters |
|---|
| 123 | * split the string, reverse and rejoin to reverse it |
|---|
| 124 | */ |
|---|
| 125 | filters = source.substring( start,start + afterfilters ).replace(/(\w+)(\(([^\)]+)\))?/g,"$1|$3").split( "|" ); |
|---|
| 126 | |
|---|
| 127 | afterfilters += 1; /* data starts after whitespace and filter list */ |
|---|
| 128 | } else { |
|---|
| 129 | /* default to escapeHTML */ |
|---|
| 130 | filters = [ "h", "" ]; |
|---|
| 131 | } |
|---|
| 132 | |
|---|
| 133 | var cmds = []; |
|---|
| 134 | var params = []; |
|---|
| 135 | for ( var j = 0; j < filters.length; j++ ) { |
|---|
| 136 | if (j % 2) |
|---|
| 137 | params.push( filters[ j ] ); |
|---|
| 138 | else |
|---|
| 139 | cmds.push( filters[ j ] ); |
|---|
| 140 | } |
|---|
| 141 | |
|---|
| 142 | /* we have to do them in reverse order */ |
|---|
| 143 | filters = cmds.reverse(); |
|---|
| 144 | |
|---|
| 145 | /* start with our original filter number */ |
|---|
| 146 | var numfilters = filters.length; |
|---|
| 147 | |
|---|
| 148 | /* add the text between [#| #] */ |
|---|
| 149 | filters.push( source.substring( start + afterfilters, end ) ); |
|---|
| 150 | |
|---|
| 151 | /* adjust each filter into a function call */ |
|---|
| 152 | /* H|substr(-1,1)|u */ |
|---|
| 153 | /* eg. u( substr( H( name ), -1, 1 ) ) */ |
|---|
| 154 | for ( var i = 0; i < numfilters; i++ ) { |
|---|
| 155 | filters[ i ] = " context.f." + filters[ i ] + "( "; |
|---|
| 156 | filters.push( ", context" ); |
|---|
| 157 | if ( params[ i ] != "" ) |
|---|
| 158 | filters.push( ", [" + params[ i ] + "]" ); |
|---|
| 159 | filters.push( " )" ); |
|---|
| 160 | } |
|---|
| 161 | |
|---|
| 162 | /* rewrite command params */ |
|---|
| 163 | filters = filters.join( "" ); |
|---|
| 164 | statements.push( "context.write( " + filters + " );"); |
|---|
| 165 | } |
|---|
| 166 | |
|---|
| 167 | /* evaluate */ |
|---|
| 168 | else |
|---|
| 169 | statements.push( source.substring( start, end ) ); |
|---|
| 170 | } |
|---|
| 171 | |
|---|
| 172 | statements.push( "} return context.close();" ); |
|---|
| 173 | this.process = new Function( "context", statements.join( "\n" ) ); |
|---|
| 174 | }, |
|---|
| 175 | |
|---|
| 176 | |
|---|
| 177 | process: function( context ) { |
|---|
| 178 | return ""; |
|---|
| 179 | }, |
|---|
| 180 | |
|---|
| 181 | |
|---|
| 182 | /* deprecated */ |
|---|
| 183 | |
|---|
| 184 | exec: function( context ) { |
|---|
| 185 | log( "Template::exec() method has been deprecated. Please use process() instead or " + |
|---|
| 186 | "the new static Template.process( name[, vars[, templates]] ) method." ); |
|---|
| 187 | return this.process( context ); |
|---|
| 188 | } |
|---|
| 189 | |
|---|
| 190 | } ); |
|---|
| 191 | |
|---|
| 192 | |
|---|
| 193 | /* static members */ |
|---|
| 194 | |
|---|
| 195 | extend( Template, { |
|---|
| 196 | templates: {}, |
|---|
| 197 | |
|---|
| 198 | |
|---|
| 199 | process: function( name, vars, templates ) { |
|---|
| 200 | var context = new Template.Context( vars, templates ); |
|---|
| 201 | return context.include( name ); |
|---|
| 202 | } |
|---|
| 203 | } ); |
|---|
| 204 | |
|---|
| 205 | |
|---|
| 206 | /* context object */ |
|---|
| 207 | |
|---|
| 208 | Template.Context = new Class( Object, { |
|---|
| 209 | init: function( vars, templates ) { |
|---|
| 210 | this.vars = vars || {}; |
|---|
| 211 | this.templates = templates || Template.templates; |
|---|
| 212 | this.stack = []; |
|---|
| 213 | this.out = []; |
|---|
| 214 | this.f = Template.Filter; |
|---|
| 215 | }, |
|---|
| 216 | |
|---|
| 217 | |
|---|
| 218 | include: function( name ) { |
|---|
| 219 | if ( !this.templates.hasOwnProperty( name ) ) { |
|---|
| 220 | log.error( "Template name " + name + " does not exist!" ); |
|---|
| 221 | return; |
|---|
| 222 | } |
|---|
| 223 | |
|---|
| 224 | if ( typeof this.templates[ name ] == "string" ) |
|---|
| 225 | this.templates[ name ] = new Template( this.templates[ name ] ); |
|---|
| 226 | try { |
|---|
| 227 | return this.templates[ name ].process( this ); |
|---|
| 228 | } catch( e ) { |
|---|
| 229 | var error = "Error while processing template:" + name + " - " + e.message; |
|---|
| 230 | log.error( error ); |
|---|
| 231 | throw error; |
|---|
| 232 | } |
|---|
| 233 | }, |
|---|
| 234 | |
|---|
| 235 | |
|---|
| 236 | write: function() { |
|---|
| 237 | this.out.push.apply( this.out, arguments ); |
|---|
| 238 | }, |
|---|
| 239 | |
|---|
| 240 | |
|---|
| 241 | writeln: function() { |
|---|
| 242 | this.write.apply( this, arguments ); |
|---|
| 243 | this.write( "\n" ); |
|---|
| 244 | }, |
|---|
| 245 | |
|---|
| 246 | |
|---|
| 247 | clear: function() { |
|---|
| 248 | this.out.length = 0; |
|---|
| 249 | }, |
|---|
| 250 | |
|---|
| 251 | |
|---|
| 252 | exit: function() { |
|---|
| 253 | return this.getOutput(); |
|---|
| 254 | }, |
|---|
| 255 | |
|---|
| 256 | |
|---|
| 257 | getOutput: function() { |
|---|
| 258 | return this.out.join( "" ); |
|---|
| 259 | }, |
|---|
| 260 | |
|---|
| 261 | |
|---|
| 262 | open: function() { |
|---|
| 263 | this.stack.push( this.out ); |
|---|
| 264 | this.out = []; |
|---|
| 265 | }, |
|---|
| 266 | |
|---|
| 267 | |
|---|
| 268 | close: function() { |
|---|
| 269 | var result = this.getOutput(); |
|---|
| 270 | this.out = this.stack.pop() || []; |
|---|
| 271 | return result; |
|---|
| 272 | } |
|---|
| 273 | } ); |
|---|
| 274 | |
|---|
| 275 | |
|---|
| 276 | /* filters */ |
|---|
| 277 | |
|---|
| 278 | Template.Filter = { |
|---|
| 279 | /* interpolate */ |
|---|
| 280 | i: function( string, context ) { |
|---|
| 281 | if ( ( typeof string != "string" ) && string && string.toString ) |
|---|
| 282 | string = string.toString(); |
|---|
| 283 | return string.interpolate( context.vars ); |
|---|
| 284 | }, |
|---|
| 285 | |
|---|
| 286 | |
|---|
| 287 | /* escapeHTML */ |
|---|
| 288 | h: function( string, context ) { |
|---|
| 289 | if ( ( typeof string != "string" ) && string && string.toString ) |
|---|
| 290 | string = string.toString(); |
|---|
| 291 | return ( typeof string == "string" ) |
|---|
| 292 | ? string.encodeHTML() : "".encodeHTML( string ); |
|---|
| 293 | }, |
|---|
| 294 | |
|---|
| 295 | |
|---|
| 296 | /* unescapeHTML */ |
|---|
| 297 | H: function( string, context ) { |
|---|
| 298 | if ( ( typeof string != "string" ) && string && string.toString ) |
|---|
| 299 | string = string.toString(); |
|---|
| 300 | return ( typeof string == "string" ) |
|---|
| 301 | ? string.decodeHTML() : "".encodeHTML( string ); |
|---|
| 302 | }, |
|---|
| 303 | |
|---|
| 304 | |
|---|
| 305 | /* decodeURI */ |
|---|
| 306 | U: function( string, context ) { |
|---|
| 307 | return decodeURI( string ); |
|---|
| 308 | }, |
|---|
| 309 | |
|---|
| 310 | |
|---|
| 311 | /* escapeURI */ |
|---|
| 312 | u: function( string, context ) { |
|---|
| 313 | return encodeURI( string ).replace( /\//g, "%2F" ); |
|---|
| 314 | }, |
|---|
| 315 | |
|---|
| 316 | |
|---|
| 317 | /* lowercase */ |
|---|
| 318 | lc: function( string, context ) { |
|---|
| 319 | if ( ( typeof string != "string" ) && string && string.toString ) |
|---|
| 320 | string = string.toString(); |
|---|
| 321 | return ( typeof string == "string" ) |
|---|
| 322 | ? string.toLowerCase() : "".toLowerCase( string ); |
|---|
| 323 | }, |
|---|
| 324 | |
|---|
| 325 | |
|---|
| 326 | /* uppercase */ |
|---|
| 327 | uc: function( string, context ) { |
|---|
| 328 | if ( ( typeof string != "string" ) && string && string.toString ) |
|---|
| 329 | string = string.toString(); |
|---|
| 330 | return ( typeof string == "string" ) |
|---|
| 331 | ? string.toUpperCase() : "".toUpperCase( string ); |
|---|
| 332 | }, |
|---|
| 333 | |
|---|
| 334 | |
|---|
| 335 | /* substr */ |
|---|
| 336 | substr: function( string, context, params ) { |
|---|
| 337 | if( !params ) |
|---|
| 338 | throw "Template Filter Error: substr() requires at least one parameter"; |
|---|
| 339 | |
|---|
| 340 | /* allow negative offset */ |
|---|
| 341 | if( params[ 0 ] < 0 ) |
|---|
| 342 | params[ 0 ] = string.length + params[ 0 ]; |
|---|
| 343 | |
|---|
| 344 | return String.substr.apply( string, params ); |
|---|
| 345 | }, |
|---|
| 346 | |
|---|
| 347 | |
|---|
| 348 | /* removes whitepace before and after */ |
|---|
| 349 | ws: function( string, context ) { |
|---|
| 350 | if ( ( typeof string != "string" ) && string && string.toString ) |
|---|
| 351 | string = string.toString(); |
|---|
| 352 | return ( typeof string == "string" ) |
|---|
| 353 | ? string.replace( /^\s+/g, "" ).replace( /\s+$/g, "" ) : string; |
|---|
| 354 | }, |
|---|
| 355 | |
|---|
| 356 | |
|---|
| 357 | /* trims to length and adds elipsis */ |
|---|
| 358 | trim: function( string, context, params ) { |
|---|
| 359 | if ( !params ) |
|---|
| 360 | throw "Template Filter Error: trim() requires at least one parameter"; |
|---|
| 361 | |
|---|
| 362 | if ( ( typeof string != "string" ) && string && string.toString ) |
|---|
| 363 | string = string.toString(); |
|---|
| 364 | |
|---|
| 365 | if ( ( typeof string == "string" ) && string.length > params[ 0 ] ) { |
|---|
| 366 | string = string.substr( 0, params[ 0 ] ); |
|---|
| 367 | /* don't trunc on a word */ |
|---|
| 368 | var newstr = string.replace( /\w+$/, "" ); |
|---|
| 369 | return ( ( newstr == "" ) ? string : newstr ) + "\u2026"; |
|---|
| 370 | } else |
|---|
| 371 | return string; |
|---|
| 372 | }, |
|---|
| 373 | |
|---|
| 374 | |
|---|
| 375 | /* returns YYYY-MM-DD from an iso string like: 1995-02-05T13:00:00.000-08:00 */ |
|---|
| 376 | date: function( string, context ) { |
|---|
| 377 | if ( ( typeof string != "string" ) && string && string.toString ) |
|---|
| 378 | string = string.toString(); |
|---|
| 379 | var date = Date.fromISOString( string ); |
|---|
| 380 | return ( date ) ? date.toISODateString() : ""; |
|---|
| 381 | }, |
|---|
| 382 | |
|---|
| 383 | |
|---|
| 384 | localeDate: function( string, context ) { |
|---|
| 385 | if ( ( typeof string != "string" ) && string && string.toString ) |
|---|
| 386 | string = string.toString(); |
|---|
| 387 | var date = Date.fromISOString( string ); |
|---|
| 388 | return ( date ) ? date.toLocaleString() : ""; |
|---|
| 389 | }, |
|---|
| 390 | |
|---|
| 391 | |
|---|
| 392 | /* remove html tags */ |
|---|
| 393 | rt: function( string, context ) { |
|---|
| 394 | if ( ( typeof string != "string" ) && string && string.toString ) |
|---|
| 395 | string = string.toString(); |
|---|
| 396 | return ( typeof string == "string" ) |
|---|
| 397 | ? string.replace( /<\/?[^>]+>/gi, "" ) : string; |
|---|
| 398 | }, |
|---|
| 399 | |
|---|
| 400 | |
|---|
| 401 | rp: function( string, params ) { |
|---|
| 402 | if ( ( typeof string != "string" ) && string && string.toString ) |
|---|
| 403 | string = string.toString(); |
|---|
| 404 | |
|---|
| 405 | if ( ( typeof string == "string" ) && params.length == 2 ) { |
|---|
| 406 | return string.replace( params[ 0 ], params[ 1 ] ); |
|---|
| 407 | } else |
|---|
| 408 | return string; |
|---|
| 409 | } |
|---|
| 410 | }; |
|---|