1" Vim indent script for HTML 2" Header: "{{{ 3" Maintainer: Bram Moolenaar 4" Original Author: Andy Wokula <[email protected]> 5" Last Change: 2016 Mar 30 6" Version: 1.0 7" Description: HTML indent script with cached state for faster indenting on a 8" range of lines. 9" Supports template systems through hooks. 10" Supports Closure stylesheets. 11" 12" Credits: 13" indent/html.vim (2006 Jun 05) from J. Zellner 14" indent/css.vim (2006 Dec 20) from N. Weibull 15" 16" History: 17" 2014 June (v1.0) overhaul (Bram) 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 33" "j1" is included to make cindent() work better with Javascript. 34setlocal cino=j1 35" "J1" should be included, but it doen't work properly before 7.4.355. 36if has("patch-7.4.355") 37 setlocal cino+=J1 38endif 39" Before patch 7.4.355 indenting after "(function() {" does not work well, add 40" )2 to limit paren search. 41if !has("patch-7.4.355") 42 setlocal cino+=)2 43endif 44 45" Needed for % to work when finding start/end of a tag. 46setlocal matchpairs+=<:> 47 48let b:undo_indent = "setlocal inde< indk< cino<" 49 50" b:hi_indent keeps state to speed up indenting consecutive lines. 51let b:hi_indent = {"lnum": -1} 52 53"""""" Code below this is loaded only once. """"" 54if exists("*HtmlIndent") && !exists('g:force_reload_html') 55 call HtmlIndent_CheckUserSettings() 56 finish 57endif 58 59" shiftwidth() exists since patch 7.3.694 60if exists('*shiftwidth') 61 let s:ShiftWidth = function('shiftwidth') 62else 63 func! s:ShiftWidth() 64 return &shiftwidth 65 endfunc 66endif 67 68" Allow for line continuation below. 69let s:cpo_save = &cpo 70set cpo-=C 71"}}} 72 73" Check and process settings from b:html_indent and g:html_indent... variables. 74" Prefer using buffer-local settings over global settings, so that there can 75" be defaults for all HTML files and exceptions for specific types of HTML 76" files. 77func! HtmlIndent_CheckUserSettings() 78 "{{{ 79 let inctags = '' 80 if exists("b:html_indent_inctags") 81 let inctags = b:html_indent_inctags 82 elseif exists("g:html_indent_inctags") 83 let inctags = g:html_indent_inctags 84 endif 85 let b:hi_tags = {} 86 if len(inctags) > 0 87 call s:AddITags(b:hi_tags, split(inctags, ",")) 88 endif 89 90 let autotags = '' 91 if exists("b:html_indent_autotags") 92 let autotags = b:html_indent_autotags 93 elseif exists("g:html_indent_autotags") 94 let autotags = g:html_indent_autotags 95 endif 96 let b:hi_removed_tags = {} 97 if len(autotags) > 0 98 call s:RemoveITags(b:hi_removed_tags, split(autotags, ",")) 99 endif 100 101 " Syntax names indicating being inside a string of an attribute value. 102 let string_names = [] 103 if exists("b:html_indent_string_names") 104 let string_names = b:html_indent_string_names 105 elseif exists("g:html_indent_string_names") 106 let string_names = g:html_indent_string_names 107 endif 108 let b:hi_insideStringNames = ['htmlString'] 109 if len(string_names) > 0 110 for s in string_names 111 call add(b:hi_insideStringNames, s) 112 endfor 113 endif 114 115 " Syntax names indicating being inside a tag. 116 let tag_names = [] 117 if exists("b:html_indent_tag_names") 118 let tag_names = b:html_indent_tag_names 119 elseif exists("g:html_indent_tag_names") 120 let tag_names = g:html_indent_tag_names 121 endif 122 let b:hi_insideTagNames = ['htmlTag', 'htmlScriptTag'] 123 if len(tag_names) > 0 124 for s in tag_names 125 call add(b:hi_insideTagNames, s) 126 endfor 127 endif 128 129 let indone = {"zero": 0 130 \,"auto": "indent(prevnonblank(v:lnum-1))" 131 \,"inc": "b:hi_indent.blocktagind + s:ShiftWidth()"} 132 133 let script1 = '' 134 if exists("b:html_indent_script1") 135 let script1 = b:html_indent_script1 136 elseif exists("g:html_indent_script1") 137 let script1 = g:html_indent_script1 138 endif 139 if len(script1) > 0 140 let b:hi_js1indent = get(indone, script1, indone.zero) 141 else 142 let b:hi_js1indent = 0 143 endif 144 145 let style1 = '' 146 if exists("b:html_indent_style1") 147 let style1 = b:html_indent_style1 148 elseif exists("g:html_indent_style1") 149 let style1 = g:html_indent_style1 150 endif 151 if len(style1) > 0 152 let b:hi_css1indent = get(indone, style1, indone.zero) 153 else 154 let b:hi_css1indent = 0 155 endif 156 157 if !exists('b:html_indent_line_limit') 158 if exists('g:html_indent_line_limit') 159 let b:html_indent_line_limit = g:html_indent_line_limit 160 else 161 let b:html_indent_line_limit = 200 162 endif 163 endif 164endfunc "}}} 165 166" Init Script Vars 167"{{{ 168let b:hi_lasttick = 0 169let b:hi_newstate = {} 170let s:countonly = 0 171 "}}} 172 173" Fill the s:indent_tags dict with known tags. 174" The key is "tagname" or "/tagname". {{{ 175" The value is: 176" 1 opening tag 177" 2 "pre" 178" 3 "script" 179" 4 "style" 180" 5 comment start 181" 6 conditional comment start 182" -1 closing tag 183" -2 "/pre" 184" -3 "/script" 185" -4 "/style" 186" -5 comment end 187" -6 conditional comment end 188let s:indent_tags = {} 189let s:endtags = [0,0,0,0,0,0,0] " long enough for the highest index 190"}}} 191 192" Add a list of tag names for a pair of <tag> </tag> to "tags". 193func! s:AddITags(tags, taglist) 194 "{{{ 195 for itag in a:taglist 196 let a:tags[itag] = 1 197 let a:tags['/' . itag] = -1 198 endfor 199endfunc "}}} 200 201" Take a list of tag name pairs that are not to be used as tag pairs. 202func! s:RemoveITags(tags, taglist) 203 "{{{ 204 for itag in a:taglist 205 let a:tags[itag] = 1 206 let a:tags['/' . itag] = 1 207 endfor 208endfunc "}}} 209 210" Add a block tag, that is a tag with a different kind of indenting. 211func! s:AddBlockTag(tag, id, ...) 212 "{{{ 213 if !(a:id >= 2 && a:id < len(s:endtags)) 214 echoerr 'AddBlockTag ' . a:id 215 return 216 endif 217 let s:indent_tags[a:tag] = a:id 218 if a:0 == 0 219 let s:indent_tags['/' . a:tag] = -a:id 220 let s:endtags[a:id] = "</" . a:tag . ">" 221 else 222 let s:indent_tags[a:1] = -a:id 223 let s:endtags[a:id] = a:1 224 endif 225endfunc "}}} 226 227" Add known tag pairs. 228" Self-closing tags and tags that are sometimes {{{ 229" self-closing (e.g., <p>) are not here (when encountering </p> we can find 230" the matching <p>, but not the other way around). 231" Old HTML tags: 232call s:AddITags(s:indent_tags, [ 233 \ 'a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big', 234 \ 'blockquote', 'body', 'button', 'caption', 'center', 'cite', 'code', 235 \ 'colgroup', 'del', 'dfn', 'dir', 'div', 'dl', 'em', 'fieldset', 'font', 236 \ 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'html', 237 \ 'i', 'iframe', 'ins', 'kbd', 'label', 'legend', 'li', 238 \ 'map', 'menu', 'noframes', 'noscript', 'object', 'ol', 239 \ 'optgroup', 'q', 's', 'samp', 'select', 'small', 'span', 'strong', 'sub', 240 \ 'sup', 'table', 'textarea', 'title', 'tt', 'u', 'ul', 'var', 'th', 'td', 241 \ 'tr', 'tbody', 'tfoot', 'thead']) 242 243" New HTML5 elements: 244call s:AddITags(s:indent_tags, [ 245 \ 'area', 'article', 'aside', 'audio', 'bdi', 'canvas', 246 \ 'command', 'data', 'datalist', 'details', 'embed', 'figcaption', 247 \ 'figure', 'footer', 'header', 'keygen', 'mark', 'meter', 'nav', 'output', 248 \ 'progress', 'rp', 'rt', 'ruby', 'section', 'source', 'summary', 'svg', 249 \ 'time', 'track', 'video', 'wbr']) 250 251" Tags added for web components: 252call s:AddITags(s:indent_tags, [ 253 \ 'content', 'shadow', 'template']) 254"}}} 255 256" Add Block Tags: these contain alien content 257"{{{ 258call s:AddBlockTag('pre', 2) 259call s:AddBlockTag('script', 3) 260call s:AddBlockTag('style', 4) 261call s:AddBlockTag('<!--', 5, '-->') 262call s:AddBlockTag('<!--[', 6, '![endif]-->') 263"}}} 264 265" Return non-zero when "tagname" is an opening tag, not being a block tag, for 266" which there should be a closing tag. Can be used by scripts that include 267" HTML indenting. 268func! HtmlIndent_IsOpenTag(tagname) 269 "{{{ 270 if get(s:indent_tags, a:tagname) == 1 271 return 1 272 endif 273 return get(b:hi_tags, a:tagname) == 1 274endfunc "}}} 275 276" Get the value for "tagname", taking care of buffer-local tags. 277func! s:get_tag(tagname) 278 "{{{ 279 let i = get(s:indent_tags, a:tagname) 280 if (i == 1 || i == -1) && get(b:hi_removed_tags, a:tagname) != 0 281 return 0 282 endif 283 if i == 0 284 let i = get(b:hi_tags, a:tagname) 285 endif 286 return i 287endfunc "}}} 288 289" Count the number of start and end tags in "text". 290func! s:CountITags(text) 291 "{{{ 292 " Store the result in s:curind and s:nextrel. 293 let s:curind = 0 " relative indent steps for current line [unit &sw]: 294 let s:nextrel = 0 " relative indent steps for next line [unit &sw]: 295 let s:block = 0 " assume starting outside of a block 296 let s:countonly = 1 " don't change state 297 call substitute(a:text, '<\zs/\=\w\+\(-\w\+\)*\>\|<!--\[\|\[endif\]-->\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g') 298 let s:countonly = 0 299endfunc "}}} 300 301" Count the number of start and end tags in text. 302func! s:CountTagsAndState(text) 303 "{{{ 304 " Store the result in s:curind and s:nextrel. Update b:hi_newstate.block. 305 let s:curind = 0 " relative indent steps for current line [unit &sw]: 306 let s:nextrel = 0 " relative indent steps for next line [unit &sw]: 307 308 let s:block = b:hi_newstate.block 309 let tmp = substitute(a:text, '<\zs/\=\w\+\(-\w\+\)*\>\|<!--\[\|\[endif\]-->\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g') 310 if s:block == 3 311 let b:hi_newstate.scripttype = s:GetScriptType(matchstr(tmp, '\C.*<SCRIPT\>\zs[^>]*')) 312 endif 313 let b:hi_newstate.block = s:block 314endfunc "}}} 315 316" Used by s:CountITags() and s:CountTagsAndState(). 317func! s:CheckTag(itag) 318 "{{{ 319 " Returns an empty string or "SCRIPT". 320 " a:itag can be "tag" or "/tag" or "<!--" or "-->" 321 if (s:CheckCustomTag(a:itag)) 322 return "" 323 endif 324 let ind = s:get_tag(a:itag) 325 if ind == -1 326 " closing tag 327 if s:block != 0 328 " ignore itag within a block 329 return "" 330 endif 331 if s:nextrel == 0 332 let s:curind -= 1 333 else 334 let s:nextrel -= 1 335 endif 336 elseif ind == 1 337 " opening tag 338 if s:block != 0 339 return "" 340 endif 341 let s:nextrel += 1 342 elseif ind != 0 343 " block-tag (opening or closing) 344 return s:CheckBlockTag(a:itag, ind) 345 " else ind==0 (other tag found): keep indent 346 endif 347 return "" 348endfunc "}}} 349 350" Used by s:CheckTag(). Returns an empty string or "SCRIPT". 351func! s:CheckBlockTag(blocktag, ind) 352 "{{{ 353 if a:ind > 0 354 " a block starts here 355 if s:block != 0 356 " already in a block (nesting) - ignore 357 " especially ignore comments after other blocktags 358 return "" 359 endif 360 let s:block = a:ind " block type 361 if s:countonly 362 return "" 363 endif 364 let b:hi_newstate.blocklnr = v:lnum 365 " save allover indent for the endtag 366 let b:hi_newstate.blocktagind = b:hi_indent.baseindent + (s:nextrel + s:curind) * s:ShiftWidth() 367 if a:ind == 3 368 return "SCRIPT" " all except this must be lowercase 369 " line is to be checked again for the type attribute 370 endif 371 else 372 let s:block = 0 373 " we get here if starting and closing a block-tag on the same line 374 endif 375 return "" 376endfunc "}}} 377 378" Used by s:CheckTag(). 379func! s:CheckCustomTag(ctag) 380 "{{{ 381 " Returns 1 if ctag is the tag for a custom element, 0 otherwise. 382 " a:ctag can be "tag" or "/tag" or "<!--" or "-->" 383 let pattern = '\%\(\w\+-\)\+\w\+' 384 if match(a:ctag, pattern) == -1 385 return 0 386 endif 387 if matchstr(a:ctag, '\/\ze.\+') == "/" 388 " closing tag 389 if s:block != 0 390 " ignore ctag within a block 391 return 1 392 endif 393 if s:nextrel == 0 394 let s:curind -= 1 395 else 396 let s:nextrel -= 1 397 endif 398 else 399 " opening tag 400 if s:block != 0 401 return 1 402 endif 403 let s:nextrel += 1 404 endif 405 return 1 406endfunc "}}} 407 408" Return the <script> type: either "javascript" or "" 409func! s:GetScriptType(str) 410 "{{{ 411 if a:str == "" || a:str =~ "java" 412 return "javascript" 413 else 414 return "" 415 endif 416endfunc "}}} 417 418" Look back in the file, starting at a:lnum - 1, to compute a state for the 419" start of line a:lnum. Return the new state. 420func! s:FreshState(lnum) 421 "{{{ 422 " A state is to know ALL relevant details about the 423 " lines 1..a:lnum-1, initial calculating (here!) can be slow, but updating is 424 " fast (incremental). 425 " TODO: this should be split up in detecting the block type and computing the 426 " indent for the block type, so that when we do not know the indent we do 427 " not need to clear the whole state and re-detect the block type again. 428 " State: 429 " lnum last indented line == prevnonblank(a:lnum - 1) 430 " block = 0 a:lnum located within special tag: 0:none, 2:<pre>, 431 " 3:<script>, 4:<style>, 5:<!--, 6:<!--[ 432 " baseindent use this indent for line a:lnum as a start - kind of 433 " autoindent (if block==0) 434 " scripttype = '' type attribute of a script tag (if block==3) 435 " blocktagind indent for current opening (get) and closing (set) 436 " blocktag (if block!=0) 437 " blocklnr lnum of starting blocktag (if block!=0) 438 " inattr line {lnum} starts with attributes of a tag 439 let state = {} 440 let state.lnum = prevnonblank(a:lnum - 1) 441 let state.scripttype = "" 442 let state.blocktagind = -1 443 let state.block = 0 444 let state.baseindent = 0 445 let state.blocklnr = 0 446 let state.inattr = 0 447 448 if state.lnum == 0 449 return state 450 endif 451 452 " Heuristic: 453 " remember startline state.lnum 454 " look back for <pre, </pre, <script, </script, <style, </style tags 455 " remember stopline 456 " if opening tag found, 457 " assume a:lnum within block 458 " else 459 " look back in result range (stopline, startline) for comment 460 " \ delimiters (<!--, -->) 461 " if comment opener found, 462 " assume a:lnum within comment 463 " else 464 " assume usual html for a:lnum 465 " if a:lnum-1 has a closing comment 466 " look back to get indent of comment opener 467 " FI 468 469 " look back for a blocktag 470 let stopline2 = v:lnum + 1 471 if has_key(b:hi_indent, 'block') && b:hi_indent.block > 5 472 let [stopline2, stopcol2] = searchpos('<!--', 'bnW') 473 endif 474 let [stopline, stopcol] = searchpos('\c<\zs\/\=\%(pre\>\|script\>\|style\>\)', "bnW") 475 if stopline > 0 && stopline < stopline2 476 " ugly ... why isn't there searchstr() 477 let tagline = tolower(getline(stopline)) 478 let blocktag = matchstr(tagline, '\/\=\%(pre\>\|script\>\|style\>\)', stopcol - 1) 479 if blocktag[0] != "/" 480 " opening tag found, assume a:lnum within block 481 let state.block = s:indent_tags[blocktag] 482 if state.block == 3 483 let state.scripttype = s:GetScriptType(matchstr(tagline, '\>[^>]*', stopcol)) 484 endif 485 let state.blocklnr = stopline 486 " check preceding tags in the line: 487 call s:CountITags(tagline[: stopcol-2]) 488 let state.blocktagind = indent(stopline) + (s:curind + s:nextrel) * s:ShiftWidth() 489 return state 490 elseif stopline == state.lnum 491 " handle special case: previous line (= state.lnum) contains a 492 " closing blocktag which is preceded by line-noise; 493 " blocktag == "/..." 494 let swendtag = match(tagline, '^\s*</') >= 0 495 if !swendtag 496 let [bline, bcol] = searchpos('<'.blocktag[1:].'\>', "bnW") 497 call s:CountITags(tolower(getline(bline)[: bcol-2])) 498 let state.baseindent = indent(bline) + (s:curind + s:nextrel) * s:ShiftWidth() 499 return state 500 endif 501 endif 502 endif 503 if stopline > stopline2 504 let stopline = stopline2 505 let stopcol = stopcol2 506 endif 507 508 " else look back for comment 509 let [comlnum, comcol, found] = searchpos('\(<!--\[\)\|\(<!--\)\|-->', 'bpnW', stopline) 510 if found == 2 || found == 3 511 " comment opener found, assume a:lnum within comment 512 let state.block = (found == 3 ? 5 : 6) 513 let state.blocklnr = comlnum 514 " check preceding tags in the line: 515 call s:CountITags(tolower(getline(comlnum)[: comcol-2])) 516 if found == 2 517 let state.baseindent = b:hi_indent.baseindent 518 endif 519 let state.blocktagind = indent(comlnum) + (s:curind + s:nextrel) * s:ShiftWidth() 520 return state 521 endif 522 523 " else within usual HTML 524 let text = tolower(getline(state.lnum)) 525 526 " Check a:lnum-1 for closing comment (we need indent from the opening line). 527 " Not when other tags follow (might be --> inside a string). 528 let comcol = stridx(text, '-->') 529 if comcol >= 0 && match(text, '[<>]', comcol) <= 0 530 call cursor(state.lnum, comcol + 1) 531 let [comlnum, comcol] = searchpos('<!--', 'bW') 532 if comlnum == state.lnum 533 let text = text[: comcol-2] 534 else 535 let text = tolower(getline(comlnum)[: comcol-2]) 536 endif 537 call s:CountITags(text) 538 let state.baseindent = indent(comlnum) + (s:curind + s:nextrel) * s:ShiftWidth() 539 " TODO check tags that follow "-->" 540 return state 541 endif 542 543 " Check if the previous line starts with end tag. 544 let swendtag = match(text, '^\s*</') >= 0 545 546 " If previous line ended in a closing tag, line up with the opening tag. 547 if !swendtag && text =~ '</\w\+\s*>\s*$' 548 call cursor(state.lnum, 99999) 549 normal! F< 550 let start_lnum = HtmlIndent_FindStartTag() 551 if start_lnum > 0 552 let state.baseindent = indent(start_lnum) 553 if col('.') > 2 554 " check for tags before the matching opening tag. 555 let text = getline(start_lnum) 556 let swendtag = match(text, '^\s*</') >= 0 557 call s:CountITags(text[: col('.') - 2]) 558 let state.baseindent += s:nextrel * s:ShiftWidth() 559 if !swendtag 560 let state.baseindent += s:curind * s:ShiftWidth() 561 endif 562 endif 563 return state 564 endif 565 endif 566 567 " Else: no comments. Skip backwards to find the tag we're inside. 568 let [state.lnum, found] = HtmlIndent_FindTagStart(state.lnum) 569 " Check if that line starts with end tag. 570 let text = getline(state.lnum) 571 let swendtag = match(text, '^\s*</') >= 0 572 call s:CountITags(tolower(text)) 573 let state.baseindent = indent(state.lnum) + s:nextrel * s:ShiftWidth() 574 if !swendtag 575 let state.baseindent += s:curind * s:ShiftWidth() 576 endif 577 return state 578endfunc "}}} 579 580" Indent inside a <pre> block: Keep indent as-is. 581func! s:Alien2() 582 "{{{ 583 return -1 584endfunc "}}} 585 586" Return the indent inside a <script> block for javascript. 587func! s:Alien3() 588 "{{{ 589 let lnum = prevnonblank(v:lnum - 1) 590 while lnum > 1 && getline(lnum) =~ '^\s*/[/*]' 591 " Skip over comments to avoid that cindent() aligns with the <script> tag 592 let lnum = prevnonblank(lnum - 1) 593 endwhile 594 if lnum == b:hi_indent.blocklnr 595 " indent for the first line after <script> 596 return eval(b:hi_js1indent) 597 endif 598 if b:hi_indent.scripttype == "javascript" 599 return cindent(v:lnum) 600 else 601 return -1 602 endif 603endfunc "}}} 604 605" Return the indent inside a <style> block. 606func! s:Alien4() 607 "{{{ 608 if prevnonblank(v:lnum-1) == b:hi_indent.blocklnr 609 " indent for first content line 610 return eval(b:hi_css1indent) 611 endif 612 return s:CSSIndent() 613endfunc "}}} 614 615" Indending inside a <style> block. Returns the indent. 616func! s:CSSIndent() 617 "{{{ 618 " This handles standard CSS and also Closure stylesheets where special lines 619 " start with @. 620 " When the line starts with '*' or the previous line starts with "/*" 621 " and does not end in "*/", use C indenting to format the comment. 622 " Adopted $VIMRUNTIME/indent/css.vim 623 let curtext = getline(v:lnum) 624 if curtext =~ '^\s*[*]' 625 \ || (v:lnum > 1 && getline(v:lnum - 1) =~ '\s*/\*' 626 \ && getline(v:lnum - 1) !~ '\*/\s*$') 627 return cindent(v:lnum) 628 endif 629 630 let min_lnum = b:hi_indent.blocklnr 631 let prev_lnum = s:CssPrevNonComment(v:lnum - 1, min_lnum) 632 let [prev_lnum, found] = HtmlIndent_FindTagStart(prev_lnum) 633 if prev_lnum <= min_lnum 634 " Just below the <style> tag, indent for first content line after comments. 635 return eval(b:hi_css1indent) 636 endif 637 638 " If the current line starts with "}" align with it's match. 639 if curtext =~ '^\s*}' 640 call cursor(v:lnum, 1) 641 try 642 normal! % 643 " Found the matching "{", align with it after skipping unfinished lines. 644 let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum) 645 return indent(align_lnum) 646 catch 647 " can't find it, try something else, but it's most likely going to be 648 " wrong 649 endtry 650 endif 651 652 " add indent after { 653 let brace_counts = HtmlIndent_CountBraces(prev_lnum) 654 let extra = brace_counts.c_open * s:ShiftWidth() 655 656 let prev_text = getline(prev_lnum) 657 let below_end_brace = prev_text =~ '}\s*$' 658 659 " Search back to align with the first line that's unfinished. 660 let align_lnum = s:CssFirstUnfinished(prev_lnum, min_lnum) 661 662 " Handle continuation lines if aligning with previous line and not after a 663 " "}". 664 if extra == 0 && align_lnum == prev_lnum && !below_end_brace 665 let prev_hasfield = prev_text =~ '^\s*[a-zA-Z0-9-]\+:' 666 let prev_special = prev_text =~ '^\s*\(/\*\|@\)' 667 if curtext =~ '^\s*\(/\*\|@\)' 668 " if the current line is not a comment or starts with @ (used by template 669 " systems) reduce indent if previous line is a continuation line 670 if !prev_hasfield && !prev_special 671 let extra = -s:ShiftWidth() 672 endif 673 else 674 let cur_hasfield = curtext =~ '^\s*[a-zA-Z0-9-]\+:' 675 let prev_unfinished = s:CssUnfinished(prev_text) 676 if !cur_hasfield && (prev_hasfield || prev_unfinished) 677 " Continuation line has extra indent if the previous line was not a 678 " continuation line. 679 let extra = s:ShiftWidth() 680 " Align with @if 681 if prev_text =~ '^\s*@if ' 682 let extra = 4 683 endif 684 elseif cur_hasfield && !prev_hasfield && !prev_special 685 " less indent below a continuation line 686 let extra = -s:ShiftWidth() 687 endif 688 endif 689 endif 690 691 if below_end_brace 692 " find matching {, if that line starts with @ it's not the start of a rule 693 " but something else from a template system 694 call cursor(prev_lnum, 1) 695 call search('}\s*$') 696 try 697 normal! % 698 " Found the matching "{", align with it. 699 let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum) 700 let special = getline(align_lnum) =~ '^\s*@' 701 catch 702 let special = 0 703 endtry 704 if special 705 " do not reduce indent below @{ ... } 706 if extra < 0 707 let extra += s:ShiftWidth() 708 endif 709 else 710 let extra -= (brace_counts.c_close - (prev_text =~ '^\s*}')) * s:ShiftWidth() 711 endif 712 endif 713 714 " if no extra indent yet... 715 if extra == 0 716 if brace_counts.p_open > brace_counts.p_close 717 " previous line has more ( than ): add a shiftwidth 718 let extra = s:ShiftWidth() 719 elseif brace_counts.p_open < brace_counts.p_close 720 " previous line has more ) than (: subtract a shiftwidth 721 let extra = -s:ShiftWidth() 722 endif 723 endif 724 725 return indent(align_lnum) + extra 726endfunc "}}} 727 728" Inside <style>: Whether a line is unfinished. 729func! s:CssUnfinished(text) 730 "{{{ 731 return a:text =~ '\s\(||\|&&\|:\)\s*$' 732endfunc "}}} 733 734" Search back for the first unfinished line above "lnum". 735func! s:CssFirstUnfinished(lnum, min_lnum) 736 "{{{ 737 let align_lnum = a:lnum 738 while align_lnum > a:min_lnum && s:CssUnfinished(getline(align_lnum - 1)) 739 let align_lnum -= 1 740 endwhile 741 return align_lnum 742endfunc "}}} 743 744" Find the non-empty line at or before "lnum" that is not a comment. 745func! s:CssPrevNonComment(lnum, stopline) 746 "{{{ 747 " caller starts from a line a:lnum + 1 that is not a comment 748 let lnum = prevnonblank(a:lnum) 749 while 1 750 let ccol = match(getline(lnum), '\*/') 751 if ccol < 0 752 " No comment end thus its something else. 753 return lnum 754 endif 755 call cursor(lnum, ccol + 1) 756 " Search back for the /* that starts the comment 757 let lnum = search('/\*', 'bW', a:stopline) 758 if indent(".") == virtcol(".") - 1 759 " The found /* is at the start of the line. Now go back to the line 760 " above it and again check if it is a comment. 761 let lnum = prevnonblank(lnum - 1) 762 else 763 " /* is after something else, thus it's not a comment line. 764 return lnum 765 endif 766 endwhile 767endfunc "}}} 768 769" Check the number of {} and () in line "lnum". Return a dict with the counts. 770func! HtmlIndent_CountBraces(lnum) 771 "{{{ 772 let brs = substitute(getline(a:lnum), '[''"].\{-}[''"]\|/\*.\{-}\*/\|/\*.*$\|[^{}()]', '', 'g') 773 let c_open = 0 774 let c_close = 0 775 let p_open = 0 776 let p_close = 0 777 for brace in split(brs, '\zs') 778 if brace == "{" 779 let c_open += 1 780 elseif brace == "}" 781 if c_open > 0 782 let c_open -= 1 783 else 784 let c_close += 1 785 endif 786 elseif brace == '(' 787 let p_open += 1 788 elseif brace == ')' 789 if p_open > 0 790 let p_open -= 1 791 else 792 let p_close += 1 793 endif 794 endif 795 endfor 796 return {'c_open': c_open, 797 \ 'c_close': c_close, 798 \ 'p_open': p_open, 799 \ 'p_close': p_close} 800endfunc "}}} 801 802" Return the indent for a comment: <!-- --> 803func! s:Alien5() 804 "{{{ 805 let curtext = getline(v:lnum) 806 if curtext =~ '^\s*\zs-->' 807 " current line starts with end of comment, line up with comment start. 808 call cursor(v:lnum, 0) 809 let lnum = search('<!--', 'b') 810 if lnum > 0 811 " TODO: what if <!-- is not at the start of the line? 812 return indent(lnum) 813 endif 814 815 " Strange, can't find it. 816 return -1 817 endif 818 819 let prevlnum = prevnonblank(v:lnum - 1) 820 let prevtext = getline(prevlnum) 821 let idx = match(prevtext, '^\s*\zs<!--') 822 if idx >= 0 823 " just below comment start, add a shiftwidth 824 return idx + s:ShiftWidth() 825 endif 826 827 " Some files add 4 spaces just below a TODO line. It's difficult to detect 828 " the end of the TODO, so let's not do that. 829 830 " Align with the previous non-blank line. 831 return indent(prevlnum) 832endfunc "}}} 833 834" Return the indent for conditional comment: <!--[ ![endif]--> 835func! s:Alien6() 836 "{{{ 837 let curtext = getline(v:lnum) 838 if curtext =~ '\s*\zs<!\[endif\]-->' 839 " current line starts with end of comment, line up with comment start. 840 let lnum = search('<!--', 'bn') 841 if lnum > 0 842 return indent(lnum) 843 endif 844 endif 845 return b:hi_indent.baseindent + s:ShiftWidth() 846endfunc "}}} 847 848" When the "lnum" line ends in ">" find the line containing the matching "<". 849func! HtmlIndent_FindTagStart(lnum) 850 "{{{ 851 " Avoids using the indent of a continuation line. 852 " Moves the cursor. 853 " Return two values: 854 " - the matching line number or "lnum". 855 " - a flag indicating whether we found the end of a tag. 856 " This method is global so that HTML-like indenters can use it. 857 " To avoid matching " > " or " < " inside a string require that the opening 858 " "<" is followed by a word character and the closing ">" comes after a 859 " non-white character. 860 let idx = match(getline(a:lnum), '\S>\s*$') 861 if idx > 0 862 call cursor(a:lnum, idx) 863 let lnum = searchpair('<\w', '' , '\S>', 'bW', '', max([a:lnum - b:html_indent_line_limit, 0])) 864 if lnum > 0 865 return [lnum, 1] 866 endif 867 endif 868 return [a:lnum, 0] 869endfunc "}}} 870 871" Find the unclosed start tag from the current cursor position. 872func! HtmlIndent_FindStartTag() 873 "{{{ 874 " The cursor must be on or before a closing tag. 875 " If found, positions the cursor at the match and returns the line number. 876 " Otherwise returns 0. 877 let tagname = matchstr(getline('.')[col('.') - 1:], '</\zs\w\+\ze') 878 let start_lnum = searchpair('<' . tagname . '\>', '', '</' . tagname . '\>', 'bW') 879 if start_lnum > 0 880 return start_lnum 881 endif 882 return 0 883endfunc "}}} 884 885" Moves the cursor from a "<" to the matching ">". 886func! HtmlIndent_FindTagEnd() 887 "{{{ 888 " Call this with the cursor on the "<" of a start tag. 889 " This will move the cursor to the ">" of the matching end tag or, when it's 890 " a self-closing tag, to the matching ">". 891 " Limited to look up to b:html_indent_line_limit lines away. 892 let text = getline('.') 893 let tagname = matchstr(text, '\w\+\|!--', col('.')) 894 if tagname == '!--' 895 call search('--\zs>') 896 elseif s:get_tag('/' . tagname) != 0 897 " tag with a closing tag, find matching "</tag>" 898 call searchpair('<' . tagname, '', '</' . tagname . '\zs>', 'W', '', line('.') + b:html_indent_line_limit) 899 else 900 " self-closing tag, find the ">" 901 call search('\S\zs>') 902 endif 903endfunc "}}} 904 905" Indenting inside a start tag. Return the correct indent or -1 if unknown. 906func! s:InsideTag(foundHtmlString) 907 "{{{ 908 if a:foundHtmlString 909 " Inside an attribute string. 910 " Align with the previous line or use an external function. 911 let lnum = v:lnum - 1 912 if lnum > 1 913 if exists('b:html_indent_tag_string_func') 914 return b:html_indent_tag_string_func(lnum) 915 endif 916 return indent(lnum) 917 endif 918 endif 919 920 " Should be another attribute: " attr="val". Align with the previous 921 " attribute start. 922 let lnum = v:lnum 923 while lnum > 1 924 let lnum -= 1 925 let text = getline(lnum) 926 " Find a match with one of these, align with "attr": 927 " attr= 928 " <tag attr= 929 " text<tag attr= 930 " <tag>text</tag>text<tag attr= 931 " For long lines search for the first match, finding the last match 932 " gets very slow. 933 if len(text) < 300 934 let idx = match(text, '.*\s\zs[_a-zA-Z0-9-]\+="') 935 else 936 let idx = match(text, '\s\zs[_a-zA-Z0-9-]\+="') 937 endif 938 if idx > 0 939 " Found the attribute. TODO: assumes spaces, no Tabs. 940 return idx 941 endif 942 endwhile 943 return -1 944endfunc "}}} 945 946" THE MAIN INDENT FUNCTION. Return the amount of indent for v:lnum. 947func! HtmlIndent() 948 "{{{ 949 if prevnonblank(v:lnum - 1) < 1 950 " First non-blank line has no indent. 951 return 0 952 endif 953 954 let curtext = tolower(getline(v:lnum)) 955 let indentunit = s:ShiftWidth() 956 957 let b:hi_newstate = {} 958 let b:hi_newstate.lnum = v:lnum 959 960 " When syntax HL is enabled, detect we are inside a tag. Indenting inside 961 " a tag works very differently. Do not do this when the line starts with 962 " "<", it gets the "htmlTag" ID but we are not inside a tag then. 963 if curtext !~ '^\s*<' 964 normal! ^ 965 let stack = synstack(v:lnum, col('.')) " assumes there are no tabs 966 let foundHtmlString = 0 967 for synid in reverse(stack) 968 let name = synIDattr(synid, "name") 969 if index(b:hi_insideStringNames, name) >= 0 970 let foundHtmlString = 1 971 elseif index(b:hi_insideTagNames, name) >= 0 972 " Yes, we are inside a tag. 973 let indent = s:InsideTag(foundHtmlString) 974 if indent >= 0 975 " Do not keep the state. TODO: could keep the block type. 976 let b:hi_indent.lnum = 0 977 return indent 978 endif 979 endif 980 endfor 981 endif 982 983 " does the line start with a closing tag? 984 let swendtag = match(curtext, '^\s*</') >= 0 985 986 if prevnonblank(v:lnum - 1) == b:hi_indent.lnum && b:hi_lasttick == b:changedtick - 1 987 " use state (continue from previous line) 988 else 989 " start over (know nothing) 990 let b:hi_indent = s:FreshState(v:lnum) 991 endif 992 993 if b:hi_indent.block >= 2 994 " within block 995 let endtag = s:endtags[b:hi_indent.block] 996 let blockend = stridx(curtext, endtag) 997 if blockend >= 0 998 " block ends here 999 let b:hi_newstate.block = 0 1000 " calc indent for REST OF LINE (may start more blocks): 1001 call s:CountTagsAndState(strpart(curtext, blockend + strlen(endtag))) 1002 if swendtag && b:hi_indent.block != 5 1003 let indent = b:hi_indent.blocktagind + s:curind * indentunit 1004 let b:hi_newstate.baseindent = indent + s:nextrel * indentunit 1005 else 1006 let indent = s:Alien{b:hi_indent.block}() 1007 let b:hi_newstate.baseindent = b:hi_indent.blocktagind + s:nextrel * indentunit 1008 endif 1009 else 1010 " block continues 1011 " indent this line with alien method 1012 let indent = s:Alien{b:hi_indent.block}() 1013 endif 1014 else 1015 " not within a block - within usual html 1016 let b:hi_newstate.block = b:hi_indent.block 1017 if swendtag 1018 " The current line starts with an end tag, align with its start tag. 1019 call cursor(v:lnum, 1) 1020 let start_lnum = HtmlIndent_FindStartTag() 1021 if start_lnum > 0 1022 " check for the line starting with something inside a tag: 1023 " <sometag <- align here 1024 " attr=val><open> not here 1025 let text = getline(start_lnum) 1026 let angle = matchstr(text, '[<>]') 1027 if angle == '>' 1028 call cursor(start_lnum, 1) 1029 normal! f>% 1030 let start_lnum = line('.') 1031 let text = getline(start_lnum) 1032 endif 1033 1034 let indent = indent(start_lnum) 1035 if col('.') > 2 1036 let swendtag = match(text, '^\s*</') >= 0 1037 call s:CountITags(text[: col('.') - 2]) 1038 let indent += s:nextrel * s:ShiftWidth() 1039 if !swendtag 1040 let indent += s:curind * s:ShiftWidth() 1041 endif 1042 endif 1043 else 1044 " not sure what to do 1045 let indent = b:hi_indent.baseindent 1046 endif 1047 let b:hi_newstate.baseindent = indent 1048 else 1049 call s:CountTagsAndState(curtext) 1050 let indent = b:hi_indent.baseindent 1051 let b:hi_newstate.baseindent = indent + (s:curind + s:nextrel) * indentunit 1052 endif 1053 endif 1054 1055 let b:hi_lasttick = b:changedtick 1056 call extend(b:hi_indent, b:hi_newstate, "force") 1057 return indent 1058endfunc "}}} 1059 1060" Check user settings when loading this script the first time. 1061call HtmlIndent_CheckUserSettings() 1062 1063let &cpo = s:cpo_save 1064unlet s:cpo_save 1065 1066" vim: fdm=marker ts=8 sw=2 tw=78 1067