UNPKG

11.1 kBJavaScriptView Raw
1
2var error = require('./lib/error');
3var runtime = {
4 version: require('./package.json').version
5};
6
7var helpers = runtime['helpers'];
8
9module.exports = runtime;
10
11function Helpers( model ) {
12 this.buffer = new Buffer();
13 this.model = model;
14 this.options = null; // added at render time
15
16 this.vl = 0;
17 this.vc = 0;
18};
19
20runtime['helpers']
21 = helpers
22 = Helpers.prototype
23 = { constructor: Helpers, config: {}, tplcache: {} };
24
25// this allows a template to return the context, and coercion
26// will handle it
27helpers.toString = helpers.toHtmlString = function(){
28 // not calling buffer.toString() results in 2x speedup
29 return this.buffer._vo.join('');//.toString();
30}
31
32///////////////////////////////////////////////////////////////////////////
33// HTML ESCAPING
34
35var HTML_REGEX = /[&<>"'`]/g
36 ,HTML_REPLACER = function(match) { return HTML_CHARS[match]; }
37 ,HTML_CHARS = {
38 "&": "&amp;"
39 ,"<": "&lt;"
40 ,">": "&gt;"
41 ,'"': "&quot;"
42 ,"'": "&#x27;"
43 ,"`": "&#x60;"
44 };
45
46helpers['raw'] = function( val ) {
47 var func = function() { return val; };
48
49 val = val != null ? val : "";
50
51 return {
52 toHtmlString: func
53 ,toString: func
54 };
55};
56
57helpers['escape'] = function( val ) {
58 var func = function() { return val; };
59
60 val = val != null ? val : "";
61
62 if ( typeof val.toHtmlString !== "function" ) {
63
64 val = val.toString().replace( HTML_REGEX, HTML_REPLACER );
65
66 return {
67 toHtmlString: func
68 ,toString: func
69 };
70 }
71
72 return val;
73};
74
75// HTML ESCAPING
76///////////////////////////////////////////////////////////////////////////
77
78
79///////////////////////////////////////////////////////////////////////////
80// BUFFER MANIPULATION
81//
82// These are to be used from within helpers, to allow for manipulation of
83// output in a sane manner.
84
85var Buffer = function() {
86 this._vo = [];
87}
88
89Buffer.prototype.mark = function( debugName ) {
90 var mark = new Mark( this, debugName );
91 mark.markedIndex = this._vo.length;
92 this._vo.push( mark.uid );
93 return mark;
94};
95
96Buffer.prototype.fromMark = function( mark ) {
97 var found = mark.findInBuffer();
98
99 if( found > -1 ){
100 // automatically destroy the mark from the buffer
101 mark.destroy();
102 // `found` will still be valid for a manual splice
103 return this._vo.splice( found, this._vo.length );
104 }
105
106 return [];
107};
108
109Buffer.prototype.spliceMark = function( mark, numToRemove, add ){
110 var found = mark.findInBuffer();
111
112 if( found > -1 ){
113 mark.destroy();
114 arguments[0] = found;
115 return this._vo.splice.apply( this._vo, arguments );
116 }
117
118 return [];
119};
120
121Buffer.prototype.empty = function() {
122 return this._vo.splice( 0, this._vo.length );
123};
124
125Buffer.prototype.push = function( buffer ) {
126 return this._vo.push( buffer );
127};
128
129Buffer.prototype.pushConcat = function( buffer ){
130 var buffers;
131 if (Array.isArray(buffer)) {
132 buffers = buffer;
133 } else if ( arguments.length > 1 ) {
134 buffers = Array.prototype.slice.call( arguments );
135 } else {
136 buffers = [buffer];
137 }
138
139 for (var i = 0; i < buffers.length; i++) {
140 this._vo.push( buffers[i] );
141 }
142
143 return this.__vo;
144}
145
146Buffer.prototype.indexOf = function( str ){
147
148 for( var i = 0; i < this._vo.length; i++ ){
149 if(
150 ( str.test && this._vo[i] && this._vo[i].search(str) > -1 )
151 || this._vo[i] == str
152 ){
153 return i;
154 }
155 }
156
157 return -1;
158}
159
160Buffer.prototype.lastIndexOf = function( str ){
161 var i = this._vo.length;
162
163 while( --i >= 0 ){
164 if(
165 ( str.test && this._vo[i] && this._vo[i].search(str) > -1 )
166 || this._vo[i] == str
167 ){
168 return i;
169 }
170 }
171
172 return -1;
173}
174
175Buffer.prototype.splice = function(){
176 return this._vo.splice.apply( this._vo, arguments );
177}
178
179Buffer.prototype.index = function( idx ){
180 return this._vo[ idx ];
181}
182
183Buffer.prototype.flush = function() {
184 return this.empty().join( "" );
185};
186
187Buffer.prototype.toString = Buffer.prototype.toHtmlString = function(){
188 // not using flush because then console.log( tpl() ) would artificially
189 // affect the output
190 return this._vo.join( "" );
191}
192
193// BUFFER MANIPULATION
194///////////////////////////////////////////////////////////////////////////
195
196///////////////////////////////////////////////////////////////////////////
197// MARKS
198// These can be used to manipulate the existing entries in the rendering
199// context. For an example, see the highlight helper.
200
201var Mark = runtime['Mark'] = function( buffer, debugName ){
202 this.uid = '[VASHMARK-'
203 + ~~( Math.random() * 10000000 )
204 + (debugName ? ':' + debugName : '')
205 + ']';
206 this.markedIndex = 0;
207 this.buffer = buffer;
208 this.destroyed = false;
209}
210
211var reMark = Mark.re = /\[VASHMARK\-\d{1,8}(?::[\s\S]+?)?]/g
212
213// tests if a string has a mark-like uid within it
214Mark.uidLike = function( str ){
215 return (str || '').search( reMark ) > -1;
216}
217
218Mark.prototype.destroy = function(){
219
220 var found = this.findInBuffer();
221
222 if( found > -1 ){
223 this.buffer.splice( found, 1 );
224 this.markedIndex = -1;
225 }
226
227 this.destroyed = true;
228}
229
230Mark.prototype.findInBuffer = function(){
231
232 if( this.destroyed ){
233 return -1;
234 }
235
236 if( this.markedIndex && this.buffer.index( this.markedIndex ) === this.uid ){
237 return this.markedIndex;
238 }
239
240 // The mark may be within a string due to block manipulation shenanigans.
241 var escaped = this.uid.replace(/(\[|\])/g, '\\$1');
242 var re = new RegExp(escaped);
243 return this.markedIndex = this.buffer.indexOf( re );
244}
245
246// MARKS
247///////////////////////////////////////////////////////////////////////////
248
249///////////////////////////////////////////////////////////////////////////
250// ERROR REPORTING
251
252// Liberally modified from https://github.com/visionmedia/jade/blob/master/jade.js
253helpers.constructor.reportError = function(e, lineno, chr, orig, lb, atRenderTime){
254
255 lb = lb || '!LB!';
256
257 var contextStr = error.context(orig, lineno, chr, lb);
258
259 e.vashlineno = lineno;
260 e.vashcharno = chr;
261 e.message = 'Problem while '
262 + (atRenderTime ? 'rendering' : 'compiling')
263 + ' template at line '
264 + lineno + ', character ' + chr
265 + '.\nOriginal message: ' + e.message + '.'
266 + '\nContext: \n\n' + contextStr + '\n\n';
267
268 throw e;
269};
270
271helpers['reportError'] = function() {
272 this.constructor.reportError.apply( this, arguments );
273};
274
275// ERROR REPORTING
276///////////////////////////////////////////////////////////////////////////
277
278///////////////////////////////////////////////////////////////////////////
279// VASH.LINK
280// Take a compiled string or function and "link" it to the current vash
281// runtime. This is necessary to allow instantiation of `Helpers` and
282// proper decompilation via `toClientString`.
283//
284// If `options.asHelper` and `options.args` are defined, the `cmpFunc` is
285// interpreted as a compiled helper, and is attached to `runtime.helpers` at
286// a property name equal to `options.asHelper`.
287
288runtime['link'] = function( cmpFunc, options ){
289
290 // TODO: allow options.filename to be used as sourceUrl?
291
292 var originalFunc
293 ,cmpOpts;
294
295 if( !options.args ){
296 // every template has these arguments
297 options.args = [options.modelName, options.helpersName, '__vopts', 'runtime'];
298 }
299
300 if( typeof cmpFunc === 'string' ){
301 originalFunc = cmpFunc;
302
303 try {
304 // do not pollute the args array for later attachment to the compiled
305 // function for later decompilation/linking
306 cmpOpts = options.args.slice();
307 cmpOpts.push(cmpFunc);
308 cmpFunc = Function.apply(null, cmpOpts);
309 } catch(e) {
310 // TODO: add flag to reportError to know if it's at compile time or runtime
311 helpers.reportError(e, 0, 0, originalFunc, /\n/, false);
312 }
313 }
314
315 // need this to enable decompilation / relinking
316 cmpFunc.options = {
317 simple: options.simple
318 ,modelName: options.modelName
319 ,helpersName: options.helpersName
320 }
321
322 var linked;
323
324 if( options.asHelper ){
325
326 cmpFunc.options.args = options.args;
327 cmpFunc.options.asHelper = options.asHelper;
328
329 linked = function(){
330 return cmpFunc.apply(this, slice.call(arguments));
331 }
332
333 helpers[options.asHelper] = linked;
334
335 } else {
336
337 linked = function( model, opts ){
338 if( options.simple ){
339 var ctx = {
340 buffer: []
341 ,escape: Helpers.prototype.escape
342 ,raw: Helpers.prototype.raw
343 }
344 return cmpFunc( model, ctx, opts, runtime );
345 }
346
347 opts = divineRuntimeTplOptions( model, opts );
348 return cmpFunc( model, (opts && opts.context) || new Helpers( model ), opts, runtime );
349 }
350 }
351
352 // show the template-specific code, instead of the generic linked function
353 linked['toString'] = function(){ return cmpFunc.toString(); }
354
355 // shortcut to show the actual linked function
356 linked['_toString'] = function(){ return Function.prototype.toString.call(linked) }
357
358 // This assumes a vash global, and should be deprecated.
359 // TODO: @deprecate
360 linked['toClientString'] = function(){
361 return 'vash.link( '
362 + cmpFunc.toString() + ', '
363 + JSON.stringify( cmpFunc.options ) + ' )';
364 }
365
366 return linked;
367}
368
369// given a model and options, allow for various tpl signatures and options:
370// ( model, {} )
371// ( model, function onRenderEnd(){} )
372// ( model )
373// and model.onRenderEnd
374function divineRuntimeTplOptions( model, opts ){
375
376 // allow for signature: model, callback
377 if( typeof opts === 'function' ) {
378 opts = { onRenderEnd: opts };
379 }
380
381 // allow for passing in onRenderEnd via model
382 if( model && model.onRenderEnd ){
383 opts = opts || {};
384
385 if( !opts.onRenderEnd ){
386 opts.onRenderEnd = model.onRenderEnd;
387 }
388
389 delete model.onRenderEnd;
390 }
391
392 // ensure options can be referenced
393 if( !opts ){
394 opts = {};
395 }
396
397 return opts;
398}
399
400// shortcut for compiled helpers
401var slice = Array.prototype.slice;
402
403// VASH.LINK
404///////////////////////////////////////////////////////////////////////////
405
406///////////////////////////////////////////////////////////////////////////
407// TPL CACHE
408
409runtime['lookup'] = function( path, model ){
410 var tpl = runtime.helpers.tplcache[path];
411 if( !tpl ){ throw new Error('Could not find template: ' + path); }
412 if( model ){ return tpl(model); }
413 else return tpl;
414};
415
416runtime['install'] = function( path, tpl ){
417 var cache = runtime.helpers.tplcache;
418 if( typeof tpl === 'string' ){
419 // Super hacky: if the calling context has a `compile` function,
420 // then `this` is likely full vash. This is simply for backwards
421 // compatibility.
422 // TODO: @deprecate
423 if ( typeof this.compile === 'function') {
424 tpl = this.compile(tpl);
425 } else {
426 throw new Error('.install(path, [string]) is not available in the standalone runtime.');
427 }
428 } else if( typeof path === 'object' ){
429 tpl = path;
430 Object.keys(tpl).forEach(function(path){
431 cache[path] = tpl[path];
432 });
433 return cache;
434 }
435 return cache[path] = tpl;
436};
437
438runtime['uninstall'] = function( path ){
439 var cache = runtime.helpers.tplcache
440 ,deleted = false;
441
442 if( typeof path === 'string' ){
443 return delete cache[path];
444 } else {
445 Object.keys(cache).forEach(function(key){
446 if( cache[key] === path ){ deleted = delete cache[key]; }
447 })
448 return deleted;
449 }
450};