blob: 2fa10cc91e3595ab0f5cf24f91e964287f7ce9de [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>
Christian Brabandte8d6f032023-08-23 20:23:07 +01004" Last Change: 2023 Aug 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)
871 let lnum = searchpair('<\w', '' , '\S>', 'bW', '', max([a:lnum - b:html_indent_line_limit, 0]))
872 if lnum > 0
873 return [lnum, 1]
Bram Moolenaar071d4272004-06-13 20:20:40 +0000874 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200875 endif
876 return [a:lnum, 0]
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200877endfunc "}}}
Bram Moolenaar071d4272004-06-13 20:20:40 +0000878
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200879" Find the unclosed start tag from the current cursor position.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200880func HtmlIndent_FindStartTag()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200881 "{{{
882 " The cursor must be on or before a closing tag.
883 " If found, positions the cursor at the match and returns the line number.
884 " Otherwise returns 0.
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100885 let tagname = matchstr(getline('.')[col('.') - 1:], '</\zs' . s:tagname . '\ze')
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200886 let start_lnum = searchpair('<' . tagname . '\>', '', '</' . tagname . '\>', 'bW')
887 if start_lnum > 0
888 return start_lnum
889 endif
890 return 0
891endfunc "}}}
Bram Moolenaar071d4272004-06-13 20:20:40 +0000892
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200893" Moves the cursor from a "<" to the matching ">".
Bram Moolenaar2346a632021-06-13 19:02:49 +0200894func HtmlIndent_FindTagEnd()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200895 "{{{
896 " Call this with the cursor on the "<" of a start tag.
897 " This will move the cursor to the ">" of the matching end tag or, when it's
898 " a self-closing tag, to the matching ">".
899 " Limited to look up to b:html_indent_line_limit lines away.
900 let text = getline('.')
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100901 let tagname = matchstr(text, s:tagname . '\|!--', col('.'))
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200902 if tagname == '!--'
903 call search('--\zs>')
904 elseif s:get_tag('/' . tagname) != 0
905 " tag with a closing tag, find matching "</tag>"
906 call searchpair('<' . tagname, '', '</' . tagname . '\zs>', 'W', '', line('.') + b:html_indent_line_limit)
907 else
908 " self-closing tag, find the ">"
909 call search('\S\zs>')
910 endif
911endfunc "}}}
Bram Moolenaarec7944a2013-06-12 21:29:15 +0200912
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200913" Indenting inside a start tag. Return the correct indent or -1 if unknown.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200914func s:InsideTag(foundHtmlString)
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200915 "{{{
916 if a:foundHtmlString
917 " Inside an attribute string.
Bram Moolenaar63b74a82019-03-24 15:09:13 +0100918 " Align with the opening quote or use an external function.
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200919 let lnum = v:lnum - 1
920 if lnum > 1
921 if exists('b:html_indent_tag_string_func')
922 return b:html_indent_tag_string_func(lnum)
923 endif
Bram Moolenaar63b74a82019-03-24 15:09:13 +0100924 " If there is a double quote in the previous line, indent with the
925 " character after it.
926 if getline(lnum) =~ '"'
927 call cursor(lnum, 0)
928 normal f"
929 return virtcol('.')
930 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200931 return indent(lnum)
932 endif
933 endif
934
935 " Should be another attribute: " attr="val". Align with the previous
936 " attribute start.
937 let lnum = v:lnum
938 while lnum > 1
939 let lnum -= 1
940 let text = getline(lnum)
941 " Find a match with one of these, align with "attr":
942 " attr=
943 " <tag attr=
944 " text<tag attr=
945 " <tag>text</tag>text<tag attr=
946 " For long lines search for the first match, finding the last match
947 " gets very slow.
948 if len(text) < 300
949 let idx = match(text, '.*\s\zs[_a-zA-Z0-9-]\+="')
950 else
951 let idx = match(text, '\s\zs[_a-zA-Z0-9-]\+="')
952 endif
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100953 if idx == -1
954 " try <tag attr
955 let idx = match(text, '<' . s:tagname . '\s\+\zs\w')
956 endif
957 if idx == -1
Bram Moolenaarc4573eb2022-01-31 15:40:56 +0000958 " after just "<tag" indent two levels more by default
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100959 let idx = match(text, '<' . s:tagname . '$')
960 if idx >= 0
Bram Moolenaar942db232021-02-13 18:14:48 +0100961 call cursor(lnum, idx + 1)
Bram Moolenaarc4573eb2022-01-31 15:40:56 +0000962 return virtcol('.') - 1 + shiftwidth() * b:hi_attr_indent
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100963 endif
964 endif
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200965 if idx > 0
Bram Moolenaarb5b75622018-03-09 22:22:21 +0100966 " Found the attribute to align with.
967 call cursor(lnum, idx)
968 return virtcol('.')
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200969 endif
970 endwhile
971 return -1
972endfunc "}}}
973
974" THE MAIN INDENT FUNCTION. Return the amount of indent for v:lnum.
Bram Moolenaar2346a632021-06-13 19:02:49 +0200975func HtmlIndent()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200976 "{{{
Bram Moolenaar9da7ff72015-01-14 12:52:36 +0100977 if prevnonblank(v:lnum - 1) < 1
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200978 " First non-blank line has no indent.
979 return 0
980 endif
981
982 let curtext = tolower(getline(v:lnum))
Bram Moolenaar3ec574f2017-06-13 18:12:01 +0200983 let indentunit = shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200984
985 let b:hi_newstate = {}
986 let b:hi_newstate.lnum = v:lnum
987
988 " When syntax HL is enabled, detect we are inside a tag. Indenting inside
989 " a tag works very differently. Do not do this when the line starts with
990 " "<", it gets the "htmlTag" ID but we are not inside a tag then.
991 if curtext !~ '^\s*<'
Bram Moolenaar7b61a542014-08-23 15:31:19 +0200992 normal! ^
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +0200993 let stack = synstack(v:lnum, col('.')) " assumes there are no tabs
994 let foundHtmlString = 0
995 for synid in reverse(stack)
996 let name = synIDattr(synid, "name")
997 if index(b:hi_insideStringNames, name) >= 0
998 let foundHtmlString = 1
999 elseif index(b:hi_insideTagNames, name) >= 0
1000 " Yes, we are inside a tag.
1001 let indent = s:InsideTag(foundHtmlString)
1002 if indent >= 0
1003 " Do not keep the state. TODO: could keep the block type.
1004 let b:hi_indent.lnum = 0
1005 return indent
1006 endif
1007 endif
1008 endfor
1009 endif
1010
1011 " does the line start with a closing tag?
1012 let swendtag = match(curtext, '^\s*</') >= 0
1013
1014 if prevnonblank(v:lnum - 1) == b:hi_indent.lnum && b:hi_lasttick == b:changedtick - 1
1015 " use state (continue from previous line)
1016 else
1017 " start over (know nothing)
1018 let b:hi_indent = s:FreshState(v:lnum)
1019 endif
1020
1021 if b:hi_indent.block >= 2
1022 " within block
1023 let endtag = s:endtags[b:hi_indent.block]
1024 let blockend = stridx(curtext, endtag)
1025 if blockend >= 0
1026 " block ends here
1027 let b:hi_newstate.block = 0
1028 " calc indent for REST OF LINE (may start more blocks):
1029 call s:CountTagsAndState(strpart(curtext, blockend + strlen(endtag)))
1030 if swendtag && b:hi_indent.block != 5
1031 let indent = b:hi_indent.blocktagind + s:curind * indentunit
1032 let b:hi_newstate.baseindent = indent + s:nextrel * indentunit
1033 else
1034 let indent = s:Alien{b:hi_indent.block}()
1035 let b:hi_newstate.baseindent = b:hi_indent.blocktagind + s:nextrel * indentunit
1036 endif
1037 else
1038 " block continues
1039 " indent this line with alien method
1040 let indent = s:Alien{b:hi_indent.block}()
1041 endif
1042 else
1043 " not within a block - within usual html
1044 let b:hi_newstate.block = b:hi_indent.block
1045 if swendtag
1046 " The current line starts with an end tag, align with its start tag.
1047 call cursor(v:lnum, 1)
1048 let start_lnum = HtmlIndent_FindStartTag()
1049 if start_lnum > 0
1050 " check for the line starting with something inside a tag:
1051 " <sometag <- align here
1052 " attr=val><open> not here
1053 let text = getline(start_lnum)
1054 let angle = matchstr(text, '[<>]')
1055 if angle == '>'
1056 call cursor(start_lnum, 1)
1057 normal! f>%
1058 let start_lnum = line('.')
1059 let text = getline(start_lnum)
1060 endif
1061
1062 let indent = indent(start_lnum)
1063 if col('.') > 2
1064 let swendtag = match(text, '^\s*</') >= 0
1065 call s:CountITags(text[: col('.') - 2])
Bram Moolenaar3ec574f2017-06-13 18:12:01 +02001066 let indent += s:nextrel * shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +02001067 if !swendtag
Bram Moolenaar3ec574f2017-06-13 18:12:01 +02001068 let indent += s:curind * shiftwidth()
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +02001069 endif
1070 endif
1071 else
1072 " not sure what to do
1073 let indent = b:hi_indent.baseindent
1074 endif
1075 let b:hi_newstate.baseindent = indent
1076 else
1077 call s:CountTagsAndState(curtext)
1078 let indent = b:hi_indent.baseindent
1079 let b:hi_newstate.baseindent = indent + (s:curind + s:nextrel) * indentunit
1080 endif
1081 endif
1082
1083 let b:hi_lasttick = b:changedtick
1084 call extend(b:hi_indent, b:hi_newstate, "force")
1085 return indent
1086endfunc "}}}
1087
1088" Check user settings when loading this script the first time.
Bram Moolenaarec7944a2013-06-12 21:29:15 +02001089call HtmlIndent_CheckUserSettings()
Bram Moolenaar071d4272004-06-13 20:20:40 +00001090
Bram Moolenaar91170f82006-05-05 21:15:17 +00001091let &cpo = s:cpo_save
1092unlet s:cpo_save
1093
Bram Moolenaar8bb1c3e2014-07-04 16:43:17 +02001094" vim: fdm=marker ts=8 sw=2 tw=78