xref: /vim-8.2.3635/runtime/indent/html.vim (revision e0720cbf)
1" Vim indent script for HTML
2" Header: "{{{
3" Maintainer:	Bram Moolenaar
4" Original Author: Andy Wokula <[email protected]>
5" Last Change:	2017 Jan 17
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" shiftwidth() exists since patch 7.3.694
55if exists('*shiftwidth')
56  let s:ShiftWidth = function('shiftwidth')
57else
58  func! s:ShiftWidth()
59    return &shiftwidth
60  endfunc
61endif
62
63" Allow for line continuation below.
64let s:cpo_save = &cpo
65set cpo-=C
66"}}}
67
68" Check and process settings from b:html_indent and g:html_indent... variables.
69" Prefer using buffer-local settings over global settings, so that there can
70" be defaults for all HTML files and exceptions for specific types of HTML
71" files.
72func! HtmlIndent_CheckUserSettings()
73  "{{{
74  let inctags = ''
75  if exists("b:html_indent_inctags")
76    let inctags = b:html_indent_inctags
77  elseif exists("g:html_indent_inctags")
78    let inctags = g:html_indent_inctags
79  endif
80  let b:hi_tags = {}
81  if len(inctags) > 0
82    call s:AddITags(b:hi_tags, split(inctags, ","))
83  endif
84
85  let autotags = ''
86  if exists("b:html_indent_autotags")
87    let autotags = b:html_indent_autotags
88  elseif exists("g:html_indent_autotags")
89    let autotags = g:html_indent_autotags
90  endif
91  let b:hi_removed_tags = {}
92  if len(autotags) > 0
93    call s:RemoveITags(b:hi_removed_tags, split(autotags, ","))
94  endif
95
96  " Syntax names indicating being inside a string of an attribute value.
97  let string_names = []
98  if exists("b:html_indent_string_names")
99    let string_names = b:html_indent_string_names
100  elseif exists("g:html_indent_string_names")
101    let string_names = g:html_indent_string_names
102  endif
103  let b:hi_insideStringNames = ['htmlString']
104  if len(string_names) > 0
105    for s in string_names
106      call add(b:hi_insideStringNames, s)
107    endfor
108  endif
109
110  " Syntax names indicating being inside a tag.
111  let tag_names = []
112  if exists("b:html_indent_tag_names")
113    let tag_names = b:html_indent_tag_names
114  elseif exists("g:html_indent_tag_names")
115    let tag_names = g:html_indent_tag_names
116  endif
117  let b:hi_insideTagNames = ['htmlTag', 'htmlScriptTag']
118  if len(tag_names) > 0
119    for s in tag_names
120      call add(b:hi_insideTagNames, s)
121    endfor
122  endif
123
124  let indone = {"zero": 0
125              \,"auto": "indent(prevnonblank(v:lnum-1))"
126              \,"inc": "b:hi_indent.blocktagind + s:ShiftWidth()"}
127
128  let script1 = ''
129  if exists("b:html_indent_script1")
130    let script1 = b:html_indent_script1
131  elseif exists("g:html_indent_script1")
132    let script1 = g:html_indent_script1
133  endif
134  if len(script1) > 0
135    let b:hi_js1indent = get(indone, script1, indone.zero)
136  else
137    let b:hi_js1indent = 0
138  endif
139
140  let style1 = ''
141  if exists("b:html_indent_style1")
142    let style1 = b:html_indent_style1
143  elseif exists("g:html_indent_style1")
144    let style1 = g:html_indent_style1
145  endif
146  if len(style1) > 0
147    let b:hi_css1indent = get(indone, style1, indone.zero)
148  else
149    let b:hi_css1indent = 0
150  endif
151
152  if !exists('b:html_indent_line_limit')
153    if exists('g:html_indent_line_limit')
154      let b:html_indent_line_limit = g:html_indent_line_limit
155    else
156      let b:html_indent_line_limit = 200
157    endif
158  endif
159endfunc "}}}
160
161" Init Script Vars
162"{{{
163let b:hi_lasttick = 0
164let b:hi_newstate = {}
165let s:countonly = 0
166 "}}}
167
168" Fill the s:indent_tags dict with known tags.
169" The key is "tagname" or "/tagname".  {{{
170" The value is:
171" 1   opening tag
172" 2   "pre"
173" 3   "script"
174" 4   "style"
175" 5   comment start
176" 6   conditional comment start
177" -1  closing tag
178" -2  "/pre"
179" -3  "/script"
180" -4  "/style"
181" -5  comment end
182" -6  conditional comment end
183let s:indent_tags = {}
184let s:endtags = [0,0,0,0,0,0,0]   " long enough for the highest index
185"}}}
186
187" Add a list of tag names for a pair of <tag> </tag> to "tags".
188func! s:AddITags(tags, taglist)
189  "{{{
190  for itag in a:taglist
191    let a:tags[itag] = 1
192    let a:tags['/' . itag] = -1
193  endfor
194endfunc "}}}
195
196" Take a list of tag name pairs that are not to be used as tag pairs.
197func! s:RemoveITags(tags, taglist)
198  "{{{
199  for itag in a:taglist
200    let a:tags[itag] = 1
201    let a:tags['/' . itag] = 1
202  endfor
203endfunc "}}}
204
205" Add a block tag, that is a tag with a different kind of indenting.
206func! s:AddBlockTag(tag, id, ...)
207  "{{{
208  if !(a:id >= 2 && a:id < len(s:endtags))
209    echoerr 'AddBlockTag ' . a:id
210    return
211  endif
212  let s:indent_tags[a:tag] = a:id
213  if a:0 == 0
214    let s:indent_tags['/' . a:tag] = -a:id
215    let s:endtags[a:id] = "</" . a:tag . ">"
216  else
217    let s:indent_tags[a:1] = -a:id
218    let s:endtags[a:id] = a:1
219  endif
220endfunc "}}}
221
222" Add known tag pairs.
223" Self-closing tags and tags that are sometimes {{{
224" self-closing (e.g., <p>) are not here (when encountering </p> we can find
225" the matching <p>, but not the other way around).
226" Old HTML tags:
227call s:AddITags(s:indent_tags, [
228    \ 'a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big',
229    \ 'blockquote', 'body', 'button', 'caption', 'center', 'cite', 'code',
230    \ 'colgroup', 'del', 'dfn', 'dir', 'div', 'dl', 'em', 'fieldset', 'font',
231    \ 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'html',
232    \ 'i', 'iframe', 'ins', 'kbd', 'label', 'legend', 'li',
233    \ 'map', 'menu', 'noframes', 'noscript', 'object', 'ol',
234    \ 'optgroup', 'q', 's', 'samp', 'select', 'small', 'span', 'strong', 'sub',
235    \ 'sup', 'table', 'textarea', 'title', 'tt', 'u', 'ul', 'var', 'th', 'td',
236    \ 'tr', 'tbody', 'tfoot', 'thead'])
237
238" New HTML5 elements:
239call s:AddITags(s:indent_tags, [
240    \ 'area', 'article', 'aside', 'audio', 'bdi', 'canvas',
241    \ 'command', 'data', 'datalist', 'details', 'embed', 'figcaption',
242    \ 'figure', 'footer', 'header', 'keygen', 'mark', 'meter', 'nav', 'output',
243    \ 'progress', 'rp', 'rt', 'ruby', 'section', 'source', 'summary', 'svg',
244    \ 'time', 'track', 'video', 'wbr'])
245
246" Tags added for web components:
247call s:AddITags(s:indent_tags, [
248    \ 'content', 'shadow', 'template'])
249"}}}
250
251" Add Block Tags: these contain alien content
252"{{{
253call s:AddBlockTag('pre', 2)
254call s:AddBlockTag('script', 3)
255call s:AddBlockTag('style', 4)
256call s:AddBlockTag('<!--', 5, '-->')
257call s:AddBlockTag('<!--[', 6, '![endif]-->')
258"}}}
259
260" Return non-zero when "tagname" is an opening tag, not being a block tag, for
261" which there should be a closing tag.  Can be used by scripts that include
262" HTML indenting.
263func! HtmlIndent_IsOpenTag(tagname)
264  "{{{
265  if get(s:indent_tags, a:tagname) == 1
266    return 1
267  endif
268  return get(b:hi_tags, a:tagname) == 1
269endfunc "}}}
270
271" Get the value for "tagname", taking care of buffer-local tags.
272func! s:get_tag(tagname)
273  "{{{
274  let i = get(s:indent_tags, a:tagname)
275  if (i == 1 || i == -1) && get(b:hi_removed_tags, a:tagname) != 0
276    return 0
277  endif
278  if i == 0
279    let i = get(b:hi_tags, a:tagname)
280  endif
281  return i
282endfunc "}}}
283
284" Count the number of start and end tags in "text".
285func! s:CountITags(text)
286  "{{{
287  " Store the result in s:curind and s:nextrel.
288  let s:curind = 0  " relative indent steps for current line [unit &sw]:
289  let s:nextrel = 0  " relative indent steps for next line [unit &sw]:
290  let s:block = 0		" assume starting outside of a block
291  let s:countonly = 1	" don't change state
292  call substitute(a:text, '<\zs/\=\w\+\(-\w\+\)*\>\|<!--\[\|\[endif\]-->\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g')
293  let s:countonly = 0
294endfunc "}}}
295
296" Count the number of start and end tags in text.
297func! s:CountTagsAndState(text)
298  "{{{
299  " Store the result in s:curind and s:nextrel.  Update b:hi_newstate.block.
300  let s:curind = 0  " relative indent steps for current line [unit &sw]:
301  let s:nextrel = 0  " relative indent steps for next line [unit &sw]:
302
303  let s:block = b:hi_newstate.block
304  let tmp = substitute(a:text, '<\zs/\=\w\+\(-\w\+\)*\>\|<!--\[\|\[endif\]-->\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g')
305  if s:block == 3
306    let b:hi_newstate.scripttype = s:GetScriptType(matchstr(tmp, '\C.*<SCRIPT\>\zs[^>]*'))
307  endif
308  let b:hi_newstate.block = s:block
309endfunc "}}}
310
311" Used by s:CountITags() and s:CountTagsAndState().
312func! s:CheckTag(itag)
313  "{{{
314  " Returns an empty string or "SCRIPT".
315  " a:itag can be "tag" or "/tag" or "<!--" or "-->"
316  if (s:CheckCustomTag(a:itag))
317    return ""
318  endif
319  let ind = s:get_tag(a:itag)
320  if ind == -1
321    " closing tag
322    if s:block != 0
323      " ignore itag within a block
324      return ""
325    endif
326    if s:nextrel == 0
327      let s:curind -= 1
328    else
329      let s:nextrel -= 1
330    endif
331  elseif ind == 1
332    " opening tag
333    if s:block != 0
334      return ""
335    endif
336    let s:nextrel += 1
337  elseif ind != 0
338    " block-tag (opening or closing)
339    return s:CheckBlockTag(a:itag, ind)
340  " else ind==0 (other tag found): keep indent
341  endif
342  return ""
343endfunc "}}}
344
345" Used by s:CheckTag(). Returns an empty string or "SCRIPT".
346func! s:CheckBlockTag(blocktag, ind)
347  "{{{
348  if a:ind > 0
349    " a block starts here
350    if s:block != 0
351      " already in a block (nesting) - ignore
352      " especially ignore comments after other blocktags
353      return ""
354    endif
355    let s:block = a:ind		" block type
356    if s:countonly
357      return ""
358    endif
359    let b:hi_newstate.blocklnr = v:lnum
360    " save allover indent for the endtag
361    let b:hi_newstate.blocktagind = b:hi_indent.baseindent + (s:nextrel + s:curind) * s:ShiftWidth()
362    if a:ind == 3
363      return "SCRIPT"    " all except this must be lowercase
364      " line is to be checked again for the type attribute
365    endif
366  else
367    let s:block = 0
368    " we get here if starting and closing a block-tag on the same line
369  endif
370  return ""
371endfunc "}}}
372
373" Used by s:CheckTag().
374func! s:CheckCustomTag(ctag)
375  "{{{
376  " Returns 1 if ctag is the tag for a custom element, 0 otherwise.
377  " a:ctag can be "tag" or "/tag" or "<!--" or "-->"
378  let pattern = '\%\(\w\+-\)\+\w\+'
379  if match(a:ctag, pattern) == -1
380    return 0
381  endif
382  if matchstr(a:ctag, '\/\ze.\+') == "/"
383    " closing tag
384    if s:block != 0
385      " ignore ctag within a block
386      return 1
387    endif
388    if s:nextrel == 0
389      let s:curind -= 1
390    else
391      let s:nextrel -= 1
392    endif
393  else
394    " opening tag
395    if s:block != 0
396      return 1
397    endif
398    let s:nextrel += 1
399  endif
400  return 1
401endfunc "}}}
402
403" Return the <script> type: either "javascript" or ""
404func! s:GetScriptType(str)
405  "{{{
406  if a:str == "" || a:str =~ "java"
407    return "javascript"
408  else
409    return ""
410  endif
411endfunc "}}}
412
413" Look back in the file, starting at a:lnum - 1, to compute a state for the
414" start of line a:lnum.  Return the new state.
415func! s:FreshState(lnum)
416  "{{{
417  " A state is to know ALL relevant details about the
418  " lines 1..a:lnum-1, initial calculating (here!) can be slow, but updating is
419  " fast (incremental).
420  " TODO: this should be split up in detecting the block type and computing the
421  " indent for the block type, so that when we do not know the indent we do
422  " not need to clear the whole state and re-detect the block type again.
423  " State:
424  "	lnum		last indented line == prevnonblank(a:lnum - 1)
425  "	block = 0	a:lnum located within special tag: 0:none, 2:<pre>,
426  "			3:<script>, 4:<style>, 5:<!--, 6:<!--[
427  "	baseindent	use this indent for line a:lnum as a start - kind of
428  "			autoindent (if block==0)
429  "	scripttype = ''	type attribute of a script tag (if block==3)
430  "	blocktagind	indent for current opening (get) and closing (set)
431  "			blocktag (if block!=0)
432  "	blocklnr	lnum of starting blocktag (if block!=0)
433  "	inattr		line {lnum} starts with attributes of a tag
434  let state = {}
435  let state.lnum = prevnonblank(a:lnum - 1)
436  let state.scripttype = ""
437  let state.blocktagind = -1
438  let state.block = 0
439  let state.baseindent = 0
440  let state.blocklnr = 0
441  let state.inattr = 0
442
443  if state.lnum == 0
444    return state
445  endif
446
447  " Heuristic:
448  " remember startline state.lnum
449  " look back for <pre, </pre, <script, </script, <style, </style tags
450  " remember stopline
451  " if opening tag found,
452  "	assume a:lnum within block
453  " else
454  "	look back in result range (stopline, startline) for comment
455  "	    \ delimiters (<!--, -->)
456  "	if comment opener found,
457  "	    assume a:lnum within comment
458  "	else
459  "	    assume usual html for a:lnum
460  "	    if a:lnum-1 has a closing comment
461  "		look back to get indent of comment opener
462  " FI
463
464  " look back for a blocktag
465  let stopline2 = v:lnum + 1
466  if has_key(b:hi_indent, 'block') && b:hi_indent.block > 5
467    let [stopline2, stopcol2] = searchpos('<!--', 'bnW')
468  endif
469  let [stopline, stopcol] = searchpos('\c<\zs\/\=\%(pre\>\|script\>\|style\>\)', "bnW")
470  if stopline > 0 && stopline < stopline2
471    " ugly ... why isn't there searchstr()
472    let tagline = tolower(getline(stopline))
473    let blocktag = matchstr(tagline, '\/\=\%(pre\>\|script\>\|style\>\)', stopcol - 1)
474    if blocktag[0] != "/"
475      " opening tag found, assume a:lnum within block
476      let state.block = s:indent_tags[blocktag]
477      if state.block == 3
478        let state.scripttype = s:GetScriptType(matchstr(tagline, '\>[^>]*', stopcol))
479      endif
480      let state.blocklnr = stopline
481      " check preceding tags in the line:
482      call s:CountITags(tagline[: stopcol-2])
483      let state.blocktagind = indent(stopline) + (s:curind + s:nextrel) * s:ShiftWidth()
484      return state
485    elseif stopline == state.lnum
486      " handle special case: previous line (= state.lnum) contains a
487      " closing blocktag which is preceded by line-noise;
488      " blocktag == "/..."
489      let swendtag = match(tagline, '^\s*</') >= 0
490      if !swendtag
491        let [bline, bcol] = searchpos('<'.blocktag[1:].'\>', "bnW")
492        call s:CountITags(tolower(getline(bline)[: bcol-2]))
493        let state.baseindent = indent(bline) + (s:curind + s:nextrel) * s:ShiftWidth()
494        return state
495      endif
496    endif
497  endif
498  if stopline > stopline2
499    let stopline = stopline2
500    let stopcol = stopcol2
501  endif
502
503  " else look back for comment
504  let [comlnum, comcol, found] = searchpos('\(<!--\[\)\|\(<!--\)\|-->', 'bpnW', stopline)
505  if found == 2 || found == 3
506    " comment opener found, assume a:lnum within comment
507    let state.block = (found == 3 ? 5 : 6)
508    let state.blocklnr = comlnum
509    " check preceding tags in the line:
510    call s:CountITags(tolower(getline(comlnum)[: comcol-2]))
511    if found == 2
512      let state.baseindent = b:hi_indent.baseindent
513    endif
514    let state.blocktagind = indent(comlnum) + (s:curind + s:nextrel) * s:ShiftWidth()
515    return state
516  endif
517
518  " else within usual HTML
519  let text = tolower(getline(state.lnum))
520
521  " Check a:lnum-1 for closing comment (we need indent from the opening line).
522  " Not when other tags follow (might be --> inside a string).
523  let comcol = stridx(text, '-->')
524  if comcol >= 0 && match(text, '[<>]', comcol) <= 0
525    call cursor(state.lnum, comcol + 1)
526    let [comlnum, comcol] = searchpos('<!--', 'bW')
527    if comlnum == state.lnum
528      let text = text[: comcol-2]
529    else
530      let text = tolower(getline(comlnum)[: comcol-2])
531    endif
532    call s:CountITags(text)
533    let state.baseindent = indent(comlnum) + (s:curind + s:nextrel) * s:ShiftWidth()
534    " TODO check tags that follow "-->"
535    return state
536  endif
537
538  " Check if the previous line starts with end tag.
539  let swendtag = match(text, '^\s*</') >= 0
540
541  " If previous line ended in a closing tag, line up with the opening tag.
542  if !swendtag && text =~ '</\w\+\s*>\s*$'
543    call cursor(state.lnum, 99999)
544    normal! F<
545    let start_lnum = HtmlIndent_FindStartTag()
546    if start_lnum > 0
547      let state.baseindent = indent(start_lnum)
548      if col('.') > 2
549        " check for tags before the matching opening tag.
550        let text = getline(start_lnum)
551        let swendtag = match(text, '^\s*</') >= 0
552        call s:CountITags(text[: col('.') - 2])
553        let state.baseindent += s:nextrel * s:ShiftWidth()
554        if !swendtag
555          let state.baseindent += s:curind * s:ShiftWidth()
556        endif
557      endif
558      return state
559    endif
560  endif
561
562  " Else: no comments. Skip backwards to find the tag we're inside.
563  let [state.lnum, found] = HtmlIndent_FindTagStart(state.lnum)
564  " Check if that line starts with end tag.
565  let text = getline(state.lnum)
566  let swendtag = match(text, '^\s*</') >= 0
567  call s:CountITags(tolower(text))
568  let state.baseindent = indent(state.lnum) + s:nextrel * s:ShiftWidth()
569  if !swendtag
570    let state.baseindent += s:curind * s:ShiftWidth()
571  endif
572  return state
573endfunc "}}}
574
575" Indent inside a <pre> block: Keep indent as-is.
576func! s:Alien2()
577  "{{{
578  return -1
579endfunc "}}}
580
581" Return the indent inside a <script> block for javascript.
582func! s:Alien3()
583  "{{{
584  let lnum = prevnonblank(v:lnum - 1)
585  while lnum > 1 && getline(lnum) =~ '^\s*/[/*]'
586    " Skip over comments to avoid that cindent() aligns with the <script> tag
587    let lnum = prevnonblank(lnum - 1)
588  endwhile
589  if lnum == b:hi_indent.blocklnr
590    " indent for the first line after <script>
591    return eval(b:hi_js1indent)
592  endif
593  if b:hi_indent.scripttype == "javascript"
594    return GetJavascriptIndent()
595  else
596    return -1
597  endif
598endfunc "}}}
599
600" Return the indent inside a <style> block.
601func! s:Alien4()
602  "{{{
603  if prevnonblank(v:lnum-1) == b:hi_indent.blocklnr
604    " indent for first content line
605    return eval(b:hi_css1indent)
606  endif
607  return s:CSSIndent()
608endfunc "}}}
609
610" Indending inside a <style> block.  Returns the indent.
611func! s:CSSIndent()
612  "{{{
613  " This handles standard CSS and also Closure stylesheets where special lines
614  " start with @.
615  " When the line starts with '*' or the previous line starts with "/*"
616  " and does not end in "*/", use C indenting to format the comment.
617  " Adopted $VIMRUNTIME/indent/css.vim
618  let curtext = getline(v:lnum)
619  if curtext =~ '^\s*[*]'
620        \ || (v:lnum > 1 && getline(v:lnum - 1) =~ '\s*/\*'
621        \     && getline(v:lnum - 1) !~ '\*/\s*$')
622    return cindent(v:lnum)
623  endif
624
625  let min_lnum = b:hi_indent.blocklnr
626  let prev_lnum = s:CssPrevNonComment(v:lnum - 1, min_lnum)
627  let [prev_lnum, found] = HtmlIndent_FindTagStart(prev_lnum)
628  if prev_lnum <= min_lnum
629    " Just below the <style> tag, indent for first content line after comments.
630    return eval(b:hi_css1indent)
631  endif
632
633  " If the current line starts with "}" align with it's match.
634  if curtext =~ '^\s*}'
635    call cursor(v:lnum, 1)
636    try
637      normal! %
638      " Found the matching "{", align with it after skipping unfinished lines.
639      let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum)
640      return indent(align_lnum)
641    catch
642      " can't find it, try something else, but it's most likely going to be
643      " wrong
644    endtry
645  endif
646
647  " add indent after {
648  let brace_counts = HtmlIndent_CountBraces(prev_lnum)
649  let extra = brace_counts.c_open * s:ShiftWidth()
650
651  let prev_text = getline(prev_lnum)
652  let below_end_brace = prev_text =~ '}\s*$'
653
654  " Search back to align with the first line that's unfinished.
655  let align_lnum = s:CssFirstUnfinished(prev_lnum, min_lnum)
656
657  " Handle continuation lines if aligning with previous line and not after a
658  " "}".
659  if extra == 0 && align_lnum == prev_lnum && !below_end_brace
660    let prev_hasfield = prev_text =~ '^\s*[a-zA-Z0-9-]\+:'
661    let prev_special = prev_text =~ '^\s*\(/\*\|@\)'
662    if curtext =~ '^\s*\(/\*\|@\)'
663      " if the current line is not a comment or starts with @ (used by template
664      " systems) reduce indent if previous line is a continuation line
665      if !prev_hasfield && !prev_special
666        let extra = -s:ShiftWidth()
667      endif
668    else
669      let cur_hasfield = curtext =~ '^\s*[a-zA-Z0-9-]\+:'
670      let prev_unfinished = s:CssUnfinished(prev_text)
671      if !cur_hasfield && (prev_hasfield || prev_unfinished)
672        " Continuation line has extra indent if the previous line was not a
673        " continuation line.
674        let extra = s:ShiftWidth()
675        " Align with @if
676        if prev_text =~ '^\s*@if '
677          let extra = 4
678        endif
679      elseif cur_hasfield && !prev_hasfield && !prev_special
680        " less indent below a continuation line
681        let extra = -s:ShiftWidth()
682      endif
683    endif
684  endif
685
686  if below_end_brace
687    " find matching {, if that line starts with @ it's not the start of a rule
688    " but something else from a template system
689    call cursor(prev_lnum, 1)
690    call search('}\s*$')
691    try
692      normal! %
693      " Found the matching "{", align with it.
694      let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum)
695      let special = getline(align_lnum) =~ '^\s*@'
696    catch
697      let special = 0
698    endtry
699    if special
700      " do not reduce indent below @{ ... }
701      if extra < 0
702        let extra += s:ShiftWidth()
703      endif
704    else
705      let extra -= (brace_counts.c_close - (prev_text =~ '^\s*}')) * s:ShiftWidth()
706    endif
707  endif
708
709  " if no extra indent yet...
710  if extra == 0
711    if brace_counts.p_open > brace_counts.p_close
712      " previous line has more ( than ): add a shiftwidth
713      let extra = s:ShiftWidth()
714    elseif brace_counts.p_open < brace_counts.p_close
715      " previous line has more ) than (: subtract a shiftwidth
716      let extra = -s:ShiftWidth()
717    endif
718  endif
719
720  return indent(align_lnum) + extra
721endfunc "}}}
722
723" Inside <style>: Whether a line is unfinished.
724func! s:CssUnfinished(text)
725  "{{{
726  return a:text =~ '\s\(||\|&&\|:\)\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 + s: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 + s: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\w\+\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, '\w\+\|!--', 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 previous line 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      return indent(lnum)
912    endif
913  endif
914
915  " Should be another attribute: " attr="val".  Align with the previous
916  " attribute start.
917  let lnum = v:lnum
918  while lnum > 1
919    let lnum -= 1
920    let text = getline(lnum)
921    " Find a match with one of these, align with "attr":
922    "       attr=
923    "  <tag attr=
924    "  text<tag attr=
925    "  <tag>text</tag>text<tag attr=
926    " For long lines search for the first match, finding the last match
927    " gets very slow.
928    if len(text) < 300
929      let idx = match(text, '.*\s\zs[_a-zA-Z0-9-]\+="')
930    else
931      let idx = match(text, '\s\zs[_a-zA-Z0-9-]\+="')
932    endif
933    if idx > 0
934      " Found the attribute.  TODO: assumes spaces, no Tabs.
935      return idx
936    endif
937  endwhile
938  return -1
939endfunc "}}}
940
941" THE MAIN INDENT FUNCTION. Return the amount of indent for v:lnum.
942func! HtmlIndent()
943  "{{{
944  if prevnonblank(v:lnum - 1) < 1
945    " First non-blank line has no indent.
946    return 0
947  endif
948
949  let curtext = tolower(getline(v:lnum))
950  let indentunit = s:ShiftWidth()
951
952  let b:hi_newstate = {}
953  let b:hi_newstate.lnum = v:lnum
954
955  " When syntax HL is enabled, detect we are inside a tag.  Indenting inside
956  " a tag works very differently. Do not do this when the line starts with
957  " "<", it gets the "htmlTag" ID but we are not inside a tag then.
958  if curtext !~ '^\s*<'
959    normal! ^
960    let stack = synstack(v:lnum, col('.'))  " assumes there are no tabs
961    let foundHtmlString = 0
962    for synid in reverse(stack)
963      let name = synIDattr(synid, "name")
964      if index(b:hi_insideStringNames, name) >= 0
965        let foundHtmlString = 1
966      elseif index(b:hi_insideTagNames, name) >= 0
967        " Yes, we are inside a tag.
968        let indent = s:InsideTag(foundHtmlString)
969        if indent >= 0
970          " Do not keep the state. TODO: could keep the block type.
971          let b:hi_indent.lnum = 0
972          return indent
973        endif
974      endif
975    endfor
976  endif
977
978  " does the line start with a closing tag?
979  let swendtag = match(curtext, '^\s*</') >= 0
980
981  if prevnonblank(v:lnum - 1) == b:hi_indent.lnum && b:hi_lasttick == b:changedtick - 1
982    " use state (continue from previous line)
983  else
984    " start over (know nothing)
985    let b:hi_indent = s:FreshState(v:lnum)
986  endif
987
988  if b:hi_indent.block >= 2
989    " within block
990    let endtag = s:endtags[b:hi_indent.block]
991    let blockend = stridx(curtext, endtag)
992    if blockend >= 0
993      " block ends here
994      let b:hi_newstate.block = 0
995      " calc indent for REST OF LINE (may start more blocks):
996      call s:CountTagsAndState(strpart(curtext, blockend + strlen(endtag)))
997      if swendtag && b:hi_indent.block != 5
998        let indent = b:hi_indent.blocktagind + s:curind * indentunit
999        let b:hi_newstate.baseindent = indent + s:nextrel * indentunit
1000      else
1001        let indent = s:Alien{b:hi_indent.block}()
1002        let b:hi_newstate.baseindent = b:hi_indent.blocktagind + s:nextrel * indentunit
1003      endif
1004    else
1005      " block continues
1006      " indent this line with alien method
1007      let indent = s:Alien{b:hi_indent.block}()
1008    endif
1009  else
1010    " not within a block - within usual html
1011    let b:hi_newstate.block = b:hi_indent.block
1012    if swendtag
1013      " The current line starts with an end tag, align with its start tag.
1014      call cursor(v:lnum, 1)
1015      let start_lnum = HtmlIndent_FindStartTag()
1016      if start_lnum > 0
1017        " check for the line starting with something inside a tag:
1018        " <sometag               <- align here
1019        "    attr=val><open>     not here
1020        let text = getline(start_lnum)
1021        let angle = matchstr(text, '[<>]')
1022        if angle == '>'
1023          call cursor(start_lnum, 1)
1024          normal! f>%
1025          let start_lnum = line('.')
1026          let text = getline(start_lnum)
1027        endif
1028
1029        let indent = indent(start_lnum)
1030        if col('.') > 2
1031          let swendtag = match(text, '^\s*</') >= 0
1032          call s:CountITags(text[: col('.') - 2])
1033          let indent += s:nextrel * s:ShiftWidth()
1034          if !swendtag
1035            let indent += s:curind * s:ShiftWidth()
1036          endif
1037        endif
1038      else
1039        " not sure what to do
1040        let indent = b:hi_indent.baseindent
1041      endif
1042      let b:hi_newstate.baseindent = indent
1043    else
1044      call s:CountTagsAndState(curtext)
1045      let indent = b:hi_indent.baseindent
1046      let b:hi_newstate.baseindent = indent + (s:curind + s:nextrel) * indentunit
1047    endif
1048  endif
1049
1050  let b:hi_lasttick = b:changedtick
1051  call extend(b:hi_indent, b:hi_newstate, "force")
1052  return indent
1053endfunc "}}}
1054
1055" Check user settings when loading this script the first time.
1056call HtmlIndent_CheckUserSettings()
1057
1058let &cpo = s:cpo_save
1059unlet s:cpo_save
1060
1061" vim: fdm=marker ts=8 sw=2 tw=78
1062