1" Vim completion script
2" Language:	XHTML 1.0 Strict
3" Maintainer:	Mikolaj Machowski ( mikmach AT wp DOT pl )
4" Last Change:	2006 Feb 6
5
6function! htmlcomplete#CompleteTags(findstart, base)
7  if a:findstart
8    " locate the start of the word
9    let line = getline('.')
10    let start = col('.') - 1
11	let curline = line('.')
12	let compl_begin = col('.') - 2
13    while start >= 0 && line[start - 1] =~ '\(\k\|[:.-]\)'
14		let start -= 1
15    endwhile
16	" Handling of entities {{{
17	if start >= 0 && line[start - 1] =~ '&'
18		let b:entitiescompl = 1
19		let b:compl_context = ''
20		return start
21	endif
22	" }}}
23	" Handling of <style> tag {{{
24	let stylestart = searchpair('<style\>', '', '<\/style\>', "bnW")
25	let styleend   = searchpair('<style\>', '', '<\/style\>', "nW")
26	if stylestart != 0 && styleend != 0
27		if stylestart <= curline && styleend >= curline
28			let start = col('.') - 1
29			let b:csscompl = 1
30			while start >= 0 && line[start - 1] =~ '\(\k\|-\)'
31				let start -= 1
32			endwhile
33		endif
34	endif
35	" }}}
36	" Handling of <script> tag {{{
37	let scriptstart = searchpair('<script\>', '', '<\/script\>', "bnW")
38	let scriptend   = searchpair('<script\>', '', '<\/script\>', "nW")
39	if scriptstart != 0 && scriptend != 0
40		if scriptstart <= curline && scriptend >= curline
41			let start = col('.') - 1
42			let b:jscompl = 1
43			let b:jsrange = [scriptstart, scriptend]
44			while start >= 0 && line[start - 1] =~ '\k'
45				let start -= 1
46			endwhile
47			" We are inside of <script> tag. But we should also get contents
48			" of all linked external files and (secondary, less probably) other <script> tags
49			" This logic could possible be done in separate function - may be
50			" reused in events scripting (also with option could be reused for
51			" CSS
52			let b:js_extfiles = []
53			let l = line('.')
54			let c = col('.')
55			call cursor(1,1)
56			while search('<\@<=script\>', 'W') && line('.') <= l
57				if synIDattr(synID(line('.'),col('.')-1,0),"name") !~? 'comment'
58					let sname = matchstr(getline('.'), '<script[^>]*src\s*=\s*\([''"]\)\zs.\{-}\ze\1')
59					if filereadable(sname)
60						let b:js_extfiles += readfile(sname)
61					endif
62				endif
63			endwhile
64			call cursor(1,1)
65			let js_scripttags = []
66			while search('<script\>', 'W') && line('.') < l
67				if matchstr(getline('.'), '<script[^>]*src') == ''
68					let js_scripttag = getline(line('.'), search('</script>', 'W'))
69					let js_scripttags += js_scripttag
70				endif
71			endwhile
72			let b:js_extfiles += js_scripttags
73			call cursor(l,c)
74			unlet! l c
75		endif
76	endif
77	" }}}
78	if !exists("b:csscompl") && !exists("b:jscompl")
79		let b:compl_context = getline('.')[0:(compl_begin)]
80		if b:compl_context !~ '<[^>]*$'
81			" Look like we may have broken tag. Check previous lines.
82			let i = 1
83			while 1
84				let context_line = getline(curline-i)
85				if context_line =~ '<[^>]*$'
86					" Yep, this is this line
87					let context_lines = getline(curline-i, curline)
88					let b:compl_context = join(context_lines, ' ')
89					break
90				elseif context_line =~ '>[^<]*$'
91					" Normal tag line, no need for completion at all
92					let b:compl_context = ''
93					break
94				endif
95				let i += 1
96			endwhile
97			" Make sure we don't have counter
98			unlet! i
99		endif
100		let b:compl_context = matchstr(b:compl_context, '.*\zs<.*')
101		" Return proper start for on-events. Without that beginning of
102		" completion will be badly reported
103		if b:compl_context =~? 'on[a-z]*\s*=\s*\(''[^'']*\|"[^"]*\)$'
104			let start = col('.') - 1
105			while start >= 0 && line[start - 1] =~ '\k'
106				let start -= 1
107			endwhile
108		endif
109	else
110		let b:compl_context = getline('.')[0:compl_begin]
111	endif
112    return start
113  else
114	" Initialize base return lists
115    let res = []
116    let res2 = []
117	" a:base is very short - we need context
118	let context = b:compl_context
119	" Check if we should do CSS completion inside of <style> tag
120	" or JS completion inside of <script> tag
121	if exists("b:csscompl")
122		unlet! b:csscompl
123		let context = b:compl_context
124		unlet! b:compl_context
125		return csscomplete#CompleteCSS(0, context)
126	elseif exists("b:jscompl")
127		unlet! b:jscompl
128		return javascriptcomplete#CompleteJS(0, a:base)
129	else
130		if len(b:compl_context) == 0 && !exists("b:entitiescompl")
131			return []
132		endif
133		let context = matchstr(b:compl_context, '.\zs.*')
134	endif
135	unlet! b:compl_context
136	" Entities completion {{{
137	if exists("b:entitiescompl")
138		unlet! b:entitiescompl
139
140		if !exists("g:xmldata_xhtml10s")
141			runtime! autoload/xml/xhtml10s.vim
142		endif
143
144	    let entities =  g:xmldata_xhtml10s['vimxmlentities']
145
146		if len(a:base) == 1
147			for m in entities
148				if m =~ '^'.a:base
149					call add(res, m.';')
150				endif
151			endfor
152			return res
153		else
154			for m in entities
155				if m =~? '^'.a:base
156					call add(res, m.';')
157				elseif m =~? a:base
158					call add(res2, m.';')
159				endif
160			endfor
161
162			return res + res2
163		endif
164
165
166	endif
167	" }}}
168	if context =~ '>'
169		" Generally if context contains > it means we are outside of tag and
170		" should abandon action - with one exception: <style> span { bo
171		if context =~ 'style[^>]\{-}>[^<]\{-}$'
172			return csscomplete#CompleteCSS(0, context)
173		elseif context =~ 'script[^>]\{-}>[^<]\{-}$'
174			let b:jsrange = [line('.'), search('<\/script\>', 'nW')]
175			return javascriptcomplete#CompleteJS(0, context)
176		else
177			return []
178		endif
179	endif
180
181	" If context contains > it means we are already outside of tag and we
182	" should abandon action
183	" If context contains white space it is attribute.
184	" It can be also value of attribute.
185	" We have to get first word to offer proper completions
186	if context == ''
187		let tag = ''
188	else
189		let tag = split(context)[0]
190	endif
191	" Get last word, it should be attr name
192	let attr = matchstr(context, '.*\s\zs.*')
193	" Possible situations where any prediction would be difficult:
194	" 1. Events attributes
195	if context =~ '\s'
196		" Sort out style, class, and on* cases
197		if context =~? "\\(on[a-z]*\\|id\\|style\\|class\\)\\s*=\\s*[\"']"
198			" Id, class completion {{{
199			if context =~? "\\(id\\|class\\)\\s*=\\s*[\"'][a-zA-Z0-9_ -]*$"
200				if context =~? "class\\s*=\\s*[\"'][a-zA-Z0-9_ -]*$"
201					let search_for = "class"
202				elseif context =~? "id\\s*=\\s*[\"'][a-zA-Z0-9_ -]*$"
203					let search_for = "id"
204				endif
205				" Handle class name completion
206				" 1. Find lines of <link stylesheet>
207				" 1a. Check file for @import
208				" 2. Extract filename(s?) of stylesheet,
209				call cursor(1,1)
210				let head = getline(search('<head\>'), search('<\/head>'))
211				let headjoined = join(copy(head), ' ')
212				if headjoined =~ '<style'
213					" Remove possibly confusing CSS operators
214					let stylehead = substitute(headjoined, '+>\*[,', ' ', 'g')
215					if search_for == 'class'
216						let styleheadlines = split(stylehead)
217						let headclasslines = filter(copy(styleheadlines), "v:val =~ '\\([a-zA-Z0-9:]\\+\\)\\?\\.[a-zA-Z0-9_-]\\+'")
218					else
219						let stylesheet = split(headjoined, '[{}]')
220						" Get all lines which fit id syntax
221						let classlines = filter(copy(stylesheet), "v:val =~ '#[a-zA-Z0-9_-]\\+'")
222						" Filter out possible color definitions
223						call filter(classlines, "v:val !~ ':\\s*#[a-zA-Z0-9_-]\\+'")
224						" Filter out complex border definitions
225						call filter(classlines, "v:val !~ '\\(none\\|hidden\\|dotted\\|dashed\\|solid\\|double\\|groove\\|ridge\\|inset\\|outset\\)\\s*#[a-zA-Z0-9_-]\\+'")
226						let templines = join(classlines, ' ')
227						let headclasslines = split(templines)
228						call filter(headclasslines, "v:val =~ '#[a-zA-Z0-9_-]\\+'")
229					endif
230					let internal = 1
231				else
232					let internal = 0
233				endif
234				let styletable = []
235				let secimportfiles = []
236				let filestable = filter(copy(head), "v:val =~ '\\(@import\\|link.*stylesheet\\)'")
237				for line in filestable
238					if line =~ "@import"
239						let styletable += [matchstr(line, "import\\s\\+\\(url(\\)\\?[\"']\\?\\zs\\f\\+\\ze")]
240					elseif line =~ "<link"
241						let styletable += [matchstr(line, "href\\s*=\\s*[\"']\\zs\\f\\+\\ze")]
242					endif
243				endfor
244				for file in styletable
245					if filereadable(file)
246						let stylesheet = readfile(file)
247						let secimport = filter(copy(stylesheet), "v:val =~ '@import'")
248						if len(secimport) > 0
249							for line in secimport
250								let secfile = matchstr(line, "import\\s\\+\\(url(\\)\\?[\"']\\?\\zs\\f\\+\\ze")
251								let secfile = fnamemodify(file, ":p:h").'/'.secfile
252								let secimportfiles += [secfile]
253							endfor
254						endif
255					endif
256				endfor
257				let cssfiles = styletable + secimportfiles
258				let classes = []
259				for file in cssfiles
260					if filereadable(file)
261						let stylesheet = readfile(file)
262						let stylefile = join(stylesheet, ' ')
263						let stylefile = substitute(stylefile, '+>\*[,', ' ', 'g')
264						if search_for == 'class'
265							let stylesheet = split(stylefile)
266							let classlines = filter(copy(stylesheet), "v:val =~ '\\([a-zA-Z0-9:]\\+\\)\\?\\.[a-zA-Z0-9_-]\\+'")
267						else
268							let stylesheet = split(stylefile, '[{}]')
269							" Get all lines which fit id syntax
270							let classlines = filter(copy(stylesheet), "v:val =~ '#[a-zA-Z0-9_-]\\+'")
271							" Filter out possible color definitions
272							call filter(classlines, "v:val !~ ':\\s*#[a-zA-Z0-9_-]\\+'")
273							" Filter out complex border definitions
274							call filter(classlines, "v:val !~ '\\(none\\|hidden\\|dotted\\|dashed\\|solid\\|double\\|groove\\|ridge\\|inset\\|outset\\)\\s*#[a-zA-Z0-9_-]\\+'")
275							let templines = join(classlines, ' ')
276							let stylelines = split(templines)
277							let classlines = filter(stylelines, "v:val =~ '#[a-zA-Z0-9_-]\\+'")
278
279						endif
280					endif
281					" We gathered classes definitions from all external files
282					let classes += classlines
283				endfor
284				if internal == 1
285					let classes += headclasslines
286				endif
287
288				if search_for == 'class'
289					let elements = {}
290					for element in classes
291						if element =~ '^\.'
292							let class = matchstr(element, '^\.\zs[a-zA-Z][a-zA-Z0-9_-]*\ze')
293							let class = substitute(class, ':.*', '', '')
294							if has_key(elements, 'common')
295								let elements['common'] .= ' '.class
296							else
297								let elements['common'] = class
298							endif
299						else
300							let class = matchstr(element, '[a-zA-Z1-6]*\.\zs[a-zA-Z][a-zA-Z0-9_-]*\ze')
301							let tagname = tolower(matchstr(element, '[a-zA-Z1-6]*\ze.'))
302							if tagname != ''
303								if has_key(elements, tagname)
304									let elements[tagname] .= ' '.class
305								else
306									let elements[tagname] = class
307								endif
308							endif
309						endif
310					endfor
311
312					if has_key(elements, tag) && has_key(elements, 'common')
313						let values = split(elements[tag]." ".elements['common'])
314					elseif has_key(elements, tag) && !has_key(elements, 'common')
315						let values = split(elements[tag])
316					elseif !has_key(elements, tag) && has_key(elements, 'common')
317						let values = split(elements['common'])
318					else
319						return []
320					endif
321
322				elseif search_for == 'id'
323					" Find used IDs
324					" 1. Catch whole file
325					let filelines = getline(1, line('$'))
326					" 2. Find lines with possible id
327					let used_id_lines = filter(filelines, 'v:val =~ "id\\s*=\\s*[\"''][a-zA-Z0-9_-]\\+"')
328					" 3a. Join all filtered lines
329					let id_string = join(used_id_lines, ' ')
330					" 3b. And split them to be sure each id is in separate item
331					let id_list = split(id_string, 'id\s*=\s*')
332					" 4. Extract id values
333					let used_id = map(id_list, 'matchstr(v:val, "[\"'']\\zs[a-zA-Z0-9_-]\\+\\ze")')
334					let joined_used_id = ','.join(used_id, ',').','
335
336					let allvalues = map(classes, 'matchstr(v:val, ".*#\\zs[a-zA-Z0-9_-]\\+")')
337
338					let values = []
339
340					for element in classes
341						if joined_used_id !~ ','.element.','
342							let values += [element]
343						endif
344
345					endfor
346
347				endif
348
349				" We need special version of sbase
350				let classbase = matchstr(context, ".*[\"']")
351				let classquote = matchstr(classbase, '.$')
352
353				let entered_class = matchstr(attr, ".*=\\s*[\"']\\zs.*")
354
355				for m in sort(values)
356					if m =~? '^'.entered_class
357						call add(res, m . classquote)
358					elseif m =~? entered_class
359						call add(res2, m . classquote)
360					endif
361				endfor
362
363				return res + res2
364
365			elseif context =~? "style\\s*=\\s*[\"'][^\"']*$"
366				return csscomplete#CompleteCSS(0, context)
367
368			endif
369			" }}}
370			" Complete on-events {{{
371			if context =~? 'on[a-z]*\s*=\s*\(''[^'']*\|"[^"]*\)$'
372				" We have to:
373				" 1. Find external files
374				let b:js_extfiles = []
375				let l = line('.')
376				let c = col('.')
377				call cursor(1,1)
378				while search('<\@<=script\>', 'W') && line('.') <= l
379					if synIDattr(synID(line('.'),col('.')-1,0),"name") !~? 'comment'
380						let sname = matchstr(getline('.'), '<script[^>]*src\s*=\s*\([''"]\)\zs.\{-}\ze\1')
381						if filereadable(sname)
382							let b:js_extfiles += readfile(sname)
383						endif
384					endif
385				endwhile
386				" 2. Find at least one <script> tag
387				call cursor(1,1)
388				let js_scripttags = []
389				while search('<script\>', 'W') && line('.') < l
390					if matchstr(getline('.'), '<script[^>]*src') == ''
391						let js_scripttag = getline(line('.'), search('</script>', 'W'))
392						let js_scripttags += js_scripttag
393					endif
394				endwhile
395				let b:js_extfiles += js_scripttags
396
397				" 3. Proper call for javascriptcomplete#CompleteJS
398				call cursor(l,c)
399				let js_context = matchstr(a:base, '\k\+$')
400				let js_shortcontext = substitute(a:base, js_context.'$', '', '')
401				let b:compl_context = context
402				let b:jsrange = [l, l]
403				unlet! l c
404				return javascriptcomplete#CompleteJS(0, js_context)
405
406			endif
407
408			" }}}
409			let stripbase = matchstr(context, ".*\\(on[a-zA-Z]*\\|style\\|class\\)\\s*=\\s*[\"']\\zs.*")
410			" Now we have context stripped from all chars up to style/class.
411			" It may fail with some strange style value combinations.
412			if stripbase !~ "[\"']"
413				return []
414			endif
415		endif
416		" Value of attribute completion {{{
417		" If attr contains =\s*[\"'] we catched value of attribute
418		if attr =~ "=\s*[\"']"
419			" Let do attribute specific completion
420			let attrname = matchstr(attr, '.*\ze\s*=')
421			let entered_value = matchstr(attr, ".*=\\s*[\"']\\zs.*")
422			let values = []
423			if attrname == 'media'
424				let values = ["screen", "tty", "tv", "projection", "handheld", "print", "braille", "aural", "all"]
425			elseif attrname == 'xml:space'
426				let values = ["preserve"]
427			elseif attrname == 'shape'
428				let values = ["rect", "circle", "poly", "default"]
429			elseif attrname == 'valuetype'
430				let values = ["data", "ref", "object"]
431			elseif attrname == 'method'
432				let values = ["get", "post"]
433			elseif attrname == 'dir'
434				let values = ["ltr", "rtl"]
435			elseif attrname == 'frame'
436				let values = ["void", "above", "below", "hsides", "lhs", "rhs", "vsides", "box", "border"]
437			elseif attrname == 'rules'
438				let values = ["none", "groups", "rows", "all"]
439			elseif attrname == 'align'
440				let values = ["left", "center", "right", "justify", "char"]
441			elseif attrname == 'valign'
442				let values = ["top", "middle", "bottom", "baseline"]
443			elseif attrname == 'scope'
444				let values = ["row", "col", "rowgroup", "colgroup"]
445			elseif attrname == 'href'
446				" Now we are looking for local anchors defined by name or id
447				if entered_value =~ '^#'
448					let file = join(getline(1, line('$')), ' ')
449					" Split it be sure there will be one id/name element in
450					" item, it will be also first word [a-zA-Z0-9_-] in element
451					let oneelement = split(file, "\\(meta \\)\\@<!\\(name\\|id\\)\\s*=\\s*[\"']")
452					for i in oneelement
453						let values += ['#'.matchstr(i, "^[a-zA-Z][a-zA-Z0-9%_-]*")]
454					endfor
455				endif
456			elseif attrname == 'type'
457				if context =~ '^input'
458					let values = ["text", "password", "checkbox", "radio", "submit", "reset", "file", "hidden", "image", "button"]
459				elseif context =~ '^button'
460					let values = ["button", "submit", "reset"]
461				elseif context =~ '^style'
462					let values = ["text/css"]
463				elseif context =~ '^script'
464					let values = ["text/javascript"]
465				endif
466			else
467				return []
468			endif
469
470			if len(values) == 0
471				return []
472			endif
473
474			" We need special version of sbase
475			let attrbase = matchstr(context, ".*[\"']")
476			let attrquote = matchstr(attrbase, '.$')
477
478			for m in values
479				" This if is needed to not offer all completions as-is
480				" alphabetically but sort them. Those beginning with entered
481				" part will be as first choices
482				if m =~ '^'.entered_value
483					call add(res, m . attrquote.' ')
484				elseif m =~ entered_value
485					call add(res2, m . attrquote.' ')
486				endif
487			endfor
488
489			return res + res2
490
491		endif
492		" }}}
493		" Attribute completion {{{
494		" Shorten context to not include last word
495		let sbase = matchstr(context, '.*\ze\s.*')
496
497		" Load data {{{
498		if !exists("g:xmldata_xhtml10s")
499			runtime! autoload/xml/xhtml10s.vim
500		endif
501		" }}}
502		"
503		let attrs = keys(g:xmldata_xhtml10s[tag][1])
504
505		for m in sort(attrs)
506			if m =~ '^'.attr
507				call add(res, m)
508			elseif m =~ attr
509				call add(res2, m)
510			endif
511		endfor
512		let menu = res + res2
513		if has_key(g:xmldata_xhtml10s, 'vimxmlattrinfo')
514			let final_menu = []
515			for i in range(len(menu))
516				let item = menu[i]
517				if has_key(g:xmldata_xhtml10s['vimxmlattrinfo'], item)
518					let m_menu = g:xmldata_xhtml10s['vimxmlattrinfo'][item][0]
519					let m_info = g:xmldata_xhtml10s['vimxmlattrinfo'][item][1]
520					if m_menu !~ 'Bool'
521						let item .= '="'
522					endif
523				else
524					let m_menu = ''
525					let m_info = ''
526					let item .= '="'
527				endif
528				let final_menu += [{'word':item, 'menu':m_menu, 'info':m_info}]
529			endfor
530		else
531			let final_menu = map(menu, 'v:val."=\""')
532		endif
533		return final_menu
534
535	endif
536	" }}}
537	" Close tag {{{
538	let b:unaryTagsStack = "base meta link hr br param img area input col"
539	if context =~ '^\/'
540		let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack")
541		return [opentag.">"]
542	endif
543	" Deal with tag completion.
544	let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack")
545	if opentag == ''
546		" Hack for sometimes failing GetLastOpenTag.
547		" As far as I tested fail isn't GLOT fault but problem
548		" of invalid document - not properly closed tags and other mish-mash.
549		" If returns empty string assume <body>. Safe bet.
550		let opentag = 'body'
551	endif
552	" }}}
553	" Load data {{{
554	if !exists("g:xmldata_xhtml10s")
555		runtime! autoload/xml/xhtml10s.vim
556	endif
557	" }}}
558	" Tag completion {{{
559
560	let tags = g:xmldata_xhtml10s[opentag][0]
561
562	for m in sort(tags)
563		if m =~ '^'.context
564			call add(res, m)
565		elseif m =~ context
566			call add(res2, m)
567		endif
568	endfor
569	let menu = res + res2
570	if has_key(g:xmldata_xhtml10s, 'vimxmltaginfo')
571		let final_menu = []
572		for i in range(len(menu))
573			let item = menu[i]
574			if has_key(g:xmldata_xhtml10s['vimxmltaginfo'], item)
575				let m_menu = g:xmldata_xhtml10s['vimxmltaginfo'][item][0]
576				let m_info = g:xmldata_xhtml10s['vimxmltaginfo'][item][1]
577			else
578				let m_menu = ''
579				let m_info = ''
580			endif
581			let final_menu += [{'word':item, 'menu':m_menu, 'info':m_info}]
582		endfor
583	else
584		let final_menu = menu
585	endif
586	return final_menu
587
588
589	" }}}
590  endif
591endfunction
592" vim:set foldmethod=marker:
593