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