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				if tag !~ '^[?!]' && len(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][m]) > 0 && g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][m][0] =~ '^BOOL$'
234					call add(res, m)
235				elseif m =~ '='
236					call add(res, m)
237				else
238					call add(res, m.'="')
239				endif
240			elseif m =~ attr
241				if tag !~ '^[?!]' && len(g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][m]) > 0 && g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[tag][1][m][0] =~ '^BOOL$'
242					call add(res, m)
243				elseif m =~ '='
244					call add(res, m)
245				else
246					call add(res2, m.'="')
247				endif
248			endif
249		endfor
250
251		return res + res2
252
253	endif
254	" Close tag
255	let b:unaryTagsStack = "base meta link hr br param img area input col"
256	if context =~ '^\/'
257		let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack")
258		return [opentag.">"]
259	endif
260
261	" Complete elements of XML structure
262	" TODO: #REQUIRED, #IMPLIED, #FIXED, #PCDATA - but these should be detected like
263	" entities - in first run
264	" keywords: CDATA, ID, IDREF, IDREFS, ENTITY, ENTITIES, NMTOKEN, NMTOKENS
265	" are hardly recognizable but keep it in reserve
266	" also: EMPTY ANY SYSTEM PUBLIC DATA
267	if context =~ '^!'
268		let tags = ['!ELEMENT', '!DOCTYPE', '!ATTLIST', '!ENTITY', '!NOTATION', '![CDATA[', '![INCLUDE[', '![IGNORE[']
269
270		for m in tags
271			if m =~ '^'.context
272				let m = substitute(m, '^!\[\?', '', '')
273				call add(res, m)
274			elseif m =~ context
275				let m = substitute(m, '^!\[\?', '', '')
276				call add(res2, m)
277			endif
278		endfor
279
280		return res + res2
281
282	endif
283
284	" Complete text declaration
285	let g:co = context
286	if context =~ '^?'
287		let tags = ['?xml']
288
289		for m in tags
290			if m =~ '^'.context
291				call add(res, substitute(m, '^?', '', ''))
292			elseif m =~ context
293				call add(res, substitute(m, '^?', '', ''))
294			endif
295		endfor
296
297		return res + res2
298
299	endif
300
301	" Deal with tag completion.
302	let opentag = xmlcomplete#GetLastOpenTag("b:unaryTagsStack")
303	let opentag = substitute(opentag, '^\k*:', '', '')
304	if opentag == ''
305		return []
306	endif
307
308	let tags = g:xmldata{'_'.g:xmldata_connection[b:xml_namespace]}[opentag][0]
309	let context = substitute(context, '^\k*:', '', '')
310
311	if b:xml_namespace == 'DEFAULT'
312		let b:xml_namespace = ''
313	else
314		let b:xml_namespace .= ':'
315	endif
316
317	for m in tags
318		if m =~ '^'.context
319			call add(res, b:xml_namespace.m)
320		elseif m =~ context
321			call add(res2, b:xml_namespace.m)
322		endif
323	endfor
324
325	return res + res2
326
327  endif
328endfunction
329
330" MM: This is greatly reduced closetag.vim used with kind permission of Steven
331"     Mueller
332"     Changes: strip all comments; delete error messages; add checking for
333"     namespace
334" Author: Steven Mueller <[email protected]>
335" Last Modified: Tue May 24 13:29:48 PDT 2005
336" Version: 0.9.1
337
338function! xmlcomplete#GetLastOpenTag(unaryTagsStack)
339	let linenum=line('.')
340	let lineend=col('.') - 1 " start: cursor position
341	let first=1              " flag for first line searched
342	let b:TagStack=''        " main stack of tags
343	let startInComment=s:InComment()
344
345	if exists("b:xml_namespace")
346		if b:xml_namespace == 'DEFAULT'
347			let tagpat='</\=\(\k\|[.-]\)\+\|/>'
348		else
349			let tagpat='</\='.b:xml_namespace.':\(\k\|[.-]\)\+\|/>'
350		endif
351	else
352		let tagpat='</\=\(\k\|[.-]\)\+\|/>'
353	endif
354	while (linenum>0)
355		let line=getline(linenum)
356		if first
357			let line=strpart(line,0,lineend)
358		else
359			let lineend=strlen(line)
360		endif
361		let b:lineTagStack=''
362		let mpos=0
363		let b:TagCol=0
364		while (mpos > -1)
365			let mpos=matchend(line,tagpat)
366			if mpos > -1
367				let b:TagCol=b:TagCol+mpos
368				let tag=matchstr(line,tagpat)
369
370				if exists('b:closetag_disable_synID') || startInComment==s:InCommentAt(linenum, b:TagCol)
371					let b:TagLine=linenum
372					call s:Push(matchstr(tag,'[^<>]\+'),'b:lineTagStack')
373				endif
374				let lineend=lineend-mpos
375				let line=strpart(line,mpos,lineend)
376			endif
377		endwhile
378		while (!s:EmptystackP('b:lineTagStack'))
379			let tag=s:Pop('b:lineTagStack')
380			if match(tag, '^/') == 0		"found end tag
381				call s:Push(tag,'b:TagStack')
382			elseif s:EmptystackP('b:TagStack') && !s:Instack(tag, a:unaryTagsStack)	"found unclosed tag
383				return tag
384			else
385				let endtag=s:Peekstack('b:TagStack')
386				if endtag == '/'.tag || endtag == '/'
387					call s:Pop('b:TagStack')	"found a open/close tag pair
388				elseif !s:Instack(tag, a:unaryTagsStack) "we have a mismatch error
389					return ''
390				endif
391			endif
392		endwhile
393		let linenum=linenum-1 | let first=0
394	endwhile
395return ''
396endfunction
397
398function! s:InComment()
399	return synIDattr(synID(line('.'), col('.'), 0), 'name') =~ 'Comment\|String'
400endfunction
401
402function! s:InCommentAt(line, col)
403	return synIDattr(synID(a:line, a:col, 0), 'name') =~ 'Comment\|String'
404endfunction
405
406function! s:SetKeywords()
407	let g:IsKeywordBak=&iskeyword
408	let &iskeyword='33-255'
409endfunction
410
411function! s:RestoreKeywords()
412	let &iskeyword=g:IsKeywordBak
413endfunction
414
415function! s:Push(el, sname)
416	if !s:EmptystackP(a:sname)
417		exe 'let '.a:sname."=a:el.' '.".a:sname
418	else
419		exe 'let '.a:sname.'=a:el'
420	endif
421endfunction
422
423function! s:EmptystackP(sname)
424	exe 'let stack='.a:sname
425	if match(stack,'^ *$') == 0
426		return 1
427	else
428		return 0
429	endif
430endfunction
431
432function! s:Instack(el, sname)
433	exe 'let stack='.a:sname
434	call s:SetKeywords()
435	let m=match(stack, '\<'.a:el.'\>')
436	call s:RestoreKeywords()
437	if m < 0
438		return 0
439	else
440		return 1
441	endif
442endfunction
443
444function! s:Peekstack(sname)
445	call s:SetKeywords()
446	exe 'let stack='.a:sname
447	let top=matchstr(stack, '\<.\{-1,}\>')
448	call s:RestoreKeywords()
449	return top
450endfunction
451
452function! s:Pop(sname)
453	if s:EmptystackP(a:sname)
454		return ''
455	endif
456	exe 'let stack='.a:sname
457	call s:SetKeywords()
458	let loc=matchend(stack,'\<.\{-1,}\>')
459	exe 'let '.a:sname.'=strpart(stack, loc+1, strlen(stack))'
460	let top=strpart(stack, match(stack, '\<'), loc)
461	call s:RestoreKeywords()
462	return top
463endfunction
464
465function! s:Clearstack(sname)
466	exe 'let '.a:sname."=''"
467endfunction
468