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