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