xref: /vim-8.2.3635/runtime/indent/html.vim (revision 51ad4eaa)
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 !cur_hasfield && (prev_hasfield || 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.
719func! s:CssUnfinished(text)
720  "{{{
721  return a:text =~ '\s\(||\|&&\|:\)\s*$'
722endfunc "}}}
723
724" Search back for the first unfinished line above "lnum".
725func! s:CssFirstUnfinished(lnum, min_lnum)
726  "{{{
727  let align_lnum = a:lnum
728  while align_lnum > a:min_lnum && s:CssUnfinished(getline(align_lnum - 1))
729    let align_lnum -= 1
730  endwhile
731  return align_lnum
732endfunc "}}}
733
734" Find the non-empty line at or before "lnum" that is not a comment.
735func! s:CssPrevNonComment(lnum, stopline)
736  "{{{
737  " caller starts from a line a:lnum + 1 that is not a comment
738  let lnum = prevnonblank(a:lnum)
739  while 1
740    let ccol = match(getline(lnum), '\*/')
741    if ccol < 0
742      " No comment end thus it's something else.
743      return lnum
744    endif
745    call cursor(lnum, ccol + 1)
746    " Search back for the /* that starts the comment
747    let lnum = search('/\*', 'bW', a:stopline)
748    if indent(".") == virtcol(".") - 1
749      " The  found /* is at the start of the line. Now go back to the line
750      " above it and again check if it is a comment.
751      let lnum = prevnonblank(lnum - 1)
752    else
753      " /* is after something else, thus it's not a comment line.
754      return lnum
755    endif
756  endwhile
757endfunc "}}}
758
759" Check the number of {} and () in line "lnum". Return a dict with the counts.
760func! HtmlIndent_CountBraces(lnum)
761  "{{{
762  let brs = substitute(getline(a:lnum), '[''"].\{-}[''"]\|/\*.\{-}\*/\|/\*.*$\|[^{}()]', '', 'g')
763  let c_open = 0
764  let c_close = 0
765  let p_open = 0
766  let p_close = 0
767  for brace in split(brs, '\zs')
768    if brace == "{"
769      let c_open += 1
770    elseif brace == "}"
771      if c_open > 0
772        let c_open -= 1
773      else
774        let c_close += 1
775      endif
776    elseif brace == '('
777      let p_open += 1
778    elseif brace == ')'
779      if p_open > 0
780        let p_open -= 1
781      else
782        let p_close += 1
783      endif
784    endif
785  endfor
786  return {'c_open': c_open,
787        \ 'c_close': c_close,
788        \ 'p_open': p_open,
789        \ 'p_close': p_close}
790endfunc "}}}
791
792" Return the indent for a comment: <!-- -->
793func! s:Alien5()
794  "{{{
795  let curtext = getline(v:lnum)
796  if curtext =~ '^\s*\zs-->'
797    " current line starts with end of comment, line up with comment start.
798    call cursor(v:lnum, 0)
799    let lnum = search('<!--', 'b')
800    if lnum > 0
801      " TODO: what if <!-- is not at the start of the line?
802      return indent(lnum)
803    endif
804
805    " Strange, can't find it.
806    return -1
807  endif
808
809  let prevlnum = prevnonblank(v:lnum - 1)
810  let prevtext = getline(prevlnum)
811  let idx = match(prevtext, '^\s*\zs<!--')
812  if idx >= 0
813    " just below comment start, add a shiftwidth
814    return idx + shiftwidth()
815  endif
816
817  " Some files add 4 spaces just below a TODO line.  It's difficult to detect
818  " the end of the TODO, so let's not do that.
819
820  " Align with the previous non-blank line.
821  return indent(prevlnum)
822endfunc "}}}
823
824" Return the indent for conditional comment: <!--[ ![endif]-->
825func! s:Alien6()
826  "{{{
827  let curtext = getline(v:lnum)
828  if curtext =~ '\s*\zs<!\[endif\]-->'
829    " current line starts with end of comment, line up with comment start.
830    let lnum = search('<!--', 'bn')
831    if lnum > 0
832      return indent(lnum)
833    endif
834  endif
835  return b:hi_indent.baseindent + shiftwidth()
836endfunc "}}}
837
838" When the "lnum" line ends in ">" find the line containing the matching "<".
839func! HtmlIndent_FindTagStart(lnum)
840  "{{{
841  " Avoids using the indent of a continuation line.
842  " Moves the cursor.
843  " Return two values:
844  " - the matching line number or "lnum".
845  " - a flag indicating whether we found the end of a tag.
846  " This method is global so that HTML-like indenters can use it.
847  " To avoid matching " > " or " < " inside a string require that the opening
848  " "<" is followed by a word character and the closing ">" comes after a
849  " non-white character.
850  let idx = match(getline(a:lnum), '\S>\s*$')
851  if idx > 0
852    call cursor(a:lnum, idx)
853    let lnum = searchpair('<\w', '' , '\S>', 'bW', '', max([a:lnum - b:html_indent_line_limit, 0]))
854    if lnum > 0
855      return [lnum, 1]
856    endif
857  endif
858  return [a:lnum, 0]
859endfunc "}}}
860
861" Find the unclosed start tag from the current cursor position.
862func! HtmlIndent_FindStartTag()
863  "{{{
864  " The cursor must be on or before a closing tag.
865  " If found, positions the cursor at the match and returns the line number.
866  " Otherwise returns 0.
867  let tagname = matchstr(getline('.')[col('.') - 1:], '</\zs' . s:tagname . '\ze')
868  let start_lnum = searchpair('<' . tagname . '\>', '', '</' . tagname . '\>', 'bW')
869  if start_lnum > 0
870    return start_lnum
871  endif
872  return 0
873endfunc "}}}
874
875" Moves the cursor from a "<" to the matching ">".
876func! HtmlIndent_FindTagEnd()
877  "{{{
878  " Call this with the cursor on the "<" of a start tag.
879  " This will move the cursor to the ">" of the matching end tag or, when it's
880  " a self-closing tag, to the matching ">".
881  " Limited to look up to b:html_indent_line_limit lines away.
882  let text = getline('.')
883  let tagname = matchstr(text, s:tagname . '\|!--', col('.'))
884  if tagname == '!--'
885    call search('--\zs>')
886  elseif s:get_tag('/' . tagname) != 0
887    " tag with a closing tag, find matching "</tag>"
888    call searchpair('<' . tagname, '', '</' . tagname . '\zs>', 'W', '', line('.') + b:html_indent_line_limit)
889  else
890    " self-closing tag, find the ">"
891    call search('\S\zs>')
892  endif
893endfunc "}}}
894
895" Indenting inside a start tag. Return the correct indent or -1 if unknown.
896func! s:InsideTag(foundHtmlString)
897  "{{{
898  if a:foundHtmlString
899    " Inside an attribute string.
900    " Align with the previous line or use an external function.
901    let lnum = v:lnum - 1
902    if lnum > 1
903      if exists('b:html_indent_tag_string_func')
904        return b:html_indent_tag_string_func(lnum)
905      endif
906      return indent(lnum)
907    endif
908  endif
909
910  " Should be another attribute: " attr="val".  Align with the previous
911  " attribute start.
912  let lnum = v:lnum
913  while lnum > 1
914    let lnum -= 1
915    let text = getline(lnum)
916    " Find a match with one of these, align with "attr":
917    "       attr=
918    "  <tag attr=
919    "  text<tag attr=
920    "  <tag>text</tag>text<tag attr=
921    " For long lines search for the first match, finding the last match
922    " gets very slow.
923    if len(text) < 300
924      let idx = match(text, '.*\s\zs[_a-zA-Z0-9-]\+="')
925    else
926      let idx = match(text, '\s\zs[_a-zA-Z0-9-]\+="')
927    endif
928    if idx == -1
929      " try <tag attr
930      let idx = match(text, '<' . s:tagname . '\s\+\zs\w')
931    endif
932    if idx == -1
933      " after just <tag indent one level more
934      let idx = match(text, '<' . s:tagname . '$')
935      if idx >= 0
936	call cursor(lnum, idx)
937	return virtcol('.') + shiftwidth()
938      endif
939    endif
940    if idx > 0
941      " Found the attribute to align with.
942      call cursor(lnum, idx)
943      return virtcol('.')
944    endif
945  endwhile
946  return -1
947endfunc "}}}
948
949" THE MAIN INDENT FUNCTION. Return the amount of indent for v:lnum.
950func! HtmlIndent()
951  "{{{
952  if prevnonblank(v:lnum - 1) < 1
953    " First non-blank line has no indent.
954    return 0
955  endif
956
957  let curtext = tolower(getline(v:lnum))
958  let indentunit = shiftwidth()
959
960  let b:hi_newstate = {}
961  let b:hi_newstate.lnum = v:lnum
962
963  " When syntax HL is enabled, detect we are inside a tag.  Indenting inside
964  " a tag works very differently. Do not do this when the line starts with
965  " "<", it gets the "htmlTag" ID but we are not inside a tag then.
966  if curtext !~ '^\s*<'
967    normal! ^
968    let stack = synstack(v:lnum, col('.'))  " assumes there are no tabs
969    let foundHtmlString = 0
970    for synid in reverse(stack)
971      let name = synIDattr(synid, "name")
972      if index(b:hi_insideStringNames, name) >= 0
973        let foundHtmlString = 1
974      elseif index(b:hi_insideTagNames, name) >= 0
975        " Yes, we are inside a tag.
976        let indent = s:InsideTag(foundHtmlString)
977        if indent >= 0
978          " Do not keep the state. TODO: could keep the block type.
979          let b:hi_indent.lnum = 0
980          return indent
981        endif
982      endif
983    endfor
984  endif
985
986  " does the line start with a closing tag?
987  let swendtag = match(curtext, '^\s*</') >= 0
988
989  if prevnonblank(v:lnum - 1) == b:hi_indent.lnum && b:hi_lasttick == b:changedtick - 1
990    " use state (continue from previous line)
991  else
992    " start over (know nothing)
993    let b:hi_indent = s:FreshState(v:lnum)
994  endif
995
996  if b:hi_indent.block >= 2
997    " within block
998    let endtag = s:endtags[b:hi_indent.block]
999    let blockend = stridx(curtext, endtag)
1000    if blockend >= 0
1001      " block ends here
1002      let b:hi_newstate.block = 0
1003      " calc indent for REST OF LINE (may start more blocks):
1004      call s:CountTagsAndState(strpart(curtext, blockend + strlen(endtag)))
1005      if swendtag && b:hi_indent.block != 5
1006        let indent = b:hi_indent.blocktagind + s:curind * indentunit
1007        let b:hi_newstate.baseindent = indent + s:nextrel * indentunit
1008      else
1009        let indent = s:Alien{b:hi_indent.block}()
1010        let b:hi_newstate.baseindent = b:hi_indent.blocktagind + s:nextrel * indentunit
1011      endif
1012    else
1013      " block continues
1014      " indent this line with alien method
1015      let indent = s:Alien{b:hi_indent.block}()
1016    endif
1017  else
1018    " not within a block - within usual html
1019    let b:hi_newstate.block = b:hi_indent.block
1020    if swendtag
1021      " The current line starts with an end tag, align with its start tag.
1022      call cursor(v:lnum, 1)
1023      let start_lnum = HtmlIndent_FindStartTag()
1024      if start_lnum > 0
1025        " check for the line starting with something inside a tag:
1026        " <sometag               <- align here
1027        "    attr=val><open>     not here
1028        let text = getline(start_lnum)
1029        let angle = matchstr(text, '[<>]')
1030        if angle == '>'
1031          call cursor(start_lnum, 1)
1032          normal! f>%
1033          let start_lnum = line('.')
1034          let text = getline(start_lnum)
1035        endif
1036
1037        let indent = indent(start_lnum)
1038        if col('.') > 2
1039          let swendtag = match(text, '^\s*</') >= 0
1040          call s:CountITags(text[: col('.') - 2])
1041          let indent += s:nextrel * shiftwidth()
1042          if !swendtag
1043            let indent += s:curind * shiftwidth()
1044          endif
1045        endif
1046      else
1047        " not sure what to do
1048        let indent = b:hi_indent.baseindent
1049      endif
1050      let b:hi_newstate.baseindent = indent
1051    else
1052      call s:CountTagsAndState(curtext)
1053      let indent = b:hi_indent.baseindent
1054      let b:hi_newstate.baseindent = indent + (s:curind + s:nextrel) * indentunit
1055    endif
1056  endif
1057
1058  let b:hi_lasttick = b:changedtick
1059  call extend(b:hi_indent, b:hi_newstate, "force")
1060  return indent
1061endfunc "}}}
1062
1063" Check user settings when loading this script the first time.
1064call HtmlIndent_CheckUserSettings()
1065
1066let &cpo = s:cpo_save
1067unlet s:cpo_save
1068
1069" vim: fdm=marker ts=8 sw=2 tw=78
1070