blob: b5374bd68a2a78198f1df4265da65a7d9a7b6a44 [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>
5" Last Change: 2014 Jul 04
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.
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 = {}
97 if autotags
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
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 Moolenaarec7944a2013-06-12 21:29:15 +0200248"}}}
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200249
250" Add Block Tags: these contain alien content
251"{{{
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200252call s:AddBlockTag('pre', 2)
253call s:AddBlockTag('script', 3)
254call s:AddBlockTag('style', 4)
255call s:AddBlockTag('<!--', 5, '-->')
256"}}}
257
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200258" Return non-zero when "tagname" is an opening tag, not being a block tag, for
259" which there should be a closing tag. Can be used by scripts that include
260" HTML indenting.
261func! HtmlIndent_IsOpenTag(tagname)
262 "{{{
263 if get(s:indent_tags, a:tagname) == 1
264 return 1
265 endif
266 return get(b:hi_tags, a:tagname) == 1
267endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200268
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200269" Get the value for "tagname", taking care of buffer-local tags.
270func! s:get_tag(tagname)
271 "{{{
272 let i = get(s:indent_tags, a:tagname)
273 if (i == 1 || i == -1) && get(b:hi_removed_tags, a:tagname) != 0
274 return 0
275 endif
276 if i == 0
277 let i = get(b:hi_tags, a:tagname)
278 endif
279 return i
280endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200281
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200282" Count the number of start and end tags in "text".
283func! s:CountITags(text)
284 "{{{
285 " Store the result in s:curind and s:nextrel.
286 let s:curind = 0 " relative indent steps for current line [unit &sw]:
287 let s:nextrel = 0 " relative indent steps for next line [unit &sw]:
288 let s:block = 0 " assume starting outside of a block
289 let s:countonly = 1 " don't change state
290 call substitute(a:text, '<\zs/\=\w\+\>\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g')
291 let s:countonly = 0
292endfunc "}}}
293
294" Count the number of start and end tags in text.
295func! s:CountTagsAndState(text)
296 "{{{
297 " Store the result in s:curind and s:nextrel. Update b:hi_newstate.block.
298 let s:curind = 0 " relative indent steps for current line [unit &sw]:
299 let s:nextrel = 0 " relative indent steps for next line [unit &sw]:
300
301 let s:block = b:hi_newstate.block
302 let tmp = substitute(a:text, '<\zs/\=\w\+\>\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g')
303 if s:block == 3
304 let b:hi_newstate.scripttype = s:GetScriptType(matchstr(tmp, '\C.*<SCRIPT\>\zs[^>]*'))
305 endif
306 let b:hi_newstate.block = s:block
307endfunc "}}}
308
309" Used by s:CountITags() and s:CountTagsAndState().
310func! s:CheckTag(itag)
311 "{{{
312 " Returns an empty string or "SCRIPT".
313 " a:itag can be "tag" or "/tag" or "<!--" or "-->"
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
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200323 else
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200324 let s:nextrel -= 1
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200325 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200326 elseif ind == 1
327 " opening tag
328 if s:block != 0
329 return ""
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200330 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200331 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 ""
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200338endfunc "}}}
339
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200340" 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 ""
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200349 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200350 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) * s: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 "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200367
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200368" Return the <script> type: either "javascript" or ""
369func! s:GetScriptType(str)
370 "{{{
371 if a:str == "" || a:str =~ "java"
372 return "javascript"
373 else
374 return ""
375 endif
376endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200377
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200378" Look back in the file, starting at a:lnum - 1, to compute a state for the
379" start of line a:lnum. Return the new state.
380func! s:FreshState(lnum)
381 "{{{
382 " A state is to know ALL relevant details about the
383 " lines 1..a:lnum-1, initial calculating (here!) can be slow, but updating is
384 " fast (incremental).
385 " TODO: this should be split up in detecting the block type and computing the
386 " indent for the block type, so that when we do not know the indent we do
387 " not need to clear the whole state and re-detect the block type again.
388 " State:
389 " lnum last indented line == prevnonblank(a:lnum - 1)
390 " block = 0 a:lnum located within special tag: 0:none, 2:<pre>,
391 " 3:<script>, 4:<style>, 5:<!--
392 " baseindent use this indent for line a:lnum as a start - kind of
393 " autoindent (if block==0)
394 " scripttype = '' type attribute of a script tag (if block==3)
395 " blocktagind indent for current opening (get) and closing (set)
396 " blocktag (if block!=0)
397 " blocklnr lnum of starting blocktag (if block!=0)
398 " inattr line {lnum} starts with attributes of a tag
399 let state = {}
400 let state.lnum = prevnonblank(a:lnum - 1)
401 let state.scripttype = ""
402 let state.blocktagind = -1
403 let state.block = 0
404 let state.baseindent = 0
405 let state.blocklnr = 0
406 let state.inattr = 0
407
408 if state.lnum == 0
409 return state
410 endif
411
412 " Heuristic:
413 " remember startline state.lnum
414 " look back for <pre, </pre, <script, </script, <style, </style tags
415 " remember stopline
416 " if opening tag found,
417 " assume a:lnum within block
418 " else
419 " look back in result range (stopline, startline) for comment
420 " \ delimiters (<!--, -->)
421 " if comment opener found,
422 " assume a:lnum within comment
423 " else
424 " assume usual html for a:lnum
425 " if a:lnum-1 has a closing comment
426 " look back to get indent of comment opener
427 " FI
428
429 " look back for a blocktag
430 call cursor(a:lnum, 1)
431 let [stopline, stopcol] = searchpos('\c<\zs\/\=\%(pre\>\|script\>\|style\>\)', "bW")
432 if stopline > 0
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200433 " fugly ... why isn't there searchstr()
434 let tagline = tolower(getline(stopline))
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200435 let blocktag = matchstr(tagline, '\/\=\%(pre\>\|script\>\|style\>\)', stopcol - 1)
436 if blocktag[0] != "/"
437 " opening tag found, assume a:lnum within block
438 let state.block = s:indent_tags[blocktag]
439 if state.block == 3
440 let state.scripttype = s:GetScriptType(matchstr(tagline, '\>[^>]*', stopcol))
441 endif
442 let state.blocklnr = stopline
443 " check preceding tags in the line:
444 call s:CountITags(tagline[: stopcol-2])
445 let state.blocktagind = indent(stopline) + (s:curind + s:nextrel) * s:ShiftWidth()
446 return state
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200447 elseif stopline == state.lnum
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200448 " handle special case: previous line (= state.lnum) contains a
449 " closing blocktag which is preceded by line-noise;
450 " blocktag == "/..."
451 let swendtag = match(tagline, '^\s*</') >= 0
452 if !swendtag
453 let [bline, bcol] = searchpos('<'.blocktag[1:].'\>', "bW")
454 call s:CountITags(tolower(getline(bline)[: bcol-2]))
455 let state.baseindent = indent(bline) + (s:curind + s:nextrel) * s:ShiftWidth()
456 return state
457 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200458 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200459 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200460
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200461 " else look back for comment
462 call cursor(a:lnum, 1)
463 let [comlnum, comcol, found] = searchpos('\(<!--\)\|-->', 'bpW', stopline)
464 if found == 2
465 " comment opener found, assume a:lnum within comment
466 let state.block = 5
467 let state.blocklnr = comlnum
468 " check preceding tags in the line:
469 call s:CountITags(tolower(getline(comlnum)[: comcol-2]))
470 let state.blocktagind = indent(comlnum) + (s:curind + s:nextrel) * s:ShiftWidth()
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200471 return state
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200472 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200473
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200474 " else within usual HTML
475 let text = tolower(getline(state.lnum))
476
477 " Check a:lnum-1 for closing comment (we need indent from the opening line).
478 " Not when other tags follow (might be --> inside a string).
479 let comcol = stridx(text, '-->')
480 if comcol >= 0 && match(text, '[<>]', comcol) <= 0
481 call cursor(state.lnum, comcol + 1)
482 let [comlnum, comcol] = searchpos('<!--', 'bW')
483 if comlnum == state.lnum
484 let text = text[: comcol-2]
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200485 else
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200486 let text = tolower(getline(comlnum)[: comcol-2])
Bram Moolenaar071d4272004-06-13 20:20:40 +0000487 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200488 call s:CountITags(text)
489 let state.baseindent = indent(comlnum) + (s:curind + s:nextrel) * s:ShiftWidth()
490 " TODO check tags that follow "-->"
491 return state
492 endif
Bram Moolenaar071d4272004-06-13 20:20:40 +0000493
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200494 " Check if the previous line starts with end tag.
495 let swendtag = match(text, '^\s*</') >= 0
496
497 " If previous line ended in a closing tag, line up with the opening tag.
498 if !swendtag && text =~ '</\w\+\s*>\s*$'
499 call cursor(state.lnum, 99999)
500 normal F<
501 let start_lnum = HtmlIndent_FindStartTag()
502 if start_lnum > 0
503 let state.baseindent = indent(start_lnum)
504 if col('.') > 2
505 " check for tags before the matching opening tag.
506 let text = getline(start_lnum)
507 let swendtag = match(text, '^\s*</') >= 0
508 call s:CountITags(text[: col('.') - 2])
509 let state.baseindent += s:nextrel * s:ShiftWidth()
510 if !swendtag
511 let state.baseindent += s:curind * s:ShiftWidth()
512 endif
513 endif
514 return state
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200515 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200516 endif
517
518 " Else: no comments. Skip backwards to find the tag we're inside.
519 let [state.lnum, found] = HtmlIndent_FindTagStart(state.lnum)
520 " Check if that line starts with end tag.
521 let text = getline(state.lnum)
522 let swendtag = match(text, '^\s*</') >= 0
523 call s:CountITags(tolower(text))
524 let state.baseindent = indent(state.lnum) + s:nextrel * s:ShiftWidth()
525 if !swendtag
526 let state.baseindent += s:curind * s:ShiftWidth()
527 endif
528 return state
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200529endfunc "}}}
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200530
531" Indent inside a <pre> block: Keep indent as-is.
532func! s:Alien2()
533 "{{{
534 return -1
535endfunc "}}}
536
537" Return the indent inside a <script> block for javascript.
538func! s:Alien3()
539 "{{{
540 let lnum = prevnonblank(v:lnum - 1)
541 while lnum > 1 && getline(lnum) =~ '^\s*/[/*]'
542 " Skip over comments to avoid that cindent() aligns with the <script> tag
543 let lnum = prevnonblank(lnum - 1)
544 endwhile
545 if lnum == b:hi_indent.blocklnr
546 " indent for the first line after <script>
547 return eval(b:hi_js1indent)
548 endif
549 if b:hi_indent.scripttype == "javascript"
550 return cindent(v:lnum)
551 else
552 return -1
553 endif
554endfunc "}}}
555
556" Return the indent inside a <style> block.
557func! s:Alien4()
558 "{{{
559 if prevnonblank(v:lnum-1) == b:hi_indent.blocklnr
560 " indent for first content line
561 return eval(b:hi_css1indent)
562 endif
563 return s:CSSIndent()
564endfunc "}}}
565
566" Indending inside a <style> block. Returns the indent.
567func! s:CSSIndent()
568 "{{{
569 " This handles standard CSS and also Closure stylesheets where special lines
570 " start with @.
571 " When the line starts with '*' or the previous line starts with "/*"
572 " and does not end in "*/", use C indenting to format the comment.
573 " Adopted $VIMRUNTIME/indent/css.vim
574 let curtext = getline(v:lnum)
575 if curtext =~ '^\s*[*]'
576 \ || (v:lnum > 1 && getline(v:lnum - 1) =~ '\s*/\*'
577 \ && getline(v:lnum - 1) !~ '\*/\s*$')
578 return cindent(v:lnum)
579 endif
580
581 let min_lnum = b:hi_indent.blocklnr
582 let prev_lnum = s:CssPrevNonComment(v:lnum - 1, min_lnum)
583 let [prev_lnum, found] = HtmlIndent_FindTagStart(prev_lnum)
584 if prev_lnum <= min_lnum
585 " Just below the <style> tag, indent for first content line after comments.
586 return eval(b:hi_css1indent)
587 endif
588
589 " If the current line starts with "}" align with it's match.
590 if curtext =~ '^\s*}'
591 call cursor(v:lnum, 1)
592 try
593 normal! %
594 " Found the matching "{", align with it after skipping unfinished lines.
595 let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum)
596 return indent(align_lnum)
597 catch
598 " can't find it, try something else, but it's most likely going to be
599 " wrong
600 endtry
601 endif
602
603 " add indent after {
604 let brace_counts = HtmlIndent_CountBraces(prev_lnum)
605 let extra = brace_counts.c_open * s:ShiftWidth()
606
607 let prev_text = getline(prev_lnum)
608 let below_end_brace = prev_text =~ '}\s*$'
609
610 " Search back to align with the first line that's unfinished.
611 let align_lnum = s:CssFirstUnfinished(prev_lnum, min_lnum)
612
613 " Handle continuation lines if aligning with previous line and not after a
614 " "}".
615 if extra == 0 && align_lnum == prev_lnum && !below_end_brace
616 let prev_hasfield = prev_text =~ '^\s*[a-zA-Z0-9-]\+:'
617 let prev_special = prev_text =~ '^\s*\(/\*\|@\)'
618 if curtext =~ '^\s*\(/\*\|@\)'
619 " if the current line is not a comment or starts with @ (used by template
620 " systems) reduce indent if previous line is a continuation line
621 if !prev_hasfield && !prev_special
622 let extra = -s:ShiftWidth()
623 endif
624 else
625 let cur_hasfield = curtext =~ '^\s*[a-zA-Z0-9-]\+:'
626 let prev_unfinished = s:CssUnfinished(prev_text)
627 if !cur_hasfield && (prev_hasfield || prev_unfinished)
628 " Continuation line has extra indent if the previous line was not a
629 " continuation line.
630 let extra = s:ShiftWidth()
631 " Align with @if
632 if prev_text =~ '^\s*@if '
633 let extra = 4
634 endif
635 elseif cur_hasfield && !prev_hasfield && !prev_special
636 " less indent below a continuation line
637 let extra = -s:ShiftWidth()
638 endif
639 endif
640 endif
641
642 if below_end_brace
643 " find matching {, if that line starts with @ it's not the start of a rule
644 " but something else from a template system
645 call cursor(prev_lnum, 1)
646 call search('}\s*$')
647 try
648 normal! %
649 " Found the matching "{", align with it.
650 let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum)
651 let special = getline(align_lnum) =~ '^\s*@'
652 catch
653 let special = 0
654 endtry
655 if special
656 " do not reduce indent below @{ ... }
657 if extra < 0
658 let extra += s:ShiftWidth()
659 endif
660 else
661 let extra -= (brace_counts.c_close - (prev_text =~ '^\s*}')) * s:ShiftWidth()
662 endif
663 endif
664
665 " if no extra indent yet...
666 if extra == 0
667 if brace_counts.p_open > brace_counts.p_close
668 " previous line has more ( than ): add a shiftwidth
669 let extra = s:ShiftWidth()
670 elseif brace_counts.p_open < brace_counts.p_close
671 " previous line has more ) than (: subtract a shiftwidth
672 let extra = -s:ShiftWidth()
673 endif
674 endif
675
676 return indent(align_lnum) + extra
677endfunc "}}}
678
679" Inside <style>: Whether a line is unfinished.
680func! s:CssUnfinished(text)
681 "{{{
682 return a:text =~ '\s\(||\|&&\|:\)\s*$'
683endfunc "}}}
684
685" Search back for the first unfinished line above "lnum".
686func! s:CssFirstUnfinished(lnum, min_lnum)
687 "{{{
688 let align_lnum = a:lnum
689 while align_lnum > a:min_lnum && s:CssUnfinished(getline(align_lnum - 1))
690 let align_lnum -= 1
691 endwhile
692 return align_lnum
693endfunc "}}}
694
695" Find the non-empty line at or before "lnum" that is not a comment.
696func! s:CssPrevNonComment(lnum, stopline)
697 "{{{
698 " caller starts from a line a:lnum + 1 that is not a comment
699 let lnum = prevnonblank(a:lnum)
700 while 1
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200701 let ccol = match(getline(lnum), '\*/')
702 if ccol < 0
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200703 " No comment end thus its something else.
704 return lnum
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200705 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200706 call cursor(lnum, ccol + 1)
707 " Search back for the /* that starts the comment
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200708 let lnum = search('/\*', 'bW', a:stopline)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200709 if indent(".") == virtcol(".") - 1
710 " The found /* is at the start of the line. Now go back to the line
711 " above it and again check if it is a comment.
712 let lnum = prevnonblank(lnum - 1)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200713 else
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200714 " /* is after something else, thus it's not a comment line.
715 return lnum
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200716 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200717 endwhile
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200718endfunc "}}}
719
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200720" Check the number of {} and () in line "lnum". Return a dict with the counts.
721func! HtmlIndent_CountBraces(lnum)
722 "{{{
723 let brs = substitute(getline(a:lnum), '[''"].\{-}[''"]\|/\*.\{-}\*/\|/\*.*$\|[^{}()]', '', 'g')
724 let c_open = 0
725 let c_close = 0
726 let p_open = 0
727 let p_close = 0
728 for brace in split(brs, '\zs')
729 if brace == "{"
730 let c_open += 1
731 elseif brace == "}"
732 if c_open > 0
733 let c_open -= 1
734 else
735 let c_close += 1
736 endif
737 elseif brace == '('
738 let p_open += 1
739 elseif brace == ')'
740 if p_open > 0
741 let p_open -= 1
742 else
743 let p_close += 1
744 endif
745 endif
746 endfor
747 return {'c_open': c_open,
748 \ 'c_close': c_close,
749 \ 'p_open': p_open,
750 \ 'p_close': p_close}
751endfunc "}}}
752
753" Return the indent for a comment: <!-- -->
754func! s:Alien5()
755 "{{{
756 let curtext = getline(v:lnum)
757 if curtext =~ '^\s*\zs-->'
758 " current line starts with end of comment, line up with comment start.
759 call cursor(v:lnum, 0)
760 let lnum = search('<!--', 'b')
761 if lnum > 0
762 " TODO: what if <!-- is not at the start of the line?
763 return indent(lnum)
764 endif
765
766 " Strange, can't find it.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200767 return -1
Bram Moolenaar946e27a2014-06-25 18:50:27 +0200768 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200769
770 let prevlnum = prevnonblank(v:lnum - 1)
771 let prevtext = getline(prevlnum)
772 let idx = match(prevtext, '^\s*\zs<!--')
773 if idx >= 0
774 " just below comment start, add a shiftwidth
775 return idx + s:ShiftWidth()
776 endif
777
778 " Some files add 4 spaces just below a TODO line. It's difficult to detect
779 " the end of the TODO, so let's not do that.
780
781 " Align with the previous non-blank line.
782 return indent(prevlnum)
Bram Moolenaar946e27a2014-06-25 18:50:27 +0200783endfunc "}}}
784
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200785" When the "lnum" line ends in ">" find the line containing the matching "<".
786func! HtmlIndent_FindTagStart(lnum)
787 "{{{
788 " Avoids using the indent of a continuation line.
789 " Moves the cursor.
790 " Return two values:
791 " - the matching line number or "lnum".
792 " - a flag indicating whether we found the end of a tag.
793 " This method is global so that HTML-like indenters can use it.
794 " To avoid matching " > " or " < " inside a string require that the opening
795 " "<" is followed by a word character and the closing ">" comes after a
796 " non-white character.
797 let idx = match(getline(a:lnum), '\S>\s*$')
798 if idx > 0
799 call cursor(a:lnum, idx)
800 let lnum = searchpair('<\w', '' , '\S>', 'bW', '', max([a:lnum - b:html_indent_line_limit, 0]))
801 if lnum > 0
802 return [lnum, 1]
Bram Moolenaar071d4272004-06-13 20:20:40 +0000803 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200804 endif
805 return [a:lnum, 0]
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200806endfunc "}}}
Bram Moolenaar071d4272004-06-13 20:20:40 +0000807
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200808" Find the unclosed start tag from the current cursor position.
809func! HtmlIndent_FindStartTag()
810 "{{{
811 " The cursor must be on or before a closing tag.
812 " If found, positions the cursor at the match and returns the line number.
813 " Otherwise returns 0.
814 let tagname = matchstr(getline('.')[col('.') - 1:], '</\zs\w\+\ze')
815 let start_lnum = searchpair('<' . tagname . '\>', '', '</' . tagname . '\>', 'bW')
816 if start_lnum > 0
817 return start_lnum
818 endif
819 return 0
820endfunc "}}}
Bram Moolenaar071d4272004-06-13 20:20:40 +0000821
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200822" Moves the cursor from a "<" to the matching ">".
823func! HtmlIndent_FindTagEnd()
824 "{{{
825 " Call this with the cursor on the "<" of a start tag.
826 " This will move the cursor to the ">" of the matching end tag or, when it's
827 " a self-closing tag, to the matching ">".
828 " Limited to look up to b:html_indent_line_limit lines away.
829 let text = getline('.')
830 let tagname = matchstr(text, '\w\+\|!--', col('.'))
831 if tagname == '!--'
832 call search('--\zs>')
833 elseif s:get_tag('/' . tagname) != 0
834 " tag with a closing tag, find matching "</tag>"
835 call searchpair('<' . tagname, '', '</' . tagname . '\zs>', 'W', '', line('.') + b:html_indent_line_limit)
836 else
837 " self-closing tag, find the ">"
838 call search('\S\zs>')
839 endif
840endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200841
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200842" Indenting inside a start tag. Return the correct indent or -1 if unknown.
843func! s:InsideTag(foundHtmlString)
844 "{{{
845 if a:foundHtmlString
846 " Inside an attribute string.
847 " Align with the previous line or use an external function.
848 let lnum = v:lnum - 1
849 if lnum > 1
850 if exists('b:html_indent_tag_string_func')
851 return b:html_indent_tag_string_func(lnum)
852 endif
853 return indent(lnum)
854 endif
855 endif
856
857 " Should be another attribute: " attr="val". Align with the previous
858 " attribute start.
859 let lnum = v:lnum
860 while lnum > 1
861 let lnum -= 1
862 let text = getline(lnum)
863 " Find a match with one of these, align with "attr":
864 " attr=
865 " <tag attr=
866 " text<tag attr=
867 " <tag>text</tag>text<tag attr=
868 " For long lines search for the first match, finding the last match
869 " gets very slow.
870 if len(text) < 300
871 let idx = match(text, '.*\s\zs[_a-zA-Z0-9-]\+="')
872 else
873 let idx = match(text, '\s\zs[_a-zA-Z0-9-]\+="')
874 endif
875 if idx > 0
876 " Found the attribute. TODO: assumes spaces, no Tabs.
877 return idx
878 endif
879 endwhile
880 return -1
881endfunc "}}}
882
883" THE MAIN INDENT FUNCTION. Return the amount of indent for v:lnum.
884func! HtmlIndent()
885 "{{{
886 if prevnonblank(v:lnum - 1) <= 1
887 " First non-blank line has no indent.
888 return 0
889 endif
890
891 let curtext = tolower(getline(v:lnum))
892 let indentunit = s:ShiftWidth()
893
894 let b:hi_newstate = {}
895 let b:hi_newstate.lnum = v:lnum
896
897 " When syntax HL is enabled, detect we are inside a tag. Indenting inside
898 " a tag works very differently. Do not do this when the line starts with
899 " "<", it gets the "htmlTag" ID but we are not inside a tag then.
900 if curtext !~ '^\s*<'
901 normal ^
902 let stack = synstack(v:lnum, col('.')) " assumes there are no tabs
903 let foundHtmlString = 0
904 for synid in reverse(stack)
905 let name = synIDattr(synid, "name")
906 if index(b:hi_insideStringNames, name) >= 0
907 let foundHtmlString = 1
908 elseif index(b:hi_insideTagNames, name) >= 0
909 " Yes, we are inside a tag.
910 let indent = s:InsideTag(foundHtmlString)
911 if indent >= 0
912 " Do not keep the state. TODO: could keep the block type.
913 let b:hi_indent.lnum = 0
914 return indent
915 endif
916 endif
917 endfor
918 endif
919
920 " does the line start with a closing tag?
921 let swendtag = match(curtext, '^\s*</') >= 0
922
923 if prevnonblank(v:lnum - 1) == b:hi_indent.lnum && b:hi_lasttick == b:changedtick - 1
924 " use state (continue from previous line)
925 else
926 " start over (know nothing)
927 let b:hi_indent = s:FreshState(v:lnum)
928 endif
929
930 if b:hi_indent.block >= 2
931 " within block
932 let endtag = s:endtags[b:hi_indent.block]
933 let blockend = stridx(curtext, endtag)
934 if blockend >= 0
935 " block ends here
936 let b:hi_newstate.block = 0
937 " calc indent for REST OF LINE (may start more blocks):
938 call s:CountTagsAndState(strpart(curtext, blockend + strlen(endtag)))
939 if swendtag && b:hi_indent.block != 5
940 let indent = b:hi_indent.blocktagind + s:curind * indentunit
941 let b:hi_newstate.baseindent = indent + s:nextrel * indentunit
942 else
943 let indent = s:Alien{b:hi_indent.block}()
944 let b:hi_newstate.baseindent = b:hi_indent.blocktagind + s:nextrel * indentunit
945 endif
946 else
947 " block continues
948 " indent this line with alien method
949 let indent = s:Alien{b:hi_indent.block}()
950 endif
951 else
952 " not within a block - within usual html
953 let b:hi_newstate.block = b:hi_indent.block
954 if swendtag
955 " The current line starts with an end tag, align with its start tag.
956 call cursor(v:lnum, 1)
957 let start_lnum = HtmlIndent_FindStartTag()
958 if start_lnum > 0
959 " check for the line starting with something inside a tag:
960 " <sometag <- align here
961 " attr=val><open> not here
962 let text = getline(start_lnum)
963 let angle = matchstr(text, '[<>]')
964 if angle == '>'
965 call cursor(start_lnum, 1)
966 normal! f>%
967 let start_lnum = line('.')
968 let text = getline(start_lnum)
969 endif
970
971 let indent = indent(start_lnum)
972 if col('.') > 2
973 let swendtag = match(text, '^\s*</') >= 0
974 call s:CountITags(text[: col('.') - 2])
975 let indent += s:nextrel * s:ShiftWidth()
976 if !swendtag
977 let indent += s:curind * s:ShiftWidth()
978 endif
979 endif
980 else
981 " not sure what to do
982 let indent = b:hi_indent.baseindent
983 endif
984 let b:hi_newstate.baseindent = indent
985 else
986 call s:CountTagsAndState(curtext)
987 let indent = b:hi_indent.baseindent
988 let b:hi_newstate.baseindent = indent + (s:curind + s:nextrel) * indentunit
989 endif
990 endif
991
992 let b:hi_lasttick = b:changedtick
993 call extend(b:hi_indent, b:hi_newstate, "force")
994 return indent
995endfunc "}}}
996
997" Check user settings when loading this script the first time.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200998call HtmlIndent_CheckUserSettings()
Bram Moolenaar071d4272004-06-13 20:20:40 +0000999
Bram Moolenaar91170f82006-05-05 21:15:17 +00001000let &cpo = s:cpo_save
1001unlet s:cpo_save
1002
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +02001003" vim: fdm=marker ts=8 sw=2 tw=78