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