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