xmlformat.vim 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. " Vim plugin for formatting XML
  2. " Last Change: 2020 Jan 06
  3. " Version: 0.3
  4. " Author: Christian Brabandt <cb@256bit.org>
  5. " Repository: https://github.com/chrisbra/vim-xml-ftplugin
  6. " License: VIM License
  7. " Documentation: see :h xmlformat.txt (TODO!)
  8. " ---------------------------------------------------------------------
  9. " Load Once: {{{1
  10. if exists("g:loaded_xmlformat") || &cp
  11. finish
  12. endif
  13. let g:loaded_xmlformat = 1
  14. let s:keepcpo = &cpo
  15. set cpo&vim
  16. " Main function: Format the input {{{1
  17. func! xmlformat#Format() abort
  18. " only allow reformatting through the gq command
  19. " (e.g. Vim is in normal mode)
  20. if mode() != 'n'
  21. " do not fall back to internal formatting
  22. return 0
  23. endif
  24. let count_orig = v:count
  25. let sw = shiftwidth()
  26. let prev = prevnonblank(v:lnum-1)
  27. let s:indent = indent(prev)/sw
  28. let result = []
  29. let lastitem = prev ? getline(prev) : ''
  30. let is_xml_decl = 0
  31. " go through every line, but don't join all content together and join it
  32. " back. We might lose empty lines
  33. let list = getline(v:lnum, (v:lnum + count_orig - 1))
  34. let current = 0
  35. for line in list
  36. " Keep empty input lines?
  37. if empty(line)
  38. call add(result, '')
  39. continue
  40. elseif line !~# '<[/]\?[^>]*>'
  41. let nextmatch = match(list, '<[/]\?[^>]*>', current)
  42. if nextmatch > -1
  43. let line .= ' '. join(list[(current + 1):(nextmatch-1)], " ")
  44. call remove(list, current+1, nextmatch-1)
  45. endif
  46. endif
  47. " split on `>`, but don't split on very first opening <
  48. " this means, items can be like ['<tag>', 'tag content</tag>']
  49. for item in split(line, '.\@<=[>]\zs')
  50. if s:EndTag(item)
  51. call s:DecreaseIndent()
  52. call add(result, s:Indent(item))
  53. elseif s:EmptyTag(lastitem)
  54. call add(result, s:Indent(item))
  55. elseif s:StartTag(lastitem) && s:IsTag(item)
  56. let s:indent += 1
  57. call add(result, s:Indent(item))
  58. else
  59. if !s:IsTag(item)
  60. " Simply split on '<', if there is one,
  61. " but reformat according to &textwidth
  62. let t=split(item, '.<\@=\zs')
  63. " if the content fits well within a single line, add it there
  64. " so that the output looks like this:
  65. "
  66. " <foobar>1</foobar>
  67. if s:TagContent(lastitem) is# s:TagContent(t[1]) && strlen(result[-1]) + strlen(item) <= s:Textwidth()
  68. let result[-1] .= item
  69. let lastitem = t[1]
  70. continue
  71. endif
  72. " t should only contain 2 items, but just be safe here
  73. if s:IsTag(lastitem)
  74. let s:indent+=1
  75. endif
  76. let result+=s:FormatContent([t[0]])
  77. if s:EndTag(t[1])
  78. call s:DecreaseIndent()
  79. endif
  80. "for y in t[1:]
  81. let result+=s:FormatContent(t[1:])
  82. "endfor
  83. else
  84. call add(result, s:Indent(item))
  85. endif
  86. endif
  87. let lastitem = item
  88. endfor
  89. let current += 1
  90. endfor
  91. if !empty(result)
  92. let lastprevline = getline(v:lnum + count_orig)
  93. let delete_lastline = v:lnum + count_orig - 1 == line('$')
  94. exe v:lnum. ",". (v:lnum + count_orig - 1). 'd'
  95. call append(v:lnum - 1, result)
  96. " Might need to remove the last line, if it became empty because of the
  97. " append() call
  98. let last = v:lnum + len(result)
  99. " do not use empty(), it returns true for `empty(0)`
  100. if getline(last) is '' && lastprevline is '' && delete_lastline
  101. exe last. 'd'
  102. endif
  103. endif
  104. " do not run internal formatter!
  105. return 0
  106. endfunc
  107. " Check if given tag is XML Declaration header {{{1
  108. func! s:IsXMLDecl(tag) abort
  109. return a:tag =~? '^\s*<?xml\s\?\%(version="[^"]*"\)\?\s\?\%(encoding="[^"]*"\)\? ?>\s*$'
  110. endfunc
  111. " Return tag indented by current level {{{1
  112. func! s:Indent(item) abort
  113. return repeat(' ', shiftwidth()*s:indent). s:Trim(a:item)
  114. endfu
  115. " Return item trimmed from leading whitespace {{{1
  116. func! s:Trim(item) abort
  117. if exists('*trim')
  118. return trim(a:item)
  119. else
  120. return matchstr(a:item, '\S\+.*')
  121. endif
  122. endfunc
  123. " Check if tag is a new opening tag <tag> {{{1
  124. func! s:StartTag(tag) abort
  125. let is_comment = s:IsComment(a:tag)
  126. return a:tag =~? '^\s*<[^/?]' && !is_comment
  127. endfunc
  128. " Check if tag is a Comment start {{{1
  129. func! s:IsComment(tag) abort
  130. return a:tag =~? '<!--'
  131. endfunc
  132. " Remove one level of indentation {{{1
  133. func! s:DecreaseIndent() abort
  134. let s:indent = (s:indent > 0 ? s:indent - 1 : 0)
  135. endfunc
  136. " Check if tag is a closing tag </tag> {{{1
  137. func! s:EndTag(tag) abort
  138. return a:tag =~? '^\s*</'
  139. endfunc
  140. " Check that the tag is actually a tag and not {{{1
  141. " something like "foobar</foobar>"
  142. func! s:IsTag(tag) abort
  143. return s:Trim(a:tag)[0] == '<'
  144. endfunc
  145. " Check if tag is empty <tag/> {{{1
  146. func! s:EmptyTag(tag) abort
  147. return a:tag =~ '/>\s*$'
  148. endfunc
  149. func! s:TagContent(tag) abort "{{{1
  150. " Return content of a tag
  151. return substitute(a:tag, '^\s*<[/]\?\([^>]*\)>\s*$', '\1', '')
  152. endfunc
  153. func! s:Textwidth() abort "{{{1
  154. " return textwidth (or 80 if not set)
  155. return &textwidth == 0 ? 80 : &textwidth
  156. endfunc
  157. " Format input line according to textwidth {{{1
  158. func! s:FormatContent(list) abort
  159. let result=[]
  160. let limit = s:Textwidth()
  161. let column=0
  162. let idx = -1
  163. let add_indent = 0
  164. let cnt = 0
  165. for item in a:list
  166. for word in split(item, '\s\+\S\+\zs')
  167. if match(word, '^\s\+$') > -1
  168. " skip empty words
  169. continue
  170. endif
  171. let column += strdisplaywidth(word, column)
  172. if match(word, "^\\s*\n\\+\\s*$") > -1
  173. call add(result, '')
  174. let idx += 1
  175. let column = 0
  176. let add_indent = 1
  177. elseif column > limit || cnt == 0
  178. let add = s:Indent(s:Trim(word))
  179. call add(result, add)
  180. let column = strdisplaywidth(add)
  181. let idx += 1
  182. else
  183. if add_indent
  184. let result[idx] = s:Indent(s:Trim(word))
  185. else
  186. let result[idx] .= ' '. s:Trim(word)
  187. endif
  188. let add_indent = 0
  189. endif
  190. let cnt += 1
  191. endfor
  192. endfor
  193. return result
  194. endfunc
  195. " Restoration And Modelines: {{{1
  196. let &cpo= s:keepcpo
  197. unlet s:keepcpo
  198. " Modeline {{{1
  199. " vim: fdm=marker fdl=0 ts=2 et sw=0 sts=-1