/******************************************************************************* | |
* Copyright (c) 2008 IBM Corporation and others. | |
* All rights reserved. This program and the accompanying materials | |
* are made available under the terms of the Eclipse Public License v1.0 | |
* which accompanies this distribution, and is available at | |
* http://www.eclipse.org/legal/epl-v10.html | |
* | |
* Contributors: | |
* IBM Corporation - initial API and implementation | |
*******************************************************************************/ | |
package org.eclipse.wst.html.ui.internal.hyperlink; | |
import java.util.ArrayList; | |
import java.util.List; | |
import java.util.NoSuchElementException; | |
import org.eclipse.core.resources.IFile; | |
import org.eclipse.core.resources.IMarker; | |
import org.eclipse.core.resources.ResourcesPlugin; | |
import org.eclipse.core.runtime.CoreException; | |
import org.eclipse.core.runtime.IPath; | |
import org.eclipse.core.runtime.IProgressMonitor; | |
import org.eclipse.core.runtime.IStatus; | |
import org.eclipse.core.runtime.Path; | |
import org.eclipse.core.runtime.Status; | |
import org.eclipse.core.runtime.jobs.Job; | |
import org.eclipse.jface.text.BadLocationException; | |
import org.eclipse.jface.text.IDocument; | |
import org.eclipse.jface.text.IRegion; | |
import org.eclipse.jface.text.ITextViewer; | |
import org.eclipse.jface.text.ITextViewerExtension5; | |
import org.eclipse.jface.text.Region; | |
import org.eclipse.jface.text.TextSelection; | |
import org.eclipse.jface.text.hyperlink.AbstractHyperlinkDetector; | |
import org.eclipse.jface.text.hyperlink.IHyperlink; | |
import org.eclipse.osgi.util.NLS; | |
import org.eclipse.swt.widgets.Display; | |
import org.eclipse.ui.IEditorPart; | |
import org.eclipse.ui.IWorkbenchPage; | |
import org.eclipse.ui.PartInitException; | |
import org.eclipse.ui.PlatformUI; | |
import org.eclipse.ui.ide.IDE; | |
import org.eclipse.ui.progress.UIJob; | |
import org.eclipse.wst.html.core.internal.provisional.HTML40Namespace; | |
import org.eclipse.wst.html.core.internal.validate.ModuleCoreSupport; | |
import org.eclipse.wst.html.ui.internal.HTMLUIMessages; | |
import org.eclipse.wst.html.ui.internal.HTMLUIPlugin; | |
import org.eclipse.wst.html.ui.internal.Logger; | |
import org.eclipse.wst.sse.core.StructuredModelManager; | |
import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; | |
import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; | |
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; | |
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; | |
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; | |
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList; | |
import org.eclipse.wst.sse.core.utils.StringUtils; | |
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; | |
import org.eclipse.wst.xml.core.internal.provisional.document.IDOMNode; | |
import org.eclipse.wst.xml.core.internal.regions.DOMRegionContext; | |
import org.w3c.dom.Element; | |
import org.w3c.dom.Node; | |
import org.w3c.dom.NodeList; | |
import com.ibm.icu.util.StringTokenizer; | |
public class AnchorHyperlinkDetector extends AbstractHyperlinkDetector { | |
static class ExternalElementHyperlink implements IHyperlink { | |
private String fAnchorName = null; | |
private Element fBaseElement = null; | |
private Display fDisplay = null; | |
private IRegion fHyperlinkRegion = null; | |
/** | |
* @param hyperlinkRegion | |
* @param anchorName | |
*/ | |
public ExternalElementHyperlink(Display display, IRegion hyperlinkRegion, String anchorName, Element baseElement) { | |
super(); | |
fDisplay = display; | |
fHyperlinkRegion = hyperlinkRegion; | |
fAnchorName = anchorName; | |
fBaseElement = baseElement; | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see org.eclipse.jface.text.hyperlink.IHyperlink#open() | |
*/ | |
IStatus _open() { | |
if (fBaseElement instanceof IDOMNode) { | |
StringTokenizer tokenizer = new StringTokenizer(fAnchorName, "#"); //$NON-NLS-1$ | |
String filename = null; | |
String anchorName = null; | |
if (tokenizer.hasMoreTokens()) { | |
try { | |
filename = tokenizer.nextToken(); | |
anchorName = tokenizer.nextToken(); | |
} | |
catch (NoSuchElementException e) { | |
// poorly formed value | |
} | |
} | |
if (filename != null && anchorName != null) { | |
// System.out.println(filename + ":" + anchorName + "-" + | |
// fBaseElement); | |
IPath basePath = new Path(((IDOMNode) fBaseElement).getModel().getBaseLocation()); | |
if (basePath.segmentCount() > 1) { | |
IPath resolved = ModuleCoreSupport.resolve(basePath, filename); | |
IFile targetFile = ResourcesPlugin.getWorkspace().getRoot().getFile(resolved); | |
if (targetFile.isAccessible()) { | |
IStructuredModel model = null; | |
int start = -1; | |
int end = -1; | |
try { | |
model = StructuredModelManager.getModelManager().getModelForRead(targetFile); | |
if (model instanceof IDOMModel) { | |
NodeList anchors = ((IDOMModel) model).getDocument().getElementsByTagNameNS("*", HTML40Namespace.ElementName.A); //$NON-NLS-1$ | |
for (int i = 0; i < anchors.getLength() && start < 0; i++) { | |
Node item = anchors.item(i); | |
Node nameNode = item.getAttributes().getNamedItem(HTML40Namespace.ATTR_NAME_NAME); | |
if (nameNode == null) | |
nameNode = item.getAttributes().getNamedItem(HTML40Namespace.ATTR_NAME_ID); | |
if (nameNode != null) { | |
String name = nameNode.getNodeValue(); | |
if (anchorName.equals(name) && nameNode instanceof IndexedRegion) { | |
start = ((IndexedRegion) nameNode).getStartOffset(); | |
end = ((IndexedRegion) nameNode).getEndOffset(); | |
} | |
} | |
} | |
anchors = ((IDOMModel) model).getDocument().getElementsByTagName(HTML40Namespace.ElementName.A); | |
for (int i = 0; i < anchors.getLength() && start < 0; i++) { | |
Node item = anchors.item(i); | |
Node nameNode = item.getAttributes().getNamedItem(HTML40Namespace.ATTR_NAME_NAME); | |
if (nameNode == null) | |
nameNode = item.getAttributes().getNamedItem(HTML40Namespace.ATTR_NAME_ID); | |
if (nameNode != null) { | |
String name = nameNode.getNodeValue(); | |
if (anchorName.equals(name) && nameNode instanceof IndexedRegion) { | |
start = ((IndexedRegion) nameNode).getStartOffset(); | |
end = ((IndexedRegion) nameNode).getEndOffset(); | |
} | |
} | |
} | |
anchors = ((IDOMModel) model).getDocument().getElementsByTagName("*"); //$NON-NLS-1$ | |
for (int i = 0; i < anchors.getLength() && start < 0; i++) { | |
Node item = anchors.item(i); | |
Node nameNode = item.getAttributes().getNamedItem(HTML40Namespace.ATTR_NAME_NAME); | |
if (nameNode == null) | |
nameNode = item.getAttributes().getNamedItem(HTML40Namespace.ATTR_NAME_ID); | |
if (nameNode != null) { | |
String name = nameNode.getNodeValue(); | |
if (anchorName.equals(name) && nameNode instanceof IndexedRegion) { | |
start = ((IndexedRegion) nameNode).getStartOffset(); | |
end = ((IndexedRegion) nameNode).getEndOffset(); | |
} | |
} | |
} | |
} | |
return open(basePath.toString(), targetFile, start, end); | |
} | |
catch (Exception e) { | |
Logger.logException(e); | |
return new Status(IStatus.ERROR, HTMLUIPlugin.ID, e.getMessage()); | |
} | |
finally { | |
if (model != null) | |
model.releaseFromRead(); | |
} | |
} | |
} | |
} | |
} | |
return Status.OK_STATUS; | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see | |
* org.eclipse.jface.text.hyperlink.IHyperlink#getHyperlinkRegion() | |
*/ | |
public IRegion getHyperlinkRegion() { | |
return fHyperlinkRegion; | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see org.eclipse.jface.text.hyperlink.IHyperlink#getHyperlinkText() | |
*/ | |
public String getHyperlinkText() { | |
return NLS.bind(HTMLUIMessages.Open, fAnchorName); | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see org.eclipse.jface.text.hyperlink.IHyperlink#getTypeLabel() | |
*/ | |
public String getTypeLabel() { | |
return null; | |
} | |
public void open() { | |
scheduleOpen(); | |
} | |
/** | |
* @param targetFile | |
* @param start | |
* @param end | |
*/ | |
private IStatus open(String base, IFile targetFile, int start, int end) throws CoreException, PartInitException { | |
IMarker temporaryMarker = null; | |
try { | |
IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(); | |
IEditorPart editor = IDE.openEditor(page, targetFile); | |
if (0 <= start && start <= end) { | |
temporaryMarker = targetFile.createMarker(IMarker.BOOKMARK); | |
temporaryMarker.setAttribute(IMarker.MESSAGE, base); | |
temporaryMarker.setAttribute(IMarker.CHAR_START, start); | |
temporaryMarker.setAttribute(IMarker.CHAR_END, end); | |
IDE.gotoMarker(editor, temporaryMarker); | |
} | |
return Status.OK_STATUS; | |
} | |
finally { | |
if (temporaryMarker != null) | |
try { | |
temporaryMarker.delete(); | |
} | |
catch (CoreException e) { | |
Logger.logException(e); | |
} | |
} | |
} | |
void scheduleOpen() { | |
Job opener = new UIJob(fDisplay, fAnchorName) { | |
public IStatus runInUIThread(IProgressMonitor monitor) { | |
return _open(); | |
} | |
}; | |
opener.setSystem(true); | |
opener.setUser(false); | |
opener.schedule(); | |
} | |
} | |
/** | |
* Links to the given target node within the text viewer. The target node | |
* is expected to implement IndexedNode and appear in that text viewer | |
* (i.e. same editor). | |
* | |
*/ | |
static class InternalElementHyperlink implements IHyperlink { | |
private IRegion fHyperlinkRegion; | |
private Node fTarget = null; | |
private ITextViewer fViewer = null; | |
/** | |
* | |
*/ | |
public InternalElementHyperlink(ITextViewer textViewer, IRegion hyperlinkRegion, Node targetNode) { | |
fHyperlinkRegion = hyperlinkRegion; | |
fTarget = targetNode; | |
fViewer = textViewer; | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see | |
* org.eclipse.jface.text.hyperlink.IHyperlink#getHyperlinkRegion() | |
*/ | |
public IRegion getHyperlinkRegion() { | |
return fHyperlinkRegion; | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see org.eclipse.jface.text.hyperlink.IHyperlink#getHyperlinkText() | |
*/ | |
public String getHyperlinkText() { | |
if (fTarget instanceof IndexedRegion) { | |
try { | |
int line = fViewer.getDocument().getLineOfOffset(((IndexedRegion) fTarget).getStartOffset()) + 1; | |
return NLS.bind(HTMLUIMessages.Hyperlink_line, new String[]{fTarget.getNodeName(), fTarget.getNodeValue(), String.valueOf(line)}); | |
} | |
catch (BadLocationException e) { | |
Logger.logException(e); | |
} | |
} | |
return NLS.bind(HTMLUIMessages.Open, fTarget.getNodeName()); | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see org.eclipse.jface.text.hyperlink.IHyperlink#getTypeLabel() | |
*/ | |
public String getTypeLabel() { | |
return null; | |
} | |
/* | |
* (non-Javadoc) | |
* | |
* @see org.eclipse.jface.text.hyperlink.IHyperlink#open() | |
*/ | |
public void open() { | |
if (fTarget instanceof IndexedRegion) { | |
int offset = ((IndexedRegion) fTarget).getStartOffset(); | |
if (fViewer instanceof ITextViewerExtension5) { | |
offset = ((ITextViewerExtension5) fViewer).modelOffset2WidgetOffset(offset); | |
} | |
fViewer.getSelectionProvider().setSelection(new TextSelection(offset, 0)); | |
fViewer.revealRange(offset, 0); | |
} | |
} | |
} | |
public AnchorHyperlinkDetector() { | |
super(); | |
} | |
private void addHyperLinkForHref(ITextViewer textViewer, IRegion linkRegion, Element element, String hrefValue, List links, Node anchor) { | |
Node nameNode = anchor.getAttributes().getNamedItem(HTML40Namespace.ATTR_NAME_HREF); | |
if (nameNode != null) { | |
String name = nameNode.getNodeValue(); | |
if (hrefValue.equals(name) && nameNode instanceof IndexedRegion) { | |
links.add(new InternalElementHyperlink(textViewer, linkRegion, nameNode)); | |
} | |
} | |
} | |
private void addHyperLinkForName(ITextViewer textViewer, IRegion linkRegion, Element element, String anchorName, List links, Node anchor) { | |
Node nameNode = anchor.getAttributes().getNamedItem(HTML40Namespace.ATTR_NAME_NAME); | |
if (nameNode != null) { | |
String name = nameNode.getNodeValue(); | |
if (anchorName.equals(name) && nameNode instanceof IndexedRegion) { | |
links.add(new InternalElementHyperlink(textViewer, linkRegion, nameNode)); | |
} | |
} | |
nameNode = anchor.getAttributes().getNamedItem(HTML40Namespace.ATTR_NAME_ID); | |
if (nameNode != null) { | |
String name = nameNode.getNodeValue(); | |
if (anchorName.equals(name) && nameNode instanceof IndexedRegion) { | |
links.add(new InternalElementHyperlink(textViewer, linkRegion, nameNode)); | |
} | |
} | |
} | |
/** | |
* @param documentRegion | |
* @param valueRegion | |
* @return | |
*/ | |
private IRegion createHyperlinkRegion(IStructuredDocumentRegion documentRegion, ITextRegion valueRegion) { | |
return new Region(documentRegion.getStartOffset(valueRegion), valueRegion.getTextLength()); | |
} | |
// link to anchors with the given name (value includes the '#') | |
IHyperlink[] createHyperlinksToAnchorNamed(ITextViewer textViewer, IRegion hyperlinkRegion, Element element, String anchorName, boolean canShowMultipleHyperlinks) { | |
List links = new ArrayList(1); | |
// >1 guards the substring-ing | |
if (anchorName.length() > 1 && anchorName.startsWith("#")) { //$NON-NLS-1$ | |
// an anchor in this document | |
NodeList anchors = null;//element.getOwnerDocument().getElementsByTagNameNS("*", HTML40Namespace.ElementName.A); //$NON-NLS-1$ | |
String internalAnchorName = anchorName.substring(1); | |
// for (int i = 0; i < anchors.getLength(); i++) { | |
// addHyperLinkForName(textViewer, hyperlinkRegion, element, internalAnchorName, links, anchors.item(i)); | |
// } | |
// anchors = element.getOwnerDocument().getElementsByTagName(HTML40Namespace.ElementName.A); | |
// for (int i = 0; i < anchors.getLength(); i++) { | |
// addHyperLinkForName(textViewer, hyperlinkRegion, element, internalAnchorName, links, anchors.item(i)); | |
// } | |
anchors = element.getOwnerDocument().getElementsByTagName("*"); //$NON-NLS-1$ | |
for (int i = 0; i < anchors.getLength(); i++) { | |
addHyperLinkForName(textViewer, hyperlinkRegion, element, internalAnchorName, links, anchors.item(i)); | |
} | |
} | |
else { | |
// another file, possibly very slow to compute ahead of time | |
links.add(new ExternalElementHyperlink(textViewer.getTextWidget().getDisplay(), hyperlinkRegion, anchorName, element)); | |
} | |
if (!links.isEmpty()) { | |
return (IHyperlink[]) links.toArray(new IHyperlink[links.size()]); | |
} | |
return null; | |
} | |
// link to anchors that link to this target | |
IHyperlink[] createReferrerHyperlinks(ITextViewer textViewer, IRegion hyperlinkRegion, Element element, String nameValue, boolean canShowMultipleHyperlinks) { | |
List links = new ArrayList(1); | |
if (nameValue.length() > 0) { | |
String target = "#" + nameValue; //$NON-NLS-1$ | |
NodeList anchors = null;//element.getOwnerDocument().getElementsByTagNameNS("*", HTML40Namespace.ElementName.A); //$NON-NLS-1$ | |
// for (int i = 0; i < anchors.getLength(); i++) { | |
// addHyperLinkForHref(textViewer, hyperlinkRegion, element, target, links, anchors.item(i)); | |
// } | |
// anchors = element.getOwnerDocument().getElementsByTagName(HTML40Namespace.ElementName.A); | |
// for (int i = 0; i < anchors.getLength(); i++) { | |
// addHyperLinkForHref(textViewer, hyperlinkRegion, element, target, links, anchors.item(i)); | |
// } | |
anchors = element.getOwnerDocument().getElementsByTagName("*"); //$NON-NLS-1$ | |
for (int i = 0; i < anchors.getLength(); i++) { | |
addHyperLinkForHref(textViewer, hyperlinkRegion, element, target, links, anchors.item(i)); | |
} | |
} | |
if (!links.isEmpty()) { | |
return (IHyperlink[]) links.toArray(new IHyperlink[links.size()]); | |
} | |
return null; | |
} | |
public IHyperlink[] detectHyperlinks(ITextViewer textViewer, IRegion region, boolean canShowMultipleHyperlinks) { | |
if (textViewer != null && region != null) { | |
IDocument document = textViewer.getDocument(); | |
if (document != null) { | |
Node currentNode = getCurrentNode(document, region.getOffset()); | |
if (currentNode != null && currentNode.getNodeType() == Node.ELEMENT_NODE) { | |
Element element = (Element) currentNode; | |
IStructuredDocumentRegion documentRegion = ((IStructuredDocument) document).getRegionAtCharacterOffset(region.getOffset()); | |
ITextRegion textRegion = documentRegion.getRegionAtCharacterOffset(region.getOffset()); | |
ITextRegion nameRegion = null; | |
ITextRegion valueRegion = null; | |
String name = null; | |
String value = null; | |
if (DOMRegionContext.XML_TAG_ATTRIBUTE_VALUE.equals(textRegion.getType())) { | |
ITextRegionList regions = documentRegion.getRegions(); | |
/* | |
* Could use 2, but there needs to be the tag open | |
* and name regions | |
*/ | |
int index = regions.indexOf(textRegion); | |
if (index >= 4) { | |
nameRegion = regions.get(index - 2); | |
valueRegion = textRegion; | |
name = documentRegion.getText(nameRegion); | |
value = StringUtils.strip(documentRegion.getText(valueRegion)); | |
} | |
} | |
else if (DOMRegionContext.XML_TAG_ATTRIBUTE_NAME.equals(textRegion.getType())) { | |
ITextRegionList regions = documentRegion.getRegions(); | |
int index = regions.indexOf(textRegion); | |
// minus 3 to leave equal and value regions | |
if (index <= regions.size() - 3) { | |
nameRegion = textRegion; | |
valueRegion = regions.get(index + 2); | |
name = documentRegion.getText(nameRegion); | |
value = StringUtils.strip(documentRegion.getText(valueRegion)); | |
} | |
} | |
if (name != null && value != null) { | |
if (HTML40Namespace.ATTR_NAME_HREF.equalsIgnoreCase(name) && value.indexOf("#") >= 0) { //$NON-NLS-1$ | |
return createHyperlinksToAnchorNamed(textViewer, createHyperlinkRegion(documentRegion, valueRegion), element, value, canShowMultipleHyperlinks); | |
} | |
if (HTML40Namespace.ATTR_NAME_NAME.equalsIgnoreCase(name)||HTML40Namespace.ATTR_NAME_ID.equalsIgnoreCase(name)) { | |
return createReferrerHyperlinks(textViewer, createHyperlinkRegion(documentRegion, valueRegion), element, value, canShowMultipleHyperlinks); | |
} | |
} | |
} | |
} | |
} | |
return null; | |
} | |
/** | |
* Returns the node the cursor is currently on in the document. null if no | |
* node is selected | |
* | |
* @param offset | |
* @return Node either element, doctype, text, or null | |
*/ | |
private Node getCurrentNode(IDocument document, int offset) { | |
// get the current node at the offset (returns either: element, | |
// doctype, text) | |
IndexedRegion inode = null; | |
IStructuredModel sModel = null; | |
try { | |
sModel = StructuredModelManager.getModelManager().getExistingModelForRead(document); | |
if (sModel != null) { | |
inode = sModel.getIndexedRegion(offset); | |
if (inode == null) { | |
inode = sModel.getIndexedRegion(offset - 1); | |
} | |
} | |
} | |
finally { | |
if (sModel != null) | |
sModel.releaseFromRead(); | |
} | |
if (inode instanceof Node) { | |
return (Node) inode; | |
} | |
return null; | |
} | |
} |