xref: /vim-8.2.3635/runtime/indent/html.vim (revision 2346a637)
1" Vim indent script for HTML
2" Maintainer:	Bram Moolenaar
3" Original Author: Andy Wokula <[email protected]>
4" Last Change:	2021 Jun 13
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 <script> itself
586    return b:hi_indent.blocktagind
587  endif
588  if lnum == b:hi_indent.blocklnr
589    " indent for the first line after <script>
590    return eval(b:hi_js1indent)
591  endif
592  if b:hi_indent.scripttype == "javascript"
593    " indent for further lines
594    return eval(b:hi_js1indent) + GetJavascriptIndent()
595  else
596    return -1
597  endif
598endfunc "}}}
599
600" Return the indent inside a <style> block.
601func s:Alien4()
602  "{{{
603  if prevnonblank(v:lnum-1) == b:hi_indent.blocklnr
604    " indent for first content line
605    return eval(b:hi_css1indent)
606  endif
607  return s:CSSIndent()
608endfunc "}}}
609
610" Indending inside a <style> block.  Returns the indent.
611func s:CSSIndent()
612  "{{{
613  " This handles standard CSS and also Closure stylesheets where special lines
614  " start with @.
615  " When the line starts with '*' or the previous line starts with "/*"
616  " and does not end in "*/", use C indenting to format the comment.
617  " Adopted $VIMRUNTIME/indent/css.vim
618  let curtext = getline(v:lnum)
619  if curtext =~ '^\s*[*]'
620        \ || (v:lnum > 1 && getline(v:lnum - 1) =~ '\s*/\*'
621        \     && getline(v:lnum - 1) !~ '\*/\s*$')
622    return cindent(v:lnum)
623  endif
624
625  let min_lnum = b:hi_indent.blocklnr
626  let prev_lnum = s:CssPrevNonComment(v:lnum - 1, min_lnum)
627  let [prev_lnum, found] = HtmlIndent_FindTagStart(prev_lnum)
628  if prev_lnum <= min_lnum
629    " Just below the <style> tag, indent for first content line after comments.
630    return eval(b:hi_css1indent)
631  endif
632
633  " If the current line starts with "}" align with its match.
634  if curtext =~ '^\s*}'
635    call cursor(v:lnum, 1)
636    try
637      normal! %
638      " Found the matching "{", align with it after skipping unfinished lines.
639      let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum)
640      return indent(align_lnum)
641    catch
642      " can't find it, try something else, but it's most likely going to be
643      " wrong
644    endtry
645  endif
646
647  " add indent after {
648  let brace_counts = HtmlIndent_CountBraces(prev_lnum)
649  let extra = brace_counts.c_open * shiftwidth()
650
651  let prev_text = getline(prev_lnum)
652  let below_end_brace = prev_text =~ '}\s*$'
653
654  " Search back to align with the first line that's unfinished.
655  let align_lnum = s:CssFirstUnfinished(prev_lnum, min_lnum)
656
657  " Handle continuation lines if aligning with previous line and not after a
658  " "}".
659  if extra == 0 && align_lnum == prev_lnum && !below_end_brace
660    let prev_hasfield = prev_text =~ '^\s*[a-zA-Z0-9-]\+:'
661    let prev_special = prev_text =~ '^\s*\(/\*\|@\)'
662    if curtext =~ '^\s*\(/\*\|@\)'
663      " if the current line is not a comment or starts with @ (used by template
664      " systems) reduce indent if previous line is a continuation line
665      if !prev_hasfield && !prev_special
666        let extra = -shiftwidth()
667      endif
668    else
669      let cur_hasfield = curtext =~ '^\s*[a-zA-Z0-9-]\+:'
670      let prev_unfinished = s:CssUnfinished(prev_text)
671      if prev_unfinished
672        " Continuation line has extra indent if the previous line was not a
673        " continuation line.
674        let extra = shiftwidth()
675        " Align with @if
676        if prev_text =~ '^\s*@if '
677          let extra = 4
678        endif
679      elseif cur_hasfield && !prev_hasfield && !prev_special
680        " less indent below a continuation line
681        let extra = -shiftwidth()
682      endif
683    endif
684  endif
685
686  if below_end_brace
687    " find matching {, if that line starts with @ it's not the start of a rule
688    " but something else from a template system
689    call cursor(prev_lnum, 1)
690    call search('}\s*$')
691    try
692      normal! %
693      " Found the matching "{", align with it.
694      let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum)
695      let special = getline(align_lnum) =~ '^\s*@'
696    catch
697      let special = 0
698    endtry
699    if special
700      " do not reduce indent below @{ ... }
701      if extra < 0
702        let extra += shiftwidth()
703      endif
704    else
705      let extra -= (brace_counts.c_close - (prev_text =~ '^\s*}')) * shiftwidth()
706    endif
707  endif
708
709  " if no extra indent yet...
710  if extra == 0
711    if brace_counts.p_open > brace_counts.p_close
712      " previous line has more ( than ): add a shiftwidth
713      let extra = shiftwidth()
714    elseif brace_counts.p_open < brace_counts.p_close
715      " previous line has more ) than (: subtract a shiftwidth
716      let extra = -shiftwidth()
717    endif
718  endif
719
720  return indent(align_lnum) + extra
721endfunc "}}}
722
723" Inside <style>: Whether a line is unfinished.
724" 	tag:
725" 	tag: blah
726" 	tag: blah &&
727" 	tag: blah ||
728func s:CssUnfinished(text)
729  "{{{
730  return a:text =~ '\(||\|&&\|:\|\k\)\s*$'
731endfunc "}}}
732
733" Search back for the first unfinished line above "lnum".
734func s:CssFirstUnfinished(lnum, min_lnum)
735  "{{{
736  let align_lnum = a:lnum
737  while align_lnum > a:min_lnum && s:CssUnfinished(getline(align_lnum - 1))
738    let align_lnum -= 1
739  endwhile
740  return align_lnum
741endfunc "}}}
742
743" Find the non-empty line at or before "lnum" that is not a comment.
744func s:CssPrevNonComment(lnum, stopline)
745  "{{{
746  " caller starts from a line a:lnum + 1 that is not a comment
747  let lnum = prevnonblank(a:lnum)
748  while 1
749    let ccol = match(getline(lnum), '\*/')
750    if ccol < 0
751      " No comment end thus it's something else.
752      return lnum
753    endif
754    call cursor(lnum, ccol + 1)
755    " Search back for the /* that starts the comment
756    let lnum = search('/\*', 'bW', a:stopline)
757    if indent(".") == virtcol(".") - 1
758      " The  found /* is at the start of the line. Now go back to the line
759      " above it and again check if it is a comment.
760      let lnum = prevnonblank(lnum - 1)
761    else
762      " /* is after something else, thus it's not a comment line.
763      return lnum
764    endif
765  endwhile
766endfunc "}}}
767
768" Check the number of {} and () in line "lnum". Return a dict with the counts.
769func HtmlIndent_CountBraces(lnum)
770  "{{{
771  let brs = substitute(getline(a:lnum), '[''"].\{-}[''"]\|/\*.\{-}\*/\|/\*.*$\|[^{}()]', '', 'g')
772  let c_open = 0
773  let c_close = 0
774  let p_open = 0
775  let p_close = 0
776  for brace in split(brs, '\zs')
777    if brace == "{"
778      let c_open += 1
779    elseif brace == "}"
780      if c_open > 0
781        let c_open -= 1
782      else
783        let c_close += 1
784      endif
785    elseif brace == '('
786      let p_open += 1
787    elseif brace == ')'
788      if p_open > 0
789        let p_open -= 1
790      else
791        let p_close += 1
792      endif
793    endif
794  endfor
795  return {'c_open': c_open,
796        \ 'c_close': c_close,
797        \ 'p_open': p_open,
798        \ 'p_close': p_close}
799endfunc "}}}
800
801" Return the indent for a comment: <!-- -->
802func s:Alien5()
803  "{{{
804  let curtext = getline(v:lnum)
805  if curtext =~ '^\s*\zs-->'
806    " current line starts with end of comment, line up with comment start.
807    call cursor(v:lnum, 0)
808    let lnum = search('<!--', 'b')
809    if lnum > 0
810      " TODO: what if <!-- is not at the start of the line?
811      return indent(lnum)
812    endif
813
814    " Strange, can't find it.
815    return -1
816  endif
817
818  let prevlnum = prevnonblank(v:lnum - 1)
819  let prevtext = getline(prevlnum)
820  let idx = match(prevtext, '^\s*\zs<!--')
821  if idx >= 0
822    " just below comment start, add a shiftwidth
823    return indent(prevlnum) + shiftwidth()
824  endif
825
826  " Some files add 4 spaces just below a TODO line.  It's difficult to detect
827  " the end of the TODO, so let's not do that.
828
829  " Align with the previous non-blank line.
830  return indent(prevlnum)
831endfunc "}}}
832
833" Return the indent for conditional comment: <!--[ ![endif]-->
834func s:Alien6()
835  "{{{
836  let curtext = getline(v:lnum)
837  if curtext =~ '\s*\zs<!\[endif\]-->'
838    " current line starts with end of comment, line up with comment start.
839    let lnum = search('<!--', 'bn')
840    if lnum > 0
841      return indent(lnum)
842    endif
843  endif
844  return b:hi_indent.baseindent + shiftwidth()
845endfunc "}}}
846
847" When the "lnum" line ends in ">" find the line containing the matching "<".
848func HtmlIndent_FindTagStart(lnum)
849  "{{{
850  " Avoids using the indent of a continuation line.
851  " Moves the cursor.
852  " Return two values:
853  " - the matching line number or "lnum".
854  " - a flag indicating whether we found the end of a tag.
855  " This method is global so that HTML-like indenters can use it.
856  " To avoid matching " > " or " < " inside a string require that the opening
857  " "<" is followed by a word character and the closing ">" comes after a
858  " non-white character.
859  let idx = match(getline(a:lnum), '\S>\s*$')
860  if idx > 0
861    call cursor(a:lnum, idx)
862    let lnum = searchpair('<\w', '' , '\S>', 'bW', '', max([a:lnum - b:html_indent_line_limit, 0]))
863    if lnum > 0
864      return [lnum, 1]
865    endif
866  endif
867  return [a:lnum, 0]
868endfunc "}}}
869
870" Find the unclosed start tag from the current cursor position.
871func HtmlIndent_FindStartTag()
872  "{{{
873  " The cursor must be on or before a closing tag.
874  " If found, positions the cursor at the match and returns the line number.
875  " Otherwise returns 0.
876  let tagname = matchstr(getline('.')[col('.') - 1:], '</\zs' . s:tagname . '\ze')
877  let start_lnum = searchpair('<' . tagname . '\>', '', '</' . tagname . '\>', 'bW')
878  if start_lnum > 0
879    return start_lnum
880  endif
881  return 0
882endfunc "}}}
883
884" Moves the cursor from a "<" to the matching ">".
885func HtmlIndent_FindTagEnd()
886  "{{{
887  " Call this with the cursor on the "<" of a start tag.
888  " This will move the cursor to the ">" of the matching end tag or, when it's
889  " a self-closing tag, to the matching ">".
890  " Limited to look up to b:html_indent_line_limit lines away.
891  let text = getline('.')
892  let tagname = matchstr(text, s:tagname . '\|!--', col('.'))
893  if tagname == '!--'
894    call search('--\zs>')
895  elseif s:get_tag('/' . tagname) != 0
896    " tag with a closing tag, find matching "</tag>"
897    call searchpair('<' . tagname, '', '</' . tagname . '\zs>', 'W', '', line('.') + b:html_indent_line_limit)
898  else
899    " self-closing tag, find the ">"
900    call search('\S\zs>')
901  endif
902endfunc "}}}
903
904" Indenting inside a start tag. Return the correct indent or -1 if unknown.
905func s:InsideTag(foundHtmlString)
906  "{{{
907  if a:foundHtmlString
908    " Inside an attribute string.
909    " Align with the opening quote or use an external function.
910    let lnum = v:lnum - 1
911    if lnum > 1
912      if exists('b:html_indent_tag_string_func')
913        return b:html_indent_tag_string_func(lnum)
914      endif
915      " If there is a double quote in the previous line, indent with the
916      " character after it.
917      if getline(lnum) =~ '"'
918	call cursor(lnum, 0)
919	normal f"
920	return virtcol('.')
921      endif
922      return indent(lnum)
923    endif
924  endif
925
926  " Should be another attribute: " attr="val".  Align with the previous
927  " attribute start.
928  let lnum = v:lnum
929  while lnum > 1
930    let lnum -= 1
931    let text = getline(lnum)
932    " Find a match with one of these, align with "attr":
933    "       attr=
934    "  <tag attr=
935    "  text<tag attr=
936    "  <tag>text</tag>text<tag attr=
937    " For long lines search for the first match, finding the last match
938    " gets very slow.
939    if len(text) < 300
940      let idx = match(text, '.*\s\zs[_a-zA-Z0-9-]\+="')
941    else
942      let idx = match(text, '\s\zs[_a-zA-Z0-9-]\+="')
943    endif
944    if idx == -1
945      " try <tag attr
946      let idx = match(text, '<' . s:tagname . '\s\+\zs\w')
947    endif
948    if idx == -1
949      " after just "<tag" indent two levels more
950      let idx = match(text, '<' . s:tagname . '$')
951      if idx >= 0
952	call cursor(lnum, idx + 1)
953	return virtcol('.') - 1 + shiftwidth() * 2
954      endif
955    endif
956    if idx > 0
957      " Found the attribute to align with.
958      call cursor(lnum, idx)
959      return virtcol('.')
960    endif
961  endwhile
962  return -1
963endfunc "}}}
964
965" THE MAIN INDENT FUNCTION. Return the amount of indent for v:lnum.
966func HtmlIndent()
967  "{{{
968  if prevnonblank(v:lnum - 1) < 1
969    " First non-blank line has no indent.
970    return 0
971  endif
972
973  let curtext = tolower(getline(v:lnum))
974  let indentunit = shiftwidth()
975
976  let b:hi_newstate = {}
977  let b:hi_newstate.lnum = v:lnum
978
979  " When syntax HL is enabled, detect we are inside a tag.  Indenting inside
980  " a tag works very differently. Do not do this when the line starts with
981  " "<", it gets the "htmlTag" ID but we are not inside a tag then.
982  if curtext !~ '^\s*<'
983    normal! ^
984    let stack = synstack(v:lnum, col('.'))  " assumes there are no tabs
985    let foundHtmlString = 0
986    for synid in reverse(stack)
987      let name = synIDattr(synid, "name")
988      if index(b:hi_insideStringNames, name) >= 0
989        let foundHtmlString = 1
990      elseif index(b:hi_insideTagNames, name) >= 0
991        " Yes, we are inside a tag.
992        let indent = s:InsideTag(foundHtmlString)
993        if indent >= 0
994          " Do not keep the state. TODO: could keep the block type.
995          let b:hi_indent.lnum = 0
996          return indent
997        endif
998      endif
999    endfor
1000  endif
1001
1002  " does the line start with a closing tag?
1003  let swendtag = match(curtext, '^\s*</') >= 0
1004
1005  if prevnonblank(v:lnum - 1) == b:hi_indent.lnum && b:hi_lasttick == b:changedtick - 1
1006    " use state (continue from previous line)
1007  else
1008    " start over (know nothing)
1009    let b:hi_indent = s:FreshState(v:lnum)
1010  endif
1011
1012  if b:hi_indent.block >= 2
1013    " within block
1014    let endtag = s:endtags[b:hi_indent.block]
1015    let blockend = stridx(curtext, endtag)
1016    if blockend >= 0
1017      " block ends here
1018      let b:hi_newstate.block = 0
1019      " calc indent for REST OF LINE (may start more blocks):
1020      call s:CountTagsAndState(strpart(curtext, blockend + strlen(endtag)))
1021      if swendtag && b:hi_indent.block != 5
1022        let indent = b:hi_indent.blocktagind + s:curind * indentunit
1023        let b:hi_newstate.baseindent = indent + s:nextrel * indentunit
1024      else
1025        let indent = s:Alien{b:hi_indent.block}()
1026        let b:hi_newstate.baseindent = b:hi_indent.blocktagind + s:nextrel * indentunit
1027      endif
1028    else
1029      " block continues
1030      " indent this line with alien method
1031      let indent = s:Alien{b:hi_indent.block}()
1032    endif
1033  else
1034    " not within a block - within usual html
1035    let b:hi_newstate.block = b:hi_indent.block
1036    if swendtag
1037      " The current line starts with an end tag, align with its start tag.
1038      call cursor(v:lnum, 1)
1039      let start_lnum = HtmlIndent_FindStartTag()
1040      if start_lnum > 0
1041        " check for the line starting with something inside a tag:
1042        " <sometag               <- align here
1043        "    attr=val><open>     not here
1044        let text = getline(start_lnum)
1045        let angle = matchstr(text, '[<>]')
1046        if angle == '>'
1047          call cursor(start_lnum, 1)
1048          normal! f>%
1049          let start_lnum = line('.')
1050          let text = getline(start_lnum)
1051        endif
1052
1053        let indent = indent(start_lnum)
1054        if col('.') > 2
1055          let swendtag = match(text, '^\s*</') >= 0
1056          call s:CountITags(text[: col('.') - 2])
1057          let indent += s:nextrel * shiftwidth()
1058          if !swendtag
1059            let indent += s:curind * shiftwidth()
1060          endif
1061        endif
1062      else
1063        " not sure what to do
1064        let indent = b:hi_indent.baseindent
1065      endif
1066      let b:hi_newstate.baseindent = indent
1067    else
1068      call s:CountTagsAndState(curtext)
1069      let indent = b:hi_indent.baseindent
1070      let b:hi_newstate.baseindent = indent + (s:curind + s:nextrel) * indentunit
1071    endif
1072  endif
1073
1074  let b:hi_lasttick = b:changedtick
1075  call extend(b:hi_indent, b:hi_newstate, "force")
1076  return indent
1077endfunc "}}}
1078
1079" Check user settings when loading this script the first time.
1080call HtmlIndent_CheckUserSettings()
1081
1082let &cpo = s:cpo_save
1083unlet s:cpo_save
1084
1085" vim: fdm=marker ts=8 sw=2 tw=78
1086