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