summaryrefslogtreecommitdiffstats
path: root/runtime/autoload/xmlformat.vim
blob: c89c8784b90bea62465812c52cf277e4b46457dc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
" Vim plugin for formatting XML
" Last Change: 2020 Jan 06
"     Version: 0.3
"      Author: Christian Brabandt <cb@256bit.org>
"  Repository: https://github.com/chrisbra/vim-xml-ftplugin
"     License: VIM License
" Documentation: see :h xmlformat.txt (TODO!)
" ---------------------------------------------------------------------
" Load Once: {{{1
if exists("g:loaded_xmlformat") || &cp
  finish
endif
let g:loaded_xmlformat = 1
let s:keepcpo       = &cpo
set cpo&vim

" Main function: Format the input {{{1
func! xmlformat#Format() abort
  " only allow reformatting through the gq command
  " (e.g. Vim is in normal mode)
  if mode() != 'n'
    " do not fall back to internal formatting
    return 0
  endif
  let count_orig = v:count
  let sw  = shiftwidth()
  let prev = prevnonblank(v:lnum-1)
  let s:indent = indent(prev)/sw
  let result = []
  let lastitem = prev ? getline(prev) : ''
  let is_xml_decl = 0
  " go through every line, but don't join all content together and join it
  " back. We might lose empty lines
  let list = getline(v:lnum, (v:lnum + count_orig - 1))
  let current = 0
  for line in list
    " Keep empty input lines?
    if empty(line)
      call add(result, '')
      continue
    elseif line !~# '<[/]\?[^>]*>'
      let nextmatch = match(list, '<[/]\?[^>]*>', current)
      if nextmatch > -1 
        let line .= ' '. join(list[(current + 1):(nextmatch-1)], " ")
        call remove(list, current+1, nextmatch-1)
      endif
    endif
    " split on `>`, but don't split on very first opening <
    " this means, items can be like ['<tag>', 'tag content</tag>']
    for item in split(line, '.\@<=[>]\zs')
      if s:EndTag(item)
        call s:DecreaseIndent()
        call add(result, s:Indent(item))
      elseif s:EmptyTag(lastitem)
        call add(result, s:Indent(item))
      elseif s:StartTag(lastitem) && s:IsTag(item)
        let s:indent += 1
        call add(result, s:Indent(item))
      else
        if !s:IsTag(item)
          " Simply split on '<', if there is one,
          " but reformat according to &textwidth
          let t=split(item, '.<\@=\zs')

          " if the content fits well within a single line, add it there
          " so that the output looks like this:
          "
          " <foobar>1</foobar>
          if s:TagContent(lastitem) is# s:TagContent(t[1]) && strlen(result[-1]) + strlen(item) <= s:Textwidth()
            let result[-1] .= item
            let lastitem = t[1]
            continue
          endif
          " t should only contain 2 items, but just be safe here
          if s:IsTag(lastitem)
            let s:indent+=1
          endif
          let result+=s:FormatContent([t[0]])
          if s:EndTag(t[1])
            call s:DecreaseIndent()
          endif
          "for y in t[1:]
            let result+=s:FormatContent(t[1:])
          "endfor
        else
          call add(result, s:Indent(item))
        endif
      endif
      let lastitem = item
    endfor
    let current += 1
  endfor

  if !empty(result)
    let lastprevline = getline(v:lnum + count_orig)
    let delete_lastline = v:lnum + count_orig - 1 == line('$')
    exe v:lnum. ",". (v:lnum + count_orig - 1). 'd'
    call append(v:lnum - 1, result)
    " Might need to remove the last line, if it became empty because of the
    " append() call
    let last = v:lnum + len(result)
    " do not use empty(), it returns true for `empty(0)`
    if getline(last) is '' && lastprevline is '' && delete_lastline
      exe last. 'd'
    endif
  endif

  " do not run internal formatter!
  return 0
endfunc
" Check if given tag is XML Declaration header {{{1
func! s:IsXMLDecl(tag) abort
  return a:tag =~? '^\s*<?xml\s\?\%(version="[^"]*"\)\?\s\?\%(encoding="[^"]*"\)\? ?>\s*$'
endfunc
" Return tag indented by current level {{{1
func! s:Indent(item) abort
  return repeat(' ', shiftwidth()*s:indent). s:Trim(a:item)
endfu
" Return item trimmed from leading whitespace {{{1
func! s:Trim(item) abort
  if exists('*trim')
    return trim(a:item)
  else
    return matchstr(a:item, '\S\+.*')
  endif
endfunc
" Check if tag is a new opening tag <tag> {{{1
func! s:StartTag(tag) abort
  let is_comment = s:IsComment(a:tag)
  return a:tag =~? '^\s*<[^/?]' && !is_comment
endfunc
" Check if tag is a Comment start {{{1
func! s:IsComment(tag) abort
  return a:tag =~? '<!--'
endfunc
" Remove one level of indentation {{{1
func! s:DecreaseIndent() abort
  let s:indent = (s:indent > 0 ? s:indent - 1 : 0)
endfunc
" Check if tag is a closing tag </tag> {{{1
func! s:EndTag(tag) abort
  return a:tag =~? '^\s*</'
endfunc
" Check that the tag is actually a tag and not {{{1
" something like "foobar</foobar>"
func! s:IsTag(tag) abort
  return s:Trim(a:tag)[0] == '<'
endfunc
" Check if tag is empty <tag/> {{{1
func! s:EmptyTag(tag) abort
  return a:tag =~ '/>\s*$'
endfunc
func! s:TagContent(tag) abort "{{{1
  " Return content of a tag
  return substitute(a:tag, '^\s*<[/]\?\([^>]*\)>\s*$', '\1', '')
endfunc
func! s:Textwidth() abort "{{{1
  " return textwidth (or 80 if not set)
  return &textwidth == 0 ? 80 : &textwidth
endfunc
" Format input line according to textwidth {{{1
func! s:FormatContent(list) abort
  let result=[]
  let limit = s:Textwidth()
  let column=0
  let idx = -1
  let add_indent = 0
  let cnt = 0
  for item in a:list
    for word in split(item, '\s\+\S\+\zs') 
      if match(word, '^\s\+$') > -1
        " skip empty words
        continue
      endif
      let column += strdisplaywidth(word, column)
      if match(word, "^\\s*\n\\+\\s*$") > -1
        call add(result, '')
        let idx += 1
        let column = 0
        let add_indent = 1
      elseif column > limit || cnt == 0
        let add = s:Indent(s:Trim(word))
        call add(result, add)
        let column = strdisplaywidth(add)
        let idx += 1
      else
        if add_indent
          let result[idx] = s:Indent(s:Trim(word))
        else
          let result[idx] .= ' '. s:Trim(word)
        endif
        let add_indent = 0
      endif
      let cnt += 1
    endfor
  endfor
  return result
endfunc
" Restoration And Modelines: {{{1
let &cpo= s:keepcpo
unlet s:keepcpo
" Modeline {{{1
" vim: fdm=marker fdl=0 ts=2 et sw=0 sts=-1