1 | # Imports
|
2 | fsUtil = require('safefs')
|
3 | pathUtil = require('path')
|
4 | extractOptsAndCallback = require('extract-opts')
|
5 | requireFresh = require('requirefresh')
|
6 |
|
7 | # Public: The exported CSON singleton
|
8 | class CSON
|
9 | # ====================================
|
10 | # Helpers
|
11 |
|
12 | # Internal: Ensure Error Type
|
13 | ensureErrorType: (err) ->
|
14 | if err instanceof Error
|
15 | return err
|
16 | else
|
17 | return @ensureErrorType(err)
|
18 |
|
19 | # Internal: Complete with callback if it exists
|
20 | complete: (result, next) ->
|
21 | # Complete
|
22 | if next
|
23 | if result instanceof Error
|
24 | next(result)
|
25 | else
|
26 | next(null, result)
|
27 | return @
|
28 | else
|
29 | return result
|
30 |
|
31 | # Internal: Fills in any missing options for use in our methods
|
32 | #
|
33 | # opts - {Object} The options to prepare
|
34 | #
|
35 | # Returns the same opts {Object} that we received
|
36 | getOptions: (opts={}) ->
|
37 | opts.format ?= null
|
38 | opts.filename ?= null
|
39 | if opts.filename
|
40 | opts.filename = pathUtil.resolve(opts.filename)
|
41 | opts.format ?= @getFormat(opts.filename)
|
42 | if opts.filename is null
|
43 | delete opts.filename
|
44 |
|
45 | opts.json ?= true
|
46 | opts.cson ?= true
|
47 | opts.javascript ?= false
|
48 | opts.coffeescript ?= false
|
49 |
|
50 | return opts
|
51 |
|
52 | # Internal: Gets the format for a file name or path
|
53 | #
|
54 | # file - {String} to get the format for
|
55 | #
|
56 | # Returns the determined format as a {String} ("json", "cson", "coffeescript", or "javascript", or null)
|
57 | getFormat: (file) ->
|
58 | return switch pathUtil.extname(file)
|
59 | when '.json'
|
60 | 'json'
|
61 | when '.cson'
|
62 | 'cson'
|
63 | when '.coffee'
|
64 | 'coffeescript'
|
65 | when '.js'
|
66 | 'javascript'
|
67 | else
|
68 | null
|
69 |
|
70 | # Internal: Helper for {::createString}, {::parseString}, {::parseFile}, {::requireFile}
|
71 | action: (args) ->
|
72 | # Prepare
|
73 | {action, prefix, suffix, data, opts, next} = args
|
74 | suffix ?= ''
|
75 | [opts, next] = extractOptsAndCallback(opts, next)
|
76 |
|
77 | # Prepare options
|
78 | switch action
|
79 | when 'requireFile', 'parseFile'
|
80 | opts.filename ?= data
|
81 |
|
82 | # Add defaults
|
83 | opts = @getOptions(opts)
|
84 |
|
85 | # Default: CSON
|
86 | if opts.format in [null, 'cson']
|
87 | if opts.cson is true
|
88 | result = @[prefix+'CSON'+suffix](data, opts)
|
89 | else
|
90 | result = new Error("CSON.#{action}: Desired format is CSON however CSON is disabled by an option")
|
91 |
|
92 | # JSON
|
93 | else if opts.format is 'json'
|
94 | if opts.json is true
|
95 | result = @[prefix+'JSON'+suffix](data, opts)
|
96 | else
|
97 | result = new Error("CSON.#{action}: Desired format is JSON however JSON is disabled by an option")
|
98 |
|
99 | # JavaScript
|
100 | else if opts.format is 'javascript'
|
101 | if opts.javascript is true
|
102 | result = @[prefix+'JS'+suffix](data, opts)
|
103 | else
|
104 | result = new Error("CSON.#{action}: Desired format is JavaScript however JavaScript is disabled by an option")
|
105 |
|
106 | # CoffeeScript
|
107 | else if opts.format is 'coffeescript'
|
108 | if opts.coffeescript is true
|
109 | result = @[prefix+'CS'+suffix](data, opts)
|
110 | else
|
111 | result = new Error("CSON.#{action}: Desired format is CoffeeScript however CoffeeScript is disabled by an option")
|
112 |
|
113 | else
|
114 | result = new Error("CSON.#{action}: Desired format is not supported")
|
115 |
|
116 | # Complete
|
117 | return @complete(result, next)
|
118 |
|
119 |
|
120 |
|
121 | # ====================================
|
122 | # Bundles
|
123 |
|
124 | # Public: {Delegates to: .createString}
|
125 | stringify: (data, replacer, indent) ->
|
126 | opts = {}
|
127 | opts.replacer = replacer
|
128 | opts.indent = indent
|
129 | return @createCSONString(data, opts)
|
130 |
|
131 | # Public: {Delegates to: .parseCSONString}
|
132 | parse: (data, opts, next) ->
|
133 | return @parseCSONString(data, opts, next)
|
134 |
|
135 | # Public: {Delegates to: .parseCSONFile}
|
136 | load: (data, opts, next) ->
|
137 | return @parseCSONFile(data, opts, next)
|
138 |
|
139 | # Public: Converts an {Object} into a {String} of the desired format
|
140 | #
|
141 | # If the format option is not specified, we default to CSON
|
142 | #
|
143 | # data - {Object} The data to convert
|
144 | # opts - {Object} The options (options may also be forwarded onto the parser library)
|
145 | # :format - {String} The format to use: "cson" (default), "json", "coffeescript", or "javascript"
|
146 | # :cson - {Boolean} Whether or not the CSON format should be allowed (defaults to `true`)
|
147 | # :json - {Boolean} Whether or not the JSON format should be allowed (defaults to `true`)
|
148 | #
|
149 | # Returns {String} or {Error}
|
150 | createString: (data, opts, next) ->
|
151 | return @action({
|
152 | action: 'createString'
|
153 | prefix: 'create'
|
154 | suffix: 'String'
|
155 | data, opts, next
|
156 | })
|
157 |
|
158 | # Public: Converts a {String} of the desired format into an {Object}
|
159 | #
|
160 | # If the format option is not specified, we default to CSON
|
161 | #
|
162 | # data - {String} The string to parse
|
163 | # opts - {Object} The options (options may also be forwarded onto the parser library)
|
164 | # :format - {String} The format to use: "cson" (default), "json", "coffeescript", or "javascript"
|
165 | # :cson - {Boolean} Whether or not the CSON format should be allowed (defaults to `true`)
|
166 | # :json - {Boolean} Whether or not the JSON format should be allowed (defaults to `true`)
|
167 | # :coffeescript - {Boolean} Whether or not the CoffeeScript format should be allowed (defaults to `false`)
|
168 | # :json - {Boolean} Whether or not the CoffeeScript format should be allowed (defaults to `json`)
|
169 | #
|
170 | # Returns {Object} or {Error}
|
171 | parseString: (data, opts, next) ->
|
172 | return @action({
|
173 | action: 'parseString'
|
174 | prefix: 'parse'
|
175 | suffix: 'String'
|
176 | data, opts, next
|
177 | })
|
178 |
|
179 | # Public: Parses a file path of the desired format into an {Object}
|
180 | #
|
181 | # If the format option is not specified, we use the filename to detect what it should be, otherwise we default to CSON
|
182 | #
|
183 | # data - {String} The file path to parse
|
184 | # opts - {Object} The options (options may also be forwarded onto the parser library)
|
185 | # :format - {String} The format to use: "cson" (default), "json", "coffeescript", or "javascript"
|
186 | # :cson - {Boolean} Whether or not the CSON format should be allowed (defaults to `true`)
|
187 | # :json - {Boolean} Whether or not the JSON format should be allowed (defaults to `true`)
|
188 | # :coffeescript - {Boolean} Whether or not the CoffeeScript format should be allowed (defaults to `false`)
|
189 | # :json - {Boolean} Whether or not the CoffeeScript format should be allowed (defaults to `json`)
|
190 | #
|
191 | # Returns {Object} or {Error}
|
192 | parseFile: (data, opts, next) ->
|
193 | return @action({
|
194 | action: 'parseFile'
|
195 | prefix: 'parse'
|
196 | suffix: 'File'
|
197 | data, opts, next
|
198 | })
|
199 |
|
200 | # Public: Requires or parses a file path of the desired format into an {Object}
|
201 | #
|
202 | # If the format option is not specified, we use the filename to detect what it should be, otherwise we default to CSON
|
203 | #
|
204 | # data - {String} The file path to require or parse
|
205 | # opts - {Object} The options (options may also be forwarded onto the parser library)
|
206 | # :format - {String} The format to use: "cson" (default), "json", "coffeescript", or "javascript"
|
207 | # :cson - {Boolean} Whether or not the CSON format should be allowed (defaults to `true`)
|
208 | # :json - {Boolean} Whether or not the JSON format should be allowed (defaults to `true`)
|
209 | # :coffeescript - {Boolean} Whether or not the CoffeeScript format should be allowed (defaults to `false`)
|
210 | # :json - {Boolean} Whether or not the CoffeeScript format should be allowed (defaults to `json`)
|
211 | #
|
212 | # Returns {Object} or {Error}
|
213 | requireFile: (data, opts, next) ->
|
214 | return @action({
|
215 | action: 'requireFile'
|
216 | prefix: 'require'
|
217 | suffix: 'File'
|
218 | data, opts, next
|
219 | })
|
220 |
|
221 |
|
222 | # ====================================
|
223 | # Creating Strings from Objects
|
224 |
|
225 | # Public: Converts an {Object} into a JSON {String}
|
226 | #
|
227 | # data - {Object} The data to convert
|
228 | # opts - {Object} The options (options may also be forwarded onto the parser library)
|
229 | # :replacer - {Boolean} The replacer option for `JSON.stringify` (defaults to `null`)
|
230 | # :indent - {Boolean} The indent option for `JSON.stringify` (defaults to two spaces ` `)
|
231 | #
|
232 | # Returns {String} or {Error}
|
233 | createJSONString: (data, opts, next) ->
|
234 | # Prepare
|
235 | [opts, next] = extractOptsAndCallback(opts, next)
|
236 | opts = @getOptions(opts)
|
237 | opts.replacer ?= null
|
238 | opts.indent ?= ' '
|
239 |
|
240 | # Stringify
|
241 | try
|
242 | result = JSON.stringify(data, opts.replacer, opts.indent)
|
243 | catch err
|
244 | result = @ensureErrorType(err)
|
245 |
|
246 | # Complete
|
247 | return @complete(result, next)
|
248 |
|
249 | # Public: Converts an {Object} into a CSON {String}
|
250 | #
|
251 | # data - {Object} The data to convert
|
252 | # opts - {Object} The options (options may also be forwarded onto the parser library)
|
253 | # :replacer - {Boolean} The replacer option for `require('cson-parser').stringify` (defaults to `null`)
|
254 | # :indent - {Boolean} The indent option for `require('cson-parser').stringify` (defaults to a single tab `'\t'`)
|
255 | #
|
256 | # Returns {String} or {Error}
|
257 | createCSONString: (data, opts, next) ->
|
258 | # Prepare
|
259 | [opts, next] = extractOptsAndCallback(opts, next)
|
260 | opts = @getOptions(opts)
|
261 | opts.replacer ?= null
|
262 | opts.indent ?= '\t'
|
263 |
|
264 | # Stringify
|
265 | try
|
266 | result = require('cson-parser').stringify(data, opts.replacer, opts.indent)
|
267 | catch err
|
268 | result = @ensureErrorType(err)
|
269 |
|
270 | # Complete
|
271 | return @complete(result, next)
|
272 |
|
273 | # Private: Not yet supported
|
274 | createCSString: (data, opts, next) ->
|
275 | # Prepare
|
276 | [opts, next] = extractOptsAndCallback(opts, next)
|
277 |
|
278 | # Stringify
|
279 | result = new Error('CSON.createCS: Creating CoffeeScript code is not yet supported')
|
280 |
|
281 | # Complete
|
282 | return @complete(result, next)
|
283 |
|
284 | ###
|
285 | Potentially we could use something like the following from CSON v1
|
286 | However the JSON.stringify gets rid of functions...
|
287 | which is the entire point of the coffeescript mode over the CSON mode...
|
288 | So until we figure out how to toString() an object and keep the functions intact,
|
289 | unsafe stringifying to CSON or CS or JS won't happen.
|
290 |
|
291 | Perhaps https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toSource
|
292 | will be of use one day
|
293 |
|
294 | src = "var result = #{JSON.stringify obj}"
|
295 | result = require('js2coffee').build(src, opts).code
|
296 | result = result.replace(/^\s*result\s*\=\s/, '')
|
297 | if /^\s/.test(result) is false
|
298 | result = result.trim()
|
299 | if typeof obj is 'object'
|
300 | unless Array.isArray(obj)
|
301 | result = '{'+result+'}' unless result is '{}'
|
302 | return result
|
303 | ###
|
304 |
|
305 |
|
306 | # Private: Not yet supported
|
307 | createJSString: (data, opts, next) ->
|
308 | # Prepare
|
309 | [opts, next] = extractOptsAndCallback(opts, next)
|
310 |
|
311 | # Stringify
|
312 | result = new Error('CSON.createJS: Creating JavaScript code is not yet supported')
|
313 |
|
314 | # Complete
|
315 | return @complete(result, next)
|
316 |
|
317 |
|
318 | # ====================================
|
319 | # Parsing Strings to Objects
|
320 |
|
321 | # Public: Parses a JSON {String} into an {Object}
|
322 | #
|
323 | # data - The JSON {String} to parse
|
324 | # opts - {Object} The options, unused
|
325 | #
|
326 | # Returns {Object} or {Error}
|
327 | parseJSONString: (data, opts, next) ->
|
328 | # Prepare
|
329 | [opts, next] = extractOptsAndCallback(opts, next)
|
330 |
|
331 | # Parse
|
332 | try
|
333 | result = JSON.parse(data)
|
334 | catch err
|
335 | result = @ensureErrorType(err)
|
336 |
|
337 | # Complete
|
338 | return @complete(result, next)
|
339 |
|
340 | # Public: Parses a CSON {String} into an {Object}
|
341 | #
|
342 | # data - The CSON {String} to parse
|
343 | # opts - {Object} The options, unused
|
344 | #
|
345 | # Returns {Object} or {Error}
|
346 | parseCSONString: (data, opts, next) ->
|
347 | # Prepare
|
348 | [opts, next] = extractOptsAndCallback(opts, next)
|
349 |
|
350 | # Parse
|
351 | try
|
352 | result = require('cson-parser').parse(data)
|
353 | catch err
|
354 | result = @ensureErrorType(err)
|
355 |
|
356 | # Complete
|
357 | return @complete(result, next)
|
358 |
|
359 | # Public: Parses a JavaScript {String} into an {Object}
|
360 | #
|
361 | # data - The JavaScript {String} to parse
|
362 | # opts - {Object} The options (also passed to require('vm').runInNewContex)
|
363 | # :context - {Object} The context option that is used in `require('vm').runInNewContext`, defaults to an empty object `{}`
|
364 | #
|
365 | # Returns {Object} or {Error}
|
366 | parseJSString: (data, opts, next) ->
|
367 | # Prepare
|
368 | [opts, next] = extractOptsAndCallback(opts, next)
|
369 | opts = @getOptions(opts)
|
370 | opts.context ?= {}
|
371 |
|
372 | # Parse
|
373 | try
|
374 | result = require('vm').runInNewContext(data, opts.context, opts)
|
375 | catch err
|
376 | result = @ensureErrorType(err)
|
377 |
|
378 | # Complete
|
379 | return @complete(result, next)
|
380 |
|
381 | # Public: Parses a CoffeeScript {String} into an {Object}
|
382 | #
|
383 | # data - The CoffeeScript {String} to parse
|
384 | # opts - {Object} The options, forwarded onto `require('coffeescript').eval`
|
385 | #
|
386 | # Returns {Object} or {Error}
|
387 | parseCSString: (data, opts, next) ->
|
388 | # Prepare
|
389 | [opts, next] = extractOptsAndCallback(opts, next)
|
390 | opts = @getOptions(opts)
|
391 |
|
392 | # Parse
|
393 | try
|
394 | result = require('coffeescript').eval(data, opts)
|
395 | catch err
|
396 | result = @ensureErrorType(err)
|
397 |
|
398 | # Complete
|
399 | return @complete(result, next)
|
400 |
|
401 |
|
402 | # ====================================
|
403 | # Parsing Files to Objects
|
404 |
|
405 | # Public: Parses a JSON file into an {Object}
|
406 | #
|
407 | # data - {String} The file path to parse
|
408 | # opts - {Object} The options, forwarded onto {::parseJSONString}
|
409 | #
|
410 | # Returns {Object} or {Error}
|
411 | parseJSONFile: (file, opts, next) ->
|
412 | # Prepare
|
413 | [opts, next] = extractOptsAndCallback(opts, next)
|
414 |
|
415 | # Parse
|
416 | result = fsUtil.readFileSync(file)
|
417 | if result instanceof Error
|
418 | result = result
|
419 | else
|
420 | result = @parseJSONString(result.toString(), opts)
|
421 |
|
422 | # Complete
|
423 | return @complete(result, next)
|
424 |
|
425 | # Public: Parses a CSON file into an {Object}
|
426 | #
|
427 | # data - {String} The file path to parse
|
428 | # opts - {Object} The options, forwarded onto {::parseCSONString}
|
429 | #
|
430 | # Returns {Object} or {Error}
|
431 | parseCSONFile: (file, opts, next) ->
|
432 | # Prepare
|
433 | [opts, next] = extractOptsAndCallback(opts, next)
|
434 |
|
435 | # Parse
|
436 | result = fsUtil.readFileSync(file)
|
437 | if result instanceof Error
|
438 | result = result
|
439 | else
|
440 | result = @parseCSONString(result.toString(), opts)
|
441 |
|
442 | # Complete
|
443 | return @complete(result, next)
|
444 |
|
445 | # Public: Parses a JAvaScript file into an {Object}
|
446 | #
|
447 | # data - {String} The file path to parse
|
448 | # opts - {Object} The options, forwarded onto {::parseJSString}
|
449 | #
|
450 | # Returns {Object} or {Error}
|
451 | parseJSFile: (file, opts, next) ->
|
452 | # Prepare
|
453 | [opts, next] = extractOptsAndCallback(opts, next)
|
454 |
|
455 | # Parse
|
456 | result = fsUtil.readFileSync(file)
|
457 | if result instanceof Error
|
458 | result = result
|
459 | else
|
460 | result = @parseJSString(result.toString(), opts)
|
461 |
|
462 | # Complete
|
463 | return @complete(result, next)
|
464 |
|
465 | # Public: Parses a CoffeeScript file into an {Object}
|
466 | #
|
467 | # data - {String} The file path to parse
|
468 | # opts - {Object} The options, forwarded onto {::parseCSString}
|
469 | #
|
470 | # Returns {Object} or {Error}
|
471 | parseCSFile: (file, opts, next) ->
|
472 | # Prepare
|
473 | [opts, next] = extractOptsAndCallback(opts, next)
|
474 |
|
475 | # Parse
|
476 | result = fsUtil.readFileSync(file)
|
477 | if result instanceof Error
|
478 | result = result
|
479 | else
|
480 | result = @parseCSString(result.toString(), opts)
|
481 |
|
482 | # Complete
|
483 | return @complete(result, next)
|
484 |
|
485 |
|
486 |
|
487 | # ====================================
|
488 | # Requiring Files to Objects
|
489 |
|
490 | # Public: {Delegates to: .parseJSONFile}
|
491 | requireJSONFile: (file, opts, next) ->
|
492 | # Prepare
|
493 | [opts, next] = extractOptsAndCallback(opts, next)
|
494 |
|
495 | # Require
|
496 | result = @parseJSONFile(file, opts)
|
497 |
|
498 | # Complete
|
499 | return @complete(result, next)
|
500 |
|
501 | # Public: {Delegates to: .parseCSONFile}
|
502 | requireCSONFile: (file, opts, next) ->
|
503 | # Prepare
|
504 | [opts, next] = extractOptsAndCallback(opts, next)
|
505 |
|
506 | # Require
|
507 | result = @parseCSONFile(file, opts)
|
508 |
|
509 | # Complete
|
510 | return @complete(result, next)
|
511 |
|
512 | # Public: Requires a JavaScript file and returns the result {Object}
|
513 | #
|
514 | # data - {String} The file path to require
|
515 | # opts - {Object} The options, unused
|
516 | #
|
517 | # Returns {Object} or {Error}
|
518 | requireJSFile: (file, opts, next) ->
|
519 | # Prepare
|
520 | [opts, next] = extractOptsAndCallback(opts, next)
|
521 |
|
522 | # Require
|
523 | try
|
524 | result = requireFresh(file)
|
525 | catch err
|
526 | result = @ensureErrorType(err)
|
527 |
|
528 | # Complete
|
529 | return @complete(result, next)
|
530 |
|
531 | # Public: Requires a CoffeeScript file and returns the result {Object}
|
532 | #
|
533 | # data - {String} The file path to require
|
534 | # opts - {Object} The options, unused
|
535 | #
|
536 | # Returns {Object} or {Error}
|
537 | requireCSFile: (file, opts, next) ->
|
538 | # Prepare
|
539 | [opts, next] = extractOptsAndCallback(opts, next)
|
540 |
|
541 | # Require
|
542 | require('coffeescript/register')
|
543 | try
|
544 | result = requireFresh(file)
|
545 | catch err
|
546 | result = @ensureErrorType(err)
|
547 |
|
548 | # Complete
|
549 | return @complete(result, next)
|
550 |
|
551 | # Export
|
552 | module.exports = new CSON()
|