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