scratch – Blame information for rev 75

Subversion Repositories:
Rev:
Rev Author Line No. Line
75 office 1  
2 Pattern = require './Pattern'
3 Unescaper = require './Unescaper'
4 Escaper = require './Escaper'
5 Utils = require './Utils'
6 ParseException = require './Exception/ParseException'
7 DumpException = require './Exception/DumpException'
8  
9 # Inline YAML parsing and dumping
10 class Inline
11  
12 # Quoted string regular expression
13 @REGEX_QUOTED_STRING: '(?:"(?:[^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'(?:[^\']*(?:\'\'[^\']*)*)\')'
14  
15 # Pre-compiled patterns
16 #
17 @PATTERN_TRAILING_COMMENTS: new Pattern '^\\s*#.*$'
18 @PATTERN_QUOTED_SCALAR: new Pattern '^'+@REGEX_QUOTED_STRING
19 @PATTERN_THOUSAND_NUMERIC_SCALAR: new Pattern '^(-|\\+)?[0-9,]+(\\.[0-9]+)?$'
20 @PATTERN_SCALAR_BY_DELIMITERS: {}
21  
22 # Settings
23 @settings: {}
24  
25  
26 # Configure YAML inline.
27 #
28 # @param [Boolean] exceptionOnInvalidType true if an exception must be thrown on invalid types (a JavaScript resource or object), false otherwise
29 # @param [Function] objectDecoder A function to deserialize custom objects, null otherwise
30 #
31 @configure: (exceptionOnInvalidType = null, objectDecoder = null) ->
32 # Update settings
33 @settings.exceptionOnInvalidType = exceptionOnInvalidType
34 @settings.objectDecoder = objectDecoder
35 return
36  
37  
38 # Converts a YAML string to a JavaScript object.
39 #
40 # @param [String] value A YAML string
41 # @param [Boolean] exceptionOnInvalidType true if an exception must be thrown on invalid types (a JavaScript resource or object), false otherwise
42 # @param [Function] objectDecoder A function to deserialize custom objects, null otherwise
43 #
44 # @return [Object] A JavaScript object representing the YAML string
45 #
46 # @throw [ParseException]
47 #
48 @parse: (value, exceptionOnInvalidType = false, objectDecoder = null) ->
49 # Update settings from last call of Inline.parse()
50 @settings.exceptionOnInvalidType = exceptionOnInvalidType
51 @settings.objectDecoder = objectDecoder
52  
53 if not value?
54 return ''
55  
56 value = Utils.trim value
57  
58 if 0 is value.length
59 return ''
60  
61 # Keep a context object to pass through static methods
62 context = {exceptionOnInvalidType, objectDecoder, i: 0}
63  
64 switch value.charAt(0)
65 when '['
66 result = @parseSequence value, context
67 ++context.i
68 when '{'
69 result = @parseMapping value, context
70 ++context.i
71 else
72 result = @parseScalar value, null, ['"', "'"], context
73  
74 # Some comments are allowed at the end
75 if @PATTERN_TRAILING_COMMENTS.replace(value[context.i..], '') isnt ''
76 throw new ParseException 'Unexpected characters near "'+value[context.i..]+'".'
77  
78 return result
79  
80  
81 # Dumps a given JavaScript variable to a YAML string.
82 #
83 # @param [Object] value The JavaScript variable to convert
84 # @param [Boolean] exceptionOnInvalidType true if an exception must be thrown on invalid types (a JavaScript resource or object), false otherwise
85 # @param [Function] objectEncoder A function to serialize custom objects, null otherwise
86 #
87 # @return [String] The YAML string representing the JavaScript object
88 #
89 # @throw [DumpException]
90 #
91 @dump: (value, exceptionOnInvalidType = false, objectEncoder = null) ->
92 if not value?
93 return 'null'
94 type = typeof value
95 if type is 'object'
96 if value instanceof Date
97 return value.toISOString()
98 else if objectEncoder?
99 result = objectEncoder value
100 if typeof result is 'string' or result?
101 return result
102 return @dumpObject value
103 if type is 'boolean'
104 return (if value then 'true' else 'false')
105 if Utils.isDigits(value)
106 return (if type is 'string' then "'"+value+"'" else String(parseInt(value)))
107 if Utils.isNumeric(value)
108 return (if type is 'string' then "'"+value+"'" else String(parseFloat(value)))
109 if type is 'number'
110 return (if value is Infinity then '.Inf' else (if value is -Infinity then '-.Inf' else (if isNaN(value) then '.NaN' else value)))
111 if Escaper.requiresDoubleQuoting value
112 return Escaper.escapeWithDoubleQuotes value
113 if Escaper.requiresSingleQuoting value
114 return Escaper.escapeWithSingleQuotes value
115 if '' is value
116 return '""'
117 if Utils.PATTERN_DATE.test value
118 return "'"+value+"'";
119 if value.toLowerCase() in ['null','~','true','false']
120 return "'"+value+"'"
121 # Default
122 return value;
123  
124  
125 # Dumps a JavaScript object to a YAML string.
126 #
127 # @param [Object] value The JavaScript object to dump
128 # @param [Boolean] exceptionOnInvalidType true if an exception must be thrown on invalid types (a JavaScript resource or object), false otherwise
129 # @param [Function] objectEncoder A function do serialize custom objects, null otherwise
130 #
131 # @return string The YAML string representing the JavaScript object
132 #
133 @dumpObject: (value, exceptionOnInvalidType, objectSupport = null) ->
134 # Array
135 if value instanceof Array
136 output = []
137 for val in value
138 output.push @dump val
139 return '['+output.join(', ')+']'
140  
141 # Mapping
142 else
143 output = []
144 for key, val of value
145 output.push @dump(key)+': '+@dump(val)
146 return '{'+output.join(', ')+'}'
147  
148  
149 # Parses a scalar to a YAML string.
150 #
151 # @param [Object] scalar
152 # @param [Array] delimiters
153 # @param [Array] stringDelimiters
154 # @param [Object] context
155 # @param [Boolean] evaluate
156 #
157 # @return [String] A YAML string
158 #
159 # @throw [ParseException] When malformed inline YAML string is parsed
160 #
161 @parseScalar: (scalar, delimiters = null, stringDelimiters = ['"', "'"], context = null, evaluate = true) ->
162 unless context?
163 context = exceptionOnInvalidType: @settings.exceptionOnInvalidType, objectDecoder: @settings.objectDecoder, i: 0
164 {i} = context
165  
166 if scalar.charAt(i) in stringDelimiters
167 # Quoted scalar
168 output = @parseQuotedScalar scalar, context
169 {i} = context
170  
171 if delimiters?
172 tmp = Utils.ltrim scalar[i..], ' '
173 if not(tmp.charAt(0) in delimiters)
174 throw new ParseException 'Unexpected characters ('+scalar[i..]+').'
175  
176 else
177 # "normal" string
178 if not delimiters
179 output = scalar[i..]
180 i += output.length
181  
182 # Remove comments
183 strpos = output.indexOf ' #'
184 if strpos isnt -1
185 output = Utils.rtrim output[0...strpos]
186  
187 else
188 joinedDelimiters = delimiters.join('|')
189 pattern = @PATTERN_SCALAR_BY_DELIMITERS[joinedDelimiters]
190 unless pattern?
191 pattern = new Pattern '^(.+?)('+joinedDelimiters+')'
192 @PATTERN_SCALAR_BY_DELIMITERS[joinedDelimiters] = pattern
193 if match = pattern.exec scalar[i..]
194 output = match[1]
195 i += output.length
196 else
197 throw new ParseException 'Malformed inline YAML string ('+scalar+').'
198  
199  
200 if evaluate
201 output = @evaluateScalar output, context
202  
203 context.i = i
204 return output
205  
206  
207 # Parses a quoted scalar to YAML.
208 #
209 # @param [String] scalar
210 # @param [Object] context
211 #
212 # @return [String] A YAML string
213 #
214 # @throw [ParseException] When malformed inline YAML string is parsed
215 #
216 @parseQuotedScalar: (scalar, context) ->
217 {i} = context
218  
219 unless match = @PATTERN_QUOTED_SCALAR.exec scalar[i..]
220 throw new ParseException 'Malformed inline YAML string ('+scalar[i..]+').'
221  
222 output = match[0].substr(1, match[0].length - 2)
223  
224 if '"' is scalar.charAt(i)
225 output = Unescaper.unescapeDoubleQuotedString output
226 else
227 output = Unescaper.unescapeSingleQuotedString output
228  
229 i += match[0].length
230  
231 context.i = i
232 return output
233  
234  
235 # Parses a sequence to a YAML string.
236 #
237 # @param [String] sequence
238 # @param [Object] context
239 #
240 # @return [String] A YAML string
241 #
242 # @throw [ParseException] When malformed inline YAML string is parsed
243 #
244 @parseSequence: (sequence, context) ->
245 output = []
246 len = sequence.length
247 {i} = context
248 i += 1
249  
250 # [foo, bar, ...]
251 while i < len
252 context.i = i
253 switch sequence.charAt(i)
254 when '['
255 # Nested sequence
256 output.push @parseSequence sequence, context
257 {i} = context
258 when '{'
259 # Nested mapping
260 output.push @parseMapping sequence, context
261 {i} = context
262 when ']'
263 return output
264 when ',', ' ', "\n"
265 # Do nothing
266 else
267 isQuoted = (sequence.charAt(i) in ['"', "'"])
268 value = @parseScalar sequence, [',', ']'], ['"', "'"], context
269 {i} = context
270  
271 if not(isQuoted) and typeof(value) is 'string' and (value.indexOf(': ') isnt -1 or value.indexOf(":\n") isnt -1)
272 # Embedded mapping?
273 try
274 value = @parseMapping '{'+value+'}'
275 catch e
276 # No, it's not
277  
278  
279 output.push value
280  
281 --i
282  
283 ++i
284  
285 throw new ParseException 'Malformed inline YAML string '+sequence
286  
287  
288 # Parses a mapping to a YAML string.
289 #
290 # @param [String] mapping
291 # @param [Object] context
292 #
293 # @return [String] A YAML string
294 #
295 # @throw [ParseException] When malformed inline YAML string is parsed
296 #
297 @parseMapping: (mapping, context) ->
298 output = {}
299 len = mapping.length
300 {i} = context
301 i += 1
302  
303 # {foo: bar, bar:foo, ...}
304 shouldContinueWhileLoop = false
305 while i < len
306 context.i = i
307 switch mapping.charAt(i)
308 when ' ', ',', "\n"
309 ++i
310 context.i = i
311 shouldContinueWhileLoop = true
312 when '}'
313 return output
314  
315 if shouldContinueWhileLoop
316 shouldContinueWhileLoop = false
317 continue
318  
319 # Key
320 key = @parseScalar mapping, [':', ' ', "\n"], ['"', "'"], context, false
321 {i} = context
322  
323 # Value
324 done = false
325  
326 while i < len
327 context.i = i
328 switch mapping.charAt(i)
329 when '['
330 # Nested sequence
331 value = @parseSequence mapping, context
332 {i} = context
333 # Spec: Keys MUST be unique; first one wins.
334 # Parser cannot abort this mapping earlier, since lines
335 # are processed sequentially.
336 if output[key] == undefined
337 output[key] = value
338 done = true
339 when '{'
340 # Nested mapping
341 value = @parseMapping mapping, context
342 {i} = context
343 # Spec: Keys MUST be unique; first one wins.
344 # Parser cannot abort this mapping earlier, since lines
345 # are processed sequentially.
346 if output[key] == undefined
347 output[key] = value
348 done = true
349 when ':', ' ', "\n"
350 # Do nothing
351 else
352 value = @parseScalar mapping, [',', '}'], ['"', "'"], context
353 {i} = context
354 # Spec: Keys MUST be unique; first one wins.
355 # Parser cannot abort this mapping earlier, since lines
356 # are processed sequentially.
357 if output[key] == undefined
358 output[key] = value
359 done = true
360 --i
361  
362 ++i
363  
364 if done
365 break
366  
367 throw new ParseException 'Malformed inline YAML string '+mapping
368  
369  
370 # Evaluates scalars and replaces magic values.
371 #
372 # @param [String] scalar
373 #
374 # @return [String] A YAML string
375 #
376 @evaluateScalar: (scalar, context) ->
377 scalar = Utils.trim(scalar)
378 scalarLower = scalar.toLowerCase()
379  
380 switch scalarLower
381 when 'null', '', '~'
382 return null
383 when 'true'
384 return true
385 when 'false'
386 return false
387 when '.inf'
388 return Infinity
389 when '.nan'
390 return NaN
391 when '-.inf'
392 return Infinity
393 else
394 firstChar = scalarLower.charAt(0)
395 switch firstChar
396 when '!'
397 firstSpace = scalar.indexOf(' ')
398 if firstSpace is -1
399 firstWord = scalarLower
400 else
401 firstWord = scalarLower[0...firstSpace]
402 switch firstWord
403 when '!'
404 if firstSpace isnt -1
405 return parseInt @parseScalar(scalar[2..])
406 return null
407 when '!str'
408 return Utils.ltrim scalar[4..]
409 when '!!str'
410 return Utils.ltrim scalar[5..]
411 when '!!int'
412 return parseInt(@parseScalar(scalar[5..]))
413 when '!!bool'
414 return Utils.parseBoolean(@parseScalar(scalar[6..]), false)
415 when '!!float'
416 return parseFloat(@parseScalar(scalar[7..]))
417 when '!!timestamp'
418 return Utils.stringToDate(Utils.ltrim(scalar[11..]))
419 else
420 unless context?
421 context = exceptionOnInvalidType: @settings.exceptionOnInvalidType, objectDecoder: @settings.objectDecoder, i: 0
422 {objectDecoder, exceptionOnInvalidType} = context
423  
424 if objectDecoder
425 # If objectDecoder function is given, we can do custom decoding of custom types
426 trimmedScalar = Utils.rtrim scalar
427 firstSpace = trimmedScalar.indexOf(' ')
428 if firstSpace is -1
429 return objectDecoder trimmedScalar, null
430 else
431 subValue = Utils.ltrim trimmedScalar[firstSpace+1..]
432 unless subValue.length > 0
433 subValue = null
434 return objectDecoder trimmedScalar[0...firstSpace], subValue
435  
436 if exceptionOnInvalidType
437 throw new ParseException 'Custom object support when parsing a YAML file has been disabled.'
438  
439 return null
440 when '0'
441 if '0x' is scalar[0...2]
442 return Utils.hexDec scalar
443 else if Utils.isDigits scalar
444 return Utils.octDec scalar
445 else if Utils.isNumeric scalar
446 return parseFloat scalar
447 else
448 return scalar
449 when '+'
450 if Utils.isDigits scalar
451 raw = scalar
452 cast = parseInt(raw)
453 if raw is String(cast)
454 return cast
455 else
456 return raw
457 else if Utils.isNumeric scalar
458 return parseFloat scalar
459 else if @PATTERN_THOUSAND_NUMERIC_SCALAR.test scalar
460 return parseFloat(scalar.replace(',', ''))
461 return scalar
462 when '-'
463 if Utils.isDigits(scalar[1..])
464 if '0' is scalar.charAt(1)
465 return -Utils.octDec(scalar[1..])
466 else
467 raw = scalar[1..]
468 cast = parseInt(raw)
469 if raw is String(cast)
470 return -cast
471 else
472 return -raw
473 else if Utils.isNumeric scalar
474 return parseFloat scalar
475 else if @PATTERN_THOUSAND_NUMERIC_SCALAR.test scalar
476 return parseFloat(scalar.replace(',', ''))
477 return scalar
478 else
479 if date = Utils.stringToDate(scalar)
480 return date
481 else if Utils.isNumeric(scalar)
482 return parseFloat scalar
483 else if @PATTERN_THOUSAND_NUMERIC_SCALAR.test scalar
484 return parseFloat(scalar.replace(',', ''))
485 return scalar
486  
487 module.exports = Inline