neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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