blob: 7eb963b7b2174539f5718594a3e1ab5435976afc [file] [log] [blame]
Bram Moolenaarec7944a2013-06-12 21:29:15 +02001" Vim indent script for HTML
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +02002" Header: "{{{
3" Maintainer: Bram Moolenaar
4" Original Author: Andy Wokula <anwoku@yahoo.de>
Bram Moolenaar541f92d2015-06-19 13:27:23 +02005" Last Change: 2015 Jun 12
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +02006" 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.
Bram Moolenaarec7944a2013-06-12 21:29:15 +020011"
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:
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020017" 2014 June (v1.0) overhaul (Bram)
Bram Moolenaar52b91d82013-06-15 21:39:51 +020018" 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)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020022"}}}
Bram Moolenaar071d4272004-06-13 20:20:40 +000023
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020024" Init Folklore, check user settings (2nd time ++)
25if exists("b:did_indent") "{{{
26 finish
Bram Moolenaar071d4272004-06-13 20:20:40 +000027endif
28let b:did_indent = 1
29
Bram Moolenaarec7944a2013-06-12 21:29:15 +020030setlocal indentexpr=HtmlIndent()
31setlocal indentkeys=o,O,<Return>,<>>,{,},!^F
Bram Moolenaar071d4272004-06-13 20:20:40 +000032
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020033" "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.
Bram Moolenaar946e27a2014-06-25 18:50:27 +020046setlocal matchpairs+=<:>
47
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020048let b:undo_indent = "setlocal inde< indk< cino<"
Bram Moolenaar071d4272004-06-13 20:20:40 +000049
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020050" 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
Bram Moolenaar071d4272004-06-13 20:20:40 +000057endif
58
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020059" shiftwidth() exists since patch 7.3.694
Bram Moolenaar52b91d82013-06-15 21:39:51 +020060if exists('*shiftwidth')
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020061 let s:ShiftWidth = function('shiftwidth')
Bram Moolenaar52b91d82013-06-15 21:39:51 +020062else
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020063 func! s:ShiftWidth()
64 return &shiftwidth
65 endfunc
Bram Moolenaar52b91d82013-06-15 21:39:51 +020066endif
67
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020068" Allow for line continuation below.
Bram Moolenaar91170f82006-05-05 21:15:17 +000069let s:cpo_save = &cpo
Bram Moolenaar071d4272004-06-13 20:20:40 +000070set cpo-=C
Bram Moolenaarec7944a2013-06-12 21:29:15 +020071"}}}
Bram Moolenaar071d4272004-06-13 20:20:40 +000072
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020073" 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
Bram Moolenaar071d4272004-06-13 20:20:40 +000089
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020090 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 = {}
Bram Moolenaar541f92d2015-06-19 13:27:23 +020097 if len(autotags) > 0
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020098 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
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200162 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200163 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200164endfunc "}}}
Bram Moolenaar071d4272004-06-13 20:20:40 +0000165
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200166" Init Script Vars
167"{{{
168let b:hi_lasttick = 0
169let b:hi_newstate = {}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200170let s:countonly = 0
171 "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200172
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200173" Fill the s:indent_tags dict with known tags.
174" The key is "tagname" or "/tagname". {{{
175" The value is:
176" 1 opening tag
177" 2 "pre"
178" 3 "script"
179" 4 "style"
180" 5 comment start
181" -1 closing tag
182" -2 "/pre"
183" -3 "/script"
184" -4 "/style"
185" -5 comment end
186let s:indent_tags = {}
187let s:endtags = [0,0,0,0,0,0] " long enough for the highest index
188"}}}
189
190" Add a list of tag names for a pair of <tag> </tag> to "tags".
191func! s:AddITags(tags, taglist)
192 "{{{
193 for itag in a:taglist
194 let a:tags[itag] = 1
195 let a:tags['/' . itag] = -1
196 endfor
197endfunc "}}}
198
199" Take a list of tag name pairs that are not to be used as tag pairs.
200func! s:RemoveITags(tags, taglist)
201 "{{{
202 for itag in a:taglist
203 let a:tags[itag] = 1
204 let a:tags['/' . itag] = 1
205 endfor
206endfunc "}}}
207
208" Add a block tag, that is a tag with a different kind of indenting.
209func! s:AddBlockTag(tag, id, ...)
210 "{{{
211 if !(a:id >= 2 && a:id < len(s:endtags))
212 echoerr 'AddBlockTag ' . a:id
213 return
214 endif
215 let s:indent_tags[a:tag] = a:id
216 if a:0 == 0
217 let s:indent_tags['/' . a:tag] = -a:id
218 let s:endtags[a:id] = "</" . a:tag . ">"
219 else
220 let s:indent_tags[a:1] = -a:id
221 let s:endtags[a:id] = a:1
222 endif
223endfunc "}}}
224
225" Add known tag pairs.
226" Self-closing tags and tags that are sometimes {{{
227" self-closing (e.g., <p>) are not here (when encountering </p> we can find
228" the matching <p>, but not the other way around).
229" Old HTML tags:
230call s:AddITags(s:indent_tags, [
231 \ 'a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big',
232 \ 'blockquote', 'body', 'button', 'caption', 'center', 'cite', 'code',
233 \ 'colgroup', 'del', 'dfn', 'dir', 'div', 'dl', 'em', 'fieldset', 'font',
234 \ 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'html',
235 \ 'i', 'iframe', 'ins', 'kbd', 'label', 'legend', 'li',
236 \ 'map', 'menu', 'noframes', 'noscript', 'object', 'ol',
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200237 \ 'optgroup', 'q', 's', 'samp', 'select', 'small', 'span', 'strong', 'sub',
238 \ 'sup', 'table', 'textarea', 'title', 'tt', 'u', 'ul', 'var', 'th', 'td',
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200239 \ 'tr', 'tbody', 'tfoot', 'thead'])
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200240
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200241" Tags added 2011 Sep 09 (especially HTML5 tags):
242call s:AddITags(s:indent_tags, [
243 \ 'area', 'article', 'aside', 'audio', 'bdi', 'canvas',
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200244 \ 'command', 'datalist', 'details', 'embed', 'figure', 'footer',
245 \ 'header', 'group', 'keygen', 'mark', 'math', 'meter', 'nav', 'output',
246 \ 'progress', 'ruby', 'section', 'svg', 'texture', 'time', 'video',
247 \ 'wbr', 'text'])
Bram Moolenaard8b77f72015-03-05 21:21:19 +0100248
249" Tags added for web components:
250call s:AddITags(s:indent_tags, [
251 \ 'content', 'shadow', 'template'])
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200252"}}}
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200253
254" Add Block Tags: these contain alien content
255"{{{
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200256call s:AddBlockTag('pre', 2)
257call s:AddBlockTag('script', 3)
258call s:AddBlockTag('style', 4)
259call s:AddBlockTag('<!--', 5, '-->')
260"}}}
261
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200262" Return non-zero when "tagname" is an opening tag, not being a block tag, for
263" which there should be a closing tag. Can be used by scripts that include
264" HTML indenting.
265func! HtmlIndent_IsOpenTag(tagname)
266 "{{{
267 if get(s:indent_tags, a:tagname) == 1
268 return 1
269 endif
270 return get(b:hi_tags, a:tagname) == 1
271endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200272
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200273" Get the value for "tagname", taking care of buffer-local tags.
274func! s:get_tag(tagname)
275 "{{{
276 let i = get(s:indent_tags, a:tagname)
277 if (i == 1 || i == -1) && get(b:hi_removed_tags, a:tagname) != 0
278 return 0
279 endif
280 if i == 0
281 let i = get(b:hi_tags, a:tagname)
282 endif
283 return i
284endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200285
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200286" Count the number of start and end tags in "text".
287func! s:CountITags(text)
288 "{{{
289 " Store the result in s:curind and s:nextrel.
290 let s:curind = 0 " relative indent steps for current line [unit &sw]:
291 let s:nextrel = 0 " relative indent steps for next line [unit &sw]:
292 let s:block = 0 " assume starting outside of a block
293 let s:countonly = 1 " don't change state
Bram Moolenaard8b77f72015-03-05 21:21:19 +0100294 call substitute(a:text, '<\zs/\=\w\+\(-\w\+\)*\>\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g')
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200295 let s:countonly = 0
296endfunc "}}}
297
298" Count the number of start and end tags in text.
299func! s:CountTagsAndState(text)
300 "{{{
301 " Store the result in s:curind and s:nextrel. Update b:hi_newstate.block.
302 let s:curind = 0 " relative indent steps for current line [unit &sw]:
303 let s:nextrel = 0 " relative indent steps for next line [unit &sw]:
304
305 let s:block = b:hi_newstate.block
Bram Moolenaard8b77f72015-03-05 21:21:19 +0100306 let tmp = substitute(a:text, '<\zs/\=\w\+\(-\w\+\)*\>\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g')
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200307 if s:block == 3
308 let b:hi_newstate.scripttype = s:GetScriptType(matchstr(tmp, '\C.*<SCRIPT\>\zs[^>]*'))
309 endif
310 let b:hi_newstate.block = s:block
311endfunc "}}}
312
313" Used by s:CountITags() and s:CountTagsAndState().
314func! s:CheckTag(itag)
315 "{{{
316 " Returns an empty string or "SCRIPT".
317 " a:itag can be "tag" or "/tag" or "<!--" or "-->"
Bram Moolenaard8b77f72015-03-05 21:21:19 +0100318 if (s:CheckCustomTag(a:itag))
319 return ""
320 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200321 let ind = s:get_tag(a:itag)
322 if ind == -1
323 " closing tag
324 if s:block != 0
325 " ignore itag within a block
326 return ""
327 endif
328 if s:nextrel == 0
329 let s:curind -= 1
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200330 else
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200331 let s:nextrel -= 1
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200332 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200333 elseif ind == 1
334 " opening tag
335 if s:block != 0
336 return ""
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200337 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200338 let s:nextrel += 1
339 elseif ind != 0
340 " block-tag (opening or closing)
341 return s:CheckBlockTag(a:itag, ind)
342 " else ind==0 (other tag found): keep indent
343 endif
344 return ""
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200345endfunc "}}}
346
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200347" Used by s:CheckTag(). Returns an empty string or "SCRIPT".
348func! s:CheckBlockTag(blocktag, ind)
349 "{{{
350 if a:ind > 0
351 " a block starts here
352 if s:block != 0
353 " already in a block (nesting) - ignore
354 " especially ignore comments after other blocktags
355 return ""
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200356 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200357 let s:block = a:ind " block type
358 if s:countonly
359 return ""
360 endif
361 let b:hi_newstate.blocklnr = v:lnum
362 " save allover indent for the endtag
363 let b:hi_newstate.blocktagind = b:hi_indent.baseindent + (s:nextrel + s:curind) * s:ShiftWidth()
364 if a:ind == 3
365 return "SCRIPT" " all except this must be lowercase
366 " line is to be checked again for the type attribute
367 endif
368 else
369 let s:block = 0
370 " we get here if starting and closing a block-tag on the same line
371 endif
372 return ""
373endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200374
Bram Moolenaard8b77f72015-03-05 21:21:19 +0100375" Used by s:CheckTag().
376func! s:CheckCustomTag(ctag)
377 "{{{
378 " Returns 1 if ctag is the tag for a custom element, 0 otherwise.
379 " a:ctag can be "tag" or "/tag" or "<!--" or "-->"
380 let pattern = '\%\(\w\+-\)\+\w\+'
381 if match(a:ctag, pattern) == -1
382 return 0
383 endif
384 if matchstr(a:ctag, '\/\ze.\+') == "/"
385 " closing tag
386 if s:block != 0
387 " ignore ctag within a block
388 return 1
389 endif
390 if s:nextrel == 0
391 let s:curind -= 1
392 else
393 let s:nextrel -= 1
394 endif
395 else
396 " opening tag
397 if s:block != 0
398 return 1
399 endif
400 let s:nextrel += 1
401 endif
402 return 1
403endfunc "}}}
404
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200405" Return the <script> type: either "javascript" or ""
406func! s:GetScriptType(str)
407 "{{{
408 if a:str == "" || a:str =~ "java"
409 return "javascript"
410 else
411 return ""
412 endif
413endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200414
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200415" Look back in the file, starting at a:lnum - 1, to compute a state for the
416" start of line a:lnum. Return the new state.
417func! s:FreshState(lnum)
418 "{{{
419 " A state is to know ALL relevant details about the
420 " lines 1..a:lnum-1, initial calculating (here!) can be slow, but updating is
421 " fast (incremental).
422 " TODO: this should be split up in detecting the block type and computing the
423 " indent for the block type, so that when we do not know the indent we do
424 " not need to clear the whole state and re-detect the block type again.
425 " State:
426 " lnum last indented line == prevnonblank(a:lnum - 1)
427 " block = 0 a:lnum located within special tag: 0:none, 2:<pre>,
428 " 3:<script>, 4:<style>, 5:<!--
429 " baseindent use this indent for line a:lnum as a start - kind of
430 " autoindent (if block==0)
431 " scripttype = '' type attribute of a script tag (if block==3)
432 " blocktagind indent for current opening (get) and closing (set)
433 " blocktag (if block!=0)
434 " blocklnr lnum of starting blocktag (if block!=0)
435 " inattr line {lnum} starts with attributes of a tag
436 let state = {}
437 let state.lnum = prevnonblank(a:lnum - 1)
438 let state.scripttype = ""
439 let state.blocktagind = -1
440 let state.block = 0
441 let state.baseindent = 0
442 let state.blocklnr = 0
443 let state.inattr = 0
444
445 if state.lnum == 0
446 return state
447 endif
448
449 " Heuristic:
450 " remember startline state.lnum
451 " look back for <pre, </pre, <script, </script, <style, </style tags
452 " remember stopline
453 " if opening tag found,
454 " assume a:lnum within block
455 " else
456 " look back in result range (stopline, startline) for comment
457 " \ delimiters (<!--, -->)
458 " if comment opener found,
459 " assume a:lnum within comment
460 " else
461 " assume usual html for a:lnum
462 " if a:lnum-1 has a closing comment
463 " look back to get indent of comment opener
464 " FI
465
466 " look back for a blocktag
467 call cursor(a:lnum, 1)
468 let [stopline, stopcol] = searchpos('\c<\zs\/\=\%(pre\>\|script\>\|style\>\)', "bW")
469 if stopline > 0
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200470 " fugly ... why isn't there searchstr()
471 let tagline = tolower(getline(stopline))
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200472 let blocktag = matchstr(tagline, '\/\=\%(pre\>\|script\>\|style\>\)', stopcol - 1)
473 if blocktag[0] != "/"
474 " opening tag found, assume a:lnum within block
475 let state.block = s:indent_tags[blocktag]
476 if state.block == 3
477 let state.scripttype = s:GetScriptType(matchstr(tagline, '\>[^>]*', stopcol))
478 endif
479 let state.blocklnr = stopline
480 " check preceding tags in the line:
481 call s:CountITags(tagline[: stopcol-2])
482 let state.blocktagind = indent(stopline) + (s:curind + s:nextrel) * s:ShiftWidth()
483 return state
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200484 elseif stopline == state.lnum
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200485 " handle special case: previous line (= state.lnum) contains a
486 " closing blocktag which is preceded by line-noise;
487 " blocktag == "/..."
488 let swendtag = match(tagline, '^\s*</') >= 0
489 if !swendtag
490 let [bline, bcol] = searchpos('<'.blocktag[1:].'\>', "bW")
491 call s:CountITags(tolower(getline(bline)[: bcol-2]))
492 let state.baseindent = indent(bline) + (s:curind + s:nextrel) * s:ShiftWidth()
493 return state
494 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200495 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200496 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200497
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200498 " else look back for comment
499 call cursor(a:lnum, 1)
500 let [comlnum, comcol, found] = searchpos('\(<!--\)\|-->', 'bpW', stopline)
501 if found == 2
502 " comment opener found, assume a:lnum within comment
503 let state.block = 5
504 let state.blocklnr = comlnum
505 " check preceding tags in the line:
506 call s:CountITags(tolower(getline(comlnum)[: comcol-2]))
507 let state.blocktagind = indent(comlnum) + (s:curind + s:nextrel) * s:ShiftWidth()
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200508 return state
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200509 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200510
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200511 " else within usual HTML
512 let text = tolower(getline(state.lnum))
513
514 " Check a:lnum-1 for closing comment (we need indent from the opening line).
515 " Not when other tags follow (might be --> inside a string).
516 let comcol = stridx(text, '-->')
517 if comcol >= 0 && match(text, '[<>]', comcol) <= 0
518 call cursor(state.lnum, comcol + 1)
519 let [comlnum, comcol] = searchpos('<!--', 'bW')
520 if comlnum == state.lnum
521 let text = text[: comcol-2]
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200522 else
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200523 let text = tolower(getline(comlnum)[: comcol-2])
Bram Moolenaar071d4272004-06-13 20:20:40 +0000524 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200525 call s:CountITags(text)
526 let state.baseindent = indent(comlnum) + (s:curind + s:nextrel) * s:ShiftWidth()
527 " TODO check tags that follow "-->"
528 return state
529 endif
Bram Moolenaar071d4272004-06-13 20:20:40 +0000530
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200531 " Check if the previous line starts with end tag.
532 let swendtag = match(text, '^\s*</') >= 0
533
534 " If previous line ended in a closing tag, line up with the opening tag.
535 if !swendtag && text =~ '</\w\+\s*>\s*$'
536 call cursor(state.lnum, 99999)
Bram Moolenaar7b61a542014-08-23 15:31:19 +0200537 normal! F<
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200538 let start_lnum = HtmlIndent_FindStartTag()
539 if start_lnum > 0
540 let state.baseindent = indent(start_lnum)
541 if col('.') > 2
542 " check for tags before the matching opening tag.
543 let text = getline(start_lnum)
544 let swendtag = match(text, '^\s*</') >= 0
545 call s:CountITags(text[: col('.') - 2])
546 let state.baseindent += s:nextrel * s:ShiftWidth()
547 if !swendtag
548 let state.baseindent += s:curind * s:ShiftWidth()
549 endif
550 endif
551 return state
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200552 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200553 endif
554
555 " Else: no comments. Skip backwards to find the tag we're inside.
556 let [state.lnum, found] = HtmlIndent_FindTagStart(state.lnum)
557 " Check if that line starts with end tag.
558 let text = getline(state.lnum)
559 let swendtag = match(text, '^\s*</') >= 0
560 call s:CountITags(tolower(text))
561 let state.baseindent = indent(state.lnum) + s:nextrel * s:ShiftWidth()
562 if !swendtag
563 let state.baseindent += s:curind * s:ShiftWidth()
564 endif
565 return state
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200566endfunc "}}}
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200567
568" Indent inside a <pre> block: Keep indent as-is.
569func! s:Alien2()
570 "{{{
571 return -1
572endfunc "}}}
573
574" Return the indent inside a <script> block for javascript.
575func! s:Alien3()
576 "{{{
577 let lnum = prevnonblank(v:lnum - 1)
578 while lnum > 1 && getline(lnum) =~ '^\s*/[/*]'
579 " Skip over comments to avoid that cindent() aligns with the <script> tag
580 let lnum = prevnonblank(lnum - 1)
581 endwhile
582 if lnum == b:hi_indent.blocklnr
583 " indent for the first line after <script>
584 return eval(b:hi_js1indent)
585 endif
586 if b:hi_indent.scripttype == "javascript"
587 return cindent(v:lnum)
588 else
589 return -1
590 endif
591endfunc "}}}
592
593" Return the indent inside a <style> block.
594func! s:Alien4()
595 "{{{
596 if prevnonblank(v:lnum-1) == b:hi_indent.blocklnr
597 " indent for first content line
598 return eval(b:hi_css1indent)
599 endif
600 return s:CSSIndent()
601endfunc "}}}
602
603" Indending inside a <style> block. Returns the indent.
604func! s:CSSIndent()
605 "{{{
606 " This handles standard CSS and also Closure stylesheets where special lines
607 " start with @.
608 " When the line starts with '*' or the previous line starts with "/*"
609 " and does not end in "*/", use C indenting to format the comment.
610 " Adopted $VIMRUNTIME/indent/css.vim
611 let curtext = getline(v:lnum)
612 if curtext =~ '^\s*[*]'
613 \ || (v:lnum > 1 && getline(v:lnum - 1) =~ '\s*/\*'
614 \ && getline(v:lnum - 1) !~ '\*/\s*$')
615 return cindent(v:lnum)
616 endif
617
618 let min_lnum = b:hi_indent.blocklnr
619 let prev_lnum = s:CssPrevNonComment(v:lnum - 1, min_lnum)
620 let [prev_lnum, found] = HtmlIndent_FindTagStart(prev_lnum)
621 if prev_lnum <= min_lnum
622 " Just below the <style> tag, indent for first content line after comments.
623 return eval(b:hi_css1indent)
624 endif
625
626 " If the current line starts with "}" align with it's match.
627 if curtext =~ '^\s*}'
628 call cursor(v:lnum, 1)
629 try
630 normal! %
631 " Found the matching "{", align with it after skipping unfinished lines.
632 let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum)
633 return indent(align_lnum)
634 catch
635 " can't find it, try something else, but it's most likely going to be
636 " wrong
637 endtry
638 endif
639
640 " add indent after {
641 let brace_counts = HtmlIndent_CountBraces(prev_lnum)
642 let extra = brace_counts.c_open * s:ShiftWidth()
643
644 let prev_text = getline(prev_lnum)
645 let below_end_brace = prev_text =~ '}\s*$'
646
647 " Search back to align with the first line that's unfinished.
648 let align_lnum = s:CssFirstUnfinished(prev_lnum, min_lnum)
649
650 " Handle continuation lines if aligning with previous line and not after a
651 " "}".
652 if extra == 0 && align_lnum == prev_lnum && !below_end_brace
653 let prev_hasfield = prev_text =~ '^\s*[a-zA-Z0-9-]\+:'
654 let prev_special = prev_text =~ '^\s*\(/\*\|@\)'
655 if curtext =~ '^\s*\(/\*\|@\)'
656 " if the current line is not a comment or starts with @ (used by template
657 " systems) reduce indent if previous line is a continuation line
658 if !prev_hasfield && !prev_special
659 let extra = -s:ShiftWidth()
660 endif
661 else
662 let cur_hasfield = curtext =~ '^\s*[a-zA-Z0-9-]\+:'
663 let prev_unfinished = s:CssUnfinished(prev_text)
664 if !cur_hasfield && (prev_hasfield || prev_unfinished)
665 " Continuation line has extra indent if the previous line was not a
666 " continuation line.
667 let extra = s:ShiftWidth()
668 " Align with @if
669 if prev_text =~ '^\s*@if '
670 let extra = 4
671 endif
672 elseif cur_hasfield && !prev_hasfield && !prev_special
673 " less indent below a continuation line
674 let extra = -s:ShiftWidth()
675 endif
676 endif
677 endif
678
679 if below_end_brace
680 " find matching {, if that line starts with @ it's not the start of a rule
681 " but something else from a template system
682 call cursor(prev_lnum, 1)
683 call search('}\s*$')
684 try
685 normal! %
686 " Found the matching "{", align with it.
687 let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum)
688 let special = getline(align_lnum) =~ '^\s*@'
689 catch
690 let special = 0
691 endtry
692 if special
693 " do not reduce indent below @{ ... }
694 if extra < 0
695 let extra += s:ShiftWidth()
696 endif
697 else
698 let extra -= (brace_counts.c_close - (prev_text =~ '^\s*}')) * s:ShiftWidth()
699 endif
700 endif
701
702 " if no extra indent yet...
703 if extra == 0
704 if brace_counts.p_open > brace_counts.p_close
705 " previous line has more ( than ): add a shiftwidth
706 let extra = s:ShiftWidth()
707 elseif brace_counts.p_open < brace_counts.p_close
708 " previous line has more ) than (: subtract a shiftwidth
709 let extra = -s:ShiftWidth()
710 endif
711 endif
712
713 return indent(align_lnum) + extra
714endfunc "}}}
715
716" Inside <style>: Whether a line is unfinished.
717func! s:CssUnfinished(text)
718 "{{{
719 return a:text =~ '\s\(||\|&&\|:\)\s*$'
720endfunc "}}}
721
722" Search back for the first unfinished line above "lnum".
723func! s:CssFirstUnfinished(lnum, min_lnum)
724 "{{{
725 let align_lnum = a:lnum
726 while align_lnum > a:min_lnum && s:CssUnfinished(getline(align_lnum - 1))
727 let align_lnum -= 1
728 endwhile
729 return align_lnum
730endfunc "}}}
731
732" Find the non-empty line at or before "lnum" that is not a comment.
733func! s:CssPrevNonComment(lnum, stopline)
734 "{{{
735 " caller starts from a line a:lnum + 1 that is not a comment
736 let lnum = prevnonblank(a:lnum)
737 while 1
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200738 let ccol = match(getline(lnum), '\*/')
739 if ccol < 0
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200740 " No comment end thus its something else.
741 return lnum
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200742 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200743 call cursor(lnum, ccol + 1)
744 " Search back for the /* that starts the comment
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200745 let lnum = search('/\*', 'bW', a:stopline)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200746 if indent(".") == virtcol(".") - 1
747 " The found /* is at the start of the line. Now go back to the line
748 " above it and again check if it is a comment.
749 let lnum = prevnonblank(lnum - 1)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200750 else
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200751 " /* is after something else, thus it's not a comment line.
752 return lnum
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200753 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200754 endwhile
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200755endfunc "}}}
756
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200757" Check the number of {} and () in line "lnum". Return a dict with the counts.
758func! HtmlIndent_CountBraces(lnum)
759 "{{{
760 let brs = substitute(getline(a:lnum), '[''"].\{-}[''"]\|/\*.\{-}\*/\|/\*.*$\|[^{}()]', '', 'g')
761 let c_open = 0
762 let c_close = 0
763 let p_open = 0
764 let p_close = 0
765 for brace in split(brs, '\zs')
766 if brace == "{"
767 let c_open += 1
768 elseif brace == "}"
769 if c_open > 0
770 let c_open -= 1
771 else
772 let c_close += 1
773 endif
774 elseif brace == '('
775 let p_open += 1
776 elseif brace == ')'
777 if p_open > 0
778 let p_open -= 1
779 else
780 let p_close += 1
781 endif
782 endif
783 endfor
784 return {'c_open': c_open,
785 \ 'c_close': c_close,
786 \ 'p_open': p_open,
787 \ 'p_close': p_close}
788endfunc "}}}
789
790" Return the indent for a comment: <!-- -->
791func! s:Alien5()
792 "{{{
793 let curtext = getline(v:lnum)
794 if curtext =~ '^\s*\zs-->'
795 " current line starts with end of comment, line up with comment start.
796 call cursor(v:lnum, 0)
797 let lnum = search('<!--', 'b')
798 if lnum > 0
799 " TODO: what if <!-- is not at the start of the line?
800 return indent(lnum)
801 endif
802
803 " Strange, can't find it.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200804 return -1
Bram Moolenaar946e27a2014-06-25 18:50:27 +0200805 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200806
807 let prevlnum = prevnonblank(v:lnum - 1)
808 let prevtext = getline(prevlnum)
809 let idx = match(prevtext, '^\s*\zs<!--')
810 if idx >= 0
811 " just below comment start, add a shiftwidth
812 return idx + s:ShiftWidth()
813 endif
814
815 " Some files add 4 spaces just below a TODO line. It's difficult to detect
816 " the end of the TODO, so let's not do that.
817
818 " Align with the previous non-blank line.
819 return indent(prevlnum)
Bram Moolenaar946e27a2014-06-25 18:50:27 +0200820endfunc "}}}
821
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200822" When the "lnum" line ends in ">" find the line containing the matching "<".
823func! HtmlIndent_FindTagStart(lnum)
824 "{{{
825 " Avoids using the indent of a continuation line.
826 " Moves the cursor.
827 " Return two values:
828 " - the matching line number or "lnum".
829 " - a flag indicating whether we found the end of a tag.
830 " This method is global so that HTML-like indenters can use it.
831 " To avoid matching " > " or " < " inside a string require that the opening
832 " "<" is followed by a word character and the closing ">" comes after a
833 " non-white character.
834 let idx = match(getline(a:lnum), '\S>\s*$')
835 if idx > 0
836 call cursor(a:lnum, idx)
837 let lnum = searchpair('<\w', '' , '\S>', 'bW', '', max([a:lnum - b:html_indent_line_limit, 0]))
838 if lnum > 0
839 return [lnum, 1]
Bram Moolenaar071d4272004-06-13 20:20:40 +0000840 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200841 endif
842 return [a:lnum, 0]
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200843endfunc "}}}
Bram Moolenaar071d4272004-06-13 20:20:40 +0000844
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200845" Find the unclosed start tag from the current cursor position.
846func! HtmlIndent_FindStartTag()
847 "{{{
848 " The cursor must be on or before a closing tag.
849 " If found, positions the cursor at the match and returns the line number.
850 " Otherwise returns 0.
851 let tagname = matchstr(getline('.')[col('.') - 1:], '</\zs\w\+\ze')
852 let start_lnum = searchpair('<' . tagname . '\>', '', '</' . tagname . '\>', 'bW')
853 if start_lnum > 0
854 return start_lnum
855 endif
856 return 0
857endfunc "}}}
Bram Moolenaar071d4272004-06-13 20:20:40 +0000858
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200859" Moves the cursor from a "<" to the matching ">".
860func! HtmlIndent_FindTagEnd()
861 "{{{
862 " Call this with the cursor on the "<" of a start tag.
863 " This will move the cursor to the ">" of the matching end tag or, when it's
864 " a self-closing tag, to the matching ">".
865 " Limited to look up to b:html_indent_line_limit lines away.
866 let text = getline('.')
867 let tagname = matchstr(text, '\w\+\|!--', col('.'))
868 if tagname == '!--'
869 call search('--\zs>')
870 elseif s:get_tag('/' . tagname) != 0
871 " tag with a closing tag, find matching "</tag>"
872 call searchpair('<' . tagname, '', '</' . tagname . '\zs>', 'W', '', line('.') + b:html_indent_line_limit)
873 else
874 " self-closing tag, find the ">"
875 call search('\S\zs>')
876 endif
877endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200878
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200879" Indenting inside a start tag. Return the correct indent or -1 if unknown.
880func! s:InsideTag(foundHtmlString)
881 "{{{
882 if a:foundHtmlString
883 " Inside an attribute string.
884 " Align with the previous line or use an external function.
885 let lnum = v:lnum - 1
886 if lnum > 1
887 if exists('b:html_indent_tag_string_func')
888 return b:html_indent_tag_string_func(lnum)
889 endif
890 return indent(lnum)
891 endif
892 endif
893
894 " Should be another attribute: " attr="val". Align with the previous
895 " attribute start.
896 let lnum = v:lnum
897 while lnum > 1
898 let lnum -= 1
899 let text = getline(lnum)
900 " Find a match with one of these, align with "attr":
901 " attr=
902 " <tag attr=
903 " text<tag attr=
904 " <tag>text</tag>text<tag attr=
905 " For long lines search for the first match, finding the last match
906 " gets very slow.
907 if len(text) < 300
908 let idx = match(text, '.*\s\zs[_a-zA-Z0-9-]\+="')
909 else
910 let idx = match(text, '\s\zs[_a-zA-Z0-9-]\+="')
911 endif
912 if idx > 0
913 " Found the attribute. TODO: assumes spaces, no Tabs.
914 return idx
915 endif
916 endwhile
917 return -1
918endfunc "}}}
919
920" THE MAIN INDENT FUNCTION. Return the amount of indent for v:lnum.
921func! HtmlIndent()
922 "{{{
Bram Moolenaar9da7ff72015-01-14 12:52:36 +0100923 if prevnonblank(v:lnum - 1) < 1
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200924 " First non-blank line has no indent.
925 return 0
926 endif
927
928 let curtext = tolower(getline(v:lnum))
929 let indentunit = s:ShiftWidth()
930
931 let b:hi_newstate = {}
932 let b:hi_newstate.lnum = v:lnum
933
934 " When syntax HL is enabled, detect we are inside a tag. Indenting inside
935 " a tag works very differently. Do not do this when the line starts with
936 " "<", it gets the "htmlTag" ID but we are not inside a tag then.
937 if curtext !~ '^\s*<'
Bram Moolenaar7b61a542014-08-23 15:31:19 +0200938 normal! ^
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200939 let stack = synstack(v:lnum, col('.')) " assumes there are no tabs
940 let foundHtmlString = 0
941 for synid in reverse(stack)
942 let name = synIDattr(synid, "name")
943 if index(b:hi_insideStringNames, name) >= 0
944 let foundHtmlString = 1
945 elseif index(b:hi_insideTagNames, name) >= 0
946 " Yes, we are inside a tag.
947 let indent = s:InsideTag(foundHtmlString)
948 if indent >= 0
949 " Do not keep the state. TODO: could keep the block type.
950 let b:hi_indent.lnum = 0
951 return indent
952 endif
953 endif
954 endfor
955 endif
956
957 " does the line start with a closing tag?
958 let swendtag = match(curtext, '^\s*</') >= 0
959
960 if prevnonblank(v:lnum - 1) == b:hi_indent.lnum && b:hi_lasttick == b:changedtick - 1
961 " use state (continue from previous line)
962 else
963 " start over (know nothing)
964 let b:hi_indent = s:FreshState(v:lnum)
965 endif
966
967 if b:hi_indent.block >= 2
968 " within block
969 let endtag = s:endtags[b:hi_indent.block]
970 let blockend = stridx(curtext, endtag)
971 if blockend >= 0
972 " block ends here
973 let b:hi_newstate.block = 0
974 " calc indent for REST OF LINE (may start more blocks):
975 call s:CountTagsAndState(strpart(curtext, blockend + strlen(endtag)))
976 if swendtag && b:hi_indent.block != 5
977 let indent = b:hi_indent.blocktagind + s:curind * indentunit
978 let b:hi_newstate.baseindent = indent + s:nextrel * indentunit
979 else
980 let indent = s:Alien{b:hi_indent.block}()
981 let b:hi_newstate.baseindent = b:hi_indent.blocktagind + s:nextrel * indentunit
982 endif
983 else
984 " block continues
985 " indent this line with alien method
986 let indent = s:Alien{b:hi_indent.block}()
987 endif
988 else
989 " not within a block - within usual html
990 let b:hi_newstate.block = b:hi_indent.block
991 if swendtag
992 " The current line starts with an end tag, align with its start tag.
993 call cursor(v:lnum, 1)
994 let start_lnum = HtmlIndent_FindStartTag()
995 if start_lnum > 0
996 " check for the line starting with something inside a tag:
997 " <sometag <- align here
998 " attr=val><open> not here
999 let text = getline(start_lnum)
1000 let angle = matchstr(text, '[<>]')
1001 if angle == '>'
1002 call cursor(start_lnum, 1)
1003 normal! f>%
1004 let start_lnum = line('.')
1005 let text = getline(start_lnum)
1006 endif
1007
1008 let indent = indent(start_lnum)
1009 if col('.') > 2
1010 let swendtag = match(text, '^\s*</') >= 0
1011 call s:CountITags(text[: col('.') - 2])
1012 let indent += s:nextrel * s:ShiftWidth()
1013 if !swendtag
1014 let indent += s:curind * s:ShiftWidth()
1015 endif
1016 endif
1017 else
1018 " not sure what to do
1019 let indent = b:hi_indent.baseindent
1020 endif
1021 let b:hi_newstate.baseindent = indent
1022 else
1023 call s:CountTagsAndState(curtext)
1024 let indent = b:hi_indent.baseindent
1025 let b:hi_newstate.baseindent = indent + (s:curind + s:nextrel) * indentunit
1026 endif
1027 endif
1028
1029 let b:hi_lasttick = b:changedtick
1030 call extend(b:hi_indent, b:hi_newstate, "force")
1031 return indent
1032endfunc "}}}
1033
1034" Check user settings when loading this script the first time.
Bram Moolenaarec7944a2013-06-12 21:29:15 +02001035call HtmlIndent_CheckUserSettings()
Bram Moolenaar071d4272004-06-13 20:20:40 +00001036
Bram Moolenaar91170f82006-05-05 21:15:17 +00001037let &cpo = s:cpo_save
1038unlet s:cpo_save
1039
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +02001040" vim: fdm=marker ts=8 sw=2 tw=78