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