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