1 // ==========================================================================
  2 // Project:   SproutCore - JavaScript Application Framework
  3 // Copyright: ©2006-2011 Strobe Inc. and contributors.
  4 //            Portions ©2008-2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 
  9 /**
 10 
 11   Generic base class to encode a view hierarchy.  `ViewCoder`s are used to
 12   collect the properties that may be included in a view design and then to 
 13   serialize that design to a JavaScript string that can be evaled.  
 14   
 15   To encode a view with a `ViewCoder`, simply call `SC.ViewCoder.encode(view)`.
 16   Most of the time, however, you will not initiate coding directly but instead
 17   work with the coder while designing an `SC.DesignerView` subclass.
 18 
 19   ## Using a Coder
 20 
 21   When you are passed an instance of a coder, you can simply write attributes
 22   into the coder using one of the many encoding methods defined on the view.
 23   Encoding methods are defined for most primitive view types.
 24   
 25       coder.string("firstName" , "Charles").string('lastName', 'Jolley');
 26   
 27   @extends SC.Object
 28 */
 29 SC.ObjectCoder = SC.Object.extend({
 30   
 31   // ..........................................................
 32   // PROPERTIES
 33   // 
 34   
 35   /** The `className` used to emit the design. */
 36   className: 'SC.Object',
 37   
 38   /** 
 39     The method to be used to create the class or object. 
 40   */
 41   extendMethodName: 'extend',
 42   
 43   /** 
 44     The default encoding method.  If an object defines this method, then a new
 45     coder will be created to encode that object.
 46   */
 47   encodeMethodName: 'encode',
 48   
 49   /** 
 50     The attributes that will be emitted.  The values all must be strings. Use 
 51     one of the encoding methods defined below to actually encode attributes.
 52   */
 53   attributes: null,
 54   
 55   // ..........................................................
 56   // ENCODING METHODS
 57   // 
 58   // Call these methods to encode various types of attributes.  They all take
 59   // the same basic params: (key, value)...
 60 
 61   /**
 62     Utility method transforms the passed value with the passed function.  
 63     Handles both Arrays and individual items.
 64   */
 65   transform: function(val, func) {
 66     
 67     // for an array, transform each value with the func and then return a
 68     // combined array.
 69     if (SC.typeOf(val) === SC.T_ARRAY) {
 70       val = val.map(function(x) { return this.transform(x, func); }, this);
 71       val = '['+val+']';
 72       
 73     // otherwise, just call transform function on the value
 74     } else {
 75       val = func.call(this, val);
 76     }
 77     return val;
 78   },
 79   
 80   /**
 81     Encodes a string of raw JavaScript.  This is the most primitive method. 
 82     You are expected to prep the value yourself.  You can pass an array to
 83     this or any other method and it will be encoded as a full array.
 84 
 85     This method also automatically handles null and undefined values.  Null
 86     values are included in the output.  Undefined values are ignored.
 87     
 88     @param key {String} the key to set
 89     @param val {String} the JavaScript
 90     @param transform {Function} optional transform function to apply to val
 91     @returns {SC.ObjectCoder} receiver
 92   */
 93   js: function(key, val, transform) {
 94     
 95     // normalize
 96     if (val===undefined) { val=key; key = undefined; }
 97     val = this.transform(val, function(x) {
 98       return (x===null) ? "null" : transform ? transform.call(this, x) : x ;
 99     });
100     
101     // save if needed.  Undefined values are ignored
102     if (key !== undefined && (val !== undefined)) {
103       this.attributes[key] = val;
104       return this ;
105     } else return val ;
106   },
107 
108   /**
109     Encodes a string, wrapping it in quotes.
110     
111     @param key {String} the key to set
112     @param val {String} the value
113     @returns {SC.ObjectCoder} receiver
114   */
115   string: function(key, val) {
116     return this.js(key, val, function(x) {
117       return '"' + x.replace(/"/g, '\\"') + '"' ;
118     });
119   },
120   
121   /**
122     Encodes a number, wrapping it in quotes.
123     
124     @param key {String} the key to set
125     @param val {Number} the value
126     @returns {SC.ObjectCoder} receiver
127   */
128   number: function(key, val) {
129     return this.js(key, val, function(x) { return x.toString(); });
130   },
131   
132   /**
133     Encodes a bool, mapped as `YES` or `NO`
134     
135     @param key {String} the key to set
136     @param val {Boolean} the value
137     @returns {SC.ObjectCoder} receiver
138   */
139   bool: function(key, val) {
140     return this.js(key, val, function(x) { return x ? "true" : "false"; });
141   },
142 
143   /**
144     Encodes an object.  This will do its best to autodetect the type of the
145     object.  You can pass an optional processing function that will be used 
146     on object members before processing to allow you to normalize.  The 
147     method signature must be:
148     
149         function convert(value, rootObject, key);
150 
151     The rootObject and key will be set to give you the context in the 
152     hierarchy.
153     
154     Generally this method will work for encoding simple value only.  If your 
155     object graph may contain SproutCore objects, you will need to encode it
156     yourself.
157     
158     @param key {String} the key to set
159     @param val {Object} the value
160     @param func {Function} optional transform func
161     @returns {SC.ObjectCoder} receiver
162   */
163   encode: function(key, val, func) {
164     // normalize params
165     if (func===undefined && val instanceof Function) {
166       func = val; val = key; key = undefined; 
167     }
168 
169     return this.js(key, val, function(cur) { 
170       if (func) cur = func.call(this, cur, null, null);
171       switch(SC.typeOf(cur)) {
172       case SC.T_STRING:
173         cur = this.string(cur);
174         break;
175 
176       case SC.T_NUMBER:
177         cur = this.number(cur);
178         break;
179 
180       case SC.T_BOOL:
181         cur = this.bool(cur);
182         break;
183 
184       case SC.T_ARRAY:
185         cur = this.array(cur, func) ;
186         break;
187 
188       case SC.T_HASH:
189         cur = this.hash(cur, func);
190         break ;
191         
192       default:
193         // otherwise, if the object has a designer attached, try to encode
194         // view.
195         cur = cur ? this.object(cur) : this.js(cur);
196       }
197       return cur ;
198     });
199   },
200   
201   /**
202     Encodes a hash of objects.  The object values must be simple objects for
203     this method to work.  You can also optionally pass a processing function
204     that will be invoked for each value, giving you a chance to convert the
205     value first.  The signature must be `(key, value, rootObject)`.
206     
207     @param key {String} the key to set
208     @param val {Object} the value
209     @param func {Function} optional transform func
210     @returns {SC.ObjectCoder} receiver
211   */
212   hash: function(key, val, func) {
213     
214     // normalize params
215     if (func===undefined && val instanceof Function) {
216       func = val; val = key; key = undefined; 
217     }
218     
219     return this.js(key, val, function(x) { 
220       var ret = [] ;
221       for(var key in x) {
222         if (!x.hasOwnProperty(key)) continue; // only include added...
223         ret.push("%@: %@".fmt(this.encode(key), this.encode(x[key], func)));
224       }
225       return "{%@}".fmt(ret.join(","));
226     });
227   },
228 
229   /**
230     Encodes a array of objects.  The object values must be simple objects for
231     this method to work.  You can also optionally pass a processing function
232     that will be invoked for each value, giving you a chance to convert the
233     value first.  The signature must be `(index, value, rootObject)`.
234     
235     @param key {String} the key to set
236     @param val {Object} the value
237     @param func {Function} optional transform func
238     @returns {SC.ObjectCoder} receiver
239   */
240   array: function(key, val, func) {
241     
242     // normalize params
243     if (func===undefined && val instanceof Function) {
244       func = val; val = key; key = undefined; 
245     }
246 
247     val = val.map(function(x) { return this.encode(x, func); }, this);
248     val = "[%@]".fmt(val.join(","));
249 
250     return this.js(key, val);
251   },
252   
253   /**
254     Attempts to encode an object.  The object must implement the 
255     encodeMethodName for this encoder, or else an exception will be raised.
256     
257     @param key {String} the key to set
258     @param val {Object} the object to encode
259     @returns {SC.ObjectCoder} receiver
260   */
261   object: function(key, val) {
262     return this.js(key, val, function(x) {
263       return this.constructor.encode(x, this);
264     });
265   },
266   
267   // ..........................................................
268   // INTERNAL SUPPORT
269   // 
270   
271   spaces: function() {
272     var spaces = this.context ? this.context.get('spaces') : '' ;
273     spaces = spaces + '  ';  
274     return spaces ;
275   }.property().cacheable(),
276   
277   /** 
278     Emits the final JavaScript output for this coder based on the current
279     attributes.
280   */
281   emit: function() {
282     
283     // return undefined if the encoding was rejected...
284     if (this.invalid) return undefined ;
285     
286     var ret = [], attrs = this.attributes, key ;
287     var methodName = this.get('extendMethodName');
288     var spaces = this.get('spaces');
289     
290     // compute attribute body...
291     for(key in attrs) {
292       if (!attrs.hasOwnProperty(key)) continue ;
293       ret.push("%@: %@".fmt(key, attrs[key]));
294     }
295     
296     if (ret.length <= 0) {
297       return "%@1%@2.%@3({})".fmt(spaces, this.className, methodName);
298     } else {
299       // handle NO class formatting..
300       ret = ret.join(",");
301       return "%@2.%@3({%@4})".fmt(spaces, this.className, methodName, ret);
302     }
303   },
304   
305   /**
306     Begins encoding with a particular object, setting the className to the 
307     object's `className`.  This is used internally by the `encode()` method.
308   */
309   begin: function(object) {
310     var methodName = this.get('encodeMethodName');
311     if (SC.typeOf(object[methodName]) !== SC.T_FUNCTION) {
312       SC.throw("Cannot encode %@ because it does not respond to %@()".fmt(object, methodName));
313     } 
314     
315     // save className for later coding
316     this.set('className', SC._object_className(object.constructor));
317 
318     // then call encode method...
319     var ret = object[methodName](this);
320     
321     // if encoding method returns NO, then encoding is not allowed.
322     // note that returning void should count as YES.
323     this.invalid = ret === NO ;
324     
325     // and return this
326     return this ;
327   },
328   
329   init: function() {
330     sc_super();
331     this.set('attributes', {});
332   },
333   
334   destroy: function() {
335     sc_super();
336     this.context = this.className = this.attributes = null ; // cleanup
337   }
338   
339 });
340 
341 SC.ObjectCoder.encode = function(object, context) {
342   // create coder and emit code...
343   var coder = this.create({ context: context });
344   var ret = coder.begin(object).emit();
345   
346   // cleanup and return
347   coder.destroy();
348   return ret ;
349 } ;
350