651 lines
25 KiB
CoffeeScript
651 lines
25 KiB
CoffeeScript
|
|
Inline = require './Inline'
|
|
Pattern = require './Pattern'
|
|
Utils = require './Utils'
|
|
ParseException = require './Exception/ParseException'
|
|
ParseMore = require './Exception/ParseMore'
|
|
|
|
# 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', 'm'
|
|
PATTERN_LEADING_COMMENTS: new Pattern '^(\\#.*?\n)+', 'm'
|
|
PATTERN_DOCUMENT_MARKER_START: new Pattern '^\\-\\-\\-.*?\n', 'm'
|
|
PATTERN_DOCUMENT_MARKER_END: new Pattern '^\\.\\.\\.\\s*$', 'm'
|
|
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 removeComments and @isCurrentLineComment()
|
|
continue
|
|
|
|
if @isCurrentLineBlank()
|
|
data.push @currentLine[newIndent..]
|
|
continue
|
|
|
|
if isItUnindentedCollection and not @isStringUnIndentedCollectionItem(@currentLine) and indent is newIndent
|
|
@moveToPreviousLine()
|
|
break
|
|
|
|
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
|
|
|
|
# Value can be multiline compact sequence or mapping or string
|
|
if value.charAt(0) in ['[', '{', '"', "'"]
|
|
while true
|
|
try
|
|
return Inline.parse value, exceptionOnInvalidType, objectDecoder
|
|
catch e
|
|
if e instanceof ParseMore and @moveToNextLine()
|
|
value += "\n" + Utils.trim(@currentLine, ' ')
|
|
else
|
|
e.parsedLine = @getRealCurrentLineNb() + 1
|
|
e.snippet = @currentLine
|
|
throw e
|
|
else
|
|
if @isNextLineIndented()
|
|
value += "\n" + @getNextEmbedBlock()
|
|
return Inline.parse value, exceptionOnInvalidType, objectDecoder
|
|
|
|
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
|