1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-02-02 22:10:06 +08:00
SpaceVim/bundle/splitjoin.vim/autoload/sj/yaml.vim
2024-06-27 18:10:36 +08:00

542 lines
13 KiB
VimL

" Array Callbacks:
" ================
function! sj#yaml#SplitArray()
let [line, line_no, whitespace] = s:ReadCurrentLine()
let prefix = ''
let array_part = ''
let indent = 1
let end_offset = 0
let nestedExp = '\v^\s*((-\s+)+)(\[.*\])$'
let line = s:StripComment(line)
" Split arrays which are map properties
" E.g.
" prop: [1, 2]
if line =~ ':\s*\[.*\]$'
let [key, array_part] = s:SplitKeyValue(line)
let prefix = key . ":\n"
" Split nested arrays
" E.g.
" - [1, 2]
elseif line =~ nestedExp
let prefix = substitute(line, nestedExp, '\1', '')
let array_part = substitute(line, nestedExp, '\3', '')
let indent = len(substitute(line, '\v[^-]', '', 'g'))
let end_offset = -1
endif
if array_part != ''
let body = substitute(array_part, '\v^\s*\[(.*)\]\s*$', '\1', '')
let array_items = s:SplitArrayBody(body)
call sj#ReplaceMotion('V', prefix . '- ' . join(array_items, "\n- "))
silent! normal! zO
call s:SetIndentWhitespace(line_no, whitespace)
call s:IncreaseIndentWhitespace(line_no + 1, line_no + len(array_items) + end_offset, whitespace, indent)
return 1
endif
return 0
endfunction
function! sj#yaml#JoinArray()
let [line, line_no, whitespace] = s:ReadCurrentLine()
let lines = []
let first_line = s:StripComment(line)
let nestedExp = '\v^(\s*(-\s+)+)(-\s+.*)$'
let join_type = ''
" Nested arrays
" E.g.
" - - 'one'
" - 'two'
if first_line =~ nestedExp && s:IsValidLineNo(line_no)
let join_type = 'nested'
let [lines, last_line_no] = s:GetChildren(line_no)
let lines = map(lines, 's:StripComment(v:val)')
let lines = [substitute(first_line, nestedExp, '\3', '')] + lines
let first_line = sj#Rtrim(substitute(first_line, nestedExp, '\1', ''))
" Normal arrays
" E.g.
" list:
" - 'one'
" - 'two'
elseif first_line =~ ':$' && s:IsValidLineNo(line_no + 1)
let join_type = 'normal'
let [lines, last_line_no] = s:GetChildren(line_no)
let lines = map(lines, 's:StripComment(v:val)')
endif
if !empty(lines)
if join_type == 'nested'
let body_lines = lines[1:len(lines)]
else
let body_lines = lines
endif
for line in body_lines
if line !~ '^\s*$' && line !~ '^\s*-'
" one non-blank line is not part of the array, it must be a nested
" construct, can't handle that
return 0
endif
if line =~ nestedExp
" can't handle nested subexpressions
return 0
endif
endfor
let lines = map(lines, 'sj#Trim(substitute(v:val, "^\\s*-", "", ""))')
let lines = filter(lines, '!sj#BlankString(v:val)')
let replacement = first_line . ' [' . s:JoinArrayItems(lines) . ']'
call sj#ReplaceLines(line_no, last_line_no, replacement)
silent! normal! zO
call s:SetIndentWhitespace(line_no, whitespace)
return 1
endif
" then there's nothing to join
return 0
endfunction
" Map Callbacks:
" ================
function! sj#yaml#SplitMap()
let [from, to] = sj#LocateBracesOnLine('{', '}')
if from >= 0 && to >= 0
let [line, line_no, whitespace] = s:ReadCurrentLine()
let line = s:StripComment(line)
let pairs = sj#ParseJsonObjectBody(from + 1, to - 1)
let body = join(pairs, "\n")
let body_start = line_no
let indent_level = 0
let end_offset = -1
" Increase indention if the map is inside a nested array.
" E.g.
" - - { one: 1 }
if line =~ '^\s*-\s'
let indent_level = s:NestedArrayLevel(line)
endif
" Move body into next line if it is a map property.
" E.g.
" prop: { one: 1 }
" - prop: { one: 1 }
if line =~ '^\v\s*(-\s+)*[^{]*:\s+\{.*'
let body = "\n" . body
let indent_level += 1
let end_offset = 0
let body_start = line_no + 1
endif
call sj#ReplaceMotion('Va{', body)
silent! normal! zO
call s:SetIndentWhitespace(line_no, whitespace)
call s:IncreaseIndentWhitespace(line_no + 1, line_no + len(pairs) + end_offset, whitespace, indent_level)
call sj#Keeppatterns(line_no . 's/\s*$//e')
if sj#settings#Read('align')
let body_end = body_start + len(pairs) - 1
call sj#Align(body_start, body_end, 'json_object')
endif
return 1
endif
return 0
endfunction
function! sj#yaml#JoinMap()
let [line, line_no, whitespace] = s:ReadCurrentLine()
if !s:IsValidLineNo(line_no + 1)
return 0
endif
let first_line = s:StripComment(line)
let lines = []
let last_line_no = 0
let join_type = ''
let nestedExp = '\v^(\s*(-\s+)+)(.*)$'
let nestedPropExp = '\v^(\s*(-\s+)+.+:)$'
" Nested in a map inside an array.
" E.g.
" - prop:
" one: 1
if first_line =~ nestedPropExp
let join_type = 'nested_in_map_in_array'
let [lines, last_line_no] = s:GetChildren(line_no)
let first_line = sj#Rtrim(substitute(first_line, nestedPropExp, '\1', ''))
" Map inside an array.
" E.g.
" - one: 1
" two: 2
elseif first_line =~ nestedExp
let join_type = 'nested_in_array'
let [lines, last_line_no] = s:GetChildren(line_no)
let lines = [substitute(first_line, nestedExp, '\3', '')] + lines
let first_line = sj#Rtrim(substitute(first_line, nestedExp, '\1', ''))
if len(lines) <= 1
" only 1 line means nothing to join in this case
return 0
endif
" Normal map
" E.g.
" map:
" one: 1
" two: 2
elseif first_line =~ '\k\+:\s*$'
let join_type = 'normal'
let [lines, last_line_no] = s:GetChildren(line_no)
endif
if len(lines) > 0
if join_type == 'nested_in_array'
let body_lines = lines[1:len(lines)]
else
let body_lines = lines
endif
if len(body_lines) > 0
let base_indent = len(matchstr(body_lines[0], '^\s*'))
endif
for line in body_lines
if line =~ '^\s*-'
" one of the lines is a part of an array, we can't handle nested subexpressions
return 0
endif
if len(matchstr(line, '^\s*')) != base_indent
" a nested map, can't handle that
return 0
endif
endfor
let lines = sj#TrimList(lines)
let lines = s:NormalizeWhitespace(lines)
let lines = map(lines, 's:StripComment(v:val)')
if sj#settings#Read('curly_brace_padding')
let replacement = first_line . ' { '. join(lines, ', ') . ' }'
else
let replacement = first_line . ' {'. join(lines, ', ') . '}'
endif
call sj#ReplaceLines(line_no, last_line_no, replacement)
silent! normal! zO
call s:SetIndentWhitespace(line_no, whitespace)
return 1
endif
return 0
endfunction
" Helper Functions:
" =================
" Reads line, line number and indention
function! s:ReadCurrentLine()
let line_no = line('.')
let line = getline(line_no)
let whitespace = s:GetIndentWhitespace(line_no)
return [line, line_no, whitespace]
endfunction
" Strip comments from string starting with a #
function! s:StripComment(s)
return substitute(a:s, '\v\s+#.*$', '', '')
endfunction
" Check if current buffer has the line number
function! s:IsValidLineNo(no)
return a:no >= 0 && a:no <= line('$')
endfunction
" Normalize whitespace, if enabled
function! s:NormalizeWhitespace(lines)
if sj#settings#Read('normalize_whitespace')
return map(a:lines, 'substitute(v:val, ":\\s\\+", ": ", "")')
endif
return a:lines
endfunction
function! s:GetIndentWhitespace(line_no)
return substitute(getline(a:line_no), '^\(\s*\).*$', '\1', '')
endfunction
function! s:SetIndentWhitespace(line_no, whitespace)
silent call sj#Keeppatterns(a:line_no . 's/^\s*/' . a:whitespace)
endfunction
function! s:IncreaseIndentWhitespace(from, to, whitespace, level)
if a:whitespace =~ "\t"
let new_whitespace = a:whitespace . repeat("\t", a:level)
else
let new_whitespace = a:whitespace . repeat(' ', &sw * a:level)
endif
for line_no in range(a:from, a:to)
call s:SetIndentWhitespace(line_no, new_whitespace)
endfor
endfunction
" Get following lines with a greater indent than the current line
function! s:GetChildren(line_no)
let line_no = a:line_no
let next_line_no = line_no + 1
let indent = indent(line_no)
let next_line = getline(next_line_no)
" Count '- ' as indent, if an object is in an array
" E.g. (GetChildren for prop_a)
" list:
" - prop_a:
" - 1
" prop_b
" - 2
let line = getline(line_no)
if line =~ '^\s*\(\-\s\s*\)..*:$'
let prefix = substitute(getline(a:line_no), '^\s*\(\-\s\s*\)..*:$', '\1', '')
let indent += len(prefix)
end
while s:IsValidLineNo(next_line_no) &&
\ (sj#BlankString(next_line) || indent(next_line_no) > indent)
let next_line_no = next_line_no + 1
let next_line = getline(next_line_no)
endwhile
let next_line_no = next_line_no - 1
" Preserve trailing empty lines
while sj#BlankString(getline(next_line_no)) && next_line_no > line_no
let next_line_no = next_line_no - 1
endwhile
return [sj#GetLines(line_no + 1, next_line_no), next_line_no]
endfunction
" Split a string into individual array items.
" E.g.
" 'one, two' => ['one', 'two']
" '{ one: 1 }, { two: 2 }' => ['{ one: 1 }', '{ two: 2 }']
function! s:SplitArrayBody(body)
let items = []
let partial_item = ''
let rest = sj#Ltrim(a:body)
while !empty(rest)
let char = rest[0]
if char == '{'
let [item, rest] = s:ReadMap(rest)
let rest = s:SkipWhitespaceUntilComma(rest)
call add(items, s:StripCurlyBrackets(item))
elseif char == '['
let [item, rest] = s:ReadArray(rest)
let rest = s:SkipWhitespaceUntilComma(rest)
call add(items, sj#Trim(item))
elseif char == '"' || char == "'"
let [item, rest] = s:ReadString(rest)
let rest = s:SkipWhitespaceUntilComma(rest)
call add(items, sj#Trim(item))
else
let [item, rest] = s:ReadUntil(rest, ',')
call add(items, sj#Trim(item))
endif
let rest = sj#Ltrim(rest[1:])
endwhile
return items
endfunction
" Read string until occurence of end_char
function! s:ReadUntil(str, end_char)
let idx = 0
while idx < len(a:str)
if a:str[idx] == a:end_char
return idx == 0
\ ? ['', a:str[1:]]
\ : [a:str[:idx-1], a:str[idx+1:]]
endif
let idx += 1
endwhile
return [a:str, '']
endfunction
" Read the next string fenced by " or '
function! s:ReadString(str)
if len(a:str) > 0
let fence = a:str[0]
if fence == '"' || fence == "'"
let [str, rest] = s:ReadUntil(a:str[1:], fence)
return [fence . str . fence, rest]
endif
endif
return ['', a:str]
endfunction
" Read the next array, including nested arrays.
" E.q.
" '[[1, 2]], [1]' => ['[[1, 2]], ', [1]']
function! s:ReadArray(str)
return s:ReadContainer(a:str, '[', ']')
endfunction
" Read the next map, including nested maps.
" E.q.
" '{ one: 1, foo: { two: 2 } }, {}' => ['{ one: 1, foo: { two: 2 } }, ', {}']
function! s:ReadMap(str)
return s:ReadContainer(a:str, '{', '}')
endfunction
function! s:ReadContainer(str, start_char, end_char)
let content = ''
let rest = a:str
let depth = 0
while !empty(rest)
let char = rest[0]
let rest = rest[1:]
let content .= char
if char == a:start_char
let depth += 1
elseif char == a:end_char
let depth -= 1
if depth == 0 | break | endif
endif
endwhile
return [content, rest]
endfunction
" skip whitespace and next comma
function! s:SkipWhitespaceUntilComma(str)
let [space, rest] = s:ReadUntil(a:str, ',')
if !sj#BlankString(space)
throw '"' . space . '" is not whitespace!'
end
return rest
endfunction
function! s:JoinArrayItems(items)
return join(map(a:items, 's:AddCurlyBrackets(v:val)'), ', ')
endfunction
" Add curly brackets if required for joining
" E.g.
" 'one: 1' => '{ one: 1 }'
" 'one' => 'one'
function! s:AddCurlyBrackets(line)
let line = sj#Trim(a:line)
if line !~ '^\v\[.*\]$' && line !~ '^\v\{.*\}$'
let [key, value] = s:SplitKeyValue(line)
if key != ''
return '{ ' . a:line . ' }'
endif
endif
return a:line
endfunction
" Strip curly brackets if possible
" E.g.
" '{ one: 1 }' => 'one: 1'
" '{ one: 1, two: 2 }' => '{ one: 1, two: 2 }'
function! s:StripCurlyBrackets(item)
let item = sj#Trim(a:item)
if item =~ '^{.*}$'
let parser = sj#argparser#js#Construct(2, len(item) - 1, item)
call parser.Process()
if len(parser.args) == 1
let item = substitute(item, '^{\s*', '', '')
let item = substitute(item, '\s*}$', '', '')
endif
endif
return item
endfunction
" Split a string into key and value
" E.g.
" 'one: 1' => ['one', '1']
" 'one' => ['', 'one']
" 'one:' => ['one', '']
" 'a:val' => ['', 'a:val']
function! s:SplitKeyValue(line)
let line = sj#Trim(a:line)
let parts = []
let first_char = line[0]
let key = ''
let value = ''
" Read line starts with a fenced string. E.g
" 'one': 1
" 'one'
if first_char == '"' || first_char == "'"
let [key, rest] = s:ReadString(line)
let [_, value] = s:ReadUntil(rest, ':')
else
let parts = split(line . ' ', ': ')
let [key, value] = [parts[0], join(parts[1:], ': ')]
endif
if value == '' && a:line !~ '\s*:$'
let [key, value] = ['', key]
endif
return [sj#Trim(key), sj#Trim(value)]
endfunction
" Calculate the nesting level of an array item
" E.g.
" - foo => 1
" - - bar => 2
function! s:NestedArrayLevel(line)
let prefix = substitute(a:line, '\v^\s*((-\s+)+).*', '\1', '')
let levels = substitute(prefix, '[^-]', '', 'g')
return len(levels)
endfunction