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