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