1" Vim completion script
2" Language:	XML
3" Maintainer:	Mikolaj Machowski ( mikmach AT wp DOT pl )
4" Last Change:	2013 Jun 29
5" Version: 1.9
6"
7" Changelog:
8" 1.9 - 2007 Aug 15
9" 		- fix closing of namespaced tags (Johannes Weiss)
10" 1.8 - 2006 Jul 18
11"       - allow for closing of xml tags even when data file isn't available
12
13" This function will create Dictionary with users namespace strings and values
14" canonical (system) names of data files.  Names should be lowercase,
15" descriptive to avoid any future conflicts. For example 'xhtml10s' should be
16" name for data of XHTML 1.0 Strict and 'xhtml10t' for XHTML 1.0 Transitional
17" User interface will be provided by XMLns command defined in ftplugin/xml.vim
18" Currently supported canonicals are:
19" xhtml10s - XHTML 1.0 Strict
20" xsl      - XSL
21function! xmlcomplete#CreateConnection(canonical, ...) " {{{
22
23	" When only one argument provided treat name as default namespace (without
24	" 'prefix:').
25	if exists("a:1")
26		let users = a:1
27	else
28		let users = 'DEFAULT'
29	endif
30
31	" Source data file. Due to suspected errors in autoload do it with
32	" :runtime.
33	" TODO: make it properly (using autoload, that is) later
34	exe "runtime autoload/xml/".a:canonical.".vim"
35
36	" Remove all traces of unexisting files to return [] when trying
37	" omnicomplete something
38	" TODO: give warning about non-existing canonicals - should it be?
39	if !exists("g:xmldata_".a:canonical)
40		unlet! g:xmldata_connection
41		return 0
42	endif
43
44	" We need to initialize Dictionary to add key-value pair
45	if !exists("g:xmldata_connection")
46		let g:xmldata_connection = {}
47	endif
48
49	let g:xmldata_connection[users] = a:canonical
50
51endfunction
52" }}}
53
54function! xmlcomplete#CreateEntConnection(...) " {{{
55	if a:0 > 0
56		let g:xmldata_entconnect = a:1
57	else
58		let g:xmldata_entconnect = 'DEFAULT'
59	endif
60endfunction
61" }}}
62
63function! xmlcomplete#CompleteTags(findstart, base)
64  if a:findstart
65    " locate the start of the word
66	let curline = line('.')
67    let line = getline('.')
68    let start = col('.') - 1
69	let compl_begin = col('.') - 2
70
71    while start >= 0 && line[start - 1] =~ '\(\k\|[:.-]\)'
72		let start -= 1
73    endwhile
74
75	if start >= 0 && line[start - 1] =~ '&'
76		let b:entitiescompl = 1
77		let b:compl_context = ''
78		return start
79	endif
80
81	let b:compl_context = getline('.')[0:(compl_begin)]
82	if b:compl_context !~ '<[^>]*$'
83		" Look like we may have broken tag. Check previous lines. Up to
84		" 10?
85		let i = 1
86		while 1
87			let context_line = getline(curline-i)
88			if context_line =~ '<[^>]*$'
89				" Yep, this is this line
90				let context_lines = getline(curline-i, curline-1) + [b:compl_context]
91				let b:compl_context = join(context_lines, ' ')
92				break
93			elseif context_line =~ '>[^<]*$' || i == curline
94				" Normal tag line, no need for completion at all
95				" OR reached first line without tag at all
96				let b:compl_context = ''
97				break
98			endif
99			let i += 1
100		endwhile
101		" Make sure we don't have counter
102		unlet! i
103	endif
104	let b:compl_context = matchstr(b:compl_context, '.*\zs<.*')
105
106	" Make sure we will have only current namespace
107	unlet! b:xml_namespace
108	let b:xml_namespace = matchstr(b:compl_context, '^<\zs\k*\ze:')
109	if b:xml_namespace == ''
110		let b:xml_namespace = 'DEFAULT'
111	endif
112
113    return start
114
115  else
116	" Initialize base return lists
117    let res = []
118    let res2 = []
119	" a:base is very short - we need context
120	if len(b:compl_context) == 0  && !exists("b:entitiescompl")
121		return []
122	endif
123	let context = matchstr(b:compl_context, '^<\zs.*')
124	unlet! b:compl_context
125	" There is no connection of namespace and data file.
126	if !exists("g:xmldata_connection") || g:xmldata_connection == {}
127		" There is still possibility we may do something - eg. close tag
128		let b:unaryTagsStack = "base meta link hr br param img area input col"
129		if context =~ '^\/'
130			let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack")
131			return [opentag.">"]
132		else
133			return []
134		endif
135	endif
136
137	" Make entities completion
138	if exists("b:entitiescompl")
139		unlet! b:entitiescompl
140
141		if !exists("g:xmldata_entconnect") || g:xmldata_entconnect == 'DEFAULT'
142			let values =  g:xmldata{'_'.g:xmldata_connection['DEFAULT']}['vimxmlentities']
143		else
144			let values =  g:xmldata{'_'.g:xmldata_entconnect}['vimxmlentities']
145		endif
146
147		" Get only lines with entity declarations but throw out
148		" parameter-entities - they may be completed in future
149		let entdecl = filter(getline(1, "$"), 'v:val =~ "<!ENTITY\\s\\+[^%]"')
150
151		if len(entdecl) > 0
152			let intent = map(copy(entdecl), 'matchstr(v:val, "<!ENTITY\\s\\+\\zs\\(\\k\\|[.-:]\\)\\+\\ze")')
153			let values = intent + values
154		endif
155
156		if len(a:base) == 1
157			for m in values
158				if m =~ '^'.a:base
159					call add(res, m.';')
160				endif
161			endfor
162			return res
163		else
164			for m in values
165				if m =~? '^'.a:base
166					call add(res, m.';')
167				elseif m =~? a:base
168					call add(res2, m.';')
169				endif
170			endfor
171
172			return res + res2
173		endif
174
175	endif
176	if context =~ '>'
177		" Generally if context contains > it means we are outside of tag and
178		" should abandon action
179		return []
180	endif
181
182    " find tags matching with "a:base"
183	" If a:base contains white space it is attribute.
184	" It could be also value of attribute...
185	" We have to get first word to offer
186	" proper completions
187	if context == ''
188		let tag = ''
189	else
190		let tag = split(context)[0]
191	endif
192	" Get rid of namespace
193	let tag = substitute(tag, '^'.b:xml_namespace.':', '', '')
194
195
196	" Get last word, it should be attr name
197	let attr = matchstr(context, '.*\s\zs.*')
198	" Possible situations where any prediction would be difficult:
199	" 1. Events attributes
200	if context =~ '\s'
201
202		" If attr contains =\s*[\"'] we catch value of attribute
203		if attr =~ "=\s*[\"']" || attr =~ "=\s*$"
204			" Let do attribute specific completion
205			let attrname = matchstr(attr, '.*\ze\s*=')
206			let entered_value = matchstr(attr, ".*=\\s*[\"']\\?\\zs.*")
207
208			if tag =~ '^[?!]'
209				" Return nothing if we are inside of ! or ? tag
210				return []
211			else
212				if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, tag) && has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1], attrname)
213					let values = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][attrname]
214				else
215					return []
216				endif
217			endif
218
219			if len(values) == 0
220				return []
221			endif
222
223			" We need special version of sbase
224			let attrbase = matchstr(context, ".*[\"']")
225			let attrquote = matchstr(attrbase, '.$')
226			if attrquote !~ "['\"]"
227				let attrquoteopen = '"'
228				let attrquote = '"'
229			else
230				let attrquoteopen = ''
231			endif
232
233			for m in values
234				" This if is needed to not offer all completions as-is
235				" alphabetically but sort them. Those beginning with entered
236				" part will be as first choices
237				if m =~ '^'.entered_value
238					call add(res, attrquoteopen . m . attrquote.' ')
239				elseif m =~ entered_value
240					call add(res2, attrquoteopen . m . attrquote.' ')
241				endif
242			endfor
243
244			return res + res2
245
246		endif
247
248		if tag =~ '?xml'
249			" Two possible arguments for <?xml> plus variation
250			let attrs = ['encoding', 'version="1.0"', 'version']
251		elseif tag =~ '^!'
252			" Don't make completion at all
253			"
254			return []
255		else
256            if !has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, tag)
257				" Abandon when data file isn't complete
258 				return []
259 			endif
260			let attrs = keys(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1])
261		endif
262
263		for m in sort(attrs)
264			if m =~ '^'.attr
265				call add(res, m)
266			elseif m =~ attr
267				call add(res2, m)
268			endif
269		endfor
270		let menu = res + res2
271		let final_menu = []
272		if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, 'vimxmlattrinfo')
273			for i in range(len(menu))
274				let item = menu[i]
275				if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmlattrinfo'], item)
276					let m_menu = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmlattrinfo'][item][0]
277					let m_info = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmlattrinfo'][item][1]
278				else
279					let m_menu = ''
280					let m_info = ''
281				endif
282				if tag !~ '^[?!]' && len(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][item]) > 0 && g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][item][0] =~ '^\(BOOL\|'.item.'\)$'
283					let item = item
284				else
285					let item .= '="'
286				endif
287				let final_menu += [{'word':item, 'menu':m_menu, 'info':m_info}]
288			endfor
289		else
290			for i in range(len(menu))
291				let item = menu[i]
292				if tag !~ '^[?!]' && len(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][item]) > 0 && g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][item][0] =~ '^\(BOOL\|'.item.'\)$'
293					let item = item
294				else
295					let item .= '="'
296				endif
297				let final_menu += [item]
298			endfor
299		endif
300		return final_menu
301
302	endif
303	" Close tag
304	let b:unaryTagsStack = "base meta link hr br param img area input col"
305	if context =~ '^\/'
306		let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack")
307		return [opentag.">"]
308	endif
309
310	" Complete elements of XML structure
311	" TODO: #REQUIRED, #IMPLIED, #FIXED, #PCDATA - but these should be detected like
312	" entities - in first run
313	" keywords: CDATA, ID, IDREF, IDREFS, ENTITY, ENTITIES, NMTOKEN, NMTOKENS
314	" are hardly recognizable but keep it in reserve
315	" also: EMPTY ANY SYSTEM PUBLIC DATA
316	if context =~ '^!'
317		let tags = ['!ELEMENT', '!DOCTYPE', '!ATTLIST', '!ENTITY', '!NOTATION', '![CDATA[', '![INCLUDE[', '![IGNORE[']
318
319		for m in tags
320			if m =~ '^'.context
321				let m = substitute(m, '^!\[\?', '', '')
322				call add(res, m)
323			elseif m =~ context
324				let m = substitute(m, '^!\[\?', '', '')
325				call add(res2, m)
326			endif
327		endfor
328
329		return res + res2
330
331	endif
332
333	" Complete text declaration
334	if context =~ '^?'
335		let tags = ['?xml']
336
337		for m in tags
338			if m =~ '^'.context
339				call add(res, substitute(m, '^?', '', ''))
340			elseif m =~ context
341				call add(res, substitute(m, '^?', '', ''))
342			endif
343		endfor
344
345		return res + res2
346
347	endif
348
349	" Deal with tag completion.
350	let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack")
351	let opentag = substitute(opentag, '^\k*:', '', '')
352	if opentag == ''
353		"return []
354	    let tags = keys(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]})
355		call filter(tags, 'v:val !~ "^vimxml"')
356	else
357		if !has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, opentag)
358			" Abandon when data file isn't complete
359			return []
360		endif
361		let tags = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[opentag][0]
362	endif
363
364	let context = substitute(context, '^\k*:', '', '')
365
366	for m in tags
367		if m =~ '^'.context
368			call add(res, m)
369		elseif m =~ context
370			call add(res2, m)
371		endif
372	endfor
373	let menu = res + res2
374	if b:xml_namespace == 'DEFAULT'
375		let xml_namespace = ''
376	else
377		let xml_namespace = b:xml_namespace.':'
378	endif
379	if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, 'vimxmltaginfo')
380		let final_menu = []
381		for i in range(len(menu))
382			let item = menu[i]
383			if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmltaginfo'], item)
384				let m_menu = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmltaginfo'][item][0]
385				let m_info = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmltaginfo'][item][1]
386			else
387				let m_menu = ''
388				let m_info = ''
389			endif
390			let final_menu += [{'word':xml_namespace.item, 'menu':m_menu, 'info':m_info}]
391		endfor
392	else
393		let final_menu = map(menu, 'xml_namespace.v:val')
394	endif
395
396	return final_menu
397
398  endif
399endfunction
400
401" MM: This is severely reduced closetag.vim used with kind permission of Steven
402"     Mueller
403"     Changes: strip all comments; delete error messages; add checking for
404"     namespace
405" Author: Steven Mueller <diffusor@ugcs.caltech.edu>
406" Last Modified: Tue May 24 13:29:48 PDT 2005
407" Version: 0.9.1
408
409function! xmlcomplete#GetLastOpenTag(unaryTagsStack)
410	let linenum=line('.')
411	let lineend=col('.') - 1 " start: cursor position
412	let first=1              " flag for first line searched
413	let b:TagStack=''        " main stack of tags
414	let startInComment=s:InComment()
415
416	if exists("b:xml_namespace")
417		if b:xml_namespace == 'DEFAULT'
418			let tagpat='</\=\(\k\|[.:-]\)\+\|/>'
419		else
420			let tagpat='</\='.b:xml_namespace.':\(\k\|[.-]\)\+\|/>'
421		endif
422	else
423		let tagpat='</\=\(\k\|[.:-]\)\+\|/>'
424	endif
425	while (linenum>0)
426		let line=getline(linenum)
427		if first
428			let line=strpart(line,0,lineend)
429		else
430			let lineend=strlen(line)
431		endif
432		let b:lineTagStack=''
433		let mpos=0
434		let b:TagCol=0
435		while (mpos > -1)
436			let mpos=matchend(line,tagpat)
437			if mpos > -1
438				let b:TagCol=b:TagCol+mpos
439				let tag=matchstr(line,tagpat)
440
441				if exists('b:closetag_disable_synID') || startInComment==s:InCommentAt(linenum, b:TagCol)
442					let b:TagLine=linenum
443					call s:Push(matchstr(tag,'[^<>]\+'),'b:lineTagStack')
444				endif
445				let lineend=lineend-mpos
446				let line=strpart(line,mpos,lineend)
447			endif
448		endwhile
449		while (!s:EmptystackP('b:lineTagStack'))
450			let tag=s:Pop('b:lineTagStack')
451			if match(tag, '^/') == 0		"found end tag
452				call s:Push(tag,'b:TagStack')
453			elseif s:EmptystackP('b:TagStack') && !s:Instack(tag, a:unaryTagsStack)	"found unclosed tag
454				return tag
455			else
456				let endtag=s:Peekstack('b:TagStack')
457				if endtag == '/'.tag || endtag == '/'
458					call s:Pop('b:TagStack')	"found a open/close tag pair
459				elseif !s:Instack(tag, a:unaryTagsStack) "we have a mismatch error
460					return ''
461				endif
462			endif
463		endwhile
464		let linenum=linenum-1 | let first=0
465	endwhile
466return ''
467endfunction
468
469function! s:InComment()
470	return synIDattr(synID(line('.'), col('.'), 0), 'name') =~ 'Comment\|String'
471endfunction
472
473function! s:InCommentAt(line, col)
474	return synIDattr(synID(a:line, a:col, 0), 'name') =~ 'Comment\|String'
475endfunction
476
477function! s:SetKeywords()
478	let s:IsKeywordBak=&l:iskeyword
479	let &l:iskeyword='33-255'
480endfunction
481
482function! s:RestoreKeywords()
483	let &l:iskeyword=s:IsKeywordBak
484endfunction
485
486function! s:Push(el, sname)
487	if !s:EmptystackP(a:sname)
488		exe 'let '.a:sname."=a:el.' '.".a:sname
489	else
490		exe 'let '.a:sname.'=a:el'
491	endif
492endfunction
493
494function! s:EmptystackP(sname)
495	exe 'let stack='.a:sname
496	if match(stack,'^ *$') == 0
497		return 1
498	else
499		return 0
500	endif
501endfunction
502
503function! s:Instack(el, sname)
504	exe 'let stack='.a:sname
505	call s:SetKeywords()
506	let m=match(stack, '\<'.a:el.'\>')
507	call s:RestoreKeywords()
508	if m < 0
509		return 0
510	else
511		return 1
512	endif
513endfunction
514
515function! s:Peekstack(sname)
516	call s:SetKeywords()
517	exe 'let stack='.a:sname
518	let top=matchstr(stack, '\<.\{-1,}\>')
519	call s:RestoreKeywords()
520	return top
521endfunction
522
523function! s:Pop(sname)
524	if s:EmptystackP(a:sname)
525		return ''
526	endif
527	exe 'let stack='.a:sname
528	call s:SetKeywords()
529	let loc=matchend(stack,'\<.\{-1,}\>')
530	exe 'let '.a:sname.'=strpart(stack, loc+1, strlen(stack))'
531	let top=strpart(stack, match(stack, '\<'), loc)
532	call s:RestoreKeywords()
533	return top
534endfunction
535
536function! s:Clearstack(sname)
537	exe 'let '.a:sname."=''"
538endfunction
539" vim:set foldmethod=marker:
540