blob: 671a4d1971e09177fcfeafe3601be62022826131 [file] [log] [blame]
Bram Moolenaarec7944a2013-06-12 21:29:15 +02001" Vim indent script for HTML
Christian Brabandte8d6f032023-08-23 20:23:07 +01002" Maintainer: The Vim Project <https://github.com/vim/vim>
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +02003" Original Author: Andy Wokula <anwoku@yahoo.de>
Aliaksei Budavei4e3df442025-04-13 21:00:42 +03004" Last Change: 2025 Apr 13
Bram Moolenaar7ff78462020-07-10 22:00:53 +02005" Version: 1.0 "{{{
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +02006" Description: HTML indent script with cached state for faster indenting on a
7" range of lines.
8" Supports template systems through hooks.
9" Supports Closure stylesheets.
Bram Moolenaarec7944a2013-06-12 21:29:15 +020010"
11" Credits:
12" indent/html.vim (2006 Jun 05) from J. Zellner
13" indent/css.vim (2006 Dec 20) from N. Weibull
14"
15" History:
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020016" 2014 June (v1.0) overhaul (Bram)
Bram Moolenaar52b91d82013-06-15 21:39:51 +020017" 2012 Oct 21 (v0.9) added support for shiftwidth()
18" 2011 Sep 09 (v0.8) added HTML5 tags (thx to J. Zuckerman)
19" 2008 Apr 28 (v0.6) revised customization
20" 2008 Mar 09 (v0.5) fixed 'indk' issue (thx to C.J. Robinson)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020021"}}}
Bram Moolenaar071d4272004-06-13 20:20:40 +000022
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020023" Init Folklore, check user settings (2nd time ++)
24if exists("b:did_indent") "{{{
25 finish
Bram Moolenaar071d4272004-06-13 20:20:40 +000026endif
Bram Moolenaar690afe12017-01-28 18:34:47 +010027
28" Load the Javascript indent script first, it defines GetJavascriptIndent().
29" Undo the rest.
30" Load base python indent.
31if !exists('*GetJavascriptIndent')
32 runtime! indent/javascript.vim
33endif
Bram Moolenaar071d4272004-06-13 20:20:40 +000034let b:did_indent = 1
35
Bram Moolenaarec7944a2013-06-12 21:29:15 +020036setlocal indentexpr=HtmlIndent()
37setlocal indentkeys=o,O,<Return>,<>>,{,},!^F
Bram Moolenaar071d4272004-06-13 20:20:40 +000038
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020039" Needed for % to work when finding start/end of a tag.
Bram Moolenaar946e27a2014-06-25 18:50:27 +020040setlocal matchpairs+=<:>
41
Bram Moolenaar690afe12017-01-28 18:34:47 +010042let b:undo_indent = "setlocal inde< indk<"
Bram Moolenaar071d4272004-06-13 20:20:40 +000043
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020044" b:hi_indent keeps state to speed up indenting consecutive lines.
45let b:hi_indent = {"lnum": -1}
46
47"""""" Code below this is loaded only once. """""
48if exists("*HtmlIndent") && !exists('g:force_reload_html')
49 call HtmlIndent_CheckUserSettings()
50 finish
Bram Moolenaar071d4272004-06-13 20:20:40 +000051endif
52
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020053" Allow for line continuation below.
Bram Moolenaar91170f82006-05-05 21:15:17 +000054let s:cpo_save = &cpo
Bram Moolenaar071d4272004-06-13 20:20:40 +000055set cpo-=C
Bram Moolenaarec7944a2013-06-12 21:29:15 +020056"}}}
Bram Moolenaar071d4272004-06-13 20:20:40 +000057
Bram Moolenaarb5b75622018-03-09 22:22:21 +010058" Pattern to match the name of a tag, including custom elements.
59let s:tagname = '\w\+\(-\w\+\)*'
60
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020061" Check and process settings from b:html_indent and g:html_indent... variables.
62" Prefer using buffer-local settings over global settings, so that there can
63" be defaults for all HTML files and exceptions for specific types of HTML
64" files.
Bram Moolenaar2346a632021-06-13 19:02:49 +020065func HtmlIndent_CheckUserSettings()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020066 "{{{
67 let inctags = ''
68 if exists("b:html_indent_inctags")
69 let inctags = b:html_indent_inctags
70 elseif exists("g:html_indent_inctags")
71 let inctags = g:html_indent_inctags
72 endif
73 let b:hi_tags = {}
74 if len(inctags) > 0
75 call s:AddITags(b:hi_tags, split(inctags, ","))
76 endif
Bram Moolenaar071d4272004-06-13 20:20:40 +000077
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020078 let autotags = ''
79 if exists("b:html_indent_autotags")
80 let autotags = b:html_indent_autotags
81 elseif exists("g:html_indent_autotags")
82 let autotags = g:html_indent_autotags
83 endif
84 let b:hi_removed_tags = {}
Bram Moolenaar541f92d2015-06-19 13:27:23 +020085 if len(autotags) > 0
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +020086 call s:RemoveITags(b:hi_removed_tags, split(autotags, ","))
87 endif
88
89 " Syntax names indicating being inside a string of an attribute value.
90 let string_names = []
91 if exists("b:html_indent_string_names")
92 let string_names = b:html_indent_string_names
93 elseif exists("g:html_indent_string_names")
94 let string_names = g:html_indent_string_names
95 endif
96 let b:hi_insideStringNames = ['htmlString']
97 if len(string_names) > 0
98 for s in string_names
99 call add(b:hi_insideStringNames, s)
100 endfor
101 endif
102
103 " Syntax names indicating being inside a tag.
104 let tag_names = []
105 if exists("b:html_indent_tag_names")
106 let tag_names = b:html_indent_tag_names
107 elseif exists("g:html_indent_tag_names")
108 let tag_names = g:html_indent_tag_names
109 endif
110 let b:hi_insideTagNames = ['htmlTag', 'htmlScriptTag']
111 if len(tag_names) > 0
112 for s in tag_names
113 call add(b:hi_insideTagNames, s)
114 endfor
115 endif
116
117 let indone = {"zero": 0
118 \,"auto": "indent(prevnonblank(v:lnum-1))"
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200119 \,"inc": "b:hi_indent.blocktagind + shiftwidth()"}
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200120
121 let script1 = ''
122 if exists("b:html_indent_script1")
123 let script1 = b:html_indent_script1
124 elseif exists("g:html_indent_script1")
125 let script1 = g:html_indent_script1
126 endif
127 if len(script1) > 0
128 let b:hi_js1indent = get(indone, script1, indone.zero)
129 else
130 let b:hi_js1indent = 0
131 endif
132
133 let style1 = ''
134 if exists("b:html_indent_style1")
135 let style1 = b:html_indent_style1
136 elseif exists("g:html_indent_style1")
137 let style1 = g:html_indent_style1
138 endif
139 if len(style1) > 0
140 let b:hi_css1indent = get(indone, style1, indone.zero)
141 else
142 let b:hi_css1indent = 0
143 endif
144
145 if !exists('b:html_indent_line_limit')
146 if exists('g:html_indent_line_limit')
147 let b:html_indent_line_limit = g:html_indent_line_limit
148 else
149 let b:html_indent_line_limit = 200
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200150 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200151 endif
Bram Moolenaarc4573eb2022-01-31 15:40:56 +0000152
153 if exists('b:html_indent_attribute')
154 let b:hi_attr_indent = b:html_indent_attribute
155 elseif exists('g:html_indent_attribute')
156 let b:hi_attr_indent = g:html_indent_attribute
157 else
158 let b:hi_attr_indent = 2
159 endif
160
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200161endfunc "}}}
Bram Moolenaar071d4272004-06-13 20:20:40 +0000162
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200163" Init Script Vars
164"{{{
165let b:hi_lasttick = 0
166let b:hi_newstate = {}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200167let s:countonly = 0
168 "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200169
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200170" Fill the s:indent_tags dict with known tags.
171" The key is "tagname" or "/tagname". {{{
172" The value is:
173" 1 opening tag
174" 2 "pre"
175" 3 "script"
176" 4 "style"
177" 5 comment start
Bram Moolenaarca635012015-09-25 20:34:21 +0200178" 6 conditional comment start
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200179" -1 closing tag
180" -2 "/pre"
181" -3 "/script"
182" -4 "/style"
183" -5 comment end
Bram Moolenaarca635012015-09-25 20:34:21 +0200184" -6 conditional comment end
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200185let s:indent_tags = {}
Bram Moolenaarca635012015-09-25 20:34:21 +0200186let s:endtags = [0,0,0,0,0,0,0] " long enough for the highest index
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200187"}}}
188
189" Add a list of tag names for a pair of <tag> </tag> to "tags".
Bram Moolenaar2346a632021-06-13 19:02:49 +0200190func s:AddITags(tags, taglist)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200191 "{{{
192 for itag in a:taglist
193 let a:tags[itag] = 1
194 let a:tags['/' . itag] = -1
195 endfor
196endfunc "}}}
197
198" Take a list of tag name pairs that are not to be used as tag pairs.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200199func s:RemoveITags(tags, taglist)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200200 "{{{
201 for itag in a:taglist
202 let a:tags[itag] = 1
203 let a:tags['/' . itag] = 1
204 endfor
205endfunc "}}}
206
207" Add a block tag, that is a tag with a different kind of indenting.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200208func s:AddBlockTag(tag, id, ...)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200209 "{{{
210 if !(a:id >= 2 && a:id < len(s:endtags))
211 echoerr 'AddBlockTag ' . a:id
212 return
213 endif
214 let s:indent_tags[a:tag] = a:id
215 if a:0 == 0
216 let s:indent_tags['/' . a:tag] = -a:id
217 let s:endtags[a:id] = "</" . a:tag . ">"
218 else
219 let s:indent_tags[a:1] = -a:id
220 let s:endtags[a:id] = a:1
221 endif
222endfunc "}}}
223
224" Add known tag pairs.
225" Self-closing tags and tags that are sometimes {{{
226" self-closing (e.g., <p>) are not here (when encountering </p> we can find
Bram Moolenaard47d5222018-12-09 20:43:55 +0100227" the matching <p>, but not the other way around).
228" Known self-closing tags: " 'p', 'img', 'source', 'area', 'keygen', 'track',
229" 'wbr'.
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200230" Old HTML tags:
231call s:AddITags(s:indent_tags, [
232 \ 'a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big',
233 \ 'blockquote', 'body', 'button', 'caption', 'center', 'cite', 'code',
Bram Moolenaar73fef332020-06-21 22:12:03 +0200234 \ 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', 'em', 'fieldset', 'font',
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200235 \ 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'html',
236 \ 'i', 'iframe', 'ins', 'kbd', 'label', 'legend', 'li',
237 \ 'map', 'menu', 'noframes', 'noscript', 'object', 'ol',
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200238 \ 'optgroup', 'q', 's', 'samp', 'select', 'small', 'span', 'strong', 'sub',
239 \ 'sup', 'table', 'textarea', 'title', 'tt', 'u', 'ul', 'var', 'th', 'td',
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200240 \ 'tr', 'tbody', 'tfoot', 'thead'])
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200241
Bram Moolenaar939a1ab2016-04-10 01:31:25 +0200242" New HTML5 elements:
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200243call s:AddITags(s:indent_tags, [
Bram Moolenaard47d5222018-12-09 20:43:55 +0100244 \ 'article', 'aside', 'audio', 'bdi', 'canvas', 'command', 'data',
245 \ 'datalist', 'details', 'dialog', 'embed', 'figcaption', 'figure',
246 \ 'footer', 'header', 'hgroup', 'main', 'mark', 'meter', 'nav', 'output',
247 \ 'picture', 'progress', 'rp', 'rt', 'ruby', 'section', 'summary',
248 \ 'svg', 'time', 'video'])
Bram Moolenaard8b77f72015-03-05 21:21:19 +0100249
250" Tags added for web components:
251call s:AddITags(s:indent_tags, [
252 \ 'content', 'shadow', 'template'])
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200253"}}}
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200254
255" Add Block Tags: these contain alien content
256"{{{
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200257call s:AddBlockTag('pre', 2)
258call s:AddBlockTag('script', 3)
259call s:AddBlockTag('style', 4)
260call s:AddBlockTag('<!--', 5, '-->')
Bram Moolenaarca635012015-09-25 20:34:21 +0200261call s:AddBlockTag('<!--[', 6, '![endif]-->')
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200262"}}}
263
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200264" Return non-zero when "tagname" is an opening tag, not being a block tag, for
265" which there should be a closing tag. Can be used by scripts that include
266" HTML indenting.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200267func HtmlIndent_IsOpenTag(tagname)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200268 "{{{
269 if get(s:indent_tags, a:tagname) == 1
270 return 1
271 endif
272 return get(b:hi_tags, a:tagname) == 1
273endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200274
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200275" Get the value for "tagname", taking care of buffer-local tags.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200276func s:get_tag(tagname)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200277 "{{{
278 let i = get(s:indent_tags, a:tagname)
279 if (i == 1 || i == -1) && get(b:hi_removed_tags, a:tagname) != 0
280 return 0
281 endif
282 if i == 0
283 let i = get(b:hi_tags, a:tagname)
284 endif
285 return i
286endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200287
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200288" Count the number of start and end tags in "text".
Bram Moolenaar2346a632021-06-13 19:02:49 +0200289func s:CountITags(text)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200290 "{{{
291 " Store the result in s:curind and s:nextrel.
292 let s:curind = 0 " relative indent steps for current line [unit &sw]:
293 let s:nextrel = 0 " relative indent steps for next line [unit &sw]:
294 let s:block = 0 " assume starting outside of a block
295 let s:countonly = 1 " don't change state
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100296 call substitute(a:text, '<\zs/\=' . s:tagname . '\>\|<!--\[\|\[endif\]-->\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g')
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200297 let s:countonly = 0
298endfunc "}}}
299
300" Count the number of start and end tags in text.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200301func s:CountTagsAndState(text)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200302 "{{{
303 " Store the result in s:curind and s:nextrel. Update b:hi_newstate.block.
304 let s:curind = 0 " relative indent steps for current line [unit &sw]:
305 let s:nextrel = 0 " relative indent steps for next line [unit &sw]:
306
307 let s:block = b:hi_newstate.block
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100308 let tmp = substitute(a:text, '<\zs/\=' . s:tagname . '\>\|<!--\[\|\[endif\]-->\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g')
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200309 if s:block == 3
310 let b:hi_newstate.scripttype = s:GetScriptType(matchstr(tmp, '\C.*<SCRIPT\>\zs[^>]*'))
311 endif
312 let b:hi_newstate.block = s:block
313endfunc "}}}
314
315" Used by s:CountITags() and s:CountTagsAndState().
Bram Moolenaar2346a632021-06-13 19:02:49 +0200316func s:CheckTag(itag)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200317 "{{{
318 " Returns an empty string or "SCRIPT".
319 " a:itag can be "tag" or "/tag" or "<!--" or "-->"
Bram Moolenaard8b77f72015-03-05 21:21:19 +0100320 if (s:CheckCustomTag(a:itag))
321 return ""
322 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200323 let ind = s:get_tag(a:itag)
324 if ind == -1
325 " closing tag
326 if s:block != 0
327 " ignore itag within a block
328 return ""
329 endif
330 if s:nextrel == 0
331 let s:curind -= 1
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200332 else
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200333 let s:nextrel -= 1
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200334 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200335 elseif ind == 1
336 " opening tag
337 if s:block != 0
338 return ""
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200339 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200340 let s:nextrel += 1
341 elseif ind != 0
342 " block-tag (opening or closing)
343 return s:CheckBlockTag(a:itag, ind)
344 " else ind==0 (other tag found): keep indent
345 endif
346 return ""
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200347endfunc "}}}
348
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200349" Used by s:CheckTag(). Returns an empty string or "SCRIPT".
Bram Moolenaar2346a632021-06-13 19:02:49 +0200350func s:CheckBlockTag(blocktag, ind)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200351 "{{{
352 if a:ind > 0
353 " a block starts here
354 if s:block != 0
355 " already in a block (nesting) - ignore
356 " especially ignore comments after other blocktags
357 return ""
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200358 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200359 let s:block = a:ind " block type
360 if s:countonly
361 return ""
362 endif
363 let b:hi_newstate.blocklnr = v:lnum
364 " save allover indent for the endtag
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200365 let b:hi_newstate.blocktagind = b:hi_indent.baseindent + (s:nextrel + s:curind) * shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200366 if a:ind == 3
367 return "SCRIPT" " all except this must be lowercase
368 " line is to be checked again for the type attribute
369 endif
370 else
371 let s:block = 0
372 " we get here if starting and closing a block-tag on the same line
373 endif
374 return ""
375endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200376
Bram Moolenaard8b77f72015-03-05 21:21:19 +0100377" Used by s:CheckTag().
Bram Moolenaar2346a632021-06-13 19:02:49 +0200378func s:CheckCustomTag(ctag)
Bram Moolenaard8b77f72015-03-05 21:21:19 +0100379 "{{{
380 " Returns 1 if ctag is the tag for a custom element, 0 otherwise.
381 " a:ctag can be "tag" or "/tag" or "<!--" or "-->"
382 let pattern = '\%\(\w\+-\)\+\w\+'
383 if match(a:ctag, pattern) == -1
384 return 0
385 endif
386 if matchstr(a:ctag, '\/\ze.\+') == "/"
387 " closing tag
388 if s:block != 0
389 " ignore ctag within a block
390 return 1
391 endif
392 if s:nextrel == 0
393 let s:curind -= 1
394 else
395 let s:nextrel -= 1
396 endif
397 else
398 " opening tag
399 if s:block != 0
400 return 1
401 endif
402 let s:nextrel += 1
403 endif
404 return 1
405endfunc "}}}
406
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200407" Return the <script> type: either "javascript" or ""
Bram Moolenaar2346a632021-06-13 19:02:49 +0200408func s:GetScriptType(str)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200409 "{{{
410 if a:str == "" || a:str =~ "java"
411 return "javascript"
412 else
413 return ""
414 endif
415endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200416
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200417" Look back in the file, starting at a:lnum - 1, to compute a state for the
418" start of line a:lnum. Return the new state.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200419func s:FreshState(lnum)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200420 "{{{
421 " A state is to know ALL relevant details about the
422 " lines 1..a:lnum-1, initial calculating (here!) can be slow, but updating is
423 " fast (incremental).
424 " TODO: this should be split up in detecting the block type and computing the
425 " indent for the block type, so that when we do not know the indent we do
426 " not need to clear the whole state and re-detect the block type again.
427 " State:
428 " lnum last indented line == prevnonblank(a:lnum - 1)
429 " block = 0 a:lnum located within special tag: 0:none, 2:<pre>,
Bram Moolenaarca635012015-09-25 20:34:21 +0200430 " 3:<script>, 4:<style>, 5:<!--, 6:<!--[
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200431 " baseindent use this indent for line a:lnum as a start - kind of
432 " autoindent (if block==0)
433 " scripttype = '' type attribute of a script tag (if block==3)
434 " blocktagind indent for current opening (get) and closing (set)
435 " blocktag (if block!=0)
436 " blocklnr lnum of starting blocktag (if block!=0)
437 " inattr line {lnum} starts with attributes of a tag
438 let state = {}
439 let state.lnum = prevnonblank(a:lnum - 1)
440 let state.scripttype = ""
441 let state.blocktagind = -1
442 let state.block = 0
443 let state.baseindent = 0
444 let state.blocklnr = 0
445 let state.inattr = 0
446
447 if state.lnum == 0
448 return state
449 endif
450
451 " Heuristic:
452 " remember startline state.lnum
453 " look back for <pre, </pre, <script, </script, <style, </style tags
454 " remember stopline
455 " if opening tag found,
456 " assume a:lnum within block
457 " else
458 " look back in result range (stopline, startline) for comment
459 " \ delimiters (<!--, -->)
460 " if comment opener found,
461 " assume a:lnum within comment
462 " else
463 " assume usual html for a:lnum
464 " if a:lnum-1 has a closing comment
465 " look back to get indent of comment opener
466 " FI
467
468 " look back for a blocktag
Bram Moolenaarca635012015-09-25 20:34:21 +0200469 let stopline2 = v:lnum + 1
470 if has_key(b:hi_indent, 'block') && b:hi_indent.block > 5
471 let [stopline2, stopcol2] = searchpos('<!--', 'bnW')
472 endif
473 let [stopline, stopcol] = searchpos('\c<\zs\/\=\%(pre\>\|script\>\|style\>\)', "bnW")
474 if stopline > 0 && stopline < stopline2
475 " ugly ... why isn't there searchstr()
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200476 let tagline = tolower(getline(stopline))
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200477 let blocktag = matchstr(tagline, '\/\=\%(pre\>\|script\>\|style\>\)', stopcol - 1)
478 if blocktag[0] != "/"
479 " opening tag found, assume a:lnum within block
480 let state.block = s:indent_tags[blocktag]
481 if state.block == 3
482 let state.scripttype = s:GetScriptType(matchstr(tagline, '\>[^>]*', stopcol))
483 endif
484 let state.blocklnr = stopline
485 " check preceding tags in the line:
486 call s:CountITags(tagline[: stopcol-2])
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200487 let state.blocktagind = indent(stopline) + (s:curind + s:nextrel) * shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200488 return state
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200489 elseif stopline == state.lnum
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200490 " handle special case: previous line (= state.lnum) contains a
491 " closing blocktag which is preceded by line-noise;
492 " blocktag == "/..."
493 let swendtag = match(tagline, '^\s*</') >= 0
494 if !swendtag
Bram Moolenaarca635012015-09-25 20:34:21 +0200495 let [bline, bcol] = searchpos('<'.blocktag[1:].'\>', "bnW")
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200496 call s:CountITags(tolower(getline(bline)[: bcol-2]))
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200497 let state.baseindent = indent(bline) + (s:curind + s:nextrel) * shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200498 return state
499 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200500 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200501 endif
Bram Moolenaarca635012015-09-25 20:34:21 +0200502 if stopline > stopline2
503 let stopline = stopline2
504 let stopcol = stopcol2
505 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200506
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200507 " else look back for comment
Bram Moolenaarca635012015-09-25 20:34:21 +0200508 let [comlnum, comcol, found] = searchpos('\(<!--\[\)\|\(<!--\)\|-->', 'bpnW', stopline)
509 if found == 2 || found == 3
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200510 " comment opener found, assume a:lnum within comment
Bram Moolenaarca635012015-09-25 20:34:21 +0200511 let state.block = (found == 3 ? 5 : 6)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200512 let state.blocklnr = comlnum
513 " check preceding tags in the line:
514 call s:CountITags(tolower(getline(comlnum)[: comcol-2]))
Bram Moolenaarca635012015-09-25 20:34:21 +0200515 if found == 2
516 let state.baseindent = b:hi_indent.baseindent
517 endif
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200518 let state.blocktagind = indent(comlnum) + (s:curind + s:nextrel) * shiftwidth()
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200519 return state
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200520 endif
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200521
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200522 " else within usual HTML
523 let text = tolower(getline(state.lnum))
524
525 " Check a:lnum-1 for closing comment (we need indent from the opening line).
526 " Not when other tags follow (might be --> inside a string).
527 let comcol = stridx(text, '-->')
528 if comcol >= 0 && match(text, '[<>]', comcol) <= 0
529 call cursor(state.lnum, comcol + 1)
530 let [comlnum, comcol] = searchpos('<!--', 'bW')
531 if comlnum == state.lnum
532 let text = text[: comcol-2]
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200533 else
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200534 let text = tolower(getline(comlnum)[: comcol-2])
Bram Moolenaar071d4272004-06-13 20:20:40 +0000535 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200536 call s:CountITags(text)
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200537 let state.baseindent = indent(comlnum) + (s:curind + s:nextrel) * shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200538 " TODO check tags that follow "-->"
539 return state
540 endif
Bram Moolenaar071d4272004-06-13 20:20:40 +0000541
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200542 " Check if the previous line starts with end tag.
543 let swendtag = match(text, '^\s*</') >= 0
544
545 " If previous line ended in a closing tag, line up with the opening tag.
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100546 if !swendtag && text =~ '</' . s:tagname . '\s*>\s*$'
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200547 call cursor(state.lnum, 99999)
Bram Moolenaar7b61a542014-08-23 15:31:19 +0200548 normal! F<
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200549 let start_lnum = HtmlIndent_FindStartTag()
550 if start_lnum > 0
551 let state.baseindent = indent(start_lnum)
552 if col('.') > 2
553 " check for tags before the matching opening tag.
554 let text = getline(start_lnum)
555 let swendtag = match(text, '^\s*</') >= 0
556 call s:CountITags(text[: col('.') - 2])
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200557 let state.baseindent += s:nextrel * shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200558 if !swendtag
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200559 let state.baseindent += s:curind * shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200560 endif
561 endif
562 return state
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200563 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200564 endif
565
566 " Else: no comments. Skip backwards to find the tag we're inside.
567 let [state.lnum, found] = HtmlIndent_FindTagStart(state.lnum)
568 " Check if that line starts with end tag.
569 let text = getline(state.lnum)
570 let swendtag = match(text, '^\s*</') >= 0
571 call s:CountITags(tolower(text))
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200572 let state.baseindent = indent(state.lnum) + s:nextrel * shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200573 if !swendtag
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200574 let state.baseindent += s:curind * shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200575 endif
576 return state
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200577endfunc "}}}
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200578
579" Indent inside a <pre> block: Keep indent as-is.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200580func s:Alien2()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200581 "{{{
582 return -1
583endfunc "}}}
584
585" Return the indent inside a <script> block for javascript.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200586func s:Alien3()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200587 "{{{
588 let lnum = prevnonblank(v:lnum - 1)
589 while lnum > 1 && getline(lnum) =~ '^\s*/[/*]'
590 " Skip over comments to avoid that cindent() aligns with the <script> tag
591 let lnum = prevnonblank(lnum - 1)
592 endwhile
Bram Moolenaar2346a632021-06-13 19:02:49 +0200593 if lnum < b:hi_indent.blocklnr
594 " indent for <script> itself
595 return b:hi_indent.blocktagind
596 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200597 if lnum == b:hi_indent.blocklnr
598 " indent for the first line after <script>
599 return eval(b:hi_js1indent)
600 endif
601 if b:hi_indent.scripttype == "javascript"
Bram Moolenaar2346a632021-06-13 19:02:49 +0200602 " indent for further lines
Bram Moolenaar2ecbe532022-07-29 21:36:21 +0100603 return GetJavascriptIndent()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200604 else
605 return -1
606 endif
607endfunc "}}}
608
609" Return the indent inside a <style> block.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200610func s:Alien4()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200611 "{{{
612 if prevnonblank(v:lnum-1) == b:hi_indent.blocklnr
613 " indent for first content line
614 return eval(b:hi_css1indent)
615 endif
616 return s:CSSIndent()
617endfunc "}}}
618
619" Indending inside a <style> block. Returns the indent.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200620func s:CSSIndent()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200621 "{{{
622 " This handles standard CSS and also Closure stylesheets where special lines
623 " start with @.
624 " When the line starts with '*' or the previous line starts with "/*"
625 " and does not end in "*/", use C indenting to format the comment.
626 " Adopted $VIMRUNTIME/indent/css.vim
627 let curtext = getline(v:lnum)
628 if curtext =~ '^\s*[*]'
629 \ || (v:lnum > 1 && getline(v:lnum - 1) =~ '\s*/\*'
630 \ && getline(v:lnum - 1) !~ '\*/\s*$')
631 return cindent(v:lnum)
632 endif
633
634 let min_lnum = b:hi_indent.blocklnr
635 let prev_lnum = s:CssPrevNonComment(v:lnum - 1, min_lnum)
636 let [prev_lnum, found] = HtmlIndent_FindTagStart(prev_lnum)
637 if prev_lnum <= min_lnum
638 " Just below the <style> tag, indent for first content line after comments.
639 return eval(b:hi_css1indent)
640 endif
641
Bram Moolenaarba3ff532018-11-04 14:45:49 +0100642 " If the current line starts with "}" align with its match.
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200643 if curtext =~ '^\s*}'
644 call cursor(v:lnum, 1)
645 try
646 normal! %
647 " Found the matching "{", align with it after skipping unfinished lines.
648 let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum)
649 return indent(align_lnum)
650 catch
651 " can't find it, try something else, but it's most likely going to be
652 " wrong
653 endtry
654 endif
655
656 " add indent after {
657 let brace_counts = HtmlIndent_CountBraces(prev_lnum)
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200658 let extra = brace_counts.c_open * shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200659
660 let prev_text = getline(prev_lnum)
661 let below_end_brace = prev_text =~ '}\s*$'
662
663 " Search back to align with the first line that's unfinished.
664 let align_lnum = s:CssFirstUnfinished(prev_lnum, min_lnum)
665
666 " Handle continuation lines if aligning with previous line and not after a
667 " "}".
668 if extra == 0 && align_lnum == prev_lnum && !below_end_brace
669 let prev_hasfield = prev_text =~ '^\s*[a-zA-Z0-9-]\+:'
670 let prev_special = prev_text =~ '^\s*\(/\*\|@\)'
671 if curtext =~ '^\s*\(/\*\|@\)'
672 " if the current line is not a comment or starts with @ (used by template
673 " systems) reduce indent if previous line is a continuation line
674 if !prev_hasfield && !prev_special
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200675 let extra = -shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200676 endif
677 else
678 let cur_hasfield = curtext =~ '^\s*[a-zA-Z0-9-]\+:'
679 let prev_unfinished = s:CssUnfinished(prev_text)
Bram Moolenaarfc65cab2018-08-28 22:58:02 +0200680 if prev_unfinished
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200681 " Continuation line has extra indent if the previous line was not a
682 " continuation line.
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200683 let extra = shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200684 " Align with @if
685 if prev_text =~ '^\s*@if '
686 let extra = 4
687 endif
688 elseif cur_hasfield && !prev_hasfield && !prev_special
689 " less indent below a continuation line
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200690 let extra = -shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200691 endif
692 endif
693 endif
694
695 if below_end_brace
696 " find matching {, if that line starts with @ it's not the start of a rule
697 " but something else from a template system
698 call cursor(prev_lnum, 1)
699 call search('}\s*$')
700 try
701 normal! %
702 " Found the matching "{", align with it.
703 let align_lnum = s:CssFirstUnfinished(line('.'), min_lnum)
704 let special = getline(align_lnum) =~ '^\s*@'
705 catch
706 let special = 0
707 endtry
708 if special
709 " do not reduce indent below @{ ... }
710 if extra < 0
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200711 let extra += shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200712 endif
713 else
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200714 let extra -= (brace_counts.c_close - (prev_text =~ '^\s*}')) * shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200715 endif
716 endif
717
718 " if no extra indent yet...
719 if extra == 0
720 if brace_counts.p_open > brace_counts.p_close
721 " previous line has more ( than ): add a shiftwidth
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200722 let extra = shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200723 elseif brace_counts.p_open < brace_counts.p_close
724 " previous line has more ) than (: subtract a shiftwidth
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200725 let extra = -shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200726 endif
727 endif
728
729 return indent(align_lnum) + extra
730endfunc "}}}
731
732" Inside <style>: Whether a line is unfinished.
Bram Moolenaarfc65cab2018-08-28 22:58:02 +0200733" tag:
734" tag: blah
735" tag: blah &&
736" tag: blah ||
Bram Moolenaar2346a632021-06-13 19:02:49 +0200737func s:CssUnfinished(text)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200738 "{{{
Bram Moolenaarfc65cab2018-08-28 22:58:02 +0200739 return a:text =~ '\(||\|&&\|:\|\k\)\s*$'
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200740endfunc "}}}
741
742" Search back for the first unfinished line above "lnum".
Bram Moolenaar2346a632021-06-13 19:02:49 +0200743func s:CssFirstUnfinished(lnum, min_lnum)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200744 "{{{
745 let align_lnum = a:lnum
746 while align_lnum > a:min_lnum && s:CssUnfinished(getline(align_lnum - 1))
747 let align_lnum -= 1
748 endwhile
749 return align_lnum
750endfunc "}}}
751
752" Find the non-empty line at or before "lnum" that is not a comment.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200753func s:CssPrevNonComment(lnum, stopline)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200754 "{{{
755 " caller starts from a line a:lnum + 1 that is not a comment
756 let lnum = prevnonblank(a:lnum)
757 while 1
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200758 let ccol = match(getline(lnum), '\*/')
759 if ccol < 0
Bram Moolenaar3e496b02016-09-25 22:11:48 +0200760 " No comment end thus it's something else.
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200761 return lnum
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200762 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200763 call cursor(lnum, ccol + 1)
764 " Search back for the /* that starts the comment
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200765 let lnum = search('/\*', 'bW', a:stopline)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200766 if indent(".") == virtcol(".") - 1
767 " The found /* is at the start of the line. Now go back to the line
768 " above it and again check if it is a comment.
769 let lnum = prevnonblank(lnum - 1)
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200770 else
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200771 " /* is after something else, thus it's not a comment line.
772 return lnum
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200773 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200774 endwhile
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200775endfunc "}}}
776
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200777" Check the number of {} and () in line "lnum". Return a dict with the counts.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200778func HtmlIndent_CountBraces(lnum)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200779 "{{{
780 let brs = substitute(getline(a:lnum), '[''"].\{-}[''"]\|/\*.\{-}\*/\|/\*.*$\|[^{}()]', '', 'g')
781 let c_open = 0
782 let c_close = 0
783 let p_open = 0
784 let p_close = 0
785 for brace in split(brs, '\zs')
786 if brace == "{"
787 let c_open += 1
788 elseif brace == "}"
789 if c_open > 0
790 let c_open -= 1
791 else
792 let c_close += 1
793 endif
794 elseif brace == '('
795 let p_open += 1
796 elseif brace == ')'
797 if p_open > 0
798 let p_open -= 1
799 else
800 let p_close += 1
801 endif
802 endif
803 endfor
804 return {'c_open': c_open,
805 \ 'c_close': c_close,
806 \ 'p_open': p_open,
807 \ 'p_close': p_close}
808endfunc "}}}
809
810" Return the indent for a comment: <!-- -->
Bram Moolenaar2346a632021-06-13 19:02:49 +0200811func s:Alien5()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200812 "{{{
813 let curtext = getline(v:lnum)
814 if curtext =~ '^\s*\zs-->'
815 " current line starts with end of comment, line up with comment start.
816 call cursor(v:lnum, 0)
817 let lnum = search('<!--', 'b')
818 if lnum > 0
819 " TODO: what if <!-- is not at the start of the line?
820 return indent(lnum)
821 endif
822
823 " Strange, can't find it.
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200824 return -1
Bram Moolenaar946e27a2014-06-25 18:50:27 +0200825 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200826
827 let prevlnum = prevnonblank(v:lnum - 1)
828 let prevtext = getline(prevlnum)
829 let idx = match(prevtext, '^\s*\zs<!--')
830 if idx >= 0
831 " just below comment start, add a shiftwidth
Bram Moolenaar4072ba52020-12-23 13:56:35 +0100832 return indent(prevlnum) + shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200833 endif
834
835 " Some files add 4 spaces just below a TODO line. It's difficult to detect
836 " the end of the TODO, so let's not do that.
837
838 " Align with the previous non-blank line.
839 return indent(prevlnum)
Bram Moolenaar946e27a2014-06-25 18:50:27 +0200840endfunc "}}}
841
Bram Moolenaarca635012015-09-25 20:34:21 +0200842" Return the indent for conditional comment: <!--[ ![endif]-->
Bram Moolenaar2346a632021-06-13 19:02:49 +0200843func s:Alien6()
Bram Moolenaarca635012015-09-25 20:34:21 +0200844 "{{{
845 let curtext = getline(v:lnum)
846 if curtext =~ '\s*\zs<!\[endif\]-->'
847 " current line starts with end of comment, line up with comment start.
848 let lnum = search('<!--', 'bn')
849 if lnum > 0
850 return indent(lnum)
851 endif
852 endif
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200853 return b:hi_indent.baseindent + shiftwidth()
Bram Moolenaarca635012015-09-25 20:34:21 +0200854endfunc "}}}
855
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200856" When the "lnum" line ends in ">" find the line containing the matching "<".
Bram Moolenaar2346a632021-06-13 19:02:49 +0200857func HtmlIndent_FindTagStart(lnum)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200858 "{{{
859 " Avoids using the indent of a continuation line.
860 " Moves the cursor.
861 " Return two values:
862 " - the matching line number or "lnum".
863 " - a flag indicating whether we found the end of a tag.
864 " This method is global so that HTML-like indenters can use it.
865 " To avoid matching " > " or " < " inside a string require that the opening
866 " "<" is followed by a word character and the closing ">" comes after a
867 " non-white character.
868 let idx = match(getline(a:lnum), '\S>\s*$')
869 if idx > 0
870 call cursor(a:lnum, idx)
Aliaksei Budavei4e3df442025-04-13 21:00:42 +0300871 " VIM_INDENT_TEST_TRACE_START
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200872 let lnum = searchpair('<\w', '' , '\S>', 'bW', '', max([a:lnum - b:html_indent_line_limit, 0]))
Aliaksei Budavei4e3df442025-04-13 21:00:42 +0300873 " VIM_INDENT_TEST_TRACE_END HtmlIndent_FindTagStart
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200874 if lnum > 0
875 return [lnum, 1]
Bram Moolenaar071d4272004-06-13 20:20:40 +0000876 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200877 endif
878 return [a:lnum, 0]
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200879endfunc "}}}
Bram Moolenaar071d4272004-06-13 20:20:40 +0000880
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200881" Find the unclosed start tag from the current cursor position.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200882func HtmlIndent_FindStartTag()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200883 "{{{
884 " The cursor must be on or before a closing tag.
885 " If found, positions the cursor at the match and returns the line number.
886 " Otherwise returns 0.
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100887 let tagname = matchstr(getline('.')[col('.') - 1:], '</\zs' . s:tagname . '\ze')
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200888 let start_lnum = searchpair('<' . tagname . '\>', '', '</' . tagname . '\>', 'bW')
889 if start_lnum > 0
890 return start_lnum
891 endif
892 return 0
893endfunc "}}}
Bram Moolenaar071d4272004-06-13 20:20:40 +0000894
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200895" Moves the cursor from a "<" to the matching ">".
Bram Moolenaar2346a632021-06-13 19:02:49 +0200896func HtmlIndent_FindTagEnd()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200897 "{{{
898 " Call this with the cursor on the "<" of a start tag.
899 " This will move the cursor to the ">" of the matching end tag or, when it's
900 " a self-closing tag, to the matching ">".
901 " Limited to look up to b:html_indent_line_limit lines away.
902 let text = getline('.')
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100903 let tagname = matchstr(text, s:tagname . '\|!--', col('.'))
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200904 if tagname == '!--'
905 call search('--\zs>')
906 elseif s:get_tag('/' . tagname) != 0
907 " tag with a closing tag, find matching "</tag>"
Aliaksei Budavei4e3df442025-04-13 21:00:42 +0300908 " VIM_INDENT_TEST_TRACE_START
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200909 call searchpair('<' . tagname, '', '</' . tagname . '\zs>', 'W', '', line('.') + b:html_indent_line_limit)
Aliaksei Budavei4e3df442025-04-13 21:00:42 +0300910 " VIM_INDENT_TEST_TRACE_END HtmlIndent_FindTagEnd
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200911 else
912 " self-closing tag, find the ">"
913 call search('\S\zs>')
914 endif
915endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200916
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200917" Indenting inside a start tag. Return the correct indent or -1 if unknown.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200918func s:InsideTag(foundHtmlString)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200919 "{{{
920 if a:foundHtmlString
921 " Inside an attribute string.
Bram Moolenaar63b74a82019-03-24 15:09:13 +0100922 " Align with the opening quote or use an external function.
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200923 let lnum = v:lnum - 1
924 if lnum > 1
925 if exists('b:html_indent_tag_string_func')
926 return b:html_indent_tag_string_func(lnum)
927 endif
Bram Moolenaar63b74a82019-03-24 15:09:13 +0100928 " If there is a double quote in the previous line, indent with the
929 " character after it.
930 if getline(lnum) =~ '"'
931 call cursor(lnum, 0)
932 normal f"
933 return virtcol('.')
934 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200935 return indent(lnum)
936 endif
937 endif
938
939 " Should be another attribute: " attr="val". Align with the previous
940 " attribute start.
941 let lnum = v:lnum
942 while lnum > 1
943 let lnum -= 1
944 let text = getline(lnum)
945 " Find a match with one of these, align with "attr":
946 " attr=
947 " <tag attr=
948 " text<tag attr=
949 " <tag>text</tag>text<tag attr=
950 " For long lines search for the first match, finding the last match
951 " gets very slow.
952 if len(text) < 300
953 let idx = match(text, '.*\s\zs[_a-zA-Z0-9-]\+="')
954 else
955 let idx = match(text, '\s\zs[_a-zA-Z0-9-]\+="')
956 endif
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100957 if idx == -1
958 " try <tag attr
959 let idx = match(text, '<' . s:tagname . '\s\+\zs\w')
960 endif
961 if idx == -1
Bram Moolenaarc4573eb2022-01-31 15:40:56 +0000962 " after just "<tag" indent two levels more by default
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100963 let idx = match(text, '<' . s:tagname . '$')
964 if idx >= 0
Bram Moolenaar942db232021-02-13 18:14:48 +0100965 call cursor(lnum, idx + 1)
Bram Moolenaarc4573eb2022-01-31 15:40:56 +0000966 return virtcol('.') - 1 + shiftwidth() * b:hi_attr_indent
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100967 endif
968 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200969 if idx > 0
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100970 " Found the attribute to align with.
971 call cursor(lnum, idx)
972 return virtcol('.')
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200973 endif
974 endwhile
975 return -1
976endfunc "}}}
977
978" THE MAIN INDENT FUNCTION. Return the amount of indent for v:lnum.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200979func HtmlIndent()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200980 "{{{
Bram Moolenaar9da7ff72015-01-14 12:52:36 +0100981 if prevnonblank(v:lnum - 1) < 1
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200982 " First non-blank line has no indent.
983 return 0
984 endif
985
986 let curtext = tolower(getline(v:lnum))
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200987 let indentunit = shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200988
989 let b:hi_newstate = {}
990 let b:hi_newstate.lnum = v:lnum
991
992 " When syntax HL is enabled, detect we are inside a tag. Indenting inside
993 " a tag works very differently. Do not do this when the line starts with
994 " "<", it gets the "htmlTag" ID but we are not inside a tag then.
995 if curtext !~ '^\s*<'
Bram Moolenaar7b61a542014-08-23 15:31:19 +0200996 normal! ^
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200997 let stack = synstack(v:lnum, col('.')) " assumes there are no tabs
998 let foundHtmlString = 0
999 for synid in reverse(stack)
1000 let name = synIDattr(synid, "name")
1001 if index(b:hi_insideStringNames, name) >= 0
1002 let foundHtmlString = 1
1003 elseif index(b:hi_insideTagNames, name) >= 0
1004 " Yes, we are inside a tag.
1005 let indent = s:InsideTag(foundHtmlString)
1006 if indent >= 0
1007 " Do not keep the state. TODO: could keep the block type.
1008 let b:hi_indent.lnum = 0
1009 return indent
1010 endif
1011 endif
1012 endfor
1013 endif
1014
1015 " does the line start with a closing tag?
1016 let swendtag = match(curtext, '^\s*</') >= 0
1017
1018 if prevnonblank(v:lnum - 1) == b:hi_indent.lnum && b:hi_lasttick == b:changedtick - 1
1019 " use state (continue from previous line)
1020 else
1021 " start over (know nothing)
1022 let b:hi_indent = s:FreshState(v:lnum)
1023 endif
1024
1025 if b:hi_indent.block >= 2
1026 " within block
1027 let endtag = s:endtags[b:hi_indent.block]
1028 let blockend = stridx(curtext, endtag)
1029 if blockend >= 0
1030 " block ends here
1031 let b:hi_newstate.block = 0
1032 " calc indent for REST OF LINE (may start more blocks):
1033 call s:CountTagsAndState(strpart(curtext, blockend + strlen(endtag)))
1034 if swendtag && b:hi_indent.block != 5
1035 let indent = b:hi_indent.blocktagind + s:curind * indentunit
1036 let b:hi_newstate.baseindent = indent + s:nextrel * indentunit
1037 else
1038 let indent = s:Alien{b:hi_indent.block}()
1039 let b:hi_newstate.baseindent = b:hi_indent.blocktagind + s:nextrel * indentunit
1040 endif
1041 else
1042 " block continues
1043 " indent this line with alien method
1044 let indent = s:Alien{b:hi_indent.block}()
1045 endif
1046 else
1047 " not within a block - within usual html
1048 let b:hi_newstate.block = b:hi_indent.block
1049 if swendtag
1050 " The current line starts with an end tag, align with its start tag.
1051 call cursor(v:lnum, 1)
1052 let start_lnum = HtmlIndent_FindStartTag()
1053 if start_lnum > 0
1054 " check for the line starting with something inside a tag:
1055 " <sometag <- align here
1056 " attr=val><open> not here
1057 let text = getline(start_lnum)
1058 let angle = matchstr(text, '[<>]')
1059 if angle == '>'
1060 call cursor(start_lnum, 1)
1061 normal! f>%
1062 let start_lnum = line('.')
1063 let text = getline(start_lnum)
1064 endif
1065
1066 let indent = indent(start_lnum)
1067 if col('.') > 2
1068 let swendtag = match(text, '^\s*</') >= 0
1069 call s:CountITags(text[: col('.') - 2])
Bram Moolenaar3ec574f2017-06-13 18:12:01 +02001070 let indent += s:nextrel * shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +02001071 if !swendtag
Bram Moolenaar3ec574f2017-06-13 18:12:01 +02001072 let indent += s:curind * shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +02001073 endif
1074 endif
1075 else
1076 " not sure what to do
1077 let indent = b:hi_indent.baseindent
1078 endif
1079 let b:hi_newstate.baseindent = indent
1080 else
1081 call s:CountTagsAndState(curtext)
1082 let indent = b:hi_indent.baseindent
1083 let b:hi_newstate.baseindent = indent + (s:curind + s:nextrel) * indentunit
1084 endif
1085 endif
1086
1087 let b:hi_lasttick = b:changedtick
1088 call extend(b:hi_indent, b:hi_newstate, "force")
1089 return indent
1090endfunc "}}}
1091
1092" Check user settings when loading this script the first time.
Bram Moolenaarec7944a2013-06-12 21:29:15 +02001093call HtmlIndent_CheckUserSettings()
Bram Moolenaar071d4272004-06-13 20:20:40 +00001094
Bram Moolenaar91170f82006-05-05 21:15:17 +00001095let &cpo = s:cpo_save
1096unlet s:cpo_save
1097
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +02001098" vim: fdm=marker ts=8 sw=2 tw=78