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