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