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