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