xref: /vim-8.2.3635/runtime/indent/html.vim (revision dee2e315)
1" Vim indent script for HTML
2" General: "{{{
3" File:		html.vim (Vimscript #2075)
4" Author:	Andy Wokula <[email protected]>
5" Last Change:	2013 Jun 12
6" Rev Days:     13
7" Version:	0.9
8" Vim Version:	Vim7
9" Description:
10"   Improved version of the distributed html indent script, faster on a
11"   range of lines.
12"
13" Credits:
14"	indent/html.vim (2006 Jun 05) from J. Zellner
15"	indent/css.vim (2006 Dec 20) from N. Weibull
16"
17" History:
18" 2012 Oct 21	(v0.9) added support for shiftwidth()
19" 2011 Sep 09	(v0.8) added HTML5 tags (thx to J. Zuckerman)
20" 2008 Apr 28	(v0.6) revised customization
21" 2008 Mar 09	(v0.5) fixed 'indk' issue (thx to C.J. Robinson)
22" }}}
23
24" Init Folklore, check user settings (2nd time ++) "{{{
25if exists("b:did_indent")
26    finish
27endif
28let b:did_indent = 1
29
30setlocal indentexpr=HtmlIndent()
31setlocal indentkeys=o,O,<Return>,<>>,{,},!^F
32
33let b:indent = {"lnum": -1}
34let b:undo_indent = "set inde< indk<| unlet b:indent"
35
36" Load Once:
37if exists("*HtmlIndent")
38    call HtmlIndent_CheckUserSettings()
39    finish
40endif
41
42" Patch 7.3.694
43if exists('*shiftwidth')
44    let s:ShiftWidth = function('shiftwidth')
45else
46    func! s:ShiftWidth()
47	return &shiftwidth
48    endfunc
49endif
50
51let s:cpo_save = &cpo
52set cpo-=C
53"}}}
54
55func! HtmlIndent_CheckUserSettings() "{{{
56    if exists("g:html_indent_inctags")
57	call s:AddITags(split(g:html_indent_inctags, ","))
58    endif
59    if exists("g:html_indent_autotags")
60	call s:RemoveITags(split(g:html_indent_autotags, ","))
61    endif
62
63    let indone = {"zero": 0
64		\,"auto": "indent(prevnonblank(v:lnum-1))"
65		\,"inc": "b:indent.blocktagind + s:ShiftWidth()"}
66    if exists("g:html_indent_script1")
67	let s:js1indent = get(indone, g:html_indent_script1, indone.zero)
68    endif
69    if exists("g:html_indent_style1")
70	let s:css1indent = get(indone, g:html_indent_style1, indone.zero)
71    endif
72endfunc "}}}
73
74" Init Script Vars  "{{{
75let s:usestate = 1
76let s:css1indent = 0
77let s:js1indent = 0
78" not to be changed:
79let s:endtags = [0,0,0,0,0,0,0,0]   " some places unused
80let s:newstate = {}
81let s:countonly = 0
82 "}}}
83func! s:AddITags(taglist) "{{{
84    for itag in a:taglist
85	let s:indent_tags[itag] = 1
86	let s:indent_tags['/'.itag] = -1
87    endfor
88endfunc "}}}
89func! s:AddBlockTag(tag, id, ...) "{{{
90    if !(a:id >= 2 && a:id < 2+len(s:endtags))
91	return
92    endif
93    let s:indent_tags[a:tag] = a:id
94    if a:0 == 0
95	let s:indent_tags['/'.a:tag] = -a:id
96	let s:endtags[a:id-2] = "</".a:tag.">"
97    else
98	let s:indent_tags[a:1] = -a:id
99	let s:endtags[a:id-2] = a:1
100    endif
101endfunc "}}}
102func! s:RemoveITags(taglist) "{{{
103    " remove itags (protect blocktags from being removed)
104    for itag in a:taglist
105	if !has_key(s:indent_tags, itag) || s:indent_tags[itag] != 1
106	    continue
107	endif
108	unlet s:indent_tags[itag]
109	if itag =~ '^\w\+$'
110	    unlet s:indent_tags["/".itag]
111	endif
112    endfor
113endfunc "}}}
114" Add Indent Tags: {{{
115if !exists("s:indent_tags")
116    let s:indent_tags = {}
117endif
118
119" old tags:
120call s:AddITags(['a', 'abbr', 'acronym', 'address', 'b', 'bdo', 'big',
121    \ 'blockquote', 'button', 'caption', 'center', 'cite', 'code', 'colgroup',
122    \ 'del', 'dfn', 'dir', 'div', 'dl', 'em', 'fieldset', 'font', 'form',
123    \ 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'i', 'iframe', 'ins', 'kbd',
124    \ 'label', 'legend', 'map', 'menu', 'noframes', 'noscript', 'object', 'ol',
125    \ 'optgroup', 'q', 's', 'samp', 'select', 'small', 'span', 'strong', 'sub',
126    \ 'sup', 'table', 'textarea', 'title', 'tt', 'u', 'ul', 'var', 'th', 'td',
127    \ 'tr', 'tfoot', 'thead'])
128
129" tags added 2011 Sep 09 (especially HTML5 tags):
130call s:AddITags(['area', 'article', 'aside', 'audio', 'bdi', 'canvas',
131    \ 'command', 'datalist', 'details', 'embed', 'figure', 'footer',
132    \ 'header', 'group', 'keygen', 'mark', 'math', 'meter', 'nav', 'output',
133    \ 'progress', 'ruby', 'section', 'svg', 'texture', 'time', 'video',
134    \ 'wbr', 'text'])
135
136"}}}
137" Add Block Tags: contain alien content "{{{
138call s:AddBlockTag('pre', 2)
139call s:AddBlockTag('script', 3)
140call s:AddBlockTag('style', 4)
141call s:AddBlockTag('<!--', 5, '-->')
142"}}}
143
144func! s:CountITags(...) "{{{
145
146    " relative indent steps for current line [unit &sw]:
147    let s:curind = 0
148    " relative indent steps for next line [unit &sw]:
149    let s:nextrel = 0
150
151    if a:0==0
152	let s:block = s:newstate.block
153	let tmpline = substitute(s:curline, '<\zs\/\=\w\+\>\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g')
154	if s:block == 3
155	    let s:newstate.scripttype = s:GetScriptType(matchstr(tmpline, '\C.*<SCRIPT\>\zs[^>]*'))
156	endif
157	let s:newstate.block = s:block
158    else
159	let s:block = 0		" assume starting outside of a block
160	let s:countonly = 1	" don't change state
161	let tmpline = substitute(s:altline, '<\zs\/\=\w\+\>\|<!--\|-->', '\=s:CheckTag(submatch(0))', 'g')
162	let s:countonly = 0
163    endif
164endfunc "}}}
165func! s:CheckTag(itag) "{{{
166    " "tag" or "/tag" or "<!--" or "-->"
167    let ind = get(s:indent_tags, a:itag)
168    if ind == -1
169	" closing tag
170	if s:block != 0
171	    " ignore itag within a block
172	    return "foo"
173	endif
174	if s:nextrel == 0
175	    let s:curind -= 1
176	else
177	    let s:nextrel -= 1
178	endif
179	" if s:curind >= 1
180	"     let s:curind -= 1
181	" else
182	"     let s:nextrel -= 1
183	" endif
184    elseif ind == 1
185	" opening tag
186	if s:block != 0
187	    return "foo"
188	endif
189	let s:nextrel += 1
190    elseif ind != 0
191	" block-tag (opening or closing)
192	return s:Blocktag(a:itag, ind)
193    endif
194    " else ind==0 (other tag found): keep indent
195    return "foo"   " no matter
196endfunc "}}}
197func! s:Blocktag(blocktag, ind) "{{{
198    if a:ind > 0
199	" a block starts here
200	if s:block != 0
201	    " already in a block (nesting) - ignore
202	    " especially ignore comments after other blocktags
203	    return "foo"
204	endif
205	let s:block = a:ind		" block type
206	if s:countonly
207	    return "foo"
208	endif
209	let s:newstate.blocklnr = v:lnum
210	" save allover indent for the endtag
211	let s:newstate.blocktagind = b:indent.baseindent + (s:nextrel + s:curind) * s:ShiftWidth()
212	if a:ind == 3
213	    return "SCRIPT"    " all except this must be lowercase
214	    " line is to be checked again for the type attribute
215	endif
216    else
217	let s:block = 0
218	" we get here if starting and closing block-tag on same line
219    endif
220    return "foo"
221endfunc "}}}
222func! s:GetScriptType(str) "{{{
223    if a:str == "" || a:str =~ "java"
224	return "javascript"
225    else
226	return ""
227    endif
228endfunc "}}}
229
230func! s:FreshState(lnum) "{{{
231    " Look back in the file (lines 1 to a:lnum-1) to calc a state for line
232    " a:lnum.  A state is to know ALL relevant details about the lines
233    " 1..a:lnum-1, initial calculating (here!) can be slow, but updating is
234    " fast (incremental).
235    " State:
236    "	lnum		last indented line == prevnonblank(a:lnum - 1)
237    "	block = 0	a:lnum located within special tag: 0:none, 2:<pre>,
238    "			3:<script>, 4:<style>, 5:<!--
239    "	baseindent	use this indent for line a:lnum as a start - kind of
240    "			autoindent (if block==0)
241    "	scripttype = ''	type attribute of a script tag (if block==3)
242    "	blocktagind	indent for current opening (get) and closing (set)
243    "			blocktag (if block!=0)
244    "	blocklnr	lnum of starting blocktag (if block!=0)
245    "	inattr		line {lnum} starts with attributes of a tag
246    let state = {}
247    let state.lnum = prevnonblank(a:lnum - 1)
248    let state.scripttype = ""
249    let state.blocktagind = -1
250    let state.block = 0
251    let state.baseindent = 0
252    let state.blocklnr = 0
253    let state.inattr = 0
254
255    if state.lnum == 0
256	return state
257    endif
258
259    " Heuristic:
260    " remember startline state.lnum
261    " look back for <pre, </pre, <script, </script, <style, </style tags
262    " remember stopline
263    " if opening tag found,
264    "	assume a:lnum within block
265    " else
266    "	look back in result range (stopline, startline) for comment
267    "	    \ delimiters (<!--, -->)
268    "	if comment opener found,
269    "	    assume a:lnum within comment
270    "	else
271    "	    assume usual html for a:lnum
272    "	    if a:lnum-1 has a closing comment
273    "		look back to get indent of comment opener
274    " FI
275
276    " look back for blocktag
277    call cursor(a:lnum, 1)
278    let [stopline, stopcol] = searchpos('\c<\zs\/\=\%(pre\>\|script\>\|style\>\)', "bW")
279    " fugly ... why isn't there searchstr()
280    let tagline = tolower(getline(stopline))
281    let blocktag = matchstr(tagline, '\/\=\%(pre\>\|script\>\|style\>\)', stopcol-1)
282    if stopline > 0 && blocktag[0] != "/"
283	" opening tag found, assume a:lnum within block
284	let state.block = s:indent_tags[blocktag]
285	if state.block == 3
286	    let state.scripttype = s:GetScriptType(matchstr(tagline, '\>[^>]*', stopcol))
287	endif
288	let state.blocklnr = stopline
289	" check preceding tags in the line:
290	let s:altline = tagline[: stopcol-2]
291	call s:CountITags(1)
292	let state.blocktagind = indent(stopline) + (s:curind + s:nextrel) * s:ShiftWidth()
293	return state
294    elseif stopline == state.lnum
295	" handle special case: previous line (= state.lnum) contains a
296	" closing blocktag which is preceded by line-noise;
297	" blocktag == "/..."
298	let swendtag = match(tagline, '^\s*</') >= 0
299	if !swendtag
300	    let [bline, bcol] = searchpos('<'.blocktag[1:].'\>', "bW")
301	    let s:altline = tolower(getline(bline)[: bcol-2])
302	    call s:CountITags(1)
303	    let state.baseindent = indent(bline) + (s:nextrel+s:curline) * s:ShiftWidth()
304	    return state
305	endif
306    endif
307
308    " else look back for comment
309    call cursor(a:lnum, 1)
310    let [comline, comcol, found] = searchpos('\(<!--\)\|-->', 'bpW', stopline)
311    if found == 2
312	" comment opener found, assume a:lnum within comment
313	let state.block = 5
314	let state.blocklnr = comline
315	" check preceding tags in the line:
316	let s:altline = tolower(getline(comline)[: comcol-2])
317	call s:CountITags(1)
318	let state.blocktagind = indent(comline) + (s:curind + s:nextrel) * s:ShiftWidth()
319	return state
320    endif
321
322    " else within usual html
323    let s:altline = tolower(getline(state.lnum))
324    " check a:lnum-1 for closing comment (we need indent from the opening line)
325    let comcol = stridx(s:altline, '-->')
326    if comcol >= 0
327	call cursor(state.lnum, comcol+1)
328	let [comline, comcol] = searchpos('<!--', 'bW')
329	if comline == state.lnum
330	    let s:altline = s:altline[: comcol-2]
331	else
332	    let s:altline = tolower(getline(comline)[: comcol-2])
333	endif
334	call s:CountITags(1)
335	let state.baseindent = indent(comline) + (s:nextrel+s:curline) * s:ShiftWidth()
336	return state
337	" TODO check tags that follow "-->"
338    endif
339
340    " else no comments
341    call s:CountITags(1)
342    let state.baseindent = indent(state.lnum) + s:nextrel * s:ShiftWidth()
343    " line starts with end tag
344    let swendtag = match(s:altline, '^\s*</') >= 0
345    if !swendtag
346	let state.baseindent += s:curind * s:ShiftWidth()
347    endif
348    return state
349endfunc "}}}
350
351func! s:Alien2() "{{{
352    " <pre> block
353    return -1
354endfunc "}}}
355func! s:Alien3() "{{{
356    " <script> javascript
357    if prevnonblank(v:lnum-1) == b:indent.blocklnr
358	" indent for the first line after <script>
359	return eval(s:js1indent)
360    endif
361    if b:indent.scripttype == "javascript"
362	return cindent(v:lnum)
363    else
364	return -1
365    endif
366endfunc "}}}
367func! s:Alien4() "{{{
368    " <style>
369    if prevnonblank(v:lnum-1) == b:indent.blocklnr
370	" indent for first content line
371	return eval(s:css1indent)
372    endif
373    return s:CSSIndent()
374endfunc
375
376func! s:CSSIndent() "{{{
377    " adopted $VIMRUNTIME/indent/css.vim
378    if getline(v:lnum) =~ '^\s*[*}]'
379	return cindent(v:lnum)
380    endif
381    let minline = b:indent.blocklnr
382    let pnum = s:css_prevnoncomment(v:lnum - 1, minline)
383    if pnum <= minline
384	" < is to catch errors
385	" indent for first content line after comments
386	return eval(s:css1indent)
387    endif
388    let ind = indent(pnum) + s:css_countbraces(pnum, 1) * s:ShiftWidth()
389    let pline = getline(pnum)
390    if pline =~ '}\s*$'
391	let ind -= (s:css_countbraces(pnum, 0) - (pline =~ '^\s*}')) * s:ShiftWidth()
392    endif
393    return ind
394endfunc "}}}
395func! s:css_prevnoncomment(lnum, stopline) "{{{
396    " caller starts from a line a:lnum-1 that is not a comment
397    let lnum = prevnonblank(a:lnum)
398    let ccol = match(getline(lnum), '\*/')
399    if ccol < 0
400	return lnum
401    endif
402    call cursor(lnum, ccol+1)
403    let lnum = search('/\*', 'bW', a:stopline)
404    if indent(".") == virtcol(".")-1
405	return prevnonblank(lnum-1)
406    else
407	return lnum
408    endif
409endfunc "}}}
410func! s:css_countbraces(lnum, count_open) "{{{
411    let brs = substitute(getline(a:lnum),'[''"].\{-}[''"]\|/\*.\{-}\*/\|/\*.*$\|[^{}]','','g')
412    let n_open = 0
413    let n_close = 0
414    for brace in split(brs, '\zs')
415	if brace == "{"
416	    let n_open += 1
417	elseif brace == "}"
418	    if n_open > 0
419		let n_open -= 1
420	    else
421		let n_close += 1
422	    endif
423	endif
424    endfor
425    return a:count_open ? n_open : n_close
426endfunc "}}}
427
428"}}}
429func! s:Alien5() "{{{
430    " <!-- -->
431    return -1
432endfunc "}}}
433
434func! HtmlIndent() "{{{
435    let s:curline = tolower(getline(v:lnum))
436    let indentunit = s:ShiftWidth()
437
438    let s:newstate = {}
439    let s:newstate.lnum = v:lnum
440
441    " does the line start with a closing tag?
442    let swendtag = match(s:curline, '^\s*</') >= 0
443
444    if prevnonblank(v:lnum-1) == b:indent.lnum && s:usestate
445	" use state (continue from previous line)
446    else
447	" start over (know nothing)
448	let b:indent = s:FreshState(v:lnum)
449    endif
450
451    if b:indent.block >= 2
452	" within block
453	let endtag = s:endtags[b:indent.block-2]
454	let blockend = stridx(s:curline, endtag)
455	if blockend >= 0
456	    " block ends here
457	    let s:newstate.block = 0
458	    " calc indent for REST OF LINE (may start more blocks):
459	    let s:curline = strpart(s:curline, blockend+strlen(endtag))
460	    call s:CountITags()
461	    if swendtag && b:indent.block != 5
462		let indent = b:indent.blocktagind + s:curind * indentunit
463		let s:newstate.baseindent = indent + s:nextrel * indentunit
464	    else
465		let indent = s:Alien{b:indent.block}()
466		let s:newstate.baseindent = b:indent.blocktagind + s:nextrel * indentunit
467	    endif
468	    call extend(b:indent, s:newstate, "force")
469	    return indent
470	else
471	    " block continues
472	    " indent this line with alien method
473	    let indent = s:Alien{b:indent.block}()
474	    call extend(b:indent, s:newstate, "force")
475	    return indent
476	endif
477    else
478	" not within a block - within usual html
479	" if < 2 then always 0
480	let s:newstate.block = b:indent.block
481	call s:CountITags()
482	if swendtag
483	    let indent = b:indent.baseindent + s:curind * indentunit
484	    let s:newstate.baseindent = indent + s:nextrel * indentunit
485	else
486	    let indent = b:indent.baseindent
487	    let s:newstate.baseindent = indent + (s:curind + s:nextrel) * indentunit
488	endif
489	call extend(b:indent, s:newstate, "force")
490	return indent
491    endif
492
493endfunc "}}}
494
495" check user settings (first time), clear cpo, Modeline: {{{1
496
497" DEBUG:
498com! -nargs=* IndHtmlLocal <args>
499
500call HtmlIndent_CheckUserSettings()
501
502let &cpo = s:cpo_save
503unlet s:cpo_save
504
505" vim:set fdm=marker ts=8:
506