/bower_components/yaml.js/src/Parser.coffee |
@@ -0,0 +1,654 @@ |
|
Inline = require './Inline' |
Pattern = require './Pattern' |
Utils = require './Utils' |
ParseException = require './Exception/ParseException' |
|
# Parser parses YAML strings to convert them to JavaScript objects. |
# |
class Parser |
|
# Pre-compiled patterns |
# |
PATTERN_FOLDED_SCALAR_ALL: new Pattern '^(?:(?<type>![^\\|>]*)\\s+)?(?<separator>\\||>)(?<modifiers>\\+|\\-|\\d+|\\+\\d+|\\-\\d+|\\d+\\+|\\d+\\-)?(?<comments> +#.*)?$' |
PATTERN_FOLDED_SCALAR_END: new Pattern '(?<separator>\\||>)(?<modifiers>\\+|\\-|\\d+|\\+\\d+|\\-\\d+|\\d+\\+|\\d+\\-)?(?<comments> +#.*)?$' |
PATTERN_SEQUENCE_ITEM: new Pattern '^\\-((?<leadspaces>\\s+)(?<value>.+?))?\\s*$' |
PATTERN_ANCHOR_VALUE: new Pattern '^&(?<ref>[^ ]+) *(?<value>.*)' |
PATTERN_COMPACT_NOTATION: new Pattern '^(?<key>'+Inline.REGEX_QUOTED_STRING+'|[^ \'"\\{\\[].*?) *\\:(\\s+(?<value>.+?))?\\s*$' |
PATTERN_MAPPING_ITEM: new Pattern '^(?<key>'+Inline.REGEX_QUOTED_STRING+'|[^ \'"\\[\\{].*?) *\\:(\\s+(?<value>.+?))?\\s*$' |
PATTERN_DECIMAL: new Pattern '\\d+' |
PATTERN_INDENT_SPACES: new Pattern '^ +' |
PATTERN_TRAILING_LINES: new Pattern '(\n*)$' |
PATTERN_YAML_HEADER: new Pattern '^\\%YAML[: ][\\d\\.]+.*\n' |
PATTERN_LEADING_COMMENTS: new Pattern '^(\\#.*?\n)+' |
PATTERN_DOCUMENT_MARKER_START: new Pattern '^\\-\\-\\-.*?\n' |
PATTERN_DOCUMENT_MARKER_END: new Pattern '^\\.\\.\\.\\s*$' |
PATTERN_FOLDED_SCALAR_BY_INDENTATION: {} |
|
# Context types |
# |
CONTEXT_NONE: 0 |
CONTEXT_SEQUENCE: 1 |
CONTEXT_MAPPING: 2 |
|
|
# Constructor |
# |
# @param [Integer] offset The offset of YAML document (used for line numbers in error messages) |
# |
constructor: (@offset = 0) -> |
@lines = [] |
@currentLineNb = -1 |
@currentLine = '' |
@refs = {} |
|
|
# Parses a YAML string to a JavaScript value. |
# |
# @param [String] value A YAML string |
# @param [Boolean] exceptionOnInvalidType true if an exception must be thrown on invalid types (a JavaScript resource or object), false otherwise |
# @param [Function] objectDecoder A function to deserialize custom objects, null otherwise |
# |
# @return [Object] A JavaScript value |
# |
# @throw [ParseException] If the YAML is not valid |
# |
parse: (value, exceptionOnInvalidType = false, objectDecoder = null) -> |
@currentLineNb = -1 |
@currentLine = '' |
@lines = @cleanup(value).split "\n" |
|
data = null |
context = @CONTEXT_NONE |
allowOverwrite = false |
while @moveToNextLine() |
if @isCurrentLineEmpty() |
continue |
|
# Tab? |
if "\t" is @currentLine[0] |
throw new ParseException 'A YAML file cannot contain tabs as indentation.', @getRealCurrentLineNb() + 1, @currentLine |
|
isRef = mergeNode = false |
if values = @PATTERN_SEQUENCE_ITEM.exec @currentLine |
if @CONTEXT_MAPPING is context |
throw new ParseException 'You cannot define a sequence item when in a mapping' |
context = @CONTEXT_SEQUENCE |
data ?= [] |
|
if values.value? and matches = @PATTERN_ANCHOR_VALUE.exec values.value |
isRef = matches.ref |
values.value = matches.value |
|
# Array |
if not(values.value?) or '' is Utils.trim(values.value, ' ') or Utils.ltrim(values.value, ' ').indexOf('#') is 0 |
if @currentLineNb < @lines.length - 1 and not @isNextLineUnIndentedCollection() |
c = @getRealCurrentLineNb() + 1 |
parser = new Parser c |
parser.refs = @refs |
data.push parser.parse(@getNextEmbedBlock(null, true), exceptionOnInvalidType, objectDecoder) |
else |
data.push null |
|
else |
if values.leadspaces?.length and matches = @PATTERN_COMPACT_NOTATION.exec values.value |
|
# This is a compact notation element, add to next block and parse |
c = @getRealCurrentLineNb() |
parser = new Parser c |
parser.refs = @refs |
|
block = values.value |
indent = @getCurrentLineIndentation() |
if @isNextLineIndented(false) |
block += "\n"+@getNextEmbedBlock(indent + values.leadspaces.length + 1, true) |
|
data.push parser.parse block, exceptionOnInvalidType, objectDecoder |
|
else |
data.push @parseValue values.value, exceptionOnInvalidType, objectDecoder |
|
else if (values = @PATTERN_MAPPING_ITEM.exec @currentLine) and values.key.indexOf(' #') is -1 |
if @CONTEXT_SEQUENCE is context |
throw new ParseException 'You cannot define a mapping item when in a sequence' |
context = @CONTEXT_MAPPING |
data ?= {} |
|
# Force correct settings |
Inline.configure exceptionOnInvalidType, objectDecoder |
try |
key = Inline.parseScalar values.key |
catch e |
e.parsedLine = @getRealCurrentLineNb() + 1 |
e.snippet = @currentLine |
|
throw e |
|
if '<<' is key |
mergeNode = true |
allowOverwrite = true |
if values.value?.indexOf('*') is 0 |
refName = values.value[1..] |
unless @refs[refName]? |
throw new ParseException 'Reference "'+refName+'" does not exist.', @getRealCurrentLineNb() + 1, @currentLine |
|
refValue = @refs[refName] |
|
if typeof refValue isnt 'object' |
throw new ParseException 'YAML merge keys used with a scalar value instead of an object.', @getRealCurrentLineNb() + 1, @currentLine |
|
if refValue instanceof Array |
# Merge array with object |
for value, i in refValue |
data[String(i)] ?= value |
else |
# Merge objects |
for key, value of refValue |
data[key] ?= value |
|
else |
if values.value? and values.value isnt '' |
value = values.value |
else |
value = @getNextEmbedBlock() |
|
c = @getRealCurrentLineNb() + 1 |
parser = new Parser c |
parser.refs = @refs |
parsed = parser.parse value, exceptionOnInvalidType |
|
unless typeof parsed is 'object' |
throw new ParseException 'YAML merge keys used with a scalar value instead of an object.', @getRealCurrentLineNb() + 1, @currentLine |
|
if parsed instanceof Array |
# If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes |
# and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier |
# in the sequence override keys specified in later mapping nodes. |
for parsedItem in parsed |
unless typeof parsedItem is 'object' |
throw new ParseException 'Merge items must be objects.', @getRealCurrentLineNb() + 1, parsedItem |
|
if parsedItem instanceof Array |
# Merge array with object |
for value, i in parsedItem |
k = String(i) |
unless data.hasOwnProperty(k) |
data[k] = value |
else |
# Merge objects |
for key, value of parsedItem |
unless data.hasOwnProperty(key) |
data[key] = value |
|
else |
# If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the |
# current mapping, unless the key already exists in it. |
for key, value of parsed |
unless data.hasOwnProperty(key) |
data[key] = value |
|
else if values.value? and matches = @PATTERN_ANCHOR_VALUE.exec values.value |
isRef = matches.ref |
values.value = matches.value |
|
|
if mergeNode |
# Merge keys |
else if not(values.value?) or '' is Utils.trim(values.value, ' ') or Utils.ltrim(values.value, ' ').indexOf('#') is 0 |
# Hash |
# if next line is less indented or equal, then it means that the current value is null |
if not(@isNextLineIndented()) and not(@isNextLineUnIndentedCollection()) |
# Spec: Keys MUST be unique; first one wins. |
# But overwriting is allowed when a merge node is used in current block. |
if allowOverwrite or data[key] is undefined |
data[key] = null |
|
else |
c = @getRealCurrentLineNb() + 1 |
parser = new Parser c |
parser.refs = @refs |
val = parser.parse @getNextEmbedBlock(), exceptionOnInvalidType, objectDecoder |
|
# Spec: Keys MUST be unique; first one wins. |
# But overwriting is allowed when a merge node is used in current block. |
if allowOverwrite or data[key] is undefined |
data[key] = val |
|
else |
val = @parseValue values.value, exceptionOnInvalidType, objectDecoder |
|
# Spec: Keys MUST be unique; first one wins. |
# But overwriting is allowed when a merge node is used in current block. |
if allowOverwrite or data[key] is undefined |
data[key] = val |
|
else |
# 1-liner optionally followed by newline |
lineCount = @lines.length |
if 1 is lineCount or (2 is lineCount and Utils.isEmpty(@lines[1])) |
try |
value = Inline.parse @lines[0], exceptionOnInvalidType, objectDecoder |
catch e |
e.parsedLine = @getRealCurrentLineNb() + 1 |
e.snippet = @currentLine |
|
throw e |
|
if typeof value is 'object' |
if value instanceof Array |
first = value[0] |
else |
for key of value |
first = value[key] |
break |
|
if typeof first is 'string' and first.indexOf('*') is 0 |
data = [] |
for alias in value |
data.push @refs[alias[1..]] |
value = data |
|
return value |
|
else if Utils.ltrim(value).charAt(0) in ['[', '{'] |
try |
return Inline.parse value, exceptionOnInvalidType, objectDecoder |
catch e |
e.parsedLine = @getRealCurrentLineNb() + 1 |
e.snippet = @currentLine |
|
throw e |
|
throw new ParseException 'Unable to parse.', @getRealCurrentLineNb() + 1, @currentLine |
|
if isRef |
if data instanceof Array |
@refs[isRef] = data[data.length-1] |
else |
lastKey = null |
for key of data |
lastKey = key |
@refs[isRef] = data[lastKey] |
|
|
if Utils.isEmpty(data) |
return null |
else |
return data |
|
|
|
# Returns the current line number (takes the offset into account). |
# |
# @return [Integer] The current line number |
# |
getRealCurrentLineNb: -> |
return @currentLineNb + @offset |
|
|
# Returns the current line indentation. |
# |
# @return [Integer] The current line indentation |
# |
getCurrentLineIndentation: -> |
return @currentLine.length - Utils.ltrim(@currentLine, ' ').length |
|
|
# Returns the next embed block of YAML. |
# |
# @param [Integer] indentation The indent level at which the block is to be read, or null for default |
# |
# @return [String] A YAML string |
# |
# @throw [ParseException] When indentation problem are detected |
# |
getNextEmbedBlock: (indentation = null, includeUnindentedCollection = false) -> |
@moveToNextLine() |
|
if not indentation? |
newIndent = @getCurrentLineIndentation() |
|
unindentedEmbedBlock = @isStringUnIndentedCollectionItem @currentLine |
|
if not(@isCurrentLineEmpty()) and 0 is newIndent and not(unindentedEmbedBlock) |
throw new ParseException 'Indentation problem.', @getRealCurrentLineNb() + 1, @currentLine |
|
else |
newIndent = indentation |
|
|
data = [@currentLine[newIndent..]] |
|
unless includeUnindentedCollection |
isItUnindentedCollection = @isStringUnIndentedCollectionItem @currentLine |
|
# Comments must not be removed inside a string block (ie. after a line ending with "|") |
# They must not be removed inside a sub-embedded block as well |
removeCommentsPattern = @PATTERN_FOLDED_SCALAR_END |
removeComments = not removeCommentsPattern.test @currentLine |
|
while @moveToNextLine() |
indent = @getCurrentLineIndentation() |
|
if indent is newIndent |
removeComments = not removeCommentsPattern.test @currentLine |
|
if isItUnindentedCollection and not @isStringUnIndentedCollectionItem(@currentLine) and indent is newIndent |
@moveToPreviousLine() |
break |
|
if @isCurrentLineBlank() |
data.push @currentLine[newIndent..] |
continue |
|
if removeComments and @isCurrentLineComment() |
if indent is newIndent |
continue |
|
if indent >= newIndent |
data.push @currentLine[newIndent..] |
else if Utils.ltrim(@currentLine).charAt(0) is '#' |
# Don't add line with comments |
else if 0 is indent |
@moveToPreviousLine() |
break |
else |
throw new ParseException 'Indentation problem.', @getRealCurrentLineNb() + 1, @currentLine |
|
|
return data.join "\n" |
|
|
# Moves the parser to the next line. |
# |
# @return [Boolean] |
# |
moveToNextLine: -> |
if @currentLineNb >= @lines.length - 1 |
return false |
|
@currentLine = @lines[++@currentLineNb]; |
|
return true |
|
|
# Moves the parser to the previous line. |
# |
moveToPreviousLine: -> |
@currentLine = @lines[--@currentLineNb] |
return |
|
|
# Parses a YAML value. |
# |
# @param [String] value A YAML value |
# @param [Boolean] exceptionOnInvalidType true if an exception must be thrown on invalid types false otherwise |
# @param [Function] objectDecoder A function to deserialize custom objects, null otherwise |
# |
# @return [Object] A JavaScript value |
# |
# @throw [ParseException] When reference does not exist |
# |
parseValue: (value, exceptionOnInvalidType, objectDecoder) -> |
if 0 is value.indexOf('*') |
pos = value.indexOf '#' |
if pos isnt -1 |
value = value.substr(1, pos-2) |
else |
value = value[1..] |
|
if @refs[value] is undefined |
throw new ParseException 'Reference "'+value+'" does not exist.', @currentLine |
|
return @refs[value] |
|
|
if matches = @PATTERN_FOLDED_SCALAR_ALL.exec value |
modifiers = matches.modifiers ? '' |
|
foldedIndent = Math.abs(parseInt(modifiers)) |
if isNaN(foldedIndent) then foldedIndent = 0 |
val = @parseFoldedScalar matches.separator, @PATTERN_DECIMAL.replace(modifiers, ''), foldedIndent |
if matches.type? |
# Force correct settings |
Inline.configure exceptionOnInvalidType, objectDecoder |
return Inline.parseScalar matches.type+' '+val |
else |
return val |
|
try |
return Inline.parse value, exceptionOnInvalidType, objectDecoder |
catch e |
# Try to parse multiline compact sequence or mapping |
if value.charAt(0) in ['[', '{'] and e instanceof ParseException and @isNextLineIndented() |
value += "\n" + @getNextEmbedBlock() |
try |
return Inline.parse value, exceptionOnInvalidType, objectDecoder |
catch e |
e.parsedLine = @getRealCurrentLineNb() + 1 |
e.snippet = @currentLine |
|
throw e |
|
else |
e.parsedLine = @getRealCurrentLineNb() + 1 |
e.snippet = @currentLine |
|
throw e |
|
return |
|
|
# Parses a folded scalar. |
# |
# @param [String] separator The separator that was used to begin this folded scalar (| or >) |
# @param [String] indicator The indicator that was used to begin this folded scalar (+ or -) |
# @param [Integer] indentation The indentation that was used to begin this folded scalar |
# |
# @return [String] The text value |
# |
parseFoldedScalar: (separator, indicator = '', indentation = 0) -> |
notEOF = @moveToNextLine() |
if not notEOF |
return '' |
|
isCurrentLineBlank = @isCurrentLineBlank() |
text = '' |
|
# Leading blank lines are consumed before determining indentation |
while notEOF and isCurrentLineBlank |
# newline only if not EOF |
if notEOF = @moveToNextLine() |
text += "\n" |
isCurrentLineBlank = @isCurrentLineBlank() |
|
|
# Determine indentation if not specified |
if 0 is indentation |
if matches = @PATTERN_INDENT_SPACES.exec @currentLine |
indentation = matches[0].length |
|
|
if indentation > 0 |
pattern = @PATTERN_FOLDED_SCALAR_BY_INDENTATION[indentation] |
unless pattern? |
pattern = new Pattern '^ {'+indentation+'}(.*)$' |
Parser::PATTERN_FOLDED_SCALAR_BY_INDENTATION[indentation] = pattern |
|
while notEOF and (isCurrentLineBlank or matches = pattern.exec @currentLine) |
if isCurrentLineBlank |
text += @currentLine[indentation..] |
else |
text += matches[1] |
|
# newline only if not EOF |
if notEOF = @moveToNextLine() |
text += "\n" |
isCurrentLineBlank = @isCurrentLineBlank() |
|
else if notEOF |
text += "\n" |
|
|
if notEOF |
@moveToPreviousLine() |
|
|
# Remove line breaks of each lines except the empty and more indented ones |
if '>' is separator |
newText = '' |
for line in text.split "\n" |
if line.length is 0 or line.charAt(0) is ' ' |
newText = Utils.rtrim(newText, ' ') + line + "\n" |
else |
newText += line + ' ' |
text = newText |
|
if '+' isnt indicator |
# Remove any extra space or new line as we are adding them after |
text = Utils.rtrim(text) |
|
# Deal with trailing newlines as indicated |
if '' is indicator |
text = @PATTERN_TRAILING_LINES.replace text, "\n" |
else if '-' is indicator |
text = @PATTERN_TRAILING_LINES.replace text, '' |
|
return text |
|
|
# Returns true if the next line is indented. |
# |
# @return [Boolean] Returns true if the next line is indented, false otherwise |
# |
isNextLineIndented: (ignoreComments = true) -> |
currentIndentation = @getCurrentLineIndentation() |
EOF = not @moveToNextLine() |
|
if ignoreComments |
while not(EOF) and @isCurrentLineEmpty() |
EOF = not @moveToNextLine() |
else |
while not(EOF) and @isCurrentLineBlank() |
EOF = not @moveToNextLine() |
|
if EOF |
return false |
|
ret = false |
if @getCurrentLineIndentation() > currentIndentation |
ret = true |
|
@moveToPreviousLine() |
|
return ret |
|
|
# Returns true if the current line is blank or if it is a comment line. |
# |
# @return [Boolean] Returns true if the current line is empty or if it is a comment line, false otherwise |
# |
isCurrentLineEmpty: -> |
trimmedLine = Utils.trim(@currentLine, ' ') |
return trimmedLine.length is 0 or trimmedLine.charAt(0) is '#' |
|
|
# Returns true if the current line is blank. |
# |
# @return [Boolean] Returns true if the current line is blank, false otherwise |
# |
isCurrentLineBlank: -> |
return '' is Utils.trim(@currentLine, ' ') |
|
|
# Returns true if the current line is a comment line. |
# |
# @return [Boolean] Returns true if the current line is a comment line, false otherwise |
# |
isCurrentLineComment: -> |
# Checking explicitly the first char of the trim is faster than loops or strpos |
ltrimmedLine = Utils.ltrim(@currentLine, ' ') |
|
return ltrimmedLine.charAt(0) is '#' |
|
|
# Cleanups a YAML string to be parsed. |
# |
# @param [String] value The input YAML string |
# |
# @return [String] A cleaned up YAML string |
# |
cleanup: (value) -> |
if value.indexOf("\r") isnt -1 |
value = value.split("\r\n").join("\n").split("\r").join("\n") |
|
# Strip YAML header |
count = 0 |
[value, count] = @PATTERN_YAML_HEADER.replaceAll value, '' |
@offset += count |
|
# Remove leading comments |
[trimmedValue, count] = @PATTERN_LEADING_COMMENTS.replaceAll value, '', 1 |
if count is 1 |
# Items have been removed, update the offset |
@offset += Utils.subStrCount(value, "\n") - Utils.subStrCount(trimmedValue, "\n") |
value = trimmedValue |
|
# Remove start of the document marker (---) |
[trimmedValue, count] = @PATTERN_DOCUMENT_MARKER_START.replaceAll value, '', 1 |
if count is 1 |
# Items have been removed, update the offset |
@offset += Utils.subStrCount(value, "\n") - Utils.subStrCount(trimmedValue, "\n") |
value = trimmedValue |
|
# Remove end of the document marker (...) |
value = @PATTERN_DOCUMENT_MARKER_END.replace value, '' |
|
# Ensure the block is not indented |
lines = value.split("\n") |
smallestIndent = -1 |
for line in lines |
continue if Utils.trim(line, ' ').length == 0 |
indent = line.length - Utils.ltrim(line).length |
if smallestIndent is -1 or indent < smallestIndent |
smallestIndent = indent |
if smallestIndent > 0 |
for line, i in lines |
lines[i] = line[smallestIndent..] |
value = lines.join("\n") |
|
return value |
|
|
# Returns true if the next line starts unindented collection |
# |
# @return [Boolean] Returns true if the next line starts unindented collection, false otherwise |
# |
isNextLineUnIndentedCollection: (currentIndentation = null) -> |
currentIndentation ?= @getCurrentLineIndentation() |
notEOF = @moveToNextLine() |
|
while notEOF and @isCurrentLineEmpty() |
notEOF = @moveToNextLine() |
|
if false is notEOF |
return false |
|
ret = false |
if @getCurrentLineIndentation() is currentIndentation and @isStringUnIndentedCollectionItem(@currentLine) |
ret = true |
|
@moveToPreviousLine() |
|
return ret |
|
|
# Returns true if the string is un-indented collection item |
# |
# @return [Boolean] Returns true if the string is un-indented collection item, false otherwise |
# |
isStringUnIndentedCollectionItem: -> |
return @currentLine is '-' or @currentLine[0...2] is '- ' |
|
|
module.exports = Parser |