PageRenderTime 84ms CodeModel.GetById 61ms app.highlight 17ms RepoModel.GetById 0ms app.codeStats 1ms

/bundles/plugins-trunk/XML/xml/parser/XmlParser.java

#
Java | 462 lines | 352 code | 18 blank | 92 comment | 26 complexity | 46beaae25736da7775a3145c8956e1d9 MD5 | raw file
  1/*
  2 * XmlParser.java
  3 * :tabSize=8:indentSize=8:noTabs=false:
  4 * :folding=explicit:collapseFolds=1:
  5 *
  6 * Copyright (C) 2000, 2003 Slava Pestov
  7 * Portions copyright (C) 2001 David Walend
  8 * Copyright (C) 2009 Greg Knittl
  9 *
 10 * The XML plugin is licensed under the GNU General Public License, with
 11 * the following exception:
 12 *
 13 * "Permission is granted to link this code with software released under
 14 * the Apache license version 1.1, for example used by the Xerces XML
 15 * parser package."
 16 */
 17
 18package xml.parser;
 19
 20import java.util.*;
 21import org.xml.sax.helpers.NamespaceSupport;
 22import org.gjt.sp.jedit.*;
 23import org.gjt.sp.jedit.syntax.*;
 24import org.gjt.sp.jedit.textarea.StructureMatcher;
 25import sidekick.*;
 26import xml.completion.*;
 27import xml.completion.ElementDecl.AttributeDecl;
 28import xml.*;
 29import xml.XmlListCellRenderer.WithLabel;
 30
 31/**
 32 * This is the common base class for both HTML and XML Parsers.
 33 * It contains auto completion for closing element tags. 
 34 */
 35public abstract class XmlParser extends SideKickParser
 36{
 37	public static final String INSTANT_COMPLETION_TRIGGERS = "/";
 38	public static final int ELEMENT_COMPLETE = '<';
 39	public static final int ENTITY_COMPLETE = '&';
 40	public static final int ATTRIB_COMPLETE = ' ';
 41
 42	//{{{ XmlParser constructor
 43	public XmlParser(String name)
 44	{
 45		super(name);
 46		String matcherName = jEdit.getProperty("xml.structure-matcher","sidekick");
 47		if("old".equals(matcherName)) {
 48			highlight = new TagHighlight();
 49		} else {
 50			highlight = new SideKickTagHighlight();
 51		}
 52		htmlHighlight = new TagHighlight();
 53	} //}}}
 54
 55	//{{{ stop() method
 56	/**
 57	 * Stops the parse request currently in progress. It is up to the
 58	 * parser to implement this.
 59	 * @since SideKick 0.3
 60	 */
 61	public void stop()
 62	{
 63		stopped = true;
 64	} //}}}
 65
 66	//{{{ activate() method
 67	public void activate(EditPane editPane)
 68	{
 69		super.activate(editPane);
 70		if(jEdit.getBooleanProperty("xml.tag-highlight"))
 71		{
 72			StructureMatcher h;
 73			// revert to classic TagHighlight for HTML modes
 74			if(editPane.getBuffer().getMode().getName().equals("html")
 75					|| editPane.getBuffer().getMode().getName().equals("jsp"))
 76			{
 77				h = htmlHighlight;
 78			}
 79			else
 80			{
 81				h = highlight;
 82			}
 83			editPane.getTextArea().addStructureMatcher(h);
 84		}
 85	} //}}}
 86
 87	//{{{ deactivate() method
 88	public void deactivate(EditPane editPane)
 89	{
 90		// don't bother to remember which one it was... 
 91		editPane.getTextArea().removeStructureMatcher(highlight);
 92		editPane.getTextArea().removeStructureMatcher(htmlHighlight);
 93	} //}}}
 94
 95	//{{{ supportsCompletion() method
 96	public boolean supportsCompletion()
 97	{
 98		return true;
 99	} //}}}
100
101	//{{{ getInstantCompletionTriggers() method
102	public String getInstantCompletionTriggers()
103	{
104		return INSTANT_COMPLETION_TRIGGERS;
105	} //}}}
106
107	//{{{ complete() method
108	public SideKickCompletion complete(EditPane editPane, int caret)
109	// The challenge of realtime keystroke completion for xml is that
110	// the syntax may not be well formed at any given keystroke.
111	// It would be ideal to start from an incremental xml parser/validator.
112	// A Google search shows the nxml mode for EMACS is probably the
113	// most advanced implementation of this to date (I haven't tried it).
114	// complete() could check if SideKick parse on keystroke is
115	// enabled and use that xml parse tree if available.
116	//
117	// jEdit parses syntax per keystroke and just as it usually looks reasonable
118	// on the screen so it is usually a reasonable basis for suggesting completions
119	//
120	// This patch uses syntax information to improve the validity of
121	// completion popups in a number of ways, including reducing
122	// attribute completion popups in text areas.
123	// This relies on XML text areas being unparsed, Token.NULL.
124	// This true for the two xml modes I'm aware of: xml and xsl
125	//
126	// If a new xml-derived edit mode say, xhtml/css, parsed the css syntax
127	// and then used this complete method, attribute popups would appear in
128	// it's css text areas
129	// This code could add additional logic to handle such modes
130	// Better would be to have nested levels of syntax parsing
131	// so that this code could handle xml level completion for all
132	// xml derived syntaxes. Or process syntax in parallel to jEdit with a
133	// custom rules set
134	//
135	// jEdit syntax Token.END represents the newline character(s) at the end of each
136	// line. It is a visual marker that does not correspond to XML syntax.
137	// XML allows newlines in at least text areas, attribute values, comments, CDATA.
138	// I have treated Token.END as whitespace and attribute completion will pop up
139	// incorrectly in some circumstances at the start of lines after the first line,
140	// such as when a text area spans multiple lines
141	//
142	// Additional fixes:
143	// enable attribute completion after attribute values that contain /
144	// enable attribute completion for elements that span multiple lines
145	//
146	// For testing purposes, manually invoking completion from
147	// Plugins > SideKick > Show Completion Popup seems to display
148	// more exceptions in the code than keystroke activation
149	//
150	// Greg Knittl 2009-06-19
151	{
152		// caret can be at 0 when invoking completion from SideKick plugin menu
153		// or through backspace
154		// could pop up a pro forma xml declaration for caret = 0
155		if (caret == 0)
156			return null;
157		XmlParsedData data = XmlParsedData.getParsedData(editPane.getView(), false);
158		if(data==null)return null;
159
160		if(XmlPlugin.isDelegated(editPane.getTextArea()))
161			return null;
162
163		Buffer buffer = editPane.getBuffer();
164		
165		int lastchar = caret - 1;
166		int lastcharLine = buffer.getLineOfOffset(lastchar);
167		String text = buffer.getText(0,caret);
168		int lineStart = buffer.getLineStartOffset(lastcharLine);
169		
170		// get syntax tokens for the line of character before the caret
171		// ghk not sure if this duplicates jEdits syntax tokenization
172		// since this is per keystroke, performance is of some importance
173		DefaultTokenHandler tokenHandler = new DefaultTokenHandler();
174		buffer.markTokens(lastcharLine,tokenHandler);
175		Token token = tokenHandler.getTokens();
176
177		while(token.id != Token.END)
178		{
179			int next = lineStart + token.length;
180			if (lineStart <= lastchar && next > lastchar)
181				break;
182			lineStart = next;
183			token = token.next;
184		}
185		
186		// could test for comments and return at this point
187		// continuing allows some completion within comments
188		// for example when comments contain lines of valid xml
189		
190		String modename = buffer.getMode().getName();
191		
192		int mode = -1;
193		boolean insideQuote = false;
194		int wordStart = -1;
195		int attribStart = -1;
196		// iterate backwards towards start of file to find a tag
197		// or the & indicating the start of an entity
198		// i >= 1 enables attribute completion for elements spanning multiple lines
199		for(int i = lastchar; i >= 1; i--)
200		{
201			char ch = text.charAt(i);
202			if(ch == '<')
203			{
204				wordStart = i;
205				if (mode == -1) 
206					mode = ELEMENT_COMPLETE;
207				break;
208			}
209			if(ch == '&')
210			{
211				// & and ATTRIBUTE_COMPLETE is invalid because it implies whitespace in the entity
212				// can occur with attributes enclosed by misinterpreted 's that contain an entity followed by a space
213				if (mode == ATTRIB_COMPLETE) 
214					return null;
215				wordStart = i;
216				if (mode == -1) 
217					mode = ENTITY_COMPLETE;
218				break;
219			}
220			// " in text area or ' delimiting attribute values break this logic
221			// xslt often uses multiple levels of quotes for XPath
222			else if (ch == '"') 
223			{
224				insideQuote = !insideQuote;
225			}
226			// whitespace is not allowed in element tag names or entities;
227			// in xml mode attributes can only occur in Token.MARKUP or Token.END (or Token.OPERATOR when just typing the colon)
228			// this solves the problem of attributes defined with ' for xml mode
229			// xsl mode parses the markup more finely so the logic gets more complex but probably could be done
230			else if (Character.isWhitespace(ch) && !(token.id == Token.MARKUP || token.id == Token.END || (token.id == Token.OPERATOR && text.charAt(lastchar) == ':')) && modename.equals("xml")) {
231			  	return null;
232			}
233			// whitespace is not allowed in element tags or entities;
234			// no attributes allowed in text area (Token.NULL) or comments (Token.COMMENT1) so exit
235			else if (Character.isWhitespace(ch) && (token.id == Token.NULL || token.id == Token.COMMENT1)) {
236			  	return null;
237			}
238			// no break to allow loop to iterate back to find next < or &
239			// add test for not Token.NULL (text area)
240			else if (Character.isWhitespace(ch) && token.id != Token.NULL && !insideQuote && mode == -1) {
241				attribStart = i+1;
242				mode = ATTRIB_COMPLETE;
243			}
244		}
245		if (insideQuote) mode=-1;
246		String closingTag = null;
247		String word;
248
249		List allowedCompletions = new ArrayList(20);
250		Map<String, String> namespaces = data.getNamespaceBindings(caret);
251		Map<String, String> namespacesToInsert = new HashMap<String, String>();
252		Map<String, String> localNamespacesToInsert = new HashMap<String, String>();
253		
254		
255		if(wordStart != -1 && mode != -1)
256		{
257			String tolastchar = text.substring(wordStart + 1, caret);
258			// avoid ArrayIndexOutOfBoundsException for < or & followed by one or more spaces
259			if (tolastchar.trim().length() == 0)
260				word = "";
261			else
262			{
263				String firstSpace = tolastchar.split("\\s")[0];
264				if (firstSpace.length() > 0)
265					word = firstSpace;
266				else
267					word = text.substring(wordStart + 1, caret);
268			}
269
270			if(mode == ELEMENT_COMPLETE)
271			{
272				String wordWithoutPrefix = XmlParsedData.getElementLocalName(word);
273				String wordPrefix = XmlParsedData.getElementNamePrefix(word);
274				List<ElementDecl> completions = data.getAllowedElements(buffer, lastchar);
275				TagParser.Tag tag = TagParser.findLastOpenTag(text,lastchar - 1,data);
276				if(tag != null)
277					closingTag = tag.tag;
278
279				if("!--".startsWith(word))
280					allowedCompletions.add(new WithLabel(new XmlListCellRenderer.Comment()));
281				if(!data.html && "![CDATA[".startsWith(word))
282					allowedCompletions.add(new WithLabel(new XmlListCellRenderer.CDATA()));
283				if(closingTag != null && ("/" + closingTag).startsWith(word))
284				{
285					if(word.length() == 0 || !jEdit.getBooleanProperty("xml.close-complete"))
286						allowedCompletions.add(new WithLabel(new XmlListCellRenderer.ClosingTag(closingTag)));
287					else
288					{
289						// just insert immediately
290						XmlActions.completeClosingTag(
291							editPane.getView(),
292							false);
293						return null;
294					}
295				}
296
297				for(int i = 0; i < completions.size(); i++)
298				{
299					ElementDecl elementDecl = completions.get(i);
300					String elementName;
301					String elementNamespace = elementDecl.completionInfo.namespace;
302					// elementDecl.name is the local name, now we must find a qualified name
303					if(elementNamespace == null || "".equals(elementNamespace))
304					{
305						elementName = elementDecl.name;
306					}
307					else
308					{
309						String pre = namespaces.get(elementNamespace);
310						if(pre == null)
311						{
312							pre = localNamespacesToInsert.get(elementNamespace);
313						}
314						if(pre == null)
315						{
316							// handle using unknown prefix
317							// if users types "<mathml:" and mathml ns is undeclared use mathml:... as prefix 
318							if(!"".equals(wordPrefix)
319									&& elementDecl.name.startsWith(wordWithoutPrefix))
320							{
321								pre = wordPrefix;
322								namespacesToInsert.put(elementNamespace, pre);
323								localNamespacesToInsert.put(elementNamespace, pre);
324								elementName = pre + ":" + elementDecl.name;
325								
326							}
327							else
328							{
329								// handle elements in undeclared namespace and no prefix.
330								// Generate a new prefix.
331								// Store it locally, so that the declaration is not inserted when this completion is not chosen.
332								// If it's chosen, a prefix (maybe different) will be generated
333								pre = EditTagDialog.generatePrefix(namespaces, localNamespacesToInsert);
334								localNamespacesToInsert.put(elementNamespace,pre);
335								elementName = pre + ":" + elementDecl.name;
336							}
337						}
338						else
339						{
340							if("".equals(pre)){
341								elementName = elementDecl.name;
342							}else{
343								elementName = pre + ":" + elementDecl.name;
344							}
345						}
346					}
347					
348					if(elementName.startsWith(word)
349							|| (data.html && elementName.toLowerCase()
350							.startsWith(word.toLowerCase())))
351					{
352						allowedCompletions.add(new XmlListCellRenderer.WithLabel<ElementDecl>(elementName,elementDecl));
353					}
354				}
355			}
356			else if (mode == ENTITY_COMPLETE)
357			{
358				List<EntityDecl> completions = data.entities;
359				for(int i = 0; i < completions.size(); i++)
360				{
361					EntityDecl entity = completions.get(i);
362					if(entity.name.startsWith(word))
363					{
364						allowedCompletions.add(new WithLabel(entity));
365					}
366				}
367			}
368			else if (mode == ATTRIB_COMPLETE) 
369			{
370				String prefix = text.substring(attribStart, caret);
371				String wordWithoutPrefix = XmlParsedData.getElementLocalName(prefix);
372				String wordPrefix = XmlParsedData.getElementNamePrefix(prefix);
373				ElementDecl decl = data.getElementDecl(word,caret);
374				List<AttributeDecl> completions;
375				if (decl != null)
376				{
377					completions = decl.attributes;
378					for (int i=0; i<completions.size(); ++i) 
379					{
380						AttributeDecl attrDecl = completions.get(i);
381						String attrName;
382						if(attrDecl.namespace == null || "".equals(attrDecl.namespace))
383						{
384							attrName = attrDecl.name;
385						}
386						else
387						{
388							String pre = namespaces.get(attrDecl.namespace);
389							if(pre == null)
390							{
391								if(attrDecl.namespace.equals(NamespaceSupport.XMLNS))
392								{
393									attrName = "xml:"+attrDecl.name;
394								}
395								else
396								{
397									attrName = attrDecl.name;
398									// handle using unknown prefix
399									// if users types "<mathml:" and mathml ns is undeclared use mathml:... as prefix 
400									if(!"".equals(wordPrefix) && !"xml".equals(wordPrefix)
401											&& attrName.startsWith(wordWithoutPrefix))
402									{
403										pre = wordPrefix;
404										namespacesToInsert.put(attrDecl.namespace, pre);
405										attrName = pre + ":" + attrDecl.name;
406									}
407									else
408									{
409										// handle attribute in undeclared namespace and no prefix.
410										// Generate a new prefix.
411										// Store it locally, so that the declaration is not inserted when this completion is not chosen.
412										// If it's chosen, a prefix (maybe different) will be generated again
413										pre = EditTagDialog.generatePrefix(namespaces, localNamespacesToInsert);
414										localNamespacesToInsert.put(attrDecl.namespace,pre);
415										attrName = pre + ":" + attrDecl.name;
416										// this can get cumbersome, if one types 'a' expecting to get 'someprefix:attribute' because
417										// attribute will not be proposed. On the other hand if we don't put the prefix, one cannot distinguish between
418										// ns1:attr and ns2:attr...
419									}
420								}
421							}
422							else
423							{
424								attrName = pre + ":" + attrDecl.name;
425							}
426						}
427						if (attrName.startsWith(prefix))
428						{
429							allowedCompletions.add(new XmlListCellRenderer.WithLabel<AttributeDecl>(attrName,attrDecl));
430						}
431					}
432				}
433				word = prefix;
434			}
435			/* else if(mode == ID_COMPLETE)
436			{
437				else if(obj instanceof IDDecl)
438				{
439					IDDecl id = (IDDecl)obj;
440					if(id.id.startsWith(word))
441						allowedCompletions.add(id);
442				}
443			} */
444		}
445		else
446			word = "";
447
448		if(word.endsWith("/") && allowedCompletions.size() == 0)
449			return null;
450		else
451			return new XmlCompletion(editPane.getView(),allowedCompletions, namespaces, namespacesToInsert, word,data,closingTag);
452	} //}}}
453
454	//{{{ Package-private members
455	boolean stopped;
456	//}}}
457
458	//{{{ Private members
459	private StructureMatcher highlight;
460	private StructureMatcher htmlHighlight;
461	//}}}
462}