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
|