IDEA-144356 Recognize relative HTML links with anchors when parsing external Javadoc

This commit is contained in:
Dmitry Batrak
2015-09-02 19:58:05 +03:00
parent 9bb6e761ab
commit f7c123e328
7 changed files with 147 additions and 67 deletions
@@ -17,21 +17,17 @@ package com.intellij.codeInsight.javadoc;
import com.intellij.codeInsight.documentation.AbstractExternalFilter;
import com.intellij.codeInsight.documentation.DocumentationManager;
import com.intellij.codeInsight.documentation.DocumentationManagerProtocol;
import com.intellij.codeInsight.documentation.PlatformDocumentationUtil;
import com.intellij.ide.BrowserUtil;
import com.intellij.lang.java.JavaDocumentationProvider;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.NullableComputable;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.search.GlobalSearchScope;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -51,7 +47,8 @@ import java.util.regex.Pattern;
*/
public class JavaDocExternalFilter extends AbstractExternalFilter {
private final Project myProject;
private final Project myProject;
private PsiElement myElement;
private static final ParseSettings ourPackageInfoSettings = new ParseSettings(
Pattern.compile("package\\s+[^\\s]+\\s+description", Pattern.CASE_INSENSITIVE),
@@ -59,8 +56,6 @@ public class JavaDocExternalFilter extends AbstractExternalFilter {
true, false
);
protected static @NonNls final Pattern ourHTMLsuffix = Pattern.compile("[.][hH][tT][mM][lL]?");
protected static @NonNls final Pattern ourParentFolderprefix = Pattern.compile("^[.][.]/");
protected static @NonNls final Pattern ourAnchorsuffix = Pattern.compile("#(.*)$");
protected static @NonNls final Pattern ourHTMLFilesuffix = Pattern.compile("/([^/]*[.][hH][tT][mM][lL]?)$");
private static @NonNls final Pattern ourHREFselector = Pattern.compile("<A.*?HREF=\"([^>\"]*)\"", Pattern.CASE_INSENSITIVE|Pattern.DOTALL);
@@ -76,25 +71,19 @@ public class JavaDocExternalFilter extends AbstractExternalFilter {
if (BrowserUtil.isAbsoluteURL(href)) {
return href;
}
if (StringUtil.startsWithChar(href, '#')) {
return root + href;
String reference = JavaDocInfoGenerator.createReferenceForRelativeLink(href, myElement);
if (reference == null) {
if (href.startsWith("#")) {
return root + href;
}
else {
String nakedRoot = ourHTMLFilesuffix.matcher(root).replaceAll("/");
return doAnnihilate(nakedRoot + href);
}
}
else {
return reference;
}
String nakedRoot = ourHTMLFilesuffix.matcher(root).replaceAll("/");
String stripped = ourHTMLsuffix.matcher(href).replaceAll("");
int len = stripped.length();
do stripped = ourParentFolderprefix.matcher(stripped).replaceAll(""); while (len > (len = stripped.length()));
final String elementRef = stripped.replaceAll("/", ".");
final String classRef = ourAnchorsuffix.matcher(elementRef).replaceAll("");
return
(JavaPsiFacade.getInstance(myProject).findClass(classRef, GlobalSearchScope.allScope(myProject)) != null)
? DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL + elementRef
: doAnnihilate(nakedRoot + href);
}
}
};
@@ -121,6 +110,7 @@ public class JavaDocExternalFilter extends AbstractExternalFilter {
@Nullable
public String getExternalDocInfoForElement(@NotNull String docURL, final PsiElement element) throws Exception {
String externalDoc = null;
myElement = element;
String builtInServer = "http://localhost:" + BuiltInServerOptions.getInstance().getEffectiveBuiltInServerPort() + "/" + myProject.getName() + "/";
if (docURL.startsWith(builtInServer)) {
int refPosition = docURL.lastIndexOf('#');
@@ -31,6 +31,7 @@ import com.intellij.openapi.projectRoots.JavaSdkVersion;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Couple;
import com.intellij.openapi.util.JDOMUtil;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
@@ -282,57 +283,101 @@ public class JavaDocInfoGenerator {
}
protected String convertReference(@NonNls String href) {
final String originalReference = href;
String fragment = null;
int hashPosition = href.indexOf('#');
if (hashPosition >= 0) {
fragment = href.substring(hashPosition + 1);
href = href.substring(0, hashPosition);
}
if (href.isEmpty()) {
PsiElement containingClass = myElement instanceof PsiMember ? ((PsiMember)myElement).getContainingClass() : null;
PsiElement rootElement = containingClass == null ? myElement : containingClass;
return createLinkWithRef(rootElement, fragment);
}
if (!href.toLowerCase().endsWith(".htm") && !href.toLowerCase().endsWith(".html")) {
return originalReference;
}
href = href.substring(0, href.lastIndexOf('.'));
String reference = createReferenceForRelativeLink(href, myElement);
return reference == null ? href : reference;
}
/**
* Converts a relative link into {@link DocumentationManagerProtocol#PSI_ELEMENT_PROTOCOL PSI_ELEMENT_PROTOCOL}-type link if possible
*/
@Nullable
static String createReferenceForRelativeLink(@NotNull @NonNls String relativeLink, @NotNull PsiElement contextElement) {
String fragment = null;
int hashPosition = relativeLink.indexOf('#');
if (hashPosition >= 0) {
fragment = relativeLink.substring(hashPosition + 1);
relativeLink = relativeLink.substring(0, hashPosition);
}
PsiElement targetElement;
if (relativeLink.isEmpty()) {
targetElement = (contextElement instanceof PsiField || contextElement instanceof PsiMethod) ?
((PsiMember)contextElement).getContainingClass() : contextElement;
}
else {
if (!relativeLink.toLowerCase().endsWith(".htm") && !relativeLink.toLowerCase().endsWith(".html")) {
return null;
}
relativeLink = relativeLink.substring(0, relativeLink.lastIndexOf('.'));
String packageName = getPackageName(contextElement);
if (packageName == null) return null;
Couple<String> pathWithPackage = removeParentReferences(Couple.of(relativeLink, packageName));
if (pathWithPackage == null) return null;
relativeLink = pathWithPackage.first;
packageName = pathWithPackage.second;
relativeLink = relativeLink.replace('/', '.');
String qualifiedTargetClassName = packageName.isEmpty() ? relativeLink : packageName + "." + relativeLink;
targetElement = JavaPsiFacade.getInstance(contextElement.getProject()).findClass(qualifiedTargetClassName,
contextElement.getResolveScope());
}
if (targetElement == null) return null;
String rawFragment = null;
if (fragment != null && targetElement instanceof PsiClass) {
if (fragment.contains("-") || fragment.contains("(")) {
rawFragment = fragment;
fragment = null; // reference to a method
}
else {
for (PsiField field : ((PsiClass)targetElement).getFields()) {
if (field.getName().equals(fragment)) {
rawFragment = fragment;
fragment = null; // reference to a field
break;
}
}
}
}
return DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL + JavaDocUtil.getReferenceText(targetElement.getProject(), targetElement) +
(rawFragment == null ? "" : ('#' + rawFragment)) +
(fragment == null ? "" : DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL_REF_SEPARATOR + fragment);
}
/**
* Takes a pair of strings representing a relative path and a package name, and returns corresponding pair, where path is stripped of
* leading ../ elements, and package name adjusted correspondingly. Returns <code>null</code> if there are more ../ elements than package
* components.
*/
@Nullable
static Couple<String> removeParentReferences(Couple<String> pathWithContextPackage) {
String path = pathWithContextPackage.first;
String packageName = pathWithContextPackage.second;
while (path.startsWith("../")) {
if (packageName.isEmpty()) return null;
int dotPos = packageName.lastIndexOf('.');
packageName = dotPos < 0 ? "" : packageName.substring(0, dotPos);
path = path.substring(3);
}
return Couple.of(path, packageName);
}
static String getPackageName(PsiElement element) {
String packageName = null;
if (myElement instanceof PsiPackage) {
packageName = ((PsiPackage)myElement).getQualifiedName();
if (element instanceof PsiPackage) {
packageName = ((PsiPackage)element).getQualifiedName();
}
else {
PsiFile file = myElement.getContainingFile();
PsiFile file = element.getContainingFile();
if (file instanceof PsiClassOwner) {
packageName = ((PsiClassOwner)file).getPackageName();
}
}
if (packageName == null) return originalReference;
while (href.startsWith("../")) {
if (packageName.isEmpty()) return originalReference;
int dotPos = packageName.lastIndexOf('.');
packageName = dotPos < 0 ? "" : packageName.substring(0, dotPos);
href = href.substring(3);
}
href = href.replace('/', '.');
String qualifiedName = packageName.isEmpty() ? href : packageName + "." + href;
PsiClass target = JavaPsiFacade.getInstance(myProject).findClass(qualifiedName, myElement.getResolveScope());
if (target == null) return originalReference;
return createLinkWithRef(target, fragment);
return packageName;
}
private String createLinkWithRef(PsiElement psiElement, String ref) {
return DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL + JavaDocUtil.getReferenceText(myProject, psiElement) +
(ref == null ? "" : DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL_REF_SEPARATOR + ref);
}
public boolean generateDocInfoCore (final StringBuilder buffer, final boolean generatePrologueAndEpilogue) {
if (myElement instanceof PsiClass) {
generateClassJavaDoc(buffer, (PsiClass)myElement, generatePrologueAndEpilogue);
@@ -0,0 +1,37 @@
<HTML><base href="placeholder"><style type="text/css"> ul.inheritance {
margin:0;
padding:0;
}
ul.inheritance li {
display:inline;
list-style:none;
}
ul.inheritance li ul.inheritance {
margin-left:15px;
padding-left:15px;
padding-top:1px;
}
</style><!-- ======== START OF CLASS DATA ======== -->
<div class="header">
<div class="subTitle">com.jetbrains</div>
<h2 title="Class ClassWithRefLink" class="title">Class ClassWithRefLink</h2>
</div>
<div class="contentContainer">
<ul class="inheritance">
<li>java.lang.Object</li>
<li>
<ul class="inheritance">
<li>com.jetbrains.ClassWithRefLink</li>
</ul>
</li>
</ul>
<DL><br>
<pre>public class <span class="typeNameLabel">ClassWithRefLink</span>
extends java.lang.Object</pre>
<div class="block"><a href="psi_element://com.jetbrains.Test###someRef">link</a></div>
</li>
</ul>
</div>
<ul class="blockList">
<li class="blockList">
</HTML>
@@ -94,7 +94,15 @@ public class JavaExternalDocumentationTest extends PlatformTestCase {
// We're guessing style of references in javadoc by bytecode version of library class file
// but displaying quick doc should work even if javadoc was generated using a JDK not corresponding to bytecode version
public void testReferenceStyleDoesntMatchBytecodeVersion() throws Exception {
String actualText = getDocumentationText("@com.jetbrains.TestAnnotation(<caret>param = \"foo\") class Foo {}");
doTest("@com.jetbrains.TestAnnotation(<caret>param = \"foo\") class Foo {}");
}
public void testLinkWithReference() throws Exception {
doTest("class Foo { com.jetbrains.<caret>ClassWithRefLink field;}");
}
private void doTest(String text) throws Exception {
String actualText = getDocumentationText(text);
String expectedText = StringUtil.convertLineSeparators(FileUtil.loadFile(getDataFile(getTestName(false) + ".html")));
assertEquals(expectedText, replaceBaseUrlWithPlaceholder(actualText));
}