scratch – Blame information for rev 125

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