1" Vim indent script for HTML 2" Maintainer: Bram Moolenaar 3" Original Author: Andy Wokula <[email protected]> 4" Last Change: 2021 Jun 13 5" Version: 1.0 "{{{ 6" Description: HTML indent script with cached state for faster indenting on a 7" range of lines. 8" Supports template systems through hooks. 9" Supports Closure stylesheets. 10" 11" Credits: 12" indent/html.vim (2006 Jun 05) from J. Zellner 13" indent/css.vim (2006 Dec 20) from N. Weibull 14" 15" History: 16" 2014 June (v1.0) overhaul (Bram) 17" 2012 Oct 21 (v0.9) added support for shiftwidth() 18" 2011 Sep 09 (v0.8) added HTML5 tags (thx to J. Zuckerman) 19" 2008 Apr 28 (v0.6) revised customization 20" 2008 Mar 09 (v0.5) fixed 'indk' issue (thx to C.J. Robinson) 21"}}} 22 23" Init Folklore, check user settings (2nd time ++) 24if exists("b:did_indent") "{{{ 25 finish 26endif 27 28" Load the Javascript indent script first, it defines GetJavascriptIndent(). 29" Undo the rest. 30" Load base python indent. 31if !exists('*GetJavascriptIndent') 32 runtime! indent/javascript.vim 33endif 34let b:did_indent = 1 35 36setlocal indentexpr=HtmlIndent() 37setlocal indentkeys=o,O,<Return>,<>>,{,},!^F 38 39" Needed for % to work when finding start/end of a tag. 40setlocal matchpairs+=<:> 41 42let b:undo_indent = "setlocal inde< indk<" 43 44" b:hi_indent keeps state to speed up indenting consecutive lines. 45let b:hi_indent = {"lnum": -1} 46 47"""""" Code below this is loaded only once. """"" 48if exists("*HtmlIndent") && !exists('g:force_reload_html') 49 call HtmlIndent_CheckUserSettings() 50 finish 51endif 52 53" Allow for line continuation below. 54let s:cpo_save = &cpo 55set cpo-=C 56"}}} 57 58" Pattern to match the name of a tag, including custom elements. 59let s:tagname = '\w\+\(-\w\+\)*' 60 61" Check and process settings from b:html_indent and g:html_indent... variables. 62" Prefer using buffer-local settings over global settings, so that there can 63" be defaults for all HTML files and exceptions for specific types of HTML 64" files. 65func HtmlIndent_CheckUserSettings() 66 "{{{ 67 let inctags = '' 68 if exists("b:html_indent_inctags") 69 let inctags = b:html_indent_inctags 70 elseif exists("g:html_indent_inctags") 71 let inctags = g:html_indent_inctags 72 endif 73 let b:hi_tags = {} 74 if len(inctags) > 0 75 call s:AddITags(b:hi_tags, split(inctags, ",")) 76 endif 77 78 let autotags = '' 79 if exists("b:html_indent_autotags") 80 let autotags = b:html_indent_autotags 81 elseif exists("g:html_indent_autotags") 82 let autotags = g:html_indent_autotags 83 endif 84 let b:hi_removed_tags = {} 85 if len(autotags) > 0 86 call s:RemoveITags(b:hi_removed_tags, split(autotags, ",")) 87 endif 88 89 " Syntax names indicating being inside a string of an attribute value. 90 let string_names = [] 91 if exists("b:html_indent_string_names") 92 let string_names = b:html_indent_string_names 93 elseif exists("g:html_indent_string_names") 94 let string_names = g:html_indent_string_names 95 endif 96 let b:hi_insideStringNames = ['htmlString'] 97 if len(string_names) > 0 98 for s in string_names 99 call add(b:hi_insideStringNames, s) 100 endfor 101 endif 102 103 " Syntax names indicating being inside a tag. 104 let tag_names = [] 105 if exists("b:html_indent_tag_names") 106 let tag_names = b:html_indent_tag_names 107 elseif exists("g:html_indent_tag_names") 108 let tag_names = g:html_indent_tag_names 109 endif 110 let b:hi_insideTagNames = ['htmlTag', 'htmlScriptTag'] 111 if len(tag_names) > 0 112 for s in tag_names 113 call add(b:hi_insideTagNames, s) 114 endfor 115 endif 116 117 let indone = {"zero": 0 118 \,"auto": "indent(prevnonblank(v:lnum-1))" 119 \,"inc": "b:hi_indent.blocktagind + shiftwidth()"} 120 121 let script1 = '' 122 if exists("b:html_indent_script1") 123 let script1 = b:html_indent_script1 124 elseif exists("g:html_indent_script1") 125 let script1 = g:html_indent_script1 126 endif 127 if len(script1) > 0 128 let b:hi_js1indent = get(indone, script1, indone.zero) 129 else 130 let b:hi_js1indent = 0 131 endif 132 133 let style1 = '' 134 if exists("b:html_indent_style1") 135 let style1 = b:html_indent_style1 136 elseif exists("g:html_indent_style1") 137 let style1 = g:html_indent_style1 138 endif 139 if len(style1) > 0 140 let b:hi_css1indent = get(indone, style1, indone.zero) 141 else 142 let b:hi_css1indent = 0 143 endif 144 145 if !exists('b:html_indent_line_limit') 146 if exists('g:html_indent_line_limit') 147 let b:html_indent_line_limit = g:html_indent_line_limit 148 else 149 let b:html_indent_line_limit = 200 150 endif 151 endif 152endfunc "}}} 153 154" Init Script Vars 155"{{{ 156let b:hi_lasttick = 0 157let b:hi_newstate = {} 158let s:countonly = 0 159 "}}} 160 161" Fill the s:indent_tags dict with known tags. 162" The key is "tagname" or "/tagname". {{{ 163" The value is: 164" 1 opening tag 165" 2 "pre" 166" 3 "script" 167" 4 "style" 168" 5 comment start 169" 6 conditional comment start 170" -1 closing tag 171" -2 "/pre" 172" -3 "/script" 173" -4 "/style" 174" -5 comment end 175" -6 conditional comment end 176let s:indent_tags = {} 177let s:endtags = [0,0,0,0,0,0,0] " long enough for the highest index 178"}}} 179 180" Add a list of tag names for a pair of <tag> </tag> to "tags". 181func s:AddITags(tags, taglist) 182 "{{{ 183 for itag in a:taglist 184 let a:tags[itag] = 1 185 let a:tags['/' . itag] = -1 186 endfor 187endfunc "}}} 188 189" Take a list of tag name pairs that are not to be used as tag pairs. 190func s:RemoveITags(tags, taglist) 191 "{{{ 192 for itag in a:taglist 193 let a:tags[itag] = 1 194 let a:tags['/' . itag] = 1 195 endfor 196endfunc "}}} 197 198" Add a block tag, that is a tag with a different kind of indenting. 199func s:AddBlockTag(tag, id, ...) 200 "{{{ 201 if !(a:id >= 2 && a:id < len(s:endtags)) 202 echoerr 'AddBlockTag ' . a:id 203 return 204 endif 205 let s:indent_tags[a:tag] = a:id 206 if a:0 == 0 207 let s:indent_tags['/' . a:tag] = -a:id 208 let s:endtags[a:id] = "</" . a:tag . ">" 209 else 210 let s:indent_tags[a:1] = -a:id 211 let s:endtags[a:id] = a:1 212 endif 213endfunc "}}} 214 215" Add known tag pairs. 216" Self-closing tags and tags that are sometimes {{{ 217" self-closing (e.g., <p>) are not here (when encountering </p> we can find 218" the matching <p>, but not the other way around). 219" Known self-closing tags: " 'p', 'img', 'source', 'area', 'keygen', 'track', 220" 'wbr'. 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', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', '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 \ 'article', 'aside', 'audio', 'bdi', 'canvas', 'command', 'data', 236 \ 'datalist', 'details', 'dialog', 'embed', 'figcaption', 'figure', 237 \ 'footer', 'header', 'hgroup', 'main', 'mark', 'meter', 'nav', 'output', 238 \ 'picture', 'progress', 'rp', 'rt', 'ruby', 'section', 'summary', 239 \ 'svg', 'time', 'video']) 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 <script> itself 586 return b:hi_indent.blocktagind 587 endif 588 if lnum == b:hi_indent.blocklnr 589 " indent for the first line after <script> 590 return eval(b:hi_js1indent) 591 endif 592 if b:hi_indent.scripttype == "javascript" 593 " indent for further lines 594 return eval(b:hi_js1indent) + GetJavascriptIndent() 595 else 596 return -1 597 endif 598endfunc "}}} 599 600" Return the indent inside a <style> block. 601func s:Alien4() 602 "{{{ 603 if prevnonblank(v:lnum-1) == b:hi_indent.blocklnr 604 " indent for first content line 605 return eval(b:hi_css1indent) 606 endif 607 return s:CSSIndent() 608endfunc "}}} 609 610" Indending inside a <style> block. Returns the indent. 611func s:CSSIndent() 612 "{{{ 613 " This handles standard CSS and also Closure stylesheets where special lines 614 " start with @. 615 " When the line starts with '*' or the previous line starts with "/*" 616 " and does not end in "*/", use C indenting to format the comment. 617 " Adopted $VIMRUNTIME/indent/css.vim 618 let curtext = getline(v:lnum) 619 if curtext =~ '^\s*[*]' 620 \ || (v:lnum > 1 && getline(v:lnum - 1) =~ '\s*/\*' 621 \ && getline(v:lnum - 1) !~ '\*/\s*$') 622 return cindent(v:lnum) 623 endif 624 625 let min_lnum = b:hi_indent.blocklnr 626 let prev_lnum = s:CssPrevNonComment(v:lnum - 1, min_lnum) 627 let [prev_lnum, found] = HtmlIndent_FindTagStart(prev_lnum) 628 if prev_lnum <= min_lnum 629 " Just below the <style> tag, indent for first content line after comments. 630 return eval(b:hi_css1indent) 631 endif 632 633 " If the current line starts with "}" align with its match. 634 if curtext =~ '^\s*}' 635 call cursor(v:lnum, 1) 636 try 637 normal! % 638 " Found the matching "{", align with it after skipping unfinished lines. 639 let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum) 640 return indent(align_lnum) 641 catch 642 " can't find it, try something else, but it's most likely going to be 643 " wrong 644 endtry 645 endif 646 647 " add indent after { 648 let brace_counts = HtmlIndent_CountBraces(prev_lnum) 649 let extra = brace_counts.c_open * shiftwidth() 650 651 let prev_text = getline(prev_lnum) 652 let below_end_brace = prev_text =~ '}\s*$' 653 654 " Search back to align with the first line that's unfinished. 655 let align_lnum = s:CssFirstUnfinished(prev_lnum, min_lnum) 656 657 " Handle continuation lines if aligning with previous line and not after a 658 " "}". 659 if extra == 0 && align_lnum == prev_lnum && !below_end_brace 660 let prev_hasfield = prev_text =~ '^\s*[a-zA-Z0-9-]\+:' 661 let prev_special = prev_text =~ '^\s*\(/\*\|@\)' 662 if curtext =~ '^\s*\(/\*\|@\)' 663 " if the current line is not a comment or starts with @ (used by template 664 " systems) reduce indent if previous line is a continuation line 665 if !prev_hasfield && !prev_special 666 let extra = -shiftwidth() 667 endif 668 else 669 let cur_hasfield = curtext =~ '^\s*[a-zA-Z0-9-]\+:' 670 let prev_unfinished = s:CssUnfinished(prev_text) 671 if prev_unfinished 672 " Continuation line has extra indent if the previous line was not a 673 " continuation line. 674 let extra = shiftwidth() 675 " Align with @if 676 if prev_text =~ '^\s*@if ' 677 let extra = 4 678 endif 679 elseif cur_hasfield && !prev_hasfield && !prev_special 680 " less indent below a continuation line 681 let extra = -shiftwidth() 682 endif 683 endif 684 endif 685 686 if below_end_brace 687 " find matching {, if that line starts with @ it's not the start of a rule 688 " but something else from a template system 689 call cursor(prev_lnum, 1) 690 call search('}\s*$') 691 try 692 normal! % 693 " Found the matching "{", align with it. 694 let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum) 695 let special = getline(align_lnum) =~ '^\s*@' 696 catch 697 let special = 0 698 endtry 699 if special 700 " do not reduce indent below @{ ... } 701 if extra < 0 702 let extra += shiftwidth() 703 endif 704 else 705 let extra -= (brace_counts.c_close - (prev_text =~ '^\s*}')) * shiftwidth() 706 endif 707 endif 708 709 " if no extra indent yet... 710 if extra == 0 711 if brace_counts.p_open > brace_counts.p_close 712 " previous line has more ( than ): add a shiftwidth 713 let extra = shiftwidth() 714 elseif brace_counts.p_open < brace_counts.p_close 715 " previous line has more ) than (: subtract a shiftwidth 716 let extra = -shiftwidth() 717 endif 718 endif 719 720 return indent(align_lnum) + extra 721endfunc "}}} 722 723" Inside <style>: Whether a line is unfinished. 724" tag: 725" tag: blah 726" tag: blah && 727" tag: blah || 728func s:CssUnfinished(text) 729 "{{{ 730 return a:text =~ '\(||\|&&\|:\|\k\)\s*$' 731endfunc "}}} 732 733" Search back for the first unfinished line above "lnum". 734func s:CssFirstUnfinished(lnum, min_lnum) 735 "{{{ 736 let align_lnum = a:lnum 737 while align_lnum > a:min_lnum && s:CssUnfinished(getline(align_lnum - 1)) 738 let align_lnum -= 1 739 endwhile 740 return align_lnum 741endfunc "}}} 742 743" Find the non-empty line at or before "lnum" that is not a comment. 744func s:CssPrevNonComment(lnum, stopline) 745 "{{{ 746 " caller starts from a line a:lnum + 1 that is not a comment 747 let lnum = prevnonblank(a:lnum) 748 while 1 749 let ccol = match(getline(lnum), '\*/') 750 if ccol < 0 751 " No comment end thus it's something else. 752 return lnum 753 endif 754 call cursor(lnum, ccol + 1) 755 " Search back for the /* that starts the comment 756 let lnum = search('/\*', 'bW', a:stopline) 757 if indent(".") == virtcol(".") - 1 758 " The found /* is at the start of the line. Now go back to the line 759 " above it and again check if it is a comment. 760 let lnum = prevnonblank(lnum - 1) 761 else 762 " /* is after something else, thus it's not a comment line. 763 return lnum 764 endif 765 endwhile 766endfunc "}}} 767 768" Check the number of {} and () in line "lnum". Return a dict with the counts. 769func HtmlIndent_CountBraces(lnum) 770 "{{{ 771 let brs = substitute(getline(a:lnum), '[''"].\{-}[''"]\|/\*.\{-}\*/\|/\*.*$\|[^{}()]', '', 'g') 772 let c_open = 0 773 let c_close = 0 774 let p_open = 0 775 let p_close = 0 776 for brace in split(brs, '\zs') 777 if brace == "{" 778 let c_open += 1 779 elseif brace == "}" 780 if c_open > 0 781 let c_open -= 1 782 else 783 let c_close += 1 784 endif 785 elseif brace == '(' 786 let p_open += 1 787 elseif brace == ')' 788 if p_open > 0 789 let p_open -= 1 790 else 791 let p_close += 1 792 endif 793 endif 794 endfor 795 return {'c_open': c_open, 796 \ 'c_close': c_close, 797 \ 'p_open': p_open, 798 \ 'p_close': p_close} 799endfunc "}}} 800 801" Return the indent for a comment: <!-- --> 802func s:Alien5() 803 "{{{ 804 let curtext = getline(v:lnum) 805 if curtext =~ '^\s*\zs-->' 806 " current line starts with end of comment, line up with comment start. 807 call cursor(v:lnum, 0) 808 let lnum = search('<!--', 'b') 809 if lnum > 0 810 " TODO: what if <!-- is not at the start of the line? 811 return indent(lnum) 812 endif 813 814 " Strange, can't find it. 815 return -1 816 endif 817 818 let prevlnum = prevnonblank(v:lnum - 1) 819 let prevtext = getline(prevlnum) 820 let idx = match(prevtext, '^\s*\zs<!--') 821 if idx >= 0 822 " just below comment start, add a shiftwidth 823 return indent(prevlnum) + shiftwidth() 824 endif 825 826 " Some files add 4 spaces just below a TODO line. It's difficult to detect 827 " the end of the TODO, so let's not do that. 828 829 " Align with the previous non-blank line. 830 return indent(prevlnum) 831endfunc "}}} 832 833" Return the indent for conditional comment: <!--[ ![endif]--> 834func s:Alien6() 835 "{{{ 836 let curtext = getline(v:lnum) 837 if curtext =~ '\s*\zs<!\[endif\]-->' 838 " current line starts with end of comment, line up with comment start. 839 let lnum = search('<!--', 'bn') 840 if lnum > 0 841 return indent(lnum) 842 endif 843 endif 844 return b:hi_indent.baseindent + shiftwidth() 845endfunc "}}} 846 847" When the "lnum" line ends in ">" find the line containing the matching "<". 848func HtmlIndent_FindTagStart(lnum) 849 "{{{ 850 " Avoids using the indent of a continuation line. 851 " Moves the cursor. 852 " Return two values: 853 " - the matching line number or "lnum". 854 " - a flag indicating whether we found the end of a tag. 855 " This method is global so that HTML-like indenters can use it. 856 " To avoid matching " > " or " < " inside a string require that the opening 857 " "<" is followed by a word character and the closing ">" comes after a 858 " non-white character. 859 let idx = match(getline(a:lnum), '\S>\s*$') 860 if idx > 0 861 call cursor(a:lnum, idx) 862 let lnum = searchpair('<\w', '' , '\S>', 'bW', '', max([a:lnum - b:html_indent_line_limit, 0])) 863 if lnum > 0 864 return [lnum, 1] 865 endif 866 endif 867 return [a:lnum, 0] 868endfunc "}}} 869 870" Find the unclosed start tag from the current cursor position. 871func HtmlIndent_FindStartTag() 872 "{{{ 873 " The cursor must be on or before a closing tag. 874 " If found, positions the cursor at the match and returns the line number. 875 " Otherwise returns 0. 876 let tagname = matchstr(getline('.')[col('.') - 1:], '</\zs' . s:tagname . '\ze') 877 let start_lnum = searchpair('<' . tagname . '\>', '', '</' . tagname . '\>', 'bW') 878 if start_lnum > 0 879 return start_lnum 880 endif 881 return 0 882endfunc "}}} 883 884" Moves the cursor from a "<" to the matching ">". 885func HtmlIndent_FindTagEnd() 886 "{{{ 887 " Call this with the cursor on the "<" of a start tag. 888 " This will move the cursor to the ">" of the matching end tag or, when it's 889 " a self-closing tag, to the matching ">". 890 " Limited to look up to b:html_indent_line_limit lines away. 891 let text = getline('.') 892 let tagname = matchstr(text, s:tagname . '\|!--', col('.')) 893 if tagname == '!--' 894 call search('--\zs>') 895 elseif s:get_tag('/' . tagname) != 0 896 " tag with a closing tag, find matching "</tag>" 897 call searchpair('<' . tagname, '', '</' . tagname . '\zs>', 'W', '', line('.') + b:html_indent_line_limit) 898 else 899 " self-closing tag, find the ">" 900 call search('\S\zs>') 901 endif 902endfunc "}}} 903 904" Indenting inside a start tag. Return the correct indent or -1 if unknown. 905func s:InsideTag(foundHtmlString) 906 "{{{ 907 if a:foundHtmlString 908 " Inside an attribute string. 909 " Align with the opening quote or use an external function. 910 let lnum = v:lnum - 1 911 if lnum > 1 912 if exists('b:html_indent_tag_string_func') 913 return b:html_indent_tag_string_func(lnum) 914 endif 915 " If there is a double quote in the previous line, indent with the 916 " character after it. 917 if getline(lnum) =~ '"' 918 call cursor(lnum, 0) 919 normal f" 920 return virtcol('.') 921 endif 922 return indent(lnum) 923 endif 924 endif 925 926 " Should be another attribute: " attr="val". Align with the previous 927 " attribute start. 928 let lnum = v:lnum 929 while lnum > 1 930 let lnum -= 1 931 let text = getline(lnum) 932 " Find a match with one of these, align with "attr": 933 " attr= 934 " <tag attr= 935 " text<tag attr= 936 " <tag>text</tag>text<tag attr= 937 " For long lines search for the first match, finding the last match 938 " gets very slow. 939 if len(text) < 300 940 let idx = match(text, '.*\s\zs[_a-zA-Z0-9-]\+="') 941 else 942 let idx = match(text, '\s\zs[_a-zA-Z0-9-]\+="') 943 endif 944 if idx == -1 945 " try <tag attr 946 let idx = match(text, '<' . s:tagname . '\s\+\zs\w') 947 endif 948 if idx == -1 949 " after just "<tag" indent two levels more 950 let idx = match(text, '<' . s:tagname . '$') 951 if idx >= 0 952 call cursor(lnum, idx + 1) 953 return virtcol('.') - 1 + shiftwidth() * 2 954 endif 955 endif 956 if idx > 0 957 " Found the attribute to align with. 958 call cursor(lnum, idx) 959 return virtcol('.') 960 endif 961 endwhile 962 return -1 963endfunc "}}} 964 965" THE MAIN INDENT FUNCTION. Return the amount of indent for v:lnum. 966func HtmlIndent() 967 "{{{ 968 if prevnonblank(v:lnum - 1) < 1 969 " First non-blank line has no indent. 970 return 0 971 endif 972 973 let curtext = tolower(getline(v:lnum)) 974 let indentunit = shiftwidth() 975 976 let b:hi_newstate = {} 977 let b:hi_newstate.lnum = v:lnum 978 979 " When syntax HL is enabled, detect we are inside a tag. Indenting inside 980 " a tag works very differently. Do not do this when the line starts with 981 " "<", it gets the "htmlTag" ID but we are not inside a tag then. 982 if curtext !~ '^\s*<' 983 normal! ^ 984 let stack = synstack(v:lnum, col('.')) " assumes there are no tabs 985 let foundHtmlString = 0 986 for synid in reverse(stack) 987 let name = synIDattr(synid, "name") 988 if index(b:hi_insideStringNames, name) >= 0 989 let foundHtmlString = 1 990 elseif index(b:hi_insideTagNames, name) >= 0 991 " Yes, we are inside a tag. 992 let indent = s:InsideTag(foundHtmlString) 993 if indent >= 0 994 " Do not keep the state. TODO: could keep the block type. 995 let b:hi_indent.lnum = 0 996 return indent 997 endif 998 endif 999 endfor 1000 endif 1001 1002 " does the line start with a closing tag? 1003 let swendtag = match(curtext, '^\s*</') >= 0 1004 1005 if prevnonblank(v:lnum - 1) == b:hi_indent.lnum && b:hi_lasttick == b:changedtick - 1 1006 " use state (continue from previous line) 1007 else 1008 " start over (know nothing) 1009 let b:hi_indent = s:FreshState(v:lnum) 1010 endif 1011 1012 if b:hi_indent.block >= 2 1013 " within block 1014 let endtag = s:endtags[b:hi_indent.block] 1015 let blockend = stridx(curtext, endtag) 1016 if blockend >= 0 1017 " block ends here 1018 let b:hi_newstate.block = 0 1019 " calc indent for REST OF LINE (may start more blocks): 1020 call s:CountTagsAndState(strpart(curtext, blockend + strlen(endtag))) 1021 if swendtag && b:hi_indent.block != 5 1022 let indent = b:hi_indent.blocktagind + s:curind * indentunit 1023 let b:hi_newstate.baseindent = indent + s:nextrel * indentunit 1024 else 1025 let indent = s:Alien{b:hi_indent.block}() 1026 let b:hi_newstate.baseindent = b:hi_indent.blocktagind + s:nextrel * indentunit 1027 endif 1028 else 1029 " block continues 1030 " indent this line with alien method 1031 let indent = s:Alien{b:hi_indent.block}() 1032 endif 1033 else 1034 " not within a block - within usual html 1035 let b:hi_newstate.block = b:hi_indent.block 1036 if swendtag 1037 " The current line starts with an end tag, align with its start tag. 1038 call cursor(v:lnum, 1) 1039 let start_lnum = HtmlIndent_FindStartTag() 1040 if start_lnum > 0 1041 " check for the line starting with something inside a tag: 1042 " <sometag <- align here 1043 " attr=val><open> not here 1044 let text = getline(start_lnum) 1045 let angle = matchstr(text, '[<>]') 1046 if angle == '>' 1047 call cursor(start_lnum, 1) 1048 normal! f>% 1049 let start_lnum = line('.') 1050 let text = getline(start_lnum) 1051 endif 1052 1053 let indent = indent(start_lnum) 1054 if col('.') > 2 1055 let swendtag = match(text, '^\s*</') >= 0 1056 call s:CountITags(text[: col('.') - 2]) 1057 let indent += s:nextrel * shiftwidth() 1058 if !swendtag 1059 let indent += s:curind * shiftwidth() 1060 endif 1061 endif 1062 else 1063 " not sure what to do 1064 let indent = b:hi_indent.baseindent 1065 endif 1066 let b:hi_newstate.baseindent = indent 1067 else 1068 call s:CountTagsAndState(curtext) 1069 let indent = b:hi_indent.baseindent 1070 let b:hi_newstate.baseindent = indent + (s:curind + s:nextrel) * indentunit 1071 endif 1072 endif 1073 1074 let b:hi_lasttick = b:changedtick 1075 call extend(b:hi_indent, b:hi_newstate, "force") 1076 return indent 1077endfunc "}}} 1078 1079" Check user settings when loading this script the first time. 1080call HtmlIndent_CheckUserSettings() 1081 1082let &cpo = s:cpo_save 1083unlet s:cpo_save 1084 1085" vim: fdm=marker ts=8 sw=2 tw=78 1086