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