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