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