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