jsRender-rails 0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. data/.gitignore +7 -0
  2. data/Gemfile +6 -0
  3. data/LICENSE +19 -0
  4. data/README.md +56 -0
  5. data/Rakefile +6 -0
  6. data/jsRender-rails.gemspec +20 -0
  7. data/lib/jsRender-rails.rb +2 -0
  8. data/lib/jsRender-rails/engine.rb +13 -0
  9. data/lib/jsRender-rails/jsrender.rb +37 -0
  10. data/lib/jsRender-rails/version.rb +6 -0
  11. data/spec/dummy/README.rdoc +261 -0
  12. data/spec/dummy/Rakefile +7 -0
  13. data/spec/dummy/app/assets/javascripts/application.js +2 -0
  14. data/spec/dummy/app/assets/javascripts/views/user.tmpl +1 -0
  15. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  16. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  17. data/spec/dummy/app/controllers/main_controller.rb +4 -0
  18. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  19. data/spec/dummy/app/mailers/.gitkeep +0 -0
  20. data/spec/dummy/app/models/.gitkeep +0 -0
  21. data/spec/dummy/app/views/layouts/application.html.erb +12 -0
  22. data/spec/dummy/app/views/main/index.html.erb +5 -0
  23. data/spec/dummy/app/views/main/prefix.html.erb +5 -0
  24. data/spec/dummy/config.ru +4 -0
  25. data/spec/dummy/config/application.rb +56 -0
  26. data/spec/dummy/config/boot.rb +10 -0
  27. data/spec/dummy/config/database.yml +25 -0
  28. data/spec/dummy/config/environment.rb +5 -0
  29. data/spec/dummy/config/environments/development.rb +37 -0
  30. data/spec/dummy/config/environments/production.rb +67 -0
  31. data/spec/dummy/config/environments/test.rb +37 -0
  32. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  33. data/spec/dummy/config/initializers/inflections.rb +15 -0
  34. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  35. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  36. data/spec/dummy/config/initializers/session_store.rb +8 -0
  37. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  38. data/spec/dummy/config/locales/en.yml +5 -0
  39. data/spec/dummy/config/routes.rb +4 -0
  40. data/spec/dummy/db/test.sqlite3 +0 -0
  41. data/spec/dummy/lib/assets/.gitkeep +0 -0
  42. data/spec/dummy/public/404.html +26 -0
  43. data/spec/dummy/public/422.html +26 -0
  44. data/spec/dummy/public/500.html +25 -0
  45. data/spec/dummy/public/favicon.ico +0 -0
  46. data/spec/dummy/script/rails +6 -0
  47. data/spec/integration_spec.rb +24 -0
  48. data/spec/jsRender-rails/jsRender_spec.rb +28 -0
  49. data/spec/spec_helper.rb +8 -0
  50. data/vendor/assets/javascripts/jsrender.js +1036 -0
  51. metadata +184 -0
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/422.html -->
21
+ <div class="dialog">
22
+ <h1>The change you wanted was rejected.</h1>
23
+ <p>Maybe you tried to change something you didn't have access to.</p>
24
+ </div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/500.html -->
21
+ <div class="dialog">
22
+ <h1>We're sorry, but something went wrong.</h1>
23
+ </div>
24
+ </body>
25
+ </html>
File without changes
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3
+
4
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
5
+ require File.expand_path('../../config/boot', __FILE__)
6
+ require 'rails/commands'
@@ -0,0 +1,24 @@
1
+ require "spec_helper"
2
+
3
+ feature "Using jsRender", js: true do
4
+ after do
5
+ Rails.application.config.jsRender.prefix = ''
6
+ end
7
+
8
+ scenario "Loading a page that uses jsRender" do
9
+ visit "/"
10
+ page.should have_content("Sebastian Pape")
11
+ end
12
+
13
+ scenario "Using template prefix", js: true do
14
+ Rails.application.config.jsRender.prefix = 'views'
15
+ visit "/prefix"
16
+ page.should have_content("Sebastian Pape")
17
+ end
18
+
19
+ scenario "Using a regular expression as a template prefix", js: true do
20
+ Rails.application.config.jsRender.prefix = %r{([^/]*/)*}
21
+ visit "/prefix"
22
+ page.should have_content("Sebastian Pape")
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ require "spec_helper"
2
+
3
+ describe JsRenderRails::JsRender do
4
+ before { Rails.application.assets.cache = {} }
5
+
6
+ it "adds jsRender to the load path" do
7
+ Rails.application.assets["jsrender"].should_not be_nil
8
+ end
9
+
10
+ it "compiles templates with the .tmpl extension" do
11
+ template = Rails.application.assets["views/user"]
12
+ template.to_s.should == %{jQuery.templates("views/user", "<div class=\\\"user\\\">{{>name}}<\\/div>\\n");}
13
+ end
14
+
15
+ context "when prefix is set" do
16
+ it "removes the prefix from the template name" do
17
+ Rails.configuration.jsRender.prefix = "views"
18
+ template = Rails.application.assets["views/user"]
19
+ template.to_s.should include('"user"')
20
+ end
21
+
22
+ it "normalizes template prefixes by removing extraneous slashes" do
23
+ Rails.configuration.jsRender.prefix = "/views/"
24
+ template = Rails.application.assets["views/user"]
25
+ template.to_s.should include('"user"')
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,8 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+
3
+ require File.expand_path("../dummy/config/environment.rb", __FILE__)
4
+
5
+ Rails.backtrace_cleaner.remove_silencers!
6
+
7
+ require "capybara/rails"
8
+ require "capybara/rspec"
@@ -0,0 +1,1036 @@
1
+ /*! JsRender v1.0pre: http://github.com/BorisMoore/jsrender */
2
+ /*
3
+ * Optimized version of jQuery Templates, for rendering to string.
4
+ * Does not require jQuery, or HTML DOM
5
+ * Integrates with JsViews (http://github.com/BorisMoore/jsviews)
6
+ * Copyright 2012, Boris Moore
7
+ * Released under the MIT License.
8
+ */
9
+ // informal pre beta commit counter: 21
10
+
11
+ (function(global, jQuery, undefined) {
12
+ // global is the this object, which is window when running in the usual browser environment.
13
+
14
+ if (jQuery && jQuery.views || global.jsviews) return; // JsRender is already loaded
15
+
16
+ //========================== Top-level vars ==========================
17
+
18
+ var versionNumber = "v1.0pre",
19
+
20
+ $, rTag, rTmplString, $extend,
21
+ // compiledTmplsCache = {},
22
+ delimOpenChar0 = "{", delimOpenChar1 = "{", delimCloseChar0 = "}", delimCloseChar1 = "}", deferChar = "!",
23
+ $viewsSub = {},
24
+ FALSE = false, TRUE = true,
25
+
26
+ rPath = /^(?:null|true|false|\d[\d.]*|([\w$]+|~([\w$]+)|#(view|([\w$]+))?)([\w$.]*?)(?:[.[]([\w$]+)\]?)?|(['"]).*\8)$/g,
27
+ // nil object helper view viewProperty pathTokens leafToken string
28
+
29
+ rParams = /(\()(?=|\s*\()|(?:([([])\s*)?(?:([#~]?[\w$.]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*!:?\/]|(=))\s*|([#~]?[\w$.]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*([)\]])([([]?))|(\s+)/g,
30
+ // lftPrn lftPrn2 path operator err eq path2 prn comma lftPrn2 apos quot rtPrn prn2 space
31
+ // (left paren? followed by (path? followed by operator) or (path followed by paren?)) or comma or apos or quot or right paren or space
32
+
33
+ rNewLine = /\r?\n/g,
34
+ rUnescapeQuotes = /\\(['"])/g,
35
+ rEscapeQuotes = /\\?(['"])/g,
36
+ rBuildHash = /\x08(~)?([^\x08]+)\x08/g,
37
+
38
+ autoTmplName = 0,
39
+ escapeMapForHtml = {
40
+ "&": "&amp;",
41
+ "<": "&lt;",
42
+ ">": "&gt;"
43
+ },
44
+ tmplAttr = "data-jsv-tmpl",
45
+ fnDeclStr = "var j=j||" + (jQuery ? "jQuery." : "js") + "views,",
46
+ htmlSpecialChar = /[\x00"&'<>]/g,
47
+ slice = Array.prototype.slice,
48
+
49
+ $render = {},
50
+
51
+ // jsviews object ($.views if jQuery is loaded)
52
+ $views = {
53
+ jsviews: versionNumber,
54
+ sub: $viewsSub, // subscription, e.g. JsViews integration
55
+ debugMode: TRUE,
56
+ render: $render,
57
+ templates: $templates,
58
+ tags: $viewsTags,
59
+ helpers: $viewsHelpers,
60
+ converters: $viewsConverters,
61
+ delimiters: $viewsDelimiters,
62
+ View: View,
63
+ _convert: convert,
64
+ _err: function(e) {
65
+ return $views.debugMode ? ("Error: " + (e.message || e)) + ". " : '';
66
+ },
67
+ _tmplFn: tmplFn,
68
+ _tag: renderTag,
69
+ error: error,
70
+ Error: JsViewsError
71
+ };
72
+
73
+ function JsViewsError(message) { // Error exception type for JsViews/JsRender
74
+ this.name = "JsRender Error",
75
+ this.message = message || "JsRender error"
76
+ }
77
+
78
+ (JsViewsError.prototype = new Error()).constructor = JsViewsError;
79
+
80
+ //========================== Top-level functions ==========================
81
+
82
+ //===================
83
+ // jsviews.delimiters
84
+ //===================
85
+
86
+ function $viewsDelimiters(openChars, closeChars, defer) {
87
+ // Set the tag opening and closing delimiters. Default is "{{" and "}}"
88
+ // openChar, closeChars: opening and closing strings, each with two characters
89
+
90
+ if (!$views.rTag || arguments.length) {
91
+ delimOpenChar0 = openChars ? "\\" + openChars.charAt(0) : delimOpenChar0; // Escape the characters - since they could be regex special characters
92
+ delimOpenChar1 = openChars ? "\\" + openChars.charAt(1) : delimOpenChar1;
93
+ delimCloseChar0 = closeChars ? "\\" + closeChars.charAt(0) : delimCloseChar0;
94
+ delimCloseChar1 = closeChars ? "\\" + closeChars.charAt(0) : delimCloseChar1;
95
+ defer = defer ? "\\" + defer : deferChar;
96
+
97
+ // Build regex with new delimiters
98
+ $views.rTag = rTag // make rTag available to JsViews (or other components) for parsing binding expressions
99
+ // tag (followed by / space or }) or cvtr+colon or html or code
100
+ = "(\\w*" + defer + ")?(?:(?:(\\w+(?=[\\/\\s" + delimCloseChar0 + "]))|(?:(\\w+)?(:)|(>)|(\\*)))"
101
+ // params
102
+ + "\\s*((?:[^" + delimCloseChar0 + "]|" + delimCloseChar0 + "(?!" + delimCloseChar1 + "))*?)";
103
+
104
+ // slash or closeBlock }}
105
+ rTag = new RegExp(delimOpenChar0 + delimOpenChar1 + rTag + "(\\/)?|(?:\\/(\\w+)))" + delimCloseChar0 + delimCloseChar1, "g");
106
+
107
+ // Default rTag: tag converter colon html code params slash closeBlock
108
+ // /{{(?:(?:(\w+(?=[\/\s}]))|(?:(\w+)?(:)|(>)|(\*)))\s*((?:[^}]|}(?!}))*?)(\/)?|(?:\/(\w+)))}}
109
+
110
+ rTmplString = new RegExp("<.*>|([^\\\\]|^)[{}]|" + delimOpenChar0 + delimOpenChar1 + ".*" + delimCloseChar0 + delimCloseChar1);
111
+ // rTmplString looks for html tags or { or } char not preceeded by \\, or JsRender tags {{xxx}}. Each of these strings are considered NOT to be jQuery selectors
112
+ }
113
+ return [delimOpenChar0, delimOpenChar1, delimCloseChar0, delimCloseChar1, deferChar];
114
+ }
115
+
116
+ //=================
117
+ // View._hlp
118
+ //=================
119
+
120
+ function getHelper(helper) {
121
+ // Helper method called as view._hlp() from compiled template, for helper functions or template parameters ~foo
122
+ var view = this,
123
+ tmplHelpers = view.tmpl.helpers || {};
124
+
125
+ helper = (
126
+ view.dynCtx && view.dynCtx[helper] !== undefined
127
+ ? view.dynCtx
128
+ : view.ctx[helper] !== undefined
129
+ ? view.ctx
130
+ : tmplHelpers[helper] !== undefined
131
+ ? tmplHelpers
132
+ : $viewsHelpers[helper] !== undefined
133
+ ? $viewsHelpers
134
+ : {}
135
+ )[helper];
136
+ return typeof helper !== "function" ? helper : function() {
137
+ return helper.apply(view, arguments);
138
+ };
139
+ }
140
+
141
+ //=================
142
+ // jsviews._convert
143
+ //=================
144
+
145
+ function convert(converter, view, self, text) {
146
+ // self is template object or link object
147
+ var linkContext = !self.markup && self || undefined,
148
+ tmplConverter = view.tmpl.converters;
149
+ tmplConverter = tmplConverter && tmplConverter[converter] || $viewsConverters[converter];
150
+ return tmplConverter ? tmplConverter.call(view, text, linkContext) : (error("Unknown converter: {{"+ converter + ":"), text);
151
+ }
152
+
153
+ //=================
154
+ // jsviews._tag
155
+ //=================
156
+
157
+ function renderTag(tag, parentView, self, content, tagInstance) {
158
+ // Called from within compiled template function, to render a nested tag
159
+ // Returns the rendered tag
160
+ var ret,
161
+ linkCtx = !self.markup && self, // self is either a template object (if rendering a tag) or a linkCtx object (if linking using a link tag)
162
+ parentTmpl = linkCtx ? linkCtx.view.tmpl : self,
163
+ tmplTags = parentTmpl.tags,
164
+ nestedTemplates = parentTmpl.templates,
165
+ props = tagInstance.props = tagInstance.props || {},
166
+ tmpl = props.tmpl,
167
+ args = arguments.length > 5 ? slice.call(arguments, 5) : [],
168
+ tagObject = tmplTags && tmplTags[tag] || $viewsTags[tag];
169
+
170
+ if (!tagObject) {
171
+ error("Unknown tag: {{"+ tag + "}}");
172
+ return "";
173
+ }
174
+ // Set the tmpl property to the content of the block tag, unless set as an override property on the tag
175
+ content = content && parentTmpl.tmpls[content - 1];
176
+ tmpl = tmpl || content || tagObject.template || undefined;
177
+ tagInstance.view = parentView;
178
+ tmpl = tagInstance.tmpl =
179
+ "" + tmpl === tmpl // if a string
180
+ ? nestedTemplates && nestedTemplates[tmpl] || $templates[tmpl] || $templates(tmpl)
181
+ : tmpl;
182
+
183
+ tagInstance.attr =
184
+ // Setting attr on tagInstance so renderContent knows whether to include template annotations.
185
+ self.attr =
186
+ // Setting attr on self.fn to ensure outputting to the correct target attribute.
187
+ self.attr || tagObject.attr;
188
+
189
+ tagInstance.tagName = tag;
190
+ tagInstance.renderContent = renderContent;
191
+ if (linkCtx) {
192
+ linkCtx.tagCtx = {
193
+ args: args,
194
+ props: props,
195
+ path: tagInstance.path,
196
+ tag: tagObject
197
+ };
198
+ }
199
+ // If render function is declared, call it. If the return result is undefined, return "", or, if a template (or content) is provide, return the rendered template (using the first parameter as data);
200
+ if (tagObject.render) {
201
+ ret = tagObject.render.apply(tagInstance, args);
202
+ }
203
+ return ret || (ret == undefined
204
+ ? (tmpl
205
+ ? tagInstance.renderContent(args[0], undefined, parentView)
206
+ : "")
207
+ : ret.toString()); // (If ret is the value 0 or false, will render to string)
208
+ }
209
+
210
+ //=================
211
+ // View constructor
212
+ //=================
213
+
214
+ function View(context, path, parentView, data, template, key, onRender, isArray) {
215
+ // Constructor for view object in view hierarchy. (Augmented by JsViews if JsViews is loaded)
216
+ var views,
217
+ self = {
218
+ data: data,
219
+ tmpl: template,
220
+ views: isArray ? [] : {},
221
+ parent: parentView,
222
+ ctx: context,
223
+ // If the data is an array, this is an 'Array View' with a views array for each child 'Instance View'
224
+ // If the data is not an array, this is an 'Instance View' with a views 'map' object for any child nested views
225
+ // _useKey is non zero if is not an 'Array View' (owning a data array). Uuse this as next key for adding to child views map
226
+ path: path,
227
+ _useKey: isArray ? 0 : 1,
228
+ _onRender: onRender,
229
+ _hlp: getHelper,
230
+ renderLink: function(index) {
231
+ var linkTmpl = this.tmpl.tmpls[index];
232
+ return linkTmpl.render(data, context, this);
233
+ }
234
+ };
235
+
236
+ if (parentView) {
237
+ views = parentView.views;
238
+ if (parentView._useKey) {
239
+ // Parent is an 'Instance View'. Add this view to its views object
240
+ // self.key = is the key in the parent view map
241
+ views[self.key = "_" + parentView._useKey++] = self;
242
+ // self.index = is index of the parent
243
+ self.index = parentView.index;
244
+ } else {
245
+ // Parent is an 'Array View'. Add this view to its views array
246
+ views.splice(
247
+ // self.key = self.index - the index in the parent view array
248
+ self.key = self.index = key !== undefined
249
+ ? key
250
+ : views.length,
251
+ 0, self);
252
+ }
253
+ }
254
+ return self;
255
+ }
256
+
257
+ //=================
258
+ // Registration
259
+ //=================
260
+
261
+ function addToStore(self, store, name, item, process) {
262
+ // Add item to named store such as templates, helpers, converters...
263
+ var key, onStore;
264
+ if (name && typeof name === "object" && !name.nodeType) {
265
+ // If name is a map, iterate over map and call store for key
266
+ for (key in name) {
267
+ store(key, name[key]);
268
+ }
269
+ return self;
270
+ }
271
+ if (item === undefined) {
272
+ item = name;
273
+ name = undefined;
274
+ }
275
+ if (onStore = $viewsSub.onBeforeStoreItem) {
276
+ // e.g. provide an external compiler or preprocess the item.
277
+ process = onStore(store, name, item, process) || process;
278
+ }
279
+ if (!name) {
280
+ item = process ? process(item) : item
281
+ } else if ("" + name === name) { // name must be a string
282
+ if (item === null) {
283
+ // If item is null, delete this entry
284
+ delete store[name];
285
+ } else {
286
+ store[name] = process ? (item = process(item, name)) : item;
287
+ }
288
+ }
289
+ if (onStore = $viewsSub.onStoreItem) {
290
+ // e.g. JsViews integration
291
+ onStore(store, name, item, process);
292
+ }
293
+ return item;
294
+ }
295
+
296
+ function compileTag(item, name) {
297
+ item = typeof item === "function" ? { render: item } : item;
298
+ item.name = name;
299
+ item.is = "tag";
300
+ return item;
301
+ }
302
+
303
+ function $templates(name, tmpl) {
304
+ // Register templates
305
+ // Setter: Use $.templates( name, tmpl ) or $.templates({ name: tmpl, ... }) to add additional templates to the registered templates collection.
306
+ // Getter: Use var tmpl = $.templates( name ) or $.templates[name] or $.templates.name to return the object for the registered template.
307
+ // Remove: Use $.templates( name, null ) to remove a registered template from $.templates.
308
+ return addToStore(this, $templates, name, tmpl, compile);
309
+ }
310
+
311
+ function $viewsTags(name, tag) {
312
+ // Register template tags
313
+ // Setter: Use $.view.tags( name, tag ) or $.view.tags({ name: tag, ... }) to add additional tags to the registered tags collection.
314
+ // Getter: Use var tag = $.views.tags( name ) or $.views.tags[name] or $.views.tags.name to return the object for the registered tag.
315
+ // Remove: Use $.view.tags( name, null ) to remove a registered tag from $.view.tags.
316
+
317
+ // When registering for {{foo a b c==d e=f}}, tag should corresponnd to a function with the signature:
318
+ // function(a,b). The 'this' pointer will be a hash with properties c and e.
319
+ return addToStore(this, $viewsTags, name, tag, compileTag);
320
+ }
321
+
322
+ function $viewsHelpers(name, helperFn) {
323
+ // Register helper functions for use in templates (or in data-link expressions if JsViews is loaded)
324
+ // Setter: Use $.view.helpers( name, helperFn ) or $.view.helpers({ name: helperFn, ... }) to add additional helpers to the registered helpers collection.
325
+ // Getter: Use var helperFn = $.views.helpers( name ) or $.views.helpers[name] or $.views.helpers.name to return the function.
326
+ // Remove: Use $.view.helpers( name, null ) to remove a registered helper function from $.view.helpers.
327
+ // Within a template, access the helper using the syntax: {{... ~myHelper(...) ...}}.
328
+ return addToStore(this, $viewsHelpers, name, helperFn);
329
+ }
330
+
331
+ function $viewsConverters(name, converterFn) {
332
+ // Register converter functions for use in templates (or in data-link expressions if JsViews is loaded)
333
+ // Setter: Use $.view.converters( name, converterFn ) or $.view.converters({ name: converterFn, ... }) to add additional converters to the registered converters collection.
334
+ // Getter: Use var converterFn = $.views.converters( name ) or $.views.converters[name] or $.views.converters.name to return the converter function.
335
+ // Remove: Use $.view.converters( name, null ) to remove a registered converter from $.view.converters.
336
+ // Within a template, access the converter using the syntax: {{myConverter:...}}.
337
+ return addToStore(this, $viewsConverters, name, converterFn);
338
+ }
339
+
340
+ //=================
341
+ // renderContent
342
+ //=================
343
+
344
+ function renderContent(data, context, parentView, key, isLayout, path, onRender) {
345
+ // Render template against data as a tree of subviews (nested template), or as a string (top-level template).
346
+ var i, l, dataItem, newView, itemResult, parentContext, tmpl, props, swapContent, mergedCtx, dynCtx, hasContext,
347
+ self = this,
348
+ result = "";
349
+
350
+ if (key === TRUE) {
351
+ swapContent = TRUE;
352
+ key = 0;
353
+ }
354
+ if (self.tagName) {
355
+ // This is a call from renderTag
356
+ tmpl = self.tmpl;
357
+ if (context || self.ctx) {
358
+ // We need to create an augmented context for the view(s) we are about to render
359
+ mergedCtx = {};
360
+ if (self.ctx) {
361
+ // self.ctx is an object with the contextual template parameters on the tag, such as ~foo: {{tag ~foo=expression...}}
362
+ $extend(mergedCtx, self.ctx);
363
+ }
364
+ if (context) {
365
+ // This is a context object passed programmatically from the tag function
366
+ $extend(mergedCtx, context);
367
+ }
368
+ }
369
+ context = mergedCtx;
370
+ props = self.props;
371
+ if ( props && props.link === FALSE ) {
372
+ // link=false setting on block tag
373
+ // We will override inherited value of link by the explicit setting link=false taken from props
374
+ // The child views of an unlinked view are also unlinked. So setting child back to true will not have any effect.
375
+ context = context || {};
376
+ context.link = FALSE;
377
+ }
378
+ parentView = parentView || self.view;
379
+ path = path || self.path;
380
+ key = key || self.key;
381
+ // onRender = self.attr === "html" && parentView && parentView._onRender;
382
+ onRender = parentView && parentView._onRender;
383
+ } else {
384
+ tmpl = self.jquery && (self[0] || error('Unknown template: "' + self.selector + '"')) // This is a call from $(selector).render
385
+ || self;
386
+ onRender = onRender || parentView && parentView._onRender;
387
+ }
388
+ if (tmpl) {
389
+ if (parentView) {
390
+ parentContext = parentView.ctx;
391
+ dynCtx = parentView.dynCtx;
392
+ if (data === parentView) {
393
+ // Inherit the data from the parent view.
394
+ // This may be the contents of an {{if}} block
395
+ // Set isLayout = true so we don't iterate the if block if the data is an array.
396
+ data = parentView.data;
397
+ isLayout = TRUE;
398
+ }
399
+ } else {
400
+ parentContext = $viewsHelpers;
401
+ }
402
+
403
+ // Set additional context on views created here, (as modified context inherited from the parent, and to be inherited by child views)
404
+ // Note: If no jQuery, $extend does not support chained copies - so limit extend() to two parameters
405
+ // TODO could make this a reusable helper for merging context.
406
+ hasContext = (context && context !== parentContext);
407
+ if (dynCtx || hasContext) {
408
+ parentContext = $extend({}, parentContext);
409
+ if (hasContext) {
410
+ $extend(parentContext, context);
411
+ }
412
+ if (dynCtx) {
413
+ $extend(parentContext, dynCtx);
414
+ }
415
+ }
416
+ context = parentContext;
417
+
418
+ if (!tmpl.fn) {
419
+ tmpl = $templates[tmpl] || $templates(tmpl);
420
+ }
421
+
422
+ if (tmpl) {
423
+ onRender = context.link !== FALSE && onRender; // If link===false, do not call onRender, so no data-linking annotations
424
+ if ($.isArray(data) && !isLayout) {
425
+ // Create a view for the array, whose child views correspond to each data item.
426
+ // (Note: if key and parentView are passed in along with parent view, treat as
427
+ // insert -e.g. from view.addViews - so parentView is already the view item for array)
428
+ newView = swapContent ? parentView : (key !== undefined && parentView) || View(context, path, parentView, data, tmpl, key, onRender, TRUE);
429
+ for (i = 0, l = data.length; i < l; i++) {
430
+ // Create a view for each data item.
431
+ dataItem = data[i];
432
+ itemResult = tmpl.fn(dataItem, View(context, path, newView, dataItem, tmpl, (key || 0) + i, onRender), $views);
433
+ result += onRender ? onRender(itemResult, tmpl, props) : itemResult;
434
+ }
435
+ } else {
436
+ // Create a view for singleton data object.
437
+ newView = swapContent ? parentView : View(context, path, parentView, data, tmpl, key, onRender);
438
+ newView._onRender = onRender;
439
+ result += tmpl.fn(data, newView, $views, returnVal);
440
+ }
441
+ return onRender ? onRender(result, tmpl, props, newView.key, path) : result;
442
+ }
443
+ }
444
+ error("No template found");
445
+ return "";
446
+ }
447
+
448
+ function returnVal(value) {
449
+ return value;
450
+ }
451
+
452
+ //===========================
453
+ // Build and compile template
454
+ //===========================
455
+
456
+ // Generate a reusable function that will serve to render a template against data
457
+ // (Compile AST then build template function)
458
+
459
+ function error(message) {
460
+ if ($views.debugMode) {
461
+ throw new $views.Error(message);
462
+ }
463
+ }
464
+
465
+ function syntaxError(message) {
466
+ error("Syntax error\n" + message);
467
+ }
468
+
469
+ function tmplFn(markup, tmpl, bind) {
470
+ // Compile markup to AST (abtract syntax tree) then build the template function code from the AST nodes
471
+ // Used for compiling templates, and also by JsViews to build functions for data link expressions
472
+
473
+ var newNode,
474
+ //result,
475
+ allowCode = tmpl && tmpl.allowCode,
476
+ astTop = [],
477
+ loc = 0,
478
+ stack = [],
479
+ content = astTop,
480
+ current = [, , , astTop];
481
+
482
+ //==== nested functions ====
483
+ function pushPreceedingContent(shift) {
484
+ shift -= loc;
485
+ if (shift) {
486
+ content.push(markup.substr(loc, shift).replace(rNewLine, "\\n"));
487
+ }
488
+ }
489
+
490
+ function blockTagCheck(tagName) {
491
+ tagName && syntaxError('Unmatched or missing tag: "{{/' + tagName + '}}" in template:\n' + markup);
492
+ }
493
+
494
+ function parseTag(all, defer, tagName, converter, colon, html, code, params, slash, closeBlock, index) {
495
+ // tag converter colon html code params slash closeBlock
496
+ // /{{(?:(?:(\w+(?=[\/!\s\}!]))|(?:(\w+)?(:)|(?:(>)|(\*)))((?:[^\}]|}(?!}))*?)(\/)?|(?:\/(\w+)))}}/g;
497
+ // Build abstract syntax tree (AST): [ tagName, converter, params, content, hash, contentMarkup, link ]
498
+ if (html) {
499
+ colon = ":";
500
+ converter = "html";
501
+ }
502
+ var current0,
503
+ hash = "",
504
+ passedCtx = "",
505
+ // Block tag if not self-closing and not {{:}} or {{>}} (special case) and not a data-link expression (has bind parameter)
506
+ block = !slash && !colon && !bind;
507
+
508
+ //==== nested helper function ====
509
+
510
+ tagName = tagName || colon;
511
+ pushPreceedingContent(index);
512
+ loc = index + all.length; // location marker - parsed up to here
513
+ if (code) {
514
+ if (allowCode) {
515
+ content.push(["*", params.replace(rUnescapeQuotes, "$1")]);
516
+ }
517
+ } else if (tagName) {
518
+ if (tagName === "else") {
519
+ current[5] = markup.substring(current[5], index); // contentMarkup for block tag
520
+ current = stack.pop();
521
+ content = current[3];
522
+ block = TRUE;
523
+ } else if (defer) {
524
+ stack.push(current);
525
+ current = ["!", , , [], ,index];
526
+ content.push(current);
527
+ content = current[3];
528
+ }
529
+ params = (params
530
+ ? parseParams(params, bind, defer)
531
+ .replace(rBuildHash, function(all, isCtx, keyValue) {
532
+ if (isCtx) {
533
+ passedCtx += keyValue + ",";
534
+ } else {
535
+ hash += keyValue + ",";
536
+ }
537
+ return "";
538
+ })
539
+ : "");
540
+ hash = hash.slice(0, -1);
541
+ params = params.slice(0, -1);
542
+ newNode = [
543
+ tagName,
544
+ converter || "",
545
+ params,
546
+ block && [],
547
+ "{" + (hash ? ("props:{" + hash + "},") : "") + "data: data" + (passedCtx ? ",ctx:{" + passedCtx.slice(0, -1) + "}" : "") + "}"
548
+ ];
549
+ content.push(newNode);
550
+ if (block) {
551
+ stack.push(current);
552
+ current = newNode;
553
+ current[5] = loc; // Store current location of open tag, to be able to add contentMarkup when we reach closing tag
554
+ } else if (defer) {
555
+ current[5] = markup.substring(current[5], loc); // contentMarkup for block tag
556
+ current = stack.pop();
557
+ }
558
+ } else if (closeBlock) {
559
+ current0 = current[0];
560
+ blockTagCheck(closeBlock !== current0 && !(closeBlock === "if" && current0 === "else") && current0);
561
+ current[5] = markup.substring(current[5], index); // contentMarkup for block tag
562
+ if (current0 === "!") {
563
+ // defer
564
+ current[5] = markup.substring(current[5], loc); // contentMarkup for block tag
565
+ current = stack.pop();
566
+ }
567
+ current = stack.pop();
568
+ }
569
+ blockTagCheck(!current && closeBlock);
570
+ content = current[3];
571
+ }
572
+ //==== /end of nested functions ====
573
+
574
+ // result = compiledTmplsCache[markup]; // Only cache if template is not named and markup length < ... Consider standard optimization for data-link="a.b.c"
575
+ // if (!result) {
576
+ // result = markup;
577
+ markup = markup.replace(rEscapeQuotes, "\\$1");
578
+ blockTagCheck(stack[0] && stack[0][3].pop()[0]);
579
+
580
+ // Build the AST (abstract syntax tree) under astTop
581
+ markup.replace(rTag, parseTag);
582
+
583
+ pushPreceedingContent(markup.length);
584
+
585
+ // result = compiledTmplsCache[result] = buildCode(astTop, tmpl);
586
+ // }
587
+ // return result;
588
+ return buildCode(astTop, tmpl);
589
+ }
590
+
591
+ function buildCode(ast, tmpl) {
592
+ // Build the template function code from the AST nodes, and set as property on the passed in template object
593
+ // Used for compiling templates, and also by JsViews to build functions for data link expressions
594
+ var node, i, l, code, hasTag, hasEncoder, getsValue, hasConverter, hasViewPath, tag, converter, params, hash, nestedTmpl, allowCode, content, attr, quot,
595
+ tmplOptions = tmpl ? {
596
+ allowCode: allowCode = tmpl.allowCode,
597
+ debug: tmpl.debug
598
+ } : {},
599
+ nested = tmpl && tmpl.tmpls;
600
+
601
+ // Use the AST (ast) to build the template function
602
+ l = ast.length;
603
+ code = (l ? "" : '"";');
604
+
605
+ for (i = 0; i < l; i++) {
606
+ // AST nodes: [ tagName, converter, params, content, hash, contentMarkup, link ]
607
+ node = ast[i];
608
+ if ("" + node === node) { // type string
609
+ code += '"' + node + '"+';
610
+ } else {
611
+ tag = node[0];
612
+ if (tag === "*") {
613
+ code = code.slice(0, i ? -1 : -3) + ";" + node[1] + (i + 1 < l ? "ret+=" : "");
614
+ } else {
615
+ converter = node[1];
616
+ params = node[2];
617
+ content = node[3];
618
+ hash = node[4];
619
+ markup = node[5];
620
+ if (tag.slice(-1) === "!") {
621
+ // Create template object for nested template
622
+ nestedTmpl = TmplObject(markup, tmplOptions, tmpl, nested.length);
623
+ // Compile to AST and then to compiled function
624
+ buildCode(content, nestedTmpl);
625
+ if (attr = /\s+[\w-]*\s*\=\s*\\['"]$/.exec(ast[i-1])) {
626
+ error("'{{!' in attribute:\n..." + ast[i-1] + "{{!...\nUse data-link");
627
+ }
628
+ code += 'view.renderLink(' + nested.length + ')+';
629
+ nestedTmpl.bound = TRUE;
630
+ nestedTmpl.fn.attr = attr || "leaf";
631
+ nested.push(nestedTmpl);
632
+ } else {
633
+ if (content) {
634
+ // Create template object for nested template
635
+ nestedTmpl = TmplObject(markup, tmplOptions, tmpl, nested.length);
636
+ // Compile to AST and then to compiled function
637
+ buildCode(content, nestedTmpl);
638
+ nested.push(nestedTmpl);
639
+ }
640
+ hasViewPath = hasViewPath || hash.indexOf("view") > -1;
641
+ code += (tag === ":"
642
+ ? (converter === "html"
643
+ ? (hasEncoder = TRUE, "h(" + params)
644
+ : converter
645
+ ? (hasConverter = TRUE, 'c("' + converter + '",view,this,' + params)
646
+ : (getsValue = TRUE, "((v=" + params + ')!=u?v:""')
647
+ )
648
+ : (hasTag = TRUE, 't("' + tag + '",view,this,'
649
+ + (content ? nested.length : '""') // For block tags, pass in the key (nested.length) to the nested content template
650
+ + "," + hash + (params ? "," : "") + params))
651
+ + ")+";
652
+ }
653
+ }
654
+ }
655
+ }
656
+ code = fnDeclStr
657
+ + (getsValue ? "v," : "")
658
+ + (hasTag ? "t=j._tag," : "")
659
+ + (hasConverter ? "c=j._convert," : "")
660
+ + (hasEncoder ? "h=j.converters.html," : "")
661
+ + "ret; try{\n\n"
662
+ + (tmplOptions.debug ? "debugger;" : "")
663
+ + (allowCode ? 'ret=' : 'return ')
664
+ + code.slice(0, -1) + ";\n\n"
665
+ + (allowCode ? "return ret;" : "")
666
+ + "}catch(e){return j._err(e);}";
667
+
668
+ try {
669
+ code = new Function("data, view, j, b, u", code);
670
+ } catch (e) {
671
+ syntaxError("Compiled template code:\n\n" + code, e);
672
+ }
673
+
674
+ // Include only the var references that are needed in the code
675
+ if (tmpl) {
676
+ tmpl.fn = code;
677
+ }
678
+ return code;
679
+ }
680
+
681
+ function parseParams(params, bind, defer) {
682
+ var named,
683
+ fnCall = {},
684
+ parenDepth = 0,
685
+ quoted = FALSE, // boolean for string content in double quotes
686
+ aposed = FALSE; // or in single quotes
687
+
688
+ function parseTokens(all, lftPrn0, lftPrn, path, operator, err, eq, path2, prn, comma, lftPrn2, apos, quot, rtPrn, prn2, space) {
689
+ // rParams = /(?:([([])\s*)?(?:([#~]?[\w$.]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*!:?\/]|(=))\s*|([#~]?[\w$.^]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*([)\]])([([]?))|(\s+)/g,
690
+ // lftPrn path operator err eq path2 prn comma lftPrn3 apos quot rtPrn prn2 space
691
+ // (left paren? followed by (path? followed by operator) or (path followed by paren?)) or comma or apos or quot or right paren or space
692
+ operator = operator || "";
693
+ lftPrn = lftPrn || lftPrn0 || lftPrn2;
694
+ path = path || path2;
695
+ prn = prn || prn2 || "";
696
+ operator = operator || "";
697
+ var bindParam = (bind || defer) && prn !== "(";
698
+
699
+ function parsePath(all, object, helper, view, viewProperty, pathTokens, leafToken) {
700
+ // rPath = /^(?:null|true|false|\d[\d.]*|([\w$]+|~([\w$]+)|#(view|([\w$]+))?)([\w$.]*?)(?:[.[]([\w$]+)\]?)?|(['"]).*\8)$/g,
701
+ // object helper view viewProperty pathTokens leafToken string
702
+ if (object) {
703
+ var leaf,
704
+ ret = (helper
705
+ ? 'view._hlp("' + helper + '")'
706
+ : view
707
+ ? "view"
708
+ : "data")
709
+ + (leafToken
710
+ ? (viewProperty
711
+ ? "." + viewProperty
712
+ : helper
713
+ ? ""
714
+ : (view ? "" : "." + object)
715
+ ) + (pathTokens || "")
716
+ : (leafToken = helper ? "" : view ? viewProperty || "" : object, ""));
717
+
718
+ leaf = (leafToken ? "." + leafToken : "");
719
+ if (!bindParam) {
720
+ ret = ret + leaf;
721
+ }
722
+ ret = ret.slice(0, 9) === "view.data"
723
+ ? ret.slice(5) // convert #view.data... to data...
724
+ : ret;
725
+ if (bindParam) {
726
+ ret = "b(" + ret + ',"' + leafToken + '")' + leaf;
727
+ }
728
+ return ret;
729
+ }
730
+ return all;
731
+ }
732
+
733
+ if (err) {
734
+ syntaxError(params);
735
+ } else {
736
+ return (aposed
737
+ // within single-quoted string
738
+ ? (aposed = !apos, (aposed ? all : '"'))
739
+ : quoted
740
+ // within double-quoted string
741
+ ? (quoted = !quot, (quoted ? all : '"'))
742
+ :
743
+ (
744
+ (lftPrn
745
+ ? (parenDepth++, lftPrn)
746
+ : "")
747
+ + (space
748
+ ? (parenDepth
749
+ ? ""
750
+ : named
751
+ ? (named = FALSE, "\b")
752
+ : ","
753
+ )
754
+ : eq
755
+ // named param
756
+ ? (parenDepth && syntaxError(params), named = TRUE, '\b' + path + ':')
757
+ : path
758
+ // path
759
+ ? (path.replace(rPath, parsePath)
760
+ + (prn
761
+ ? (fnCall[++parenDepth] = TRUE, prn)
762
+ : operator)
763
+ )
764
+ : operator
765
+ ? operator
766
+ : rtPrn
767
+ // function
768
+ ? ((fnCall[parenDepth--] = FALSE, rtPrn)
769
+ + (prn
770
+ ? (fnCall[++parenDepth] = TRUE, prn)
771
+ : "")
772
+ )
773
+ : comma
774
+ ? (fnCall[parenDepth] || syntaxError(params), ",") // We don't allow top-level literal arrays or objects
775
+ : lftPrn0
776
+ ? ""
777
+ : (aposed = apos, quoted = quot, '"')
778
+ ))
779
+ );
780
+ }
781
+ }
782
+ params = (params + " ").replace(rParams, parseTokens);
783
+ return params;
784
+ }
785
+
786
+ function compileNested(items, process, options) {
787
+ var key, nestedItem;
788
+ if (items) {
789
+ for (key in items) {
790
+ // compile nested template declarations
791
+ nestedItem = items[key];
792
+ if (!nestedItem.is) {
793
+ // Not yet compiled
794
+ items[key] = process(nestedItem, key, options);
795
+ }
796
+ }
797
+ }
798
+ }
799
+
800
+ function compile(tmpl, name, parent, options) {
801
+ // tmpl is either a template object, a selector for a template script block, the name of a compiled template, or a template object
802
+ // options is the set of template properties, c
803
+ var tmplOrMarkup, elem;
804
+
805
+ //==== nested functions ====
806
+ function tmplOrMarkupFromStr(value) {
807
+ // If value is of type string - treat as selector, or name of compiled template
808
+ // Return the template object, if already compiled, or the markup string
809
+
810
+ if (("" + value === value) || value.nodeType > 0) {
811
+ try {
812
+ elem = value.nodeType > 0
813
+ ? value
814
+ : !rTmplString.test(value)
815
+ // If value is a string and does not contain HTML or tag content, then test as selector
816
+ && jQuery && jQuery(value)[0];
817
+ // If selector is valid and returns at least one element, get first element
818
+ // If invalid, jQuery will throw. We will stay with the original string.
819
+ } catch (e) { }
820
+
821
+ if (elem) {
822
+ // Generally this is a script element.
823
+ // However we allow it to be any element, so you can for example take the content of a div,
824
+ // use it as a template, and replace it by the same content rendered against data.
825
+ // e.g. for linking the content of a div to a container, and using the initial content as template:
826
+ // $.link("#content", model, {tmpl: "#content"});
827
+
828
+ // Create a name for compiled template if none provided
829
+ value = $templates[elem.getAttribute(tmplAttr)];
830
+ if (!value) {
831
+ // Not already compiled and cached, so compile and cache the name
832
+ name = name || "_" + autoTmplName++;
833
+ elem.setAttribute(tmplAttr, name);
834
+ value = compile(elem.innerHTML, name, parent, options); // Use tmpl as options
835
+ $templates[name] = value;
836
+ }
837
+ }
838
+ return value;
839
+ }
840
+ // If value is not a string, return undefined
841
+ }
842
+
843
+ //==== Compile the template ====
844
+ tmpl = tmpl || "";
845
+ tmplOrMarkup = tmplOrMarkupFromStr(tmpl);
846
+
847
+ // If tmpl is a template object, use it for options
848
+ options = options || (tmpl.markup ? tmpl : {});
849
+ options.name = name;
850
+ options.is = "tmpl";
851
+
852
+ // If tmpl is not a markup string or a selector string, then it must be a template object
853
+ // In that case, get it from the markup property of the object
854
+ if (!tmplOrMarkup && tmpl.markup && (tmplOrMarkup = tmplOrMarkupFromStr(tmpl.markup))) {
855
+ if (tmplOrMarkup.fn && (tmplOrMarkup.debug !== tmpl.debug || tmplOrMarkup.allowCode !== tmpl.allowCode)) {
856
+ // if the string references a compiled template object, but the debug or allowCode props are different, need to recompile
857
+ tmplOrMarkup = tmplOrMarkup.markup;
858
+ }
859
+ }
860
+ if (tmplOrMarkup !== undefined) {
861
+ if (name && !parent) {
862
+ $render[name] = function() {
863
+ return tmpl.render.apply(tmpl, arguments);
864
+ };
865
+ }
866
+ if (tmplOrMarkup.fn || tmpl.fn) {
867
+ // tmpl is already compiled, so use it, or if different name is provided, clone it
868
+ if (tmplOrMarkup.fn) {
869
+ if (name && name !== tmplOrMarkup.name) {
870
+ tmpl = $extend($extend({}, tmplOrMarkup), options);
871
+ } else {
872
+ tmpl = tmplOrMarkup;
873
+ }
874
+ }
875
+ } else {
876
+ // tmplOrMarkup is a markup string, not a compiled template
877
+ // Create template object
878
+ tmpl = TmplObject(tmplOrMarkup, options, parent, 0);
879
+ // Compile to AST and then to compiled function
880
+ tmplFn(tmplOrMarkup, tmpl);
881
+ }
882
+ compileNested(options.templates, compile, tmpl);
883
+ compileNested(options.tags, compileTag);
884
+ return tmpl;
885
+ }
886
+ }
887
+ //==== /end of function compile ====
888
+
889
+ function TmplObject(markup, options, parent, key) {
890
+ // Template object constructor
891
+
892
+ // nested helper function
893
+ function extendStore(storeName) {
894
+ if (parent[storeName]) {
895
+ // Include parent items except if overridden by item of same name in options
896
+ tmpl[storeName] = $extend($extend({}, parent[storeName]), options[storeName]);
897
+ }
898
+ }
899
+
900
+ options = options || {};
901
+ var tmpl = {
902
+ markup: markup,
903
+ tmpls: [],
904
+ links: [],
905
+ render: renderContent
906
+ };
907
+
908
+ if (parent) {
909
+ if (parent.templates) {
910
+ tmpl.templates = $extend($extend({}, parent.templates), options.templates);
911
+ }
912
+ tmpl.parent = parent;
913
+ tmpl.name = parent.name + "[" + key + "]";
914
+ tmpl.key = key;
915
+ }
916
+
917
+ $extend(tmpl, options);
918
+ if (parent) {
919
+ extendStore("templates");
920
+ extendStore("tags");
921
+ extendStore("helpers");
922
+ extendStore("converters");
923
+ }
924
+ return tmpl;
925
+ }
926
+
927
+ //========================== Initialize ==========================
928
+
929
+ if (jQuery) {
930
+ ////////////////////////////////////////////////////////////////////////////////////////////////
931
+ // jQuery is loaded, so make $ the jQuery object
932
+ $ = jQuery;
933
+ $.templates = $templates;
934
+ $.render = $render;
935
+ $.views = $views;
936
+ $.fn.render = renderContent;
937
+
938
+ } else {
939
+ ////////////////////////////////////////////////////////////////////////////////////////////////
940
+ // jQuery is not loaded.
941
+
942
+ $ = global.jsviews = $views;
943
+ $.extend = function(target, source) {
944
+ var name;
945
+ target = target || {};
946
+ for (name in source) {
947
+ target[name] = source[name];
948
+ }
949
+ return target;
950
+ };
951
+
952
+ $.isArray = Array && Array.isArray || function(obj) {
953
+ return Object.prototype.toString.call(obj) === "[object Array]";
954
+ };
955
+ }
956
+
957
+ $extend = $.extend;
958
+
959
+ function replacerForHtml(ch) {
960
+ // Original code from Mike Samuel <msamuel@google.com>
961
+ return escapeMapForHtml[ch]
962
+ // Intentional assignment that caches the result of encoding ch.
963
+ || (escapeMapForHtml[ch] = "&#" + ch.charCodeAt(0) + ";");
964
+ }
965
+
966
+ //========================== Register tags ==========================
967
+
968
+ $viewsTags({
969
+ "if": function() {
970
+ var ifTag = this,
971
+ view = ifTag.view;
972
+
973
+ view.onElse = function(tagInstance, args) {
974
+ var i = 0,
975
+ l = args.length;
976
+
977
+ while (l && !args[i++]) {
978
+ // Only render content if args.length === 0 (i.e. this is an else with no condition) or if a condition argument is truey
979
+ if (i === l) {
980
+ return "";
981
+ }
982
+ }
983
+ view.onElse = undefined; // If condition satisfied, so won't run 'else'.
984
+ tagInstance.path = "";
985
+ return tagInstance.renderContent(view);
986
+ // Test is satisfied, so render content, while remaining in current data context
987
+ // By passing the view, we inherit data context from the parent view, and the content is treated as a layout template
988
+ // (so if the data is an array, it will not iterate over the data
989
+ };
990
+ return view.onElse(this, arguments);
991
+ },
992
+ "else": function() {
993
+ var view = this.view;
994
+ return view.onElse ? view.onElse(this, arguments) : "";
995
+ },
996
+ "for": function() {
997
+ var i,
998
+ self = this,
999
+ result = "",
1000
+ args = arguments,
1001
+ l = args.length;
1002
+
1003
+ if (l === 0) {
1004
+ // If no parameters, render once, with #data undefined
1005
+ l = 1;
1006
+ }
1007
+ for (i = 0; i < l; i++) {
1008
+ result += self.renderContent(args[i]);
1009
+ }
1010
+ return result;
1011
+ },
1012
+ "*": function(value) {
1013
+ return value;
1014
+ }
1015
+ });
1016
+
1017
+ //========================== Register global helpers ==========================
1018
+
1019
+ // $viewsHelpers({ // Global helper functions
1020
+ // // TODO add any useful built-in helper functions
1021
+ // });
1022
+
1023
+ //========================== Register converters ==========================
1024
+
1025
+ $viewsConverters({
1026
+ html: function(text) {
1027
+ // HTML encoding helper: Replace < > & and ' and " by corresponding entities.
1028
+ // inspired by Mike Samuel <msamuel@google.com>
1029
+ return text != undefined ? String(text).replace(htmlSpecialChar, replacerForHtml) : "";
1030
+ }
1031
+ });
1032
+
1033
+ //========================== Define default delimiters ==========================
1034
+ $viewsDelimiters();
1035
+
1036
+ })(this, this.jQuery);