1" Vim indent script for HTML 2" General: "{{{ 3" File: html.vim (Vimscript #2075) 4" Author: Andy Wokula <[email protected]> 5" Last Change: 2013 Jun 12 6" Rev Days: 13 7" Version: 0.9 8" Vim Version: Vim7 9" Description: 10" Improved version of the distributed html indent script, faster on a 11" range of lines. 12" 13" Credits: 14" indent/html.vim (2006 Jun 05) from J. Zellner 15" indent/css.vim (2006 Dec 20) from N. Weibull 16" 17" History: 18" 2012 Oct 21 (v0.9) added support for shiftwidth() 19" 2011 Sep 09 (v0.8) added HTML5 tags (thx to J. Zuckerman) 20" 2008 Apr 28 (v0.6) revised customization 21" 2008 Mar 09 (v0.5) fixed 'indk' issue (thx to C.J. Robinson) 22" }}} 23 24" Init Folklore, check user settings (2nd time ++) "{{{ 25if exists("b:did_indent") 26 finish 27endif 28let b:did_indent = 1 29 30setlocal indentexpr=HtmlIndent() 31setlocal indentkeys=o,O,<Return>,<>>,{,},!^F 32 33let b:indent = {"lnum": -1} 34let b:undo_indent = "set inde< indk<| unlet b:indent" 35 36" Load Once: 37if exists("*HtmlIndent") 38 call HtmlIndent_CheckUserSettings() 39 finish 40endif 41 42" Patch 7.3.694 43if exists('*shiftwidth') 44 let s:ShiftWidth = function('shiftwidth') 45else 46 func! s:ShiftWidth() 47 return &shiftwidth 48 endfunc 49endif 50 51let s:cpo_save = &cpo 52set cpo-=C 53"}}} 54 55func! HtmlIndent_CheckUserSettings() "{{{ 56 if exists("g:html_indent_inctags") 57 call s:AddITags(split(g:html_indent_inctags, ",")) 58 endif 59 if exists("g:html_indent_autotags") 60 call s:RemoveITags(split(g:html_indent_autotags, ",")) 61 endif 62 63 let indone = {"zero": 0 64 \,"auto": "indent(prevnonblank(v:lnum-1))" 65 \,"inc": "b:indent.blocktagind + s:ShiftWidth()"} 66 if exists("g:html_indent_script1") 67 let s:js1indent = get(indone, g:html_indent_script1, indone.zero) 68 endif 69 if exists("g:html_indent_style1") 70 let s:css1indent = get(indone, g:html_indent_style1, indone.zero) 71 endif 72endfunc "}}} 73 74" Init Script Vars "{{{ 75let s:usestate = 1 76let s:css1indent = 0 77let s:js1indent = 0 78" not to be changed: 79let s:endtags = [0,0,0,0,0,0,0,0] " some places unused 80let s:newstate = {} 81let s:countonly = 0 82 "}}} 83func! s:AddITags(taglist) "{{{ 84 for itag in a:taglist 85 let s:indent_tags[itag] = 1 86 let s:indent_tags['/'.itag] = -1 87 endfor 88endfunc "}}} 89func! s:AddBlockTag(tag, id, ...) "{{{ 90 if !(a:id >= 2 && a:id < 2+len(s:endtags)) 91 return 92 endif 93 let s:indent_tags[a:tag] = a:id 94 if a:0 == 0 95 let s:indent_tags['/'.a:tag] = -a:id 96 let s:endtags[a:id-2] = "</".a:tag.">" 97 else 98 let s:indent_tags[a:1] = -a:id 99 let s:endtags[a:id-2] = a:1 100 endif 101endfunc "}}} 102func! s:RemoveITags(taglist) "{{{ 103 " remove itags (protect blocktags from being removed) 104 for itag in a:taglist 105 if !has_key(s:indent_tags, itag) || s:indent_tags[itag] != 1 106 continue 107 endif 108 unlet s:indent_tags[itag] 109 if itag =~ '^\w\+$' 110 unlet s:indent_tags["/".itag] 111 endif 112 endfor 113endfunc "}}} 114" Add Indent Tags: {{{ 115if !exists("s:indent_tags") 116 let s:indent_tags = {} 117endif 118 119" old tags: 120call s:AddITags(['a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 121 \ 'blockquote', 'button', 'caption', 'center', 'cite', 'code', 'colgroup', 122 \ 'del', 'dfn', 'dir', 'div', 'dl', 'em', 'fieldset', 'font', 'form', 123 \ 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'i', 'iframe', 'ins', 'kbd', 124 \ 'label', 'legend', 'map', 'menu', 'noframes', 'noscript', 'object', 'ol', 125 \ 'optgroup', 'q', 's', 'samp', 'select', 'small', 'span', 'strong', 'sub', 126 \ 'sup', 'table', 'textarea', 'title', 'tt', 'u', 'ul', 'var', 'th', 'td', 127 \ 'tr', 'tfoot', 'thead']) 128 129" tags added 2011 Sep 09 (especially HTML5 tags): 130call s:AddITags(['area', 'article', 'aside', 'audio', 'bdi', 'canvas', 131 \ 'command', 'datalist', 'details', 'embed', 'figure', 'footer', 132 \ 'header', 'group', 'keygen', 'mark', 'math', 'meter', 'nav', 'output', 133 \ 'progress', 'ruby', 'section', 'svg', 'texture', 'time', 'video', 134 \ 'wbr', 'text']) 135 136"}}} 137" Add Block Tags: contain alien content "{{{ 138call s:AddBlockTag('pre', 2) 139call s:AddBlockTag('script', 3) 140call s:AddBlockTag('style', 4) 141call s:AddBlockTag('<!--', 5, '-->') 142"}}} 143 144func! s:CountITags(...) "{{{ 145 146 " relative indent steps for current line [unit &sw]: 147 let s:curind = 0 148 " relative indent steps for next line [unit &sw]: 149 let s:nextrel = 0 150 151 if a:0==0 152 let s:block = s:newstate.block 153 let tmpline = substitute(s:curline, '<\zs\/\=\w\+\>\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g') 154 if s:block == 3 155 let s:newstate.scripttype = s:GetScriptType(matchstr(tmpline, '\C.*<SCRIPT\>\zs[^>]*')) 156 endif 157 let s:newstate.block = s:block 158 else 159 let s:block = 0 " assume starting outside of a block 160 let s:countonly = 1 " don't change state 161 let tmpline = substitute(s:altline, '<\zs\/\=\w\+\>\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g') 162 let s:countonly = 0 163 endif 164endfunc "}}} 165func! s:CheckTag(itag) "{{{ 166 " "tag" or "/tag" or "<!--" or "-->" 167 let ind = get(s:indent_tags, a:itag) 168 if ind == -1 169 " closing tag 170 if s:block != 0 171 " ignore itag within a block 172 return "foo" 173 endif 174 if s:nextrel == 0 175 let s:curind -= 1 176 else 177 let s:nextrel -= 1 178 endif 179 " if s:curind >= 1 180 " let s:curind -= 1 181 " else 182 " let s:nextrel -= 1 183 " endif 184 elseif ind == 1 185 " opening tag 186 if s:block != 0 187 return "foo" 188 endif 189 let s:nextrel += 1 190 elseif ind != 0 191 " block-tag (opening or closing) 192 return s:Blocktag(a:itag, ind) 193 endif 194 " else ind==0 (other tag found): keep indent 195 return "foo" " no matter 196endfunc "}}} 197func! s:Blocktag(blocktag, ind) "{{{ 198 if a:ind > 0 199 " a block starts here 200 if s:block != 0 201 " already in a block (nesting) - ignore 202 " especially ignore comments after other blocktags 203 return "foo" 204 endif 205 let s:block = a:ind " block type 206 if s:countonly 207 return "foo" 208 endif 209 let s:newstate.blocklnr = v:lnum 210 " save allover indent for the endtag 211 let s:newstate.blocktagind = b:indent.baseindent + (s:nextrel + s:curind) * s:ShiftWidth() 212 if a:ind == 3 213 return "SCRIPT" " all except this must be lowercase 214 " line is to be checked again for the type attribute 215 endif 216 else 217 let s:block = 0 218 " we get here if starting and closing block-tag on same line 219 endif 220 return "foo" 221endfunc "}}} 222func! s:GetScriptType(str) "{{{ 223 if a:str == "" || a:str =~ "java" 224 return "javascript" 225 else 226 return "" 227 endif 228endfunc "}}} 229 230func! s:FreshState(lnum) "{{{ 231 " Look back in the file (lines 1 to a:lnum-1) to calc a state for line 232 " a:lnum. A state is to know ALL relevant details about the lines 233 " 1..a:lnum-1, initial calculating (here!) can be slow, but updating is 234 " fast (incremental). 235 " State: 236 " lnum last indented line == prevnonblank(a:lnum - 1) 237 " block = 0 a:lnum located within special tag: 0:none, 2:<pre>, 238 " 3:<script>, 4:<style>, 5:<!-- 239 " baseindent use this indent for line a:lnum as a start - kind of 240 " autoindent (if block==0) 241 " scripttype = '' type attribute of a script tag (if block==3) 242 " blocktagind indent for current opening (get) and closing (set) 243 " blocktag (if block!=0) 244 " blocklnr lnum of starting blocktag (if block!=0) 245 " inattr line {lnum} starts with attributes of a tag 246 let state = {} 247 let state.lnum = prevnonblank(a:lnum - 1) 248 let state.scripttype = "" 249 let state.blocktagind = -1 250 let state.block = 0 251 let state.baseindent = 0 252 let state.blocklnr = 0 253 let state.inattr = 0 254 255 if state.lnum == 0 256 return state 257 endif 258 259 " Heuristic: 260 " remember startline state.lnum 261 " look back for <pre, </pre, <script, </script, <style, </style tags 262 " remember stopline 263 " if opening tag found, 264 " assume a:lnum within block 265 " else 266 " look back in result range (stopline, startline) for comment 267 " \ delimiters (<!--, -->) 268 " if comment opener found, 269 " assume a:lnum within comment 270 " else 271 " assume usual html for a:lnum 272 " if a:lnum-1 has a closing comment 273 " look back to get indent of comment opener 274 " FI 275 276 " look back for blocktag 277 call cursor(a:lnum, 1) 278 let [stopline, stopcol] = searchpos('\c<\zs\/\=\%(pre\>\|script\>\|style\>\)', "bW") 279 " fugly ... why isn't there searchstr() 280 let tagline = tolower(getline(stopline)) 281 let blocktag = matchstr(tagline, '\/\=\%(pre\>\|script\>\|style\>\)', stopcol-1) 282 if stopline > 0 && blocktag[0] != "/" 283 " opening tag found, assume a:lnum within block 284 let state.block = s:indent_tags[blocktag] 285 if state.block == 3 286 let state.scripttype = s:GetScriptType(matchstr(tagline, '\>[^>]*', stopcol)) 287 endif 288 let state.blocklnr = stopline 289 " check preceding tags in the line: 290 let s:altline = tagline[: stopcol-2] 291 call s:CountITags(1) 292 let state.blocktagind = indent(stopline) + (s:curind + s:nextrel) * s:ShiftWidth() 293 return state 294 elseif stopline == state.lnum 295 " handle special case: previous line (= state.lnum) contains a 296 " closing blocktag which is preceded by line-noise; 297 " blocktag == "/..." 298 let swendtag = match(tagline, '^\s*</') >= 0 299 if !swendtag 300 let [bline, bcol] = searchpos('<'.blocktag[1:].'\>', "bW") 301 let s:altline = tolower(getline(bline)[: bcol-2]) 302 call s:CountITags(1) 303 let state.baseindent = indent(bline) + (s:nextrel+s:curline) * s:ShiftWidth() 304 return state 305 endif 306 endif 307 308 " else look back for comment 309 call cursor(a:lnum, 1) 310 let [comline, comcol, found] = searchpos('\(<!--\)\|-->', 'bpW', stopline) 311 if found == 2 312 " comment opener found, assume a:lnum within comment 313 let state.block = 5 314 let state.blocklnr = comline 315 " check preceding tags in the line: 316 let s:altline = tolower(getline(comline)[: comcol-2]) 317 call s:CountITags(1) 318 let state.blocktagind = indent(comline) + (s:curind + s:nextrel) * s:ShiftWidth() 319 return state 320 endif 321 322 " else within usual html 323 let s:altline = tolower(getline(state.lnum)) 324 " check a:lnum-1 for closing comment (we need indent from the opening line) 325 let comcol = stridx(s:altline, '-->') 326 if comcol >= 0 327 call cursor(state.lnum, comcol+1) 328 let [comline, comcol] = searchpos('<!--', 'bW') 329 if comline == state.lnum 330 let s:altline = s:altline[: comcol-2] 331 else 332 let s:altline = tolower(getline(comline)[: comcol-2]) 333 endif 334 call s:CountITags(1) 335 let state.baseindent = indent(comline) + (s:nextrel+s:curline) * s:ShiftWidth() 336 return state 337 " TODO check tags that follow "-->" 338 endif 339 340 " else no comments 341 call s:CountITags(1) 342 let state.baseindent = indent(state.lnum) + s:nextrel * s:ShiftWidth() 343 " line starts with end tag 344 let swendtag = match(s:altline, '^\s*</') >= 0 345 if !swendtag 346 let state.baseindent += s:curind * s:ShiftWidth() 347 endif 348 return state 349endfunc "}}} 350 351func! s:Alien2() "{{{ 352 " <pre> block 353 return -1 354endfunc "}}} 355func! s:Alien3() "{{{ 356 " <script> javascript 357 if prevnonblank(v:lnum-1) == b:indent.blocklnr 358 " indent for the first line after <script> 359 return eval(s:js1indent) 360 endif 361 if b:indent.scripttype == "javascript" 362 return cindent(v:lnum) 363 else 364 return -1 365 endif 366endfunc "}}} 367func! s:Alien4() "{{{ 368 " <style> 369 if prevnonblank(v:lnum-1) == b:indent.blocklnr 370 " indent for first content line 371 return eval(s:css1indent) 372 endif 373 return s:CSSIndent() 374endfunc 375 376func! s:CSSIndent() "{{{ 377 " adopted $VIMRUNTIME/indent/css.vim 378 if getline(v:lnum) =~ '^\s*[*}]' 379 return cindent(v:lnum) 380 endif 381 let minline = b:indent.blocklnr 382 let pnum = s:css_prevnoncomment(v:lnum - 1, minline) 383 if pnum <= minline 384 " < is to catch errors 385 " indent for first content line after comments 386 return eval(s:css1indent) 387 endif 388 let ind = indent(pnum) + s:css_countbraces(pnum, 1) * s:ShiftWidth() 389 let pline = getline(pnum) 390 if pline =~ '}\s*$' 391 let ind -= (s:css_countbraces(pnum, 0) - (pline =~ '^\s*}')) * s:ShiftWidth() 392 endif 393 return ind 394endfunc "}}} 395func! s:css_prevnoncomment(lnum, stopline) "{{{ 396 " caller starts from a line a:lnum-1 that is not a comment 397 let lnum = prevnonblank(a:lnum) 398 let ccol = match(getline(lnum), '\*/') 399 if ccol < 0 400 return lnum 401 endif 402 call cursor(lnum, ccol+1) 403 let lnum = search('/\*', 'bW', a:stopline) 404 if indent(".") == virtcol(".")-1 405 return prevnonblank(lnum-1) 406 else 407 return lnum 408 endif 409endfunc "}}} 410func! s:css_countbraces(lnum, count_open) "{{{ 411 let brs = substitute(getline(a:lnum),'[''"].\{-}[''"]\|/\*.\{-}\*/\|/\*.*$\|[^{}]','','g') 412 let n_open = 0 413 let n_close = 0 414 for brace in split(brs, '\zs') 415 if brace == "{" 416 let n_open += 1 417 elseif brace == "}" 418 if n_open > 0 419 let n_open -= 1 420 else 421 let n_close += 1 422 endif 423 endif 424 endfor 425 return a:count_open ? n_open : n_close 426endfunc "}}} 427 428"}}} 429func! s:Alien5() "{{{ 430 " <!-- --> 431 return -1 432endfunc "}}} 433 434func! HtmlIndent() "{{{ 435 let s:curline = tolower(getline(v:lnum)) 436 let indentunit = s:ShiftWidth() 437 438 let s:newstate = {} 439 let s:newstate.lnum = v:lnum 440 441 " does the line start with a closing tag? 442 let swendtag = match(s:curline, '^\s*</') >= 0 443 444 if prevnonblank(v:lnum-1) == b:indent.lnum && s:usestate 445 " use state (continue from previous line) 446 else 447 " start over (know nothing) 448 let b:indent = s:FreshState(v:lnum) 449 endif 450 451 if b:indent.block >= 2 452 " within block 453 let endtag = s:endtags[b:indent.block-2] 454 let blockend = stridx(s:curline, endtag) 455 if blockend >= 0 456 " block ends here 457 let s:newstate.block = 0 458 " calc indent for REST OF LINE (may start more blocks): 459 let s:curline = strpart(s:curline, blockend+strlen(endtag)) 460 call s:CountITags() 461 if swendtag && b:indent.block != 5 462 let indent = b:indent.blocktagind + s:curind * indentunit 463 let s:newstate.baseindent = indent + s:nextrel * indentunit 464 else 465 let indent = s:Alien{b:indent.block}() 466 let s:newstate.baseindent = b:indent.blocktagind + s:nextrel * indentunit 467 endif 468 call extend(b:indent, s:newstate, "force") 469 return indent 470 else 471 " block continues 472 " indent this line with alien method 473 let indent = s:Alien{b:indent.block}() 474 call extend(b:indent, s:newstate, "force") 475 return indent 476 endif 477 else 478 " not within a block - within usual html 479 " if < 2 then always 0 480 let s:newstate.block = b:indent.block 481 call s:CountITags() 482 if swendtag 483 let indent = b:indent.baseindent + s:curind * indentunit 484 let s:newstate.baseindent = indent + s:nextrel * indentunit 485 else 486 let indent = b:indent.baseindent 487 let s:newstate.baseindent = indent + (s:curind + s:nextrel) * indentunit 488 endif 489 call extend(b:indent, s:newstate, "force") 490 return indent 491 endif 492 493endfunc "}}} 494 495" check user settings (first time), clear cpo, Modeline: {{{1 496 497" DEBUG: 498com! -nargs=* IndHtmlLocal <args> 499 500call HtmlIndent_CheckUserSettings() 501 502let &cpo = s:cpo_save 503unlet s:cpo_save 504 505" vim:set fdm=marker ts=8: 506