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