neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

xmlformat.vim (6433B)


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