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