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