1" Vim completion script
2" Language:	XML
3" Maintainer:	Mikolaj Machowski ( mikmach AT wp DOT pl )
4" Last Change:	2006 Feb 18
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	    let tags = keys(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]})
324		call filter(tags, 'v:val !~ "^vimxml"')
325	else
326		let tags = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[opentag][0]
327	endif
328
329	let context = substitute(context, '^\k*:', '', '')
330
331	for m in tags
332		if m =~ '^'.context
333			call add(res, m)
334		elseif m =~ context
335			call add(res2, m)
336		endif
337	endfor
338	let menu = res + res2
339	if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}, 'vimxmltaginfo')
340		let final_menu = []
341		for i in range(len(menu))
342			let item = menu[i]
343			if has_key(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmltaginfo'], item)
344				let m_menu = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmltaginfo'][item][0]
345				let m_info = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}['vimxmltaginfo'][item][1]
346			else
347				let m_menu = ''
348				let m_info = ''
349			endif
350			if b:xml_namespace == 'DEFAULT'
351				let xml_namespace = ''
352			else
353				let xml_namespace = b:xml_namespace.':'
354			endif
355			let final_menu += [{'word':xml_namespace.item, 'menu':m_menu, 'info':m_info}]
356		endfor
357	else
358		let final_menu = menu
359	endif
360	return final_menu
361
362  endif
363endfunction
364
365" MM: This is greatly reduced closetag.vim used with kind permission of Steven
366"     Mueller
367"     Changes: strip all comments; delete error messages; add checking for
368"     namespace
369" Author: Steven Mueller <[email protected]>
370" Last Modified: Tue May 24 13:29:48 PDT 2005
371" Version: 0.9.1
372
373function! xmlcomplete#GetLastOpenTag(unaryTagsStack)
374	let linenum=line('.')
375	let lineend=col('.') - 1 " start: cursor position
376	let first=1              " flag for first line searched
377	let b:TagStack=''        " main stack of tags
378	let startInComment=s:InComment()
379
380	if exists("b:xml_namespace")
381		if b:xml_namespace == 'DEFAULT'
382			let tagpat='</\=\(\k\|[.-]\)\+\|/>'
383		else
384			let tagpat='</\='.b:xml_namespace.':\(\k\|[.-]\)\+\|/>'
385		endif
386	else
387		let tagpat='</\=\(\k\|[.-]\)\+\|/>'
388	endif
389	while (linenum>0)
390		let line=getline(linenum)
391		if first
392			let line=strpart(line,0,lineend)
393		else
394			let lineend=strlen(line)
395		endif
396		let b:lineTagStack=''
397		let mpos=0
398		let b:TagCol=0
399		while (mpos > -1)
400			let mpos=matchend(line,tagpat)
401			if mpos > -1
402				let b:TagCol=b:TagCol+mpos
403				let tag=matchstr(line,tagpat)
404
405				if exists('b:closetag_disable_synID') || startInComment==s:InCommentAt(linenum, b:TagCol)
406					let b:TagLine=linenum
407					call s:Push(matchstr(tag,'[^<>]\+'),'b:lineTagStack')
408				endif
409				let lineend=lineend-mpos
410				let line=strpart(line,mpos,lineend)
411			endif
412		endwhile
413		while (!s:EmptystackP('b:lineTagStack'))
414			let tag=s:Pop('b:lineTagStack')
415			if match(tag, '^/') == 0		"found end tag
416				call s:Push(tag,'b:TagStack')
417			elseif s:EmptystackP('b:TagStack') && !s:Instack(tag, a:unaryTagsStack)	"found unclosed tag
418				return tag
419			else
420				let endtag=s:Peekstack('b:TagStack')
421				if endtag == '/'.tag || endtag == '/'
422					call s:Pop('b:TagStack')	"found a open/close tag pair
423				elseif !s:Instack(tag, a:unaryTagsStack) "we have a mismatch error
424					return ''
425				endif
426			endif
427		endwhile
428		let linenum=linenum-1 | let first=0
429	endwhile
430return ''
431endfunction
432
433function! s:InComment()
434	return synIDattr(synID(line('.'), col('.'), 0), 'name') =~ 'Comment\|String'
435endfunction
436
437function! s:InCommentAt(line, col)
438	return synIDattr(synID(a:line, a:col, 0), 'name') =~ 'Comment\|String'
439endfunction
440
441function! s:SetKeywords()
442	let g:IsKeywordBak=&iskeyword
443	let &iskeyword='33-255'
444endfunction
445
446function! s:RestoreKeywords()
447	let &iskeyword=g:IsKeywordBak
448endfunction
449
450function! s:Push(el, sname)
451	if !s:EmptystackP(a:sname)
452		exe 'let '.a:sname."=a:el.' '.".a:sname
453	else
454		exe 'let '.a:sname.'=a:el'
455	endif
456endfunction
457
458function! s:EmptystackP(sname)
459	exe 'let stack='.a:sname
460	if match(stack,'^ *$') == 0
461		return 1
462	else
463		return 0
464	endif
465endfunction
466
467function! s:Instack(el, sname)
468	exe 'let stack='.a:sname
469	call s:SetKeywords()
470	let m=match(stack, '\<'.a:el.'\>')
471	call s:RestoreKeywords()
472	if m < 0
473		return 0
474	else
475		return 1
476	endif
477endfunction
478
479function! s:Peekstack(sname)
480	call s:SetKeywords()
481	exe 'let stack='.a:sname
482	let top=matchstr(stack, '\<.\{-1,}\>')
483	call s:RestoreKeywords()
484	return top
485endfunction
486
487function! s:Pop(sname)
488	if s:EmptystackP(a:sname)
489		return ''
490	endif
491	exe 'let stack='.a:sname
492	call s:SetKeywords()
493	let loc=matchend(stack,'\<.\{-1,}\>')
494	exe 'let '.a:sname.'=strpart(stack, loc+1, strlen(stack))'
495	let top=strpart(stack, match(stack, '\<'), loc)
496	call s:RestoreKeywords()
497	return top
498endfunction
499
500function! s:Clearstack(sname)
501	exe 'let '.a:sname."=''"
502endfunction
503