17db25fedSBram Moolenaar" Vim plugin for formatting XML
2*eab6dff1SBram Moolenaar" Last Change: 2020 Jan 06
3*eab6dff1SBram Moolenaar"     Version: 0.3
47db25fedSBram Moolenaar"      Author: Christian Brabandt <[email protected]>
591f84f6eSBram Moolenaar"  Repository: https://github.com/chrisbra/vim-xml-ftplugin
67db25fedSBram Moolenaar"     License: VIM License
77db25fedSBram Moolenaar" Documentation: see :h xmlformat.txt (TODO!)
87db25fedSBram Moolenaar" ---------------------------------------------------------------------
97db25fedSBram Moolenaar" Load Once: {{{1
107db25fedSBram Moolenaarif exists("g:loaded_xmlformat") || &cp
117db25fedSBram Moolenaar  finish
127db25fedSBram Moolenaarendif
137db25fedSBram Moolenaarlet g:loaded_xmlformat = 1
147db25fedSBram Moolenaarlet s:keepcpo       = &cpo
157db25fedSBram Moolenaarset cpo&vim
167db25fedSBram Moolenaar
177db25fedSBram Moolenaar" Main function: Format the input {{{1
18*eab6dff1SBram Moolenaarfunc! xmlformat#Format() abort
197db25fedSBram Moolenaar  " only allow reformatting through the gq command
207db25fedSBram Moolenaar  " (e.g. Vim is in normal mode)
217db25fedSBram Moolenaar  if mode() != 'n'
227db25fedSBram Moolenaar    " do not fall back to internal formatting
237db25fedSBram Moolenaar    return 0
247db25fedSBram Moolenaar  endif
2596f45c0bSBram Moolenaar  let count_orig = v:count
267db25fedSBram Moolenaar  let sw  = shiftwidth()
277db25fedSBram Moolenaar  let prev = prevnonblank(v:lnum-1)
287db25fedSBram Moolenaar  let s:indent = indent(prev)/sw
297db25fedSBram Moolenaar  let result = []
307db25fedSBram Moolenaar  let lastitem = prev ? getline(prev) : ''
317db25fedSBram Moolenaar  let is_xml_decl = 0
3296f45c0bSBram Moolenaar  " go through every line, but don't join all content together and join it
3396f45c0bSBram Moolenaar  " back. We might lose empty lines
3496f45c0bSBram Moolenaar  let list = getline(v:lnum, (v:lnum + count_orig - 1))
3596f45c0bSBram Moolenaar  let current = 0
3696f45c0bSBram Moolenaar  for line in list
3796f45c0bSBram Moolenaar    " Keep empty input lines?
3896f45c0bSBram Moolenaar    if empty(line)
3996f45c0bSBram Moolenaar      call add(result, '')
4096f45c0bSBram Moolenaar      continue
4196f45c0bSBram Moolenaar    elseif line !~# '<[/]\?[^>]*>'
4296f45c0bSBram Moolenaar      let nextmatch = match(list, '<[/]\?[^>]*>', current)
43*eab6dff1SBram Moolenaar      if nextmatch > -1
44*eab6dff1SBram Moolenaar        let line .= ' '. join(list[(current + 1):(nextmatch-1)], " ")
4596f45c0bSBram Moolenaar        call remove(list, current+1, nextmatch-1)
4696f45c0bSBram Moolenaar      endif
47*eab6dff1SBram Moolenaar    endif
4896f45c0bSBram Moolenaar    " split on `>`, but don't split on very first opening <
4996f45c0bSBram Moolenaar    " this means, items can be like ['<tag>', 'tag content</tag>']
5096f45c0bSBram Moolenaar    for item in split(line, '.\@<=[>]\zs')
517db25fedSBram Moolenaar      if s:EndTag(item)
52*eab6dff1SBram Moolenaar        call s:DecreaseIndent()
537db25fedSBram Moolenaar        call add(result, s:Indent(item))
547db25fedSBram Moolenaar      elseif s:EmptyTag(lastitem)
557db25fedSBram Moolenaar        call add(result, s:Indent(item))
567db25fedSBram Moolenaar      elseif s:StartTag(lastitem) && s:IsTag(item)
577db25fedSBram Moolenaar        let s:indent += 1
587db25fedSBram Moolenaar        call add(result, s:Indent(item))
597db25fedSBram Moolenaar      else
607db25fedSBram Moolenaar        if !s:IsTag(item)
6196f45c0bSBram Moolenaar          " Simply split on '<', if there is one,
6296f45c0bSBram Moolenaar          " but reformat according to &textwidth
637db25fedSBram Moolenaar          let t=split(item, '.<\@=\zs')
64*eab6dff1SBram Moolenaar
65*eab6dff1SBram Moolenaar          " if the content fits well within a single line, add it there
66*eab6dff1SBram Moolenaar          " so that the output looks like this:
67*eab6dff1SBram Moolenaar          "
68*eab6dff1SBram Moolenaar          " <foobar>1</foobar>
69*eab6dff1SBram Moolenaar          if s:TagContent(lastitem) is# s:TagContent(t[1]) && strlen(result[-1]) + strlen(item) <= s:Textwidth()
70*eab6dff1SBram Moolenaar            let result[-1] .= item
71*eab6dff1SBram Moolenaar            let lastitem = t[1]
72*eab6dff1SBram Moolenaar            continue
73*eab6dff1SBram Moolenaar          endif
7496f45c0bSBram Moolenaar          " t should only contain 2 items, but just be safe here
7596f45c0bSBram Moolenaar          if s:IsTag(lastitem)
767db25fedSBram Moolenaar            let s:indent+=1
7796f45c0bSBram Moolenaar          endif
7896f45c0bSBram Moolenaar          let result+=s:FormatContent([t[0]])
7996f45c0bSBram Moolenaar          if s:EndTag(t[1])
80*eab6dff1SBram Moolenaar            call s:DecreaseIndent()
8196f45c0bSBram Moolenaar          endif
8296f45c0bSBram Moolenaar          "for y in t[1:]
8396f45c0bSBram Moolenaar            let result+=s:FormatContent(t[1:])
8496f45c0bSBram Moolenaar          "endfor
857db25fedSBram Moolenaar        else
867db25fedSBram Moolenaar          call add(result, s:Indent(item))
877db25fedSBram Moolenaar        endif
887db25fedSBram Moolenaar      endif
897db25fedSBram Moolenaar      let lastitem = item
907db25fedSBram Moolenaar    endfor
9196f45c0bSBram Moolenaar    let current += 1
9296f45c0bSBram Moolenaar  endfor
937db25fedSBram Moolenaar
947db25fedSBram Moolenaar  if !empty(result)
9596f45c0bSBram Moolenaar    let lastprevline = getline(v:lnum + count_orig)
9696f45c0bSBram Moolenaar    let delete_lastline = v:lnum + count_orig - 1 == line('$')
9796f45c0bSBram Moolenaar    exe v:lnum. ",". (v:lnum + count_orig - 1). 'd'
987db25fedSBram Moolenaar    call append(v:lnum - 1, result)
997db25fedSBram Moolenaar    " Might need to remove the last line, if it became empty because of the
1007db25fedSBram Moolenaar    " append() call
1017db25fedSBram Moolenaar    let last = v:lnum + len(result)
10296f45c0bSBram Moolenaar    " do not use empty(), it returns true for `empty(0)`
10396f45c0bSBram Moolenaar    if getline(last) is '' && lastprevline is '' && delete_lastline
1047db25fedSBram Moolenaar      exe last. 'd'
1057db25fedSBram Moolenaar    endif
1067db25fedSBram Moolenaar  endif
1077db25fedSBram Moolenaar
1087db25fedSBram Moolenaar  " do not run internal formatter!
1097db25fedSBram Moolenaar  return 0
1107db25fedSBram Moolenaarendfunc
1117db25fedSBram Moolenaar" Check if given tag is XML Declaration header {{{1
112*eab6dff1SBram Moolenaarfunc! s:IsXMLDecl(tag) abort
1137db25fedSBram Moolenaar  return a:tag =~? '^\s*<?xml\s\?\%(version="[^"]*"\)\?\s\?\%(encoding="[^"]*"\)\? ?>\s*$'
1147db25fedSBram Moolenaarendfunc
1157db25fedSBram Moolenaar" Return tag indented by current level {{{1
116*eab6dff1SBram Moolenaarfunc! s:Indent(item) abort
1177db25fedSBram Moolenaar  return repeat(' ', shiftwidth()*s:indent). s:Trim(a:item)
1187db25fedSBram Moolenaarendfu
1197db25fedSBram Moolenaar" Return item trimmed from leading whitespace {{{1
120*eab6dff1SBram Moolenaarfunc! s:Trim(item) abort
1217db25fedSBram Moolenaar  if exists('*trim')
1227db25fedSBram Moolenaar    return trim(a:item)
1237db25fedSBram Moolenaar  else
1247db25fedSBram Moolenaar    return matchstr(a:item, '\S\+.*')
1257db25fedSBram Moolenaar  endif
1267db25fedSBram Moolenaarendfunc
1277db25fedSBram Moolenaar" Check if tag is a new opening tag <tag> {{{1
128*eab6dff1SBram Moolenaarfunc! s:StartTag(tag) abort
129d47d5223SBram Moolenaar  let is_comment = s:IsComment(a:tag)
130d47d5223SBram Moolenaar  return a:tag =~? '^\s*<[^/?]' && !is_comment
131d47d5223SBram Moolenaarendfunc
13296f45c0bSBram Moolenaar" Check if tag is a Comment start {{{1
133*eab6dff1SBram Moolenaarfunc! s:IsComment(tag) abort
134d47d5223SBram Moolenaar  return a:tag =~? '<!--'
1357db25fedSBram Moolenaarendfunc
1367db25fedSBram Moolenaar" Remove one level of indentation {{{1
137*eab6dff1SBram Moolenaarfunc! s:DecreaseIndent() abort
138*eab6dff1SBram Moolenaar  let s:indent = (s:indent > 0 ? s:indent - 1 : 0)
1397db25fedSBram Moolenaarendfunc
1407db25fedSBram Moolenaar" Check if tag is a closing tag </tag> {{{1
141*eab6dff1SBram Moolenaarfunc! s:EndTag(tag) abort
1427db25fedSBram Moolenaar  return a:tag =~? '^\s*</'
1437db25fedSBram Moolenaarendfunc
1447db25fedSBram Moolenaar" Check that the tag is actually a tag and not {{{1
1457db25fedSBram Moolenaar" something like "foobar</foobar>"
146*eab6dff1SBram Moolenaarfunc! s:IsTag(tag) abort
1477db25fedSBram Moolenaar  return s:Trim(a:tag)[0] == '<'
1487db25fedSBram Moolenaarendfunc
1497db25fedSBram Moolenaar" Check if tag is empty <tag/> {{{1
150*eab6dff1SBram Moolenaarfunc! s:EmptyTag(tag) abort
1517db25fedSBram Moolenaar  return a:tag =~ '/>\s*$'
1527db25fedSBram Moolenaarendfunc
153*eab6dff1SBram Moolenaarfunc! s:TagContent(tag) abort "{{{1
154*eab6dff1SBram Moolenaar  " Return content of a tag
155*eab6dff1SBram Moolenaar  return substitute(a:tag, '^\s*<[/]\?\([^>]*\)>\s*$', '\1', '')
156*eab6dff1SBram Moolenaarendfunc
157*eab6dff1SBram Moolenaarfunc! s:Textwidth() abort "{{{1
158*eab6dff1SBram Moolenaar  " return textwidth (or 80 if not set)
159*eab6dff1SBram Moolenaar  return &textwidth == 0 ? 80 : &textwidth
160*eab6dff1SBram Moolenaarendfunc
16196f45c0bSBram Moolenaar" Format input line according to textwidth {{{1
162*eab6dff1SBram Moolenaarfunc! s:FormatContent(list) abort
16396f45c0bSBram Moolenaar  let result=[]
164*eab6dff1SBram Moolenaar  let limit = s:Textwidth()
16596f45c0bSBram Moolenaar  let column=0
16696f45c0bSBram Moolenaar  let idx = -1
16796f45c0bSBram Moolenaar  let add_indent = 0
16896f45c0bSBram Moolenaar  let cnt = 0
16996f45c0bSBram Moolenaar  for item in a:list
17096f45c0bSBram Moolenaar    for word in split(item, '\s\+\S\+\zs')
171*eab6dff1SBram Moolenaar      if match(word, '^\s\+$') > -1
172*eab6dff1SBram Moolenaar        " skip empty words
173*eab6dff1SBram Moolenaar        continue
174*eab6dff1SBram Moolenaar      endif
17596f45c0bSBram Moolenaar      let column += strdisplaywidth(word, column)
17696f45c0bSBram Moolenaar      if match(word, "^\\s*\n\\+\\s*$") > -1
17796f45c0bSBram Moolenaar        call add(result, '')
17896f45c0bSBram Moolenaar        let idx += 1
17996f45c0bSBram Moolenaar        let column = 0
18096f45c0bSBram Moolenaar        let add_indent = 1
18196f45c0bSBram Moolenaar      elseif column > limit || cnt == 0
18296f45c0bSBram Moolenaar        let add = s:Indent(s:Trim(word))
18396f45c0bSBram Moolenaar        call add(result, add)
18496f45c0bSBram Moolenaar        let column = strdisplaywidth(add)
18596f45c0bSBram Moolenaar        let idx += 1
18696f45c0bSBram Moolenaar      else
18796f45c0bSBram Moolenaar        if add_indent
18896f45c0bSBram Moolenaar          let result[idx] = s:Indent(s:Trim(word))
18996f45c0bSBram Moolenaar        else
19096f45c0bSBram Moolenaar          let result[idx] .= ' '. s:Trim(word)
19196f45c0bSBram Moolenaar        endif
19296f45c0bSBram Moolenaar        let add_indent = 0
19396f45c0bSBram Moolenaar      endif
19496f45c0bSBram Moolenaar      let cnt += 1
19596f45c0bSBram Moolenaar    endfor
19696f45c0bSBram Moolenaar  endfor
19796f45c0bSBram Moolenaar  return result
19896f45c0bSBram Moolenaarendfunc
1997db25fedSBram Moolenaar" Restoration And Modelines: {{{1
2007db25fedSBram Moolenaarlet &cpo= s:keepcpo
2017db25fedSBram Moolenaarunlet s:keepcpo
2027db25fedSBram Moolenaar" Modeline {{{1
2037db25fedSBram Moolenaar" vim: fdm=marker fdl=0 ts=2 et sw=0 sts=-1
204