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