/******************************************************************************* * Copyright (c) 2008, 2016 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; /* * http://bugs.eclipse.org/475680 - possible after last offset in the file or * by a faulty implementation */ if (textRegion != 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) { int idx = -1; if (HTML40Namespace.ATTR_NAME_HREF.equalsIgnoreCase(name) && (idx = value.indexOf("#")) >= 0) { //$NON-NLS-1$ String filename = value.substring(0, idx); final String anchorName = idx < value.length() - 1? value.substring(idx + 1) : null; if (anchorName != null) { final IPath basePath = new Path(((IDOMNode) element).getModel().getBaseLocation()); if (basePath.segmentCount() > 1) { if (filename.length() == 0) { filename = basePath.lastSegment(); } final IPath resolved = ModuleCoreSupport.resolve(basePath, filename); final IFile targetFile = ResourcesPlugin.getWorkspace().getRoot().getFile(resolved); if (targetFile.isAccessible()) 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; } }