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