PageRenderTime 42ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/bundles/plugins-trunk/XML/xml/hyperlinks/HTMLHyperlinkSource.java

#
Java | 563 lines | 389 code | 47 blank | 127 comment | 80 complexity | fb9d2b6c2952fc1d1c69d4f33345e9e4 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. * HTMLHyperlinkSource.java - Hyperlink source from the XML plugin
  3. * :tabSize=8:indentSize=8:noTabs=false:
  4. * :folding=explicit:collapseFolds=1:
  5. *
  6. * Copyright (C) 2011 Eric Le Lay
  7. *
  8. * This program is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU General Public License
  10. * as published by the Free Software Foundation; either version 2
  11. * of the License, or any later version.
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program; if not, write to the Free Software
  19. * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  20. */
  21. package xml.hyperlinks;
  22. import java.net.URI;
  23. import java.util.regex.Pattern;
  24. import java.util.regex.Matcher;
  25. import java.util.Map;
  26. import java.util.HashMap;
  27. import java.util.Set;
  28. import java.util.HashSet;
  29. import java.util.Iterator;
  30. import java.util.Arrays;
  31. import javax.swing.tree.DefaultMutableTreeNode;
  32. import org.gjt.sp.jedit.View;
  33. import org.gjt.sp.jedit.jEdit;
  34. import org.gjt.sp.jedit.Buffer;
  35. import org.gjt.sp.util.Log;
  36. import sidekick.SideKickParsedData;
  37. import sidekick.IAsset;
  38. import sidekick.util.ElementUtil;
  39. import sidekick.util.Location;
  40. import sidekick.util.SideKickAsset;
  41. import sidekick.util.SideKickElement;
  42. import gatchan.jedit.hyperlinks.*;
  43. import static xml.Debug.*;
  44. import xml.Resolver;
  45. import xml.XmlParsedData;
  46. import sidekick.html.parser.html.*;
  47. import static sidekick.html.parser.html.HtmlDocument.*;
  48. import xml.completion.ElementDecl;
  49. import xml.completion.IDDecl;
  50. /**
  51. * Provides hyperlinks from HTML attributes.
  52. * Supported hyperlinks are all attributes with type URI
  53. * in the HTML 4.01 spec.
  54. * Links to other documents and anchors inside document
  55. * are supported, but fragment identifiers in other documents
  56. * are not.
  57. * the HTML/HEAD/BASE element is used to resolve URIs
  58. * if present.
  59. * No reparsing is required, contrary to XMLHyperlinkSource
  60. * which reparses the start tag.
  61. *
  62. * @author Eric Le Lay
  63. * @version $Id: HTMLHyperlinkSource.java 21198 2012-02-24 11:19:21Z kerik-sf $
  64. */
  65. public class HTMLHyperlinkSource implements HyperlinkSource
  66. {
  67. /**
  68. * Returns the hyperlink for the given offset.
  69. * returns an hyperlink as soon as pointer enters the attribute's value
  70. *
  71. * @param buffer the buffer
  72. * @param offset the offset
  73. * @return the hyperlink (or null if there is no hyperlink)
  74. */
  75. public Hyperlink getHyperlink(Buffer buffer, int offset){
  76. View view = jEdit.getActiveView();
  77. XmlParsedData data = XmlParsedData.getParsedData(view, false);
  78. if(data==null)return null;
  79. IAsset asset = data.getAssetAtOffset(offset);
  80. if(asset == null){
  81. Log.log(Log.DEBUG, HTMLHyperlinkSource.class,"no Sidekick asset here");
  82. return null;
  83. } else {
  84. int wantedLine = buffer.getLineOfOffset(offset);
  85. int wantedLineOffset = buffer.getVirtualWidth(wantedLine, offset - buffer.getLineStartOffset(wantedLine));
  86. SideKickElement elt = ((SideKickAsset)asset).getElement();
  87. Tag startTag;
  88. if(elt instanceof TagBlock){
  89. startTag = ((TagBlock)elt).startTag;
  90. }else if(elt instanceof Tag){
  91. startTag = (Tag)elt;
  92. }else{
  93. Log.log(Log.WARNING,HTMLHyperlinkSource.class,"unexpected asset type: "+elt.getClass()+", please report");
  94. startTag = null;
  95. return null;
  96. }
  97. int start = ElementUtil.createStartPosition(buffer,startTag).getOffset();
  98. int end= ElementUtil.createEndPosition(buffer,startTag).getOffset();
  99. /* if the offset is inside start tag */
  100. if(offset <= end)
  101. {
  102. AttributeList al = startTag.attributeList;
  103. if(al == null){
  104. if(DEBUG_HYPERLINKS)Log.log(Log.DEBUG,HTMLHyperlinkSource.class,"no attribute in this element");
  105. return null;
  106. }else{
  107. for(Attribute att: al.attributes){
  108. // offset is inside attribute's value
  109. if( ( att.getValueStartLocation().line < wantedLine+1
  110. || (att.getValueStartLocation().line == wantedLine+1
  111. && att.getValueStartLocation().column <= wantedLineOffset)
  112. )
  113. &&
  114. ( att.getEndLocation().line > wantedLine+1
  115. || (att.getEndLocation().line == wantedLine+1
  116. && att.getEndLocation().column > wantedLineOffset)
  117. )
  118. )
  119. {
  120. return getHyperlinkForAttribute(buffer, offset, data, startTag, att);
  121. }
  122. }
  123. if(DEBUG_HYPERLINKS)Log.log(Log.DEBUG,HTMLHyperlinkSource.class,"not inside attributes");
  124. return null;
  125. }
  126. }else{
  127. return null;
  128. }
  129. }
  130. }
  131. /**
  132. * get an hyperlink for an identified HTML attribute
  133. * @param buffer current buffer
  134. * @param offset offset where an hyperlink is required in current buffer
  135. * @param data sidekick tree for current buffer
  136. * @param asset element containing offset
  137. * @param att parsed attribute
  138. */
  139. public Hyperlink getHyperlinkForAttribute(
  140. Buffer buffer, int offset, XmlParsedData data,
  141. Tag startTag, Attribute att)
  142. {
  143. String tagLocalName = startTag.tagName;
  144. String localName = att.getName();
  145. String value = att.getValue();
  146. boolean quoted;
  147. if((value.startsWith("\"")&&value.endsWith("\""))
  148. ||(value.startsWith("'")&&value.endsWith("'")))
  149. {
  150. value = value.substring(1,value.length()-1);
  151. quoted = true;
  152. }else{
  153. quoted = false;
  154. }
  155. Hyperlink h = getHyperlinkForAttribute(buffer, offset,
  156. tagLocalName, localName, value,
  157. data, startTag, att, quoted);
  158. if(h == null) {
  159. ElementDecl eltDecl = data.getElementDecl(localName,offset);
  160. if(eltDecl == null){
  161. if(DEBUG_HYPERLINKS)Log.log(Log.DEBUG,HTMLHyperlinkSource.class,"no element declaration for "+tagLocalName);
  162. }else{
  163. ElementDecl.AttributeDecl attDecl = eltDecl.attributeHash.get(localName);
  164. if(attDecl == null){
  165. if(DEBUG_HYPERLINKS)Log.log(Log.DEBUG,HTMLHyperlinkSource.class,"no attribute declaration for "+localName);
  166. return null;
  167. }else{
  168. if("IDREF".equals(attDecl.type)){
  169. return getHyperlinkForIDREF(buffer, data, value, att, quoted);
  170. }else if("anyURI".equals(attDecl.type)){
  171. String href = resolve(value, buffer, offset, data);
  172. if(href!=null){
  173. return newJEditOpenFileHyperlink(buffer, att, href, quoted);
  174. }
  175. }else if("IDREFS".equals(attDecl.type)){
  176. return getHyperlinkForIDREFS(buffer, offset, data, value, att, quoted);
  177. }
  178. return null;
  179. }
  180. }
  181. } else {
  182. return h;
  183. }
  184. return null;
  185. }
  186. /**
  187. * creates an hyperlink to the location of the element with id (in same or another buffer).
  188. * @param buffer current buffer
  189. * @param data sidekick tree
  190. * @param id id we are looking for
  191. * @param att parsed attribute (for hyperlink boundaries)
  192. * @param quoted is the value inside quotes ?
  193. */
  194. public Hyperlink getHyperlinkForIDREF(Buffer buffer,
  195. XmlParsedData data, String id, Attribute att,
  196. boolean quoted)
  197. {
  198. IDDecl idDecl = data.getIDDecl(id);
  199. if(idDecl == null){
  200. return null;
  201. } else{
  202. return newJEditOpenFileAndGotoHyperlink(buffer, att,
  203. idDecl.uri, idDecl.line, idDecl.column, quoted);
  204. }
  205. }
  206. // {{{ getHyperlinkForIDREFS() method
  207. private static final Pattern noWSPattern = Pattern.compile("[^\\s]+");
  208. /**
  209. * creates an hyperlink to the location of the element with id (in same or another buffer)
  210. * @param buffer current buffer
  211. * @param offset offset of required hyperlink
  212. * @param data sidekick tree
  213. * @param attValue ids in the attribute
  214. * @param att parsed attribute (for hyperlink boundaries)
  215. * @param quoted is the value inside quotes ?
  216. */
  217. public Hyperlink getHyperlinkForIDREFS(Buffer buffer, int offset,
  218. XmlParsedData data, String attValue, Attribute att,
  219. boolean quoted)
  220. {
  221. int attStart = xml.ElementUtil.createOffset(buffer, att.getValueStartLocation());
  222. // +1 for the quote around the attribute value
  223. if(quoted)attStart++;
  224. Matcher m = noWSPattern.matcher(attValue);
  225. while(m.find()){
  226. int st = m.start(0);
  227. int nd = m.end(0);
  228. if(attStart + st <= offset && attStart + nd >= offset){
  229. IDDecl idDecl = data.getIDDecl(m.group(0));
  230. if(idDecl==null)return null;
  231. int start = attStart + st;
  232. int end= attStart + nd;
  233. int line = buffer.getLineOfOffset(start);
  234. return new jEditOpenFileAndGotoHyperlink(start, end, line, idDecl.uri, idDecl.line, idDecl.column);
  235. }
  236. }
  237. return null;
  238. }//}}}
  239. //{{{ getHyperlinkForAttribute(tagName,attName) method
  240. private static final Map<String,Set<String>> uriAttributes = new HashMap<String,Set<String>>();
  241. static{
  242. HashSet<String> h;
  243. h = new HashSet<String>();
  244. h.add("href");
  245. uriAttributes.put("a", h);
  246. uriAttributes.put("area", h);
  247. uriAttributes.put("link", h);
  248. h = new HashSet<String>();
  249. h.add("longdesc");
  250. h.add("usemap");
  251. uriAttributes.put("img", h);
  252. h = new HashSet<String>();
  253. h.add("cite");
  254. uriAttributes.put("q", h);
  255. uriAttributes.put("blockquote", h);
  256. uriAttributes.put("ins", h);
  257. uriAttributes.put("del", h);
  258. h = new HashSet<String>();
  259. h.add("usemap");
  260. uriAttributes.put("input", h);
  261. uriAttributes.put("object", h);
  262. h = new HashSet<String>();
  263. h.add("src");
  264. uriAttributes.put("script", h);
  265. }
  266. /**
  267. * recognize hyperlink attributes by their parent element's
  268. * namespace:localname and/or their namespace:localname
  269. */
  270. public Hyperlink getHyperlinkForAttribute(Buffer buffer, int offset,
  271. String tagLocalName,
  272. String attLocalName, String attValue,
  273. XmlParsedData data, Tag tag, Attribute att,
  274. boolean quoted)
  275. {
  276. boolean isHREF = false;
  277. if(uriAttributes.containsKey(tagLocalName)
  278. && uriAttributes.get(tagLocalName).contains(attLocalName))
  279. {
  280. if(attValue.startsWith("#")){
  281. Location found = getNamedAnchorLocation(data, attValue.substring(1));
  282. if(found != null){
  283. // OpenFileAndGoto expects real line&column,
  284. // not virtual column
  285. int toffset = xml.ElementUtil.createOffset(buffer, found);
  286. int line = buffer.getLineOfOffset(toffset);
  287. int column = toffset - buffer.getLineStartOffset(line);
  288. // it's OK to have a path
  289. String href = buffer.getPath();
  290. return newJEditOpenFileAndGotoHyperlink(
  291. buffer, att, href, line, column, quoted);
  292. }
  293. }else{
  294. String href = resolve(attValue, buffer, offset, data);
  295. if(href==null){
  296. return null;
  297. }else{
  298. return newJEditOpenFileHyperlink(
  299. buffer, att, href, quoted);
  300. }
  301. }
  302. } else if("object".equals(tagLocalName)
  303. && ("classid".equals(attLocalName)
  304. || "data".equals(attLocalName)))
  305. {
  306. /* must resolve against codebase if present */
  307. String href = resolveRelativeTo(attValue,
  308. tag.getAttributeValue("codebase"));
  309. href = resolve(href, buffer, offset, data);
  310. if(href==null){
  311. return null;
  312. }else{
  313. return newJEditOpenFileHyperlink(
  314. buffer, att, href, quoted);
  315. }
  316. } else if("object".equals(tagLocalName)
  317. && "archive".equals(attLocalName))
  318. {
  319. // +1 for the quote around the attribute value
  320. int attStart = xml.ElementUtil.createOffset(buffer, att.getValueStartLocation()) +1;
  321. Matcher m = noWSPattern.matcher(attValue);
  322. while(m.find()){
  323. int st = m.start(0);
  324. int nd = m.end(0);
  325. if(attStart + st <= offset && attStart + nd >= offset){
  326. /* must resolve against codebase if present */
  327. String href = resolveRelativeTo(m.group(0),
  328. tag.getAttributeValue("codebase"));
  329. href = resolve(href, buffer, offset, data);
  330. if(href==null)href=m.group(0);
  331. int start = attStart + st;
  332. int end= attStart + nd;
  333. int line = buffer.getLineOfOffset(start);
  334. return new jEditOpenFileHyperlink(start, end, line, href);
  335. }
  336. }
  337. }
  338. return null;
  339. }//}}}
  340. //{{{ resolveRelativeTo(href,base) methode
  341. /**
  342. * resolves using URI.resolve() href against base
  343. */
  344. String resolveRelativeTo(String href, String base){
  345. if(base == null || "".equals(base))return href;
  346. try{
  347. return URI.create(base).resolve(href).toString();
  348. }catch(IllegalArgumentException iae){
  349. Log.log(Log.WARNING,HTMLHyperlinkSource.class,"error resolving against codebase",iae);
  350. return href;
  351. }
  352. }//}}}
  353. //{{{ resolve(uri, buffer) method
  354. /**
  355. * resolve a potentially relative uri using HTML BASE element,
  356. * the buffer's URL, xml.Resolver.
  357. * Has the effect of opening the cached document if it's in cache (eg. docbook XSD if not in catalog).
  358. * Maybe this is not desirable, because if there is a relative link in this document, it won't work
  359. * because the document will be .jedit/dtds/cachexxxxx.xml and not the real url.
  360. *
  361. * @param uri text of uri to reach
  362. * @param buffer current buffer
  363. * @param offset offset in current buffer where an hyperlink is required
  364. * @param data SideKick parsed data
  365. *
  366. * @return resolved URL
  367. */
  368. public String resolve(String uri, Buffer buffer, int offset, XmlParsedData data)
  369. {
  370. String href = null;
  371. String base = xml.PathUtilities.pathToURL(buffer.getPath());
  372. // {{{ use html/head/base
  373. TagBlock html = getHTML(data);
  374. if(html != null
  375. && html.body.size()>0)
  376. {
  377. for(Iterator ith = ((TagBlock)html).body.iterator();ith.hasNext();){
  378. HtmlElement head = (HtmlElement) ith.next();
  379. if(head instanceof TagBlock){
  380. if("head".equalsIgnoreCase(((TagBlock)head).startTag.tagName))
  381. {
  382. for(Iterator it = ((TagBlock)head).body.iterator();it.hasNext();){
  383. HtmlElement e = (HtmlElement)it.next();
  384. if(e instanceof Tag
  385. && "base".equalsIgnoreCase(((Tag)e).tagName))
  386. {
  387. // Base must be absolute in HTML 4.01
  388. String preBase = ((Tag)e).getAttributeValue("href");
  389. try{
  390. // add a dummy component, otherwise xml.Resolver
  391. // removes the last part of the xml:base
  392. // FIXME: review xml.Resolver : it should only be used with URLs
  393. // for current, now, so could use URI.resolve() instead of removing
  394. // last part of the path to get the parent...
  395. base = URI.create(preBase).resolve("dummy").toString();
  396. }catch(IllegalArgumentException iae){
  397. Log.log(Log.WARNING, XMLHyperlinkSource.class, "error resolving uri", iae);
  398. }
  399. break;
  400. }
  401. }
  402. }
  403. break;
  404. }
  405. }
  406. }//}}}
  407. try{
  408. href = Resolver.instance().resolveEntityToPath(
  409. "", /*name*/
  410. "", /*publicId*/
  411. base, /*current, augmented by xml:base */
  412. uri);
  413. }catch(java.io.IOException ioe){
  414. Log.log(Log.ERROR,XMLHyperlinkSource.class,"error resolving href="+uri,ioe);
  415. }
  416. return href;
  417. }//}}}
  418. /**
  419. * create an hyperlink for attribute att.
  420. * the hyperlink will span whole attribute value
  421. * @param buffer current buffer
  422. * @param att parsed attribute
  423. * @param href uri to open
  424. * @param quoted is the value inside quotes ?
  425. */
  426. public Hyperlink newJEditOpenFileHyperlink(
  427. Buffer buffer, Attribute att, String href,
  428. boolean quoted)
  429. {
  430. int start = xml.ElementUtil.createOffset(buffer,att.getValueStartLocation());
  431. int end= xml.ElementUtil.createOffset(buffer,att.getEndLocation());
  432. if(quoted){
  433. start++;
  434. end--;
  435. }
  436. int line = buffer.getLineOfOffset(start);
  437. return new jEditOpenFileHyperlink(start, end, line, href);
  438. }
  439. /**
  440. * create an hyperlink for attribute att.
  441. * the hyperlink will span whole attribute value
  442. * @param buffer current buffer
  443. * @param att parsed attribute
  444. * @param href uri to open
  445. * @param gotoLine target line in buffer
  446. * @param gotoCol target column in buffer
  447. * @param quoted is the value inside quotes ?
  448. */
  449. public Hyperlink newJEditOpenFileAndGotoHyperlink(
  450. Buffer buffer, Attribute att, String href, int gotoLine, int gotoCol,
  451. boolean quoted)
  452. {
  453. int start = xml.ElementUtil.createOffset(buffer,att.getValueStartLocation());
  454. int end= xml.ElementUtil.createOffset(buffer,att.getEndLocation());
  455. if(quoted){
  456. start++;
  457. end--;
  458. }
  459. int line = buffer.getLineOfOffset(start);
  460. return new jEditOpenFileAndGotoHyperlink(start, end, line, href, gotoLine, gotoCol);
  461. }
  462. TagBlock getHTML(XmlParsedData data) {
  463. DefaultMutableTreeNode tn = (DefaultMutableTreeNode)data.root;
  464. DefaultMutableTreeNode docRoot = (DefaultMutableTreeNode)tn.getFirstChild();
  465. if(docRoot == null){
  466. if(DEBUG_HYPERLINKS)Log.log(Log.WARNING,HTMLHyperlinkSource.class,"not parsed ??");
  467. return null;
  468. }else{
  469. SideKickElement elt = ((SideKickAsset)data.getAsset(docRoot)).getElement();
  470. if(elt instanceof TagBlock){
  471. return (TagBlock)elt;
  472. }
  473. }
  474. return null;
  475. }
  476. private static class FoundException extends RuntimeException{}
  477. private static final FoundException foundException = new FoundException();
  478. private static final class NamedAnchorVisitor extends HtmlVisitor{
  479. private final String searchedAnchor;
  480. Location foundLoc;
  481. NamedAnchorVisitor(String searched){
  482. searchedAnchor = searched;
  483. foundLoc = null;
  484. }
  485. public void visit(Tag t) {
  486. String v = null;
  487. if("a".equalsIgnoreCase(t.tagName)){
  488. v = t.getAttributeValue("name");
  489. }
  490. if(v == null){
  491. v = t.getAttributeValue("id");
  492. }
  493. if(searchedAnchor.equals(v)){
  494. foundLoc = t.getStartLocation();
  495. throw foundException;
  496. }
  497. }
  498. }
  499. public Location getNamedAnchorLocation(XmlParsedData data, String name){
  500. TagBlock html = getHTML(data);
  501. if(html != null){
  502. NamedAnchorVisitor v = new NamedAnchorVisitor(name);
  503. try{
  504. html.accept(v);
  505. }catch(FoundException e){
  506. return v.foundLoc;
  507. }
  508. }
  509. return null;
  510. }
  511. public static HyperlinkSource create(){
  512. return new FallbackHyperlinkSource(
  513. Arrays.asList(new HTMLHyperlinkSource(),
  514. new gatchan.jedit.hyperlinks.url.URLHyperlinkSource()));
  515. }
  516. }