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