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