scratch – Blame information for rev 125

Subversion Repositories:
Rev:
Rev Author Line No. Line
75 office 1  
2 Inline = require './Inline'
3 Pattern = require './Pattern'
4 Utils = require './Utils'
5 ParseException = require './Exception/ParseException'
125 office 6 ParseMore = require './Exception/ParseMore'
75 office 7  
8 # Parser parses YAML strings to convert them to JavaScript objects.
9 #
10 class Parser
11  
12 # Pre-compiled patterns
13 #
14 PATTERN_FOLDED_SCALAR_ALL: new Pattern '^(?:(?<type>![^\\|>]*)\\s+)?(?<separator>\\||>)(?<modifiers>\\+|\\-|\\d+|\\+\\d+|\\-\\d+|\\d+\\+|\\d+\\-)?(?<comments> +#.*)?$'
15 PATTERN_FOLDED_SCALAR_END: new Pattern '(?<separator>\\||>)(?<modifiers>\\+|\\-|\\d+|\\+\\d+|\\-\\d+|\\d+\\+|\\d+\\-)?(?<comments> +#.*)?$'
16 PATTERN_SEQUENCE_ITEM: new Pattern '^\\-((?<leadspaces>\\s+)(?<value>.+?))?\\s*$'
17 PATTERN_ANCHOR_VALUE: new Pattern '^&(?<ref>[^ ]+) *(?<value>.*)'
18 PATTERN_COMPACT_NOTATION: new Pattern '^(?<key>'+Inline.REGEX_QUOTED_STRING+'|[^ \'"\\{\\[].*?) *\\:(\\s+(?<value>.+?))?\\s*$'
19 PATTERN_MAPPING_ITEM: new Pattern '^(?<key>'+Inline.REGEX_QUOTED_STRING+'|[^ \'"\\[\\{].*?) *\\:(\\s+(?<value>.+?))?\\s*$'
20 PATTERN_DECIMAL: new Pattern '\\d+'
21 PATTERN_INDENT_SPACES: new Pattern '^ +'
22 PATTERN_TRAILING_LINES: new Pattern '(\n*)$'
125 office 23 PATTERN_YAML_HEADER: new Pattern '^\\%YAML[: ][\\d\\.]+.*\n', 'm'
24 PATTERN_LEADING_COMMENTS: new Pattern '^(\\#.*?\n)+', 'm'
25 PATTERN_DOCUMENT_MARKER_START: new Pattern '^\\-\\-\\-.*?\n', 'm'
26 PATTERN_DOCUMENT_MARKER_END: new Pattern '^\\.\\.\\.\\s*$', 'm'
75 office 27 PATTERN_FOLDED_SCALAR_BY_INDENTATION: {}
28  
29 # Context types
30 #
31 CONTEXT_NONE: 0
32 CONTEXT_SEQUENCE: 1
33 CONTEXT_MAPPING: 2
34  
35  
36 # Constructor
37 #
38 # @param [Integer] offset The offset of YAML document (used for line numbers in error messages)
39 #
40 constructor: (@offset = 0) ->
41 @lines = []
42 @currentLineNb = -1
43 @currentLine = ''
44 @refs = {}
45  
46  
47 # Parses a YAML string to a JavaScript value.
48 #
49 # @param [String] value A YAML string
50 # @param [Boolean] exceptionOnInvalidType true if an exception must be thrown on invalid types (a JavaScript resource or object), false otherwise
51 # @param [Function] objectDecoder A function to deserialize custom objects, null otherwise
52 #
53 # @return [Object] A JavaScript value
54 #
55 # @throw [ParseException] If the YAML is not valid
56 #
57 parse: (value, exceptionOnInvalidType = false, objectDecoder = null) ->
58 @currentLineNb = -1
59 @currentLine = ''
60 @lines = @cleanup(value).split "\n"
61  
62 data = null
63 context = @CONTEXT_NONE
64 allowOverwrite = false
65 while @moveToNextLine()
66 if @isCurrentLineEmpty()
67 continue
68  
69 # Tab?
70 if "\t" is @currentLine[0]
71 throw new ParseException 'A YAML file cannot contain tabs as indentation.', @getRealCurrentLineNb() + 1, @currentLine
72  
73 isRef = mergeNode = false
74 if values = @PATTERN_SEQUENCE_ITEM.exec @currentLine
75 if @CONTEXT_MAPPING is context
76 throw new ParseException 'You cannot define a sequence item when in a mapping'
77 context = @CONTEXT_SEQUENCE
78 data ?= []
79  
80 if values.value? and matches = @PATTERN_ANCHOR_VALUE.exec values.value
81 isRef = matches.ref
82 values.value = matches.value
83  
84 # Array
85 if not(values.value?) or '' is Utils.trim(values.value, ' ') or Utils.ltrim(values.value, ' ').indexOf('#') is 0
86 if @currentLineNb < @lines.length - 1 and not @isNextLineUnIndentedCollection()
87 c = @getRealCurrentLineNb() + 1
88 parser = new Parser c
89 parser.refs = @refs
90 data.push parser.parse(@getNextEmbedBlock(null, true), exceptionOnInvalidType, objectDecoder)
91 else
92 data.push null
93  
94 else
95 if values.leadspaces?.length and matches = @PATTERN_COMPACT_NOTATION.exec values.value
96  
97 # This is a compact notation element, add to next block and parse
98 c = @getRealCurrentLineNb()
99 parser = new Parser c
100 parser.refs = @refs
101  
102 block = values.value
103 indent = @getCurrentLineIndentation()
104 if @isNextLineIndented(false)
105 block += "\n"+@getNextEmbedBlock(indent + values.leadspaces.length + 1, true)
106  
107 data.push parser.parse block, exceptionOnInvalidType, objectDecoder
108  
109 else
110 data.push @parseValue values.value, exceptionOnInvalidType, objectDecoder
111  
112 else if (values = @PATTERN_MAPPING_ITEM.exec @currentLine) and values.key.indexOf(' #') is -1
113 if @CONTEXT_SEQUENCE is context
114 throw new ParseException 'You cannot define a mapping item when in a sequence'
115 context = @CONTEXT_MAPPING
116 data ?= {}
117  
118 # Force correct settings
119 Inline.configure exceptionOnInvalidType, objectDecoder
120 try
121 key = Inline.parseScalar values.key
122 catch e
123 e.parsedLine = @getRealCurrentLineNb() + 1
124 e.snippet = @currentLine
125  
126 throw e
127  
128 if '<<' is key
129 mergeNode = true
130 allowOverwrite = true
131 if values.value?.indexOf('*') is 0
132 refName = values.value[1..]
133 unless @refs[refName]?
134 throw new ParseException 'Reference "'+refName+'" does not exist.', @getRealCurrentLineNb() + 1, @currentLine
135  
136 refValue = @refs[refName]
137  
138 if typeof refValue isnt 'object'
139 throw new ParseException 'YAML merge keys used with a scalar value instead of an object.', @getRealCurrentLineNb() + 1, @currentLine
140  
141 if refValue instanceof Array
142 # Merge array with object
143 for value, i in refValue
144 data[String(i)] ?= value
145 else
146 # Merge objects
147 for key, value of refValue
148 data[key] ?= value
149  
150 else
151 if values.value? and values.value isnt ''
152 value = values.value
153 else
154 value = @getNextEmbedBlock()
155  
156 c = @getRealCurrentLineNb() + 1
157 parser = new Parser c
158 parser.refs = @refs
159 parsed = parser.parse value, exceptionOnInvalidType
160  
161 unless typeof parsed is 'object'
162 throw new ParseException 'YAML merge keys used with a scalar value instead of an object.', @getRealCurrentLineNb() + 1, @currentLine
163  
164 if parsed instanceof Array
165 # If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
166 # and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
167 # in the sequence override keys specified in later mapping nodes.
168 for parsedItem in parsed
169 unless typeof parsedItem is 'object'
170 throw new ParseException 'Merge items must be objects.', @getRealCurrentLineNb() + 1, parsedItem
171  
172 if parsedItem instanceof Array
173 # Merge array with object
174 for value, i in parsedItem
175 k = String(i)
176 unless data.hasOwnProperty(k)
177 data[k] = value
178 else
179 # Merge objects
180 for key, value of parsedItem
181 unless data.hasOwnProperty(key)
182 data[key] = value
183  
184 else
185 # If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
186 # current mapping, unless the key already exists in it.
187 for key, value of parsed
188 unless data.hasOwnProperty(key)
189 data[key] = value
190  
191 else if values.value? and matches = @PATTERN_ANCHOR_VALUE.exec values.value
192 isRef = matches.ref
193 values.value = matches.value
194  
195  
196 if mergeNode
197 # Merge keys
198 else if not(values.value?) or '' is Utils.trim(values.value, ' ') or Utils.ltrim(values.value, ' ').indexOf('#') is 0
199 # Hash
200 # if next line is less indented or equal, then it means that the current value is null
201 if not(@isNextLineIndented()) and not(@isNextLineUnIndentedCollection())
202 # Spec: Keys MUST be unique; first one wins.
203 # But overwriting is allowed when a merge node is used in current block.
204 if allowOverwrite or data[key] is undefined
205 data[key] = null
206  
207 else
208 c = @getRealCurrentLineNb() + 1
209 parser = new Parser c
210 parser.refs = @refs
211 val = parser.parse @getNextEmbedBlock(), exceptionOnInvalidType, objectDecoder
212  
213 # Spec: Keys MUST be unique; first one wins.
214 # But overwriting is allowed when a merge node is used in current block.
215 if allowOverwrite or data[key] is undefined
216 data[key] = val
217  
218 else
219 val = @parseValue values.value, exceptionOnInvalidType, objectDecoder
220  
221 # Spec: Keys MUST be unique; first one wins.
222 # But overwriting is allowed when a merge node is used in current block.
223 if allowOverwrite or data[key] is undefined
224 data[key] = val
225  
226 else
227 # 1-liner optionally followed by newline
228 lineCount = @lines.length
229 if 1 is lineCount or (2 is lineCount and Utils.isEmpty(@lines[1]))
230 try
231 value = Inline.parse @lines[0], exceptionOnInvalidType, objectDecoder
232 catch e
233 e.parsedLine = @getRealCurrentLineNb() + 1
234 e.snippet = @currentLine
235  
236 throw e
237  
238 if typeof value is 'object'
239 if value instanceof Array
240 first = value[0]
241 else
242 for key of value
243 first = value[key]
244 break
245  
246 if typeof first is 'string' and first.indexOf('*') is 0
247 data = []
248 for alias in value
249 data.push @refs[alias[1..]]
250 value = data
251  
252 return value
253  
254 else if Utils.ltrim(value).charAt(0) in ['[', '{']
255 try
256 return Inline.parse value, exceptionOnInvalidType, objectDecoder
257 catch e
258 e.parsedLine = @getRealCurrentLineNb() + 1
259 e.snippet = @currentLine
260  
261 throw e
262  
263 throw new ParseException 'Unable to parse.', @getRealCurrentLineNb() + 1, @currentLine
264  
265 if isRef
266 if data instanceof Array
267 @refs[isRef] = data[data.length-1]
268 else
269 lastKey = null
270 for key of data
271 lastKey = key
272 @refs[isRef] = data[lastKey]
273  
274  
275 if Utils.isEmpty(data)
276 return null
277 else
278 return data
279  
280  
281  
282 # Returns the current line number (takes the offset into account).
283 #
284 # @return [Integer] The current line number
285 #
286 getRealCurrentLineNb: ->
287 return @currentLineNb + @offset
288  
289  
290 # Returns the current line indentation.
291 #
292 # @return [Integer] The current line indentation
293 #
294 getCurrentLineIndentation: ->
295 return @currentLine.length - Utils.ltrim(@currentLine, ' ').length
296  
297  
298 # Returns the next embed block of YAML.
299 #
300 # @param [Integer] indentation The indent level at which the block is to be read, or null for default
301 #
302 # @return [String] A YAML string
303 #
304 # @throw [ParseException] When indentation problem are detected
305 #
306 getNextEmbedBlock: (indentation = null, includeUnindentedCollection = false) ->
307 @moveToNextLine()
308  
309 if not indentation?
310 newIndent = @getCurrentLineIndentation()
311  
312 unindentedEmbedBlock = @isStringUnIndentedCollectionItem @currentLine
313  
314 if not(@isCurrentLineEmpty()) and 0 is newIndent and not(unindentedEmbedBlock)
315 throw new ParseException 'Indentation problem.', @getRealCurrentLineNb() + 1, @currentLine
316  
317 else
318 newIndent = indentation
319  
320  
321 data = [@currentLine[newIndent..]]
322  
323 unless includeUnindentedCollection
324 isItUnindentedCollection = @isStringUnIndentedCollectionItem @currentLine
325  
326 # Comments must not be removed inside a string block (ie. after a line ending with "|")
327 # They must not be removed inside a sub-embedded block as well
328 removeCommentsPattern = @PATTERN_FOLDED_SCALAR_END
329 removeComments = not removeCommentsPattern.test @currentLine
330  
331 while @moveToNextLine()
332 indent = @getCurrentLineIndentation()
333  
334 if indent is newIndent
335 removeComments = not removeCommentsPattern.test @currentLine
336  
125 office 337 if removeComments and @isCurrentLineComment()
338 continue
75 office 339  
340 if @isCurrentLineBlank()
341 data.push @currentLine[newIndent..]
342 continue
343  
125 office 344 if isItUnindentedCollection and not @isStringUnIndentedCollectionItem(@currentLine) and indent is newIndent
345 @moveToPreviousLine()
346 break
75 office 347  
348 if indent >= newIndent
349 data.push @currentLine[newIndent..]
350 else if Utils.ltrim(@currentLine).charAt(0) is '#'
351 # Don't add line with comments
352 else if 0 is indent
353 @moveToPreviousLine()
354 break
355 else
356 throw new ParseException 'Indentation problem.', @getRealCurrentLineNb() + 1, @currentLine
357  
358  
359 return data.join "\n"
360  
361  
362 # Moves the parser to the next line.
363 #
364 # @return [Boolean]
365 #
366 moveToNextLine: ->
367 if @currentLineNb >= @lines.length - 1
368 return false
369  
370 @currentLine = @lines[++@currentLineNb];
371  
372 return true
373  
374  
375 # Moves the parser to the previous line.
376 #
377 moveToPreviousLine: ->
378 @currentLine = @lines[--@currentLineNb]
379 return
380  
381  
382 # Parses a YAML value.
383 #
384 # @param [String] value A YAML value
385 # @param [Boolean] exceptionOnInvalidType true if an exception must be thrown on invalid types false otherwise
386 # @param [Function] objectDecoder A function to deserialize custom objects, null otherwise
387 #
388 # @return [Object] A JavaScript value
389 #
390 # @throw [ParseException] When reference does not exist
391 #
392 parseValue: (value, exceptionOnInvalidType, objectDecoder) ->
393 if 0 is value.indexOf('*')
394 pos = value.indexOf '#'
395 if pos isnt -1
396 value = value.substr(1, pos-2)
397 else
398 value = value[1..]
399  
400 if @refs[value] is undefined
401 throw new ParseException 'Reference "'+value+'" does not exist.', @currentLine
402  
403 return @refs[value]
404  
405  
406 if matches = @PATTERN_FOLDED_SCALAR_ALL.exec value
407 modifiers = matches.modifiers ? ''
408  
409 foldedIndent = Math.abs(parseInt(modifiers))
410 if isNaN(foldedIndent) then foldedIndent = 0
411 val = @parseFoldedScalar matches.separator, @PATTERN_DECIMAL.replace(modifiers, ''), foldedIndent
412 if matches.type?
413 # Force correct settings
414 Inline.configure exceptionOnInvalidType, objectDecoder
415 return Inline.parseScalar matches.type+' '+val
416 else
417 return val
418  
125 office 419 # Value can be multiline compact sequence or mapping or string
420 if value.charAt(0) in ['[', '{', '"', "'"]
421 while true
75 office 422 try
423 return Inline.parse value, exceptionOnInvalidType, objectDecoder
424 catch e
125 office 425 if e instanceof ParseMore and @moveToNextLine()
426 value += "\n" + Utils.trim(@currentLine, ' ')
427 else
428 e.parsedLine = @getRealCurrentLineNb() + 1
429 e.snippet = @currentLine
430 throw e
431 else
432 if @isNextLineIndented()
433 value += "\n" + @getNextEmbedBlock()
434 return Inline.parse value, exceptionOnInvalidType, objectDecoder
75 office 435  
436 return
437  
438  
439 # Parses a folded scalar.
440 #
441 # @param [String] separator The separator that was used to begin this folded scalar (| or >)
442 # @param [String] indicator The indicator that was used to begin this folded scalar (+ or -)
443 # @param [Integer] indentation The indentation that was used to begin this folded scalar
444 #
445 # @return [String] The text value
446 #
447 parseFoldedScalar: (separator, indicator = '', indentation = 0) ->
448 notEOF = @moveToNextLine()
449 if not notEOF
450 return ''
451  
452 isCurrentLineBlank = @isCurrentLineBlank()
453 text = ''
454  
455 # Leading blank lines are consumed before determining indentation
456 while notEOF and isCurrentLineBlank
457 # newline only if not EOF
458 if notEOF = @moveToNextLine()
459 text += "\n"
460 isCurrentLineBlank = @isCurrentLineBlank()
461  
462  
463 # Determine indentation if not specified
464 if 0 is indentation
465 if matches = @PATTERN_INDENT_SPACES.exec @currentLine
466 indentation = matches[0].length
467  
468  
469 if indentation > 0
470 pattern = @PATTERN_FOLDED_SCALAR_BY_INDENTATION[indentation]
471 unless pattern?
472 pattern = new Pattern '^ {'+indentation+'}(.*)$'
473 Parser::PATTERN_FOLDED_SCALAR_BY_INDENTATION[indentation] = pattern
474  
475 while notEOF and (isCurrentLineBlank or matches = pattern.exec @currentLine)
476 if isCurrentLineBlank
477 text += @currentLine[indentation..]
478 else
479 text += matches[1]
480  
481 # newline only if not EOF
482 if notEOF = @moveToNextLine()
483 text += "\n"
484 isCurrentLineBlank = @isCurrentLineBlank()
485  
486 else if notEOF
487 text += "\n"
488  
489  
490 if notEOF
491 @moveToPreviousLine()
492  
493  
494 # Remove line breaks of each lines except the empty and more indented ones
495 if '>' is separator
496 newText = ''
497 for line in text.split "\n"
498 if line.length is 0 or line.charAt(0) is ' '
499 newText = Utils.rtrim(newText, ' ') + line + "\n"
500 else
501 newText += line + ' '
502 text = newText
503  
504 if '+' isnt indicator
505 # Remove any extra space or new line as we are adding them after
506 text = Utils.rtrim(text)
507  
508 # Deal with trailing newlines as indicated
509 if '' is indicator
510 text = @PATTERN_TRAILING_LINES.replace text, "\n"
511 else if '-' is indicator
512 text = @PATTERN_TRAILING_LINES.replace text, ''
513  
514 return text
515  
516  
517 # Returns true if the next line is indented.
518 #
519 # @return [Boolean] Returns true if the next line is indented, false otherwise
520 #
521 isNextLineIndented: (ignoreComments = true) ->
522 currentIndentation = @getCurrentLineIndentation()
523 EOF = not @moveToNextLine()
524  
525 if ignoreComments
526 while not(EOF) and @isCurrentLineEmpty()
527 EOF = not @moveToNextLine()
528 else
529 while not(EOF) and @isCurrentLineBlank()
530 EOF = not @moveToNextLine()
531  
532 if EOF
533 return false
534  
535 ret = false
536 if @getCurrentLineIndentation() > currentIndentation
537 ret = true
538  
539 @moveToPreviousLine()
540  
541 return ret
542  
543  
544 # Returns true if the current line is blank or if it is a comment line.
545 #
546 # @return [Boolean] Returns true if the current line is empty or if it is a comment line, false otherwise
547 #
548 isCurrentLineEmpty: ->
549 trimmedLine = Utils.trim(@currentLine, ' ')
550 return trimmedLine.length is 0 or trimmedLine.charAt(0) is '#'
551  
552  
553 # Returns true if the current line is blank.
554 #
555 # @return [Boolean] Returns true if the current line is blank, false otherwise
556 #
557 isCurrentLineBlank: ->
558 return '' is Utils.trim(@currentLine, ' ')
559  
560  
561 # Returns true if the current line is a comment line.
562 #
563 # @return [Boolean] Returns true if the current line is a comment line, false otherwise
564 #
565 isCurrentLineComment: ->
566 # Checking explicitly the first char of the trim is faster than loops or strpos
567 ltrimmedLine = Utils.ltrim(@currentLine, ' ')
568  
569 return ltrimmedLine.charAt(0) is '#'
570  
571  
572 # Cleanups a YAML string to be parsed.
573 #
574 # @param [String] value The input YAML string
575 #
576 # @return [String] A cleaned up YAML string
577 #
578 cleanup: (value) ->
579 if value.indexOf("\r") isnt -1
580 value = value.split("\r\n").join("\n").split("\r").join("\n")
581  
582 # Strip YAML header
583 count = 0
584 [value, count] = @PATTERN_YAML_HEADER.replaceAll value, ''
585 @offset += count
586  
587 # Remove leading comments
588 [trimmedValue, count] = @PATTERN_LEADING_COMMENTS.replaceAll value, '', 1
589 if count is 1
590 # Items have been removed, update the offset
591 @offset += Utils.subStrCount(value, "\n") - Utils.subStrCount(trimmedValue, "\n")
592 value = trimmedValue
593  
594 # Remove start of the document marker (---)
595 [trimmedValue, count] = @PATTERN_DOCUMENT_MARKER_START.replaceAll value, '', 1
596 if count is 1
597 # Items have been removed, update the offset
598 @offset += Utils.subStrCount(value, "\n") - Utils.subStrCount(trimmedValue, "\n")
599 value = trimmedValue
600  
601 # Remove end of the document marker (...)
602 value = @PATTERN_DOCUMENT_MARKER_END.replace value, ''
603  
604 # Ensure the block is not indented
605 lines = value.split("\n")
606 smallestIndent = -1
607 for line in lines
608 continue if Utils.trim(line, ' ').length == 0
609 indent = line.length - Utils.ltrim(line).length
610 if smallestIndent is -1 or indent < smallestIndent
611 smallestIndent = indent
612 if smallestIndent > 0
613 for line, i in lines
614 lines[i] = line[smallestIndent..]
615 value = lines.join("\n")
616  
617 return value
618  
619  
620 # Returns true if the next line starts unindented collection
621 #
622 # @return [Boolean] Returns true if the next line starts unindented collection, false otherwise
623 #
624 isNextLineUnIndentedCollection: (currentIndentation = null) ->
625 currentIndentation ?= @getCurrentLineIndentation()
626 notEOF = @moveToNextLine()
627  
628 while notEOF and @isCurrentLineEmpty()
629 notEOF = @moveToNextLine()
630  
631 if false is notEOF
632 return false
633  
634 ret = false
635 if @getCurrentLineIndentation() is currentIndentation and @isStringUnIndentedCollectionItem(@currentLine)
636 ret = true
637  
638 @moveToPreviousLine()
639  
640 return ret
641  
642  
643 # Returns true if the string is un-indented collection item
644 #
645 # @return [Boolean] Returns true if the string is un-indented collection item, false otherwise
646 #
647 isStringUnIndentedCollectionItem: ->
648 return @currentLine is '-' or @currentLine[0...2] is '- '
649  
650  
651 module.exports = Parser