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