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