PageRenderTime 41ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

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

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