package com.intellij.javascript.flex;
import com.intellij.codeInsight.documentation.AbstractExternalFilter;
import com.intellij.codeInsight.documentation.DocumentationManagerProtocol;
import com.intellij.codeInsight.documentation.PlatformDocumentationUtil;
import com.intellij.ide.BrowserUtil;
import com.intellij.javascript.flex.resolve.ActionScriptClassResolver;
import com.intellij.lang.actionscript.psi.impl.ActionScriptFunctionImpl;
import com.intellij.lang.actionscript.psi.impl.ActionScriptVariableImpl;
import com.intellij.lang.javascript.documentation.JSDocumentationBuilder;
import com.intellij.lang.javascript.documentation.JSDocumentationProvider;
import com.intellij.lang.javascript.flex.XmlBackedJSClassImpl;
import com.intellij.lang.javascript.index.JavaScriptIndex;
import com.intellij.lang.javascript.psi.*;
import com.intellij.lang.javascript.psi.ecmal4.*;
import com.intellij.lang.javascript.psi.impl.JSOffsetBasedImplicitElement;
import com.intellij.lang.javascript.psi.impl.JSPsiImplUtils;
import com.intellij.lang.javascript.psi.jsdoc.impl.JSDocReferenceSet;
import com.intellij.lang.javascript.psi.resolve.ActionScriptResolveUtil;
import com.intellij.lang.javascript.psi.resolve.JSResolveUtil;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.io.StreamUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.JarFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileSystem;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FlexDocumentationProvider extends JSDocumentationProvider {
private static final Pattern ourMainContentDiv = Pattern.compile("<div.*?class=\"MainContent\">", Pattern.CASE_INSENSITIVE);
private static final Pattern ourDetailBodyDiv = Pattern.compile("<div.*?class=\"detailBody\">", Pattern.CASE_INSENSITIVE);
private static final Pattern ourEventDetailTd = Pattern.compile("<td.*?class=\"summaryTableDescription\">", Pattern.CASE_INSENSITIVE);
private static final Pattern ourOpeningDiv = Pattern.compile("<div.*?>", Pattern.CASE_INSENSITIVE);
private static final Pattern ourClosingDiv = Pattern.compile("</div>", Pattern.CASE_INSENSITIVE);
private static final Pattern ourOpeningTd = Pattern.compile("<td.*?>", Pattern.CASE_INSENSITIVE);
private static final Pattern ourClosingTd = Pattern.compile("</td>", Pattern.CASE_INSENSITIVE);
private static final Pattern ourSeeAlsoDiv = Pattern
.compile("<p>[ \r\n\t]*?<span .*?>See also</span>[ \r\n\t]*?</p>[ \r\n\t]*?<div .*?class=\"seeAlso\".*?>", Pattern.CASE_INSENSITIVE);
private static final Pattern ourLabelSpan =
Pattern.compile("<span .*?class=\"label\".*?>(.*?)</span>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
private static final Pattern[] ourStrip =
new Pattern[]{Pattern.compile("<span[^>]*style=\"display:none\"[^>]*>.*?</span>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL),
Pattern.compile("<p></p>", Pattern.CASE_INSENSITIVE), Pattern.compile("<hr>", Pattern.CASE_INSENSITIVE),
Pattern.compile("[ ]?class=\".*?\"", Pattern.CASE_INSENSITIVE), Pattern.compile("<!--.*?-->"),
Pattern.compile("<script.*?</script>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL),
Pattern.compile("<a[^>]*onclick=[^>]*>.*?</a>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL)};
private static final Pattern ourLinebreakInText = Pattern.compile("([^ \t\r\n>][ \t]*)([\r\n]+[ \t]*[^ \t\r\n<])");
private static final Pattern ourLinebreakBeforeCode =
Pattern.compile("(</code>[ \t]*?)([\r\n]+[ \t]*?[^ \t\r\n<])", Pattern.CASE_INSENSITIVE);
private static final Pattern ourLinebreakAfterCode =
Pattern.compile("([^ \t\r\n>][ \t]*)([\r\n]+[ \t]*?<code>)", Pattern.CASE_INSENSITIVE);
private static final String ourInsertBr = "$1<br>$2";
private static final String DISPLAY_NAME_MARKER = "$$$$DISPLAY_NAME$$$$";
private final Pattern ourDetailHeaderTable =
Pattern.compile("<table .*?class=\"classHeaderTable\".*?>.*?</table>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
private static final Pattern ourClassHeaderTable =
Pattern.compile("<table .*?class=\"classHeaderTable\".*?>.*?</table>", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
private static final Pattern ourLinkPattern = Pattern.compile("<a.*?href=\"(.*?)\".*?>(.*?)</a>", Pattern.CASE_INSENSITIVE);
private static @NonNls final Pattern ourHREFselector = Pattern.compile("<a.*?href=\"([^>\"]*)\"", Pattern.CASE_INSENSITIVE);
private static @NonNls final Pattern ourIMGselector =
Pattern.compile("<img.*?src=\"([^>\"]*)\"", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
@NonNls private static final String PACKAGE = "package";
@NonNls private static final String HTML_EXTENSION = ".html";
@NonNls private static final String PACKAGE_FILE = PACKAGE + HTML_EXTENSION;
private static final Map<String, String> DOCUMENTED_ATTRIBUTES;
static {
DOCUMENTED_ATTRIBUTES = new THashMap<>();
DOCUMENTED_ATTRIBUTES.put("Event", "event:");
DOCUMENTED_ATTRIBUTES.put("Style", "style:");
DOCUMENTED_ATTRIBUTES.put("Effect", "effect:");
}
@Override
public String generateDoc(PsiElement _element, PsiElement originalElement) {
String doc = super.generateDoc(_element, originalElement);
if (doc != null) {
return doc;
}
if (_element instanceof JSOffsetBasedImplicitElement) _element = ((JSOffsetBasedImplicitElement)_element).getElementAtOffset();
XmlTag parent = null;
if (_element instanceof XmlBackedJSClassImpl) {
parent = ((XmlBackedJSClassImpl)_element).getParent();
} else if (_element instanceof XmlToken) {
parent = PsiTreeUtil.getParentOfType(_element, XmlTag.class);
}
if (parent != null) {
PsiElement prev = PsiTreeUtil.prevLeaf(parent);
while(prev instanceof PsiWhiteSpace || (prev instanceof XmlComment && !prev.getText().startsWith("<!---"))) {
prev = PsiTreeUtil.prevLeaf(prev);
if (prev instanceof XmlToken) prev = prev.getParent();
}
if (prev instanceof XmlComment) {
return doGetCommentTextFromComment((PsiComment)prev, originalElement);
}
}
final PsiElement elementToShowDoc = findElementToShowDoc(_element);
AbstractExternalFilter docFilter = new AbstractExternalFilter() {
private final RefConvertor[] myReferenceConvertors = new RefConvertor[]{new RefConvertor(ourHREFselector) {
protected String convertReference(String origin, String link) {
if (BrowserUtil.isAbsoluteURL(link)) {
return link;
}
String resolved = getSeeAlsoLinkResolved(elementToShowDoc, link);
if (resolved != null) {
return DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL + resolved;
}
String originFile = ourAnchorSuffix.matcher(origin).replaceAll("");
if (StringUtil.startsWithChar(link, '#')) {
return originFile + link;
}
else {
String originPath = originFile.contains("/") ? originFile.substring(0, originFile.lastIndexOf("/")) : originFile;
return doAnnihilate(originPath + "/" + link);
}
}
}, new RefConvertor(ourIMGselector) {
protected String convertReference(String root, String href) {
if (StringUtil.startsWithChar(href, '#')) {
return root + href;
}
if (root.startsWith("file://") && SystemInfo.isWindows) {
root = "file:///" + root.substring("file://".length());
}
return doAnnihilate(ourHtmlFileSuffix.matcher(root).replaceAll("/") + href);
}
}
};
@Override
protected AbstractExternalFilter.RefConvertor[] getRefConverters() {
return myReferenceConvertors;
}
@Override
public String getExternalDocInfoForElement(final String docURL, final PsiElement element) throws Exception {
String result = super.getExternalDocInfoForElement(docURL, element);
if (StringUtil.isNotEmpty(result)) {
result = result.replace(DISPLAY_NAME_MARKER, ApplicationManager.getApplication().runReadAction(new Computable<CharSequence>() {
public CharSequence compute() {
return getDisplayName(element);
}
}));
}
return result;
}
@Override
protected void doBuildFromStream(String url, Reader reader, StringBuilder result) throws IOException {
String input = StreamUtil.readTextFrom(reader);
Matcher anchorMatcher = ourAnchorSuffix.matcher(url);
final int startOffset;
Pair<Pattern, Pattern> mainContentPatterns = Pair.create(ourOpeningDiv, ourClosingDiv);
if (anchorMatcher.find()) {
String name = anchorMatcher.group(1);
Pattern detailPattern = ourDetailBodyDiv;
for (Map.Entry<String, String> e : DOCUMENTED_ATTRIBUTES.entrySet()) {
if (name.startsWith(e.getValue())) {
if (!"Event".equals(e.getKey())) {
detailPattern = ourEventDetailTd;
mainContentPatterns = Pair.create(ourOpeningTd, ourClosingTd);
}
break;
}
}
name = name.replaceAll("\\)", "\\\\)").replaceAll("\\(", "\\\\(");
Matcher m = Pattern.compile("<a name=\"" + name + "\"").matcher(input);
if (!m.find()) {
return;
}
int offset = m.end();
m = detailPattern.matcher(input);
if (!m.find(offset)) {
return;
}
startOffset = m.start();
}
else {
Matcher m = ourMainContentDiv.matcher(input);
if (!m.find()) {
return;
}
startOffset = m.start();
}
TextRange description =
getRangeBetweenNested(input, new TextRange(startOffset, input.length()), mainContentPatterns.first, mainContentPatterns.second);
if (description == null) {
return;
}
Matcher m = ourSeeAlsoDiv.matcher(input);
final TextRange seeAlso;
if (findIn(m, description)) {
seeAlso = getRangeBetweenNested(input, new TextRange(m.start(), description.getEndOffset()), ourOpeningDiv, ourClosingDiv);
description = new TextRange(description.getStartOffset(), m.start());
}
else {
seeAlso = null;
}
String text = description.substring(input);
text = ourDetailHeaderTable.matcher(text).replaceAll("");
text = ourClassHeaderTable.matcher(text).replaceAll("");
result.append(HTML).append("<PRE><b>").append(DISPLAY_NAME_MARKER);
result.append("</b></PRE>");
result.append(prettyPrint(text));
if (seeAlso != null) {
result.append("<DL><DT><b>See also:</b></DT>");
int pos = seeAlso.getStartOffset();
Matcher br = Pattern.compile("<br/?>", Pattern.CASE_INSENSITIVE).matcher(input);
while (findIn(br, new TextRange(pos, seeAlso.getEndOffset()))) {
TextRange item = new TextRange(pos, br.start());
result.append("<DD>").append(makeLink(item.substring(input))).append("</DD>");
pos = br.end();
}
result.append("<DD>").append(makeLink(input.substring(pos, seeAlso.getEndOffset()))).append("</DD></DL>");
}
result.append(HTML_CLOSE);
}
};
for (String docURL : findUrls(elementToShowDoc)) {
try {
String javadoc = docFilter.getExternalDocInfoForElement(docURL, elementToShowDoc);
if (StringUtil.isNotEmpty(javadoc)) {
return javadoc;
}
}
catch (Exception e) {
//try next url
}
}
return null;
}
@NotNull
private static PsiElement findElementToShowDoc(PsiElement _element) {
PsiElement navigationElement = findNavigationElement(_element);
if (navigationElement == null) {
if (_element instanceof JSQualifiedNamedElement) {
navigationElement = findTopLevelNavigationElement((JSQualifiedNamedElement)_element);
}
else {
navigationElement = _element.getNavigationElement();
}
}
return navigationElement;
}
@Nullable
private static PsiElement findNavigationElement(PsiElement element) {
JSQualifiedNamedElement parentQualifiedElement = findParentQualifiedElement(element);
if (parentQualifiedElement == null) {
return null;
}
PsiElement navElement = findTopLevelNavigationElement(parentQualifiedElement);
if (element instanceof JSClass) {
return navElement;
}
if (element instanceof JSFunction && JSResolveUtil.findParent(element) instanceof JSClass && navElement instanceof JSClass) {
return ((JSClass)navElement).findFunctionByNameAndKind(((JSFunction)element).getName(), ((JSFunction)element).getKind());
}
if (element instanceof JSVariable && JSResolveUtil.findParent(element) instanceof JSClass && navElement instanceof JSClass) {
return ((JSClass)navElement).findFieldByName(((JSVariable)element).getName());
}
JSAttribute attribute = null;
if (element instanceof JSAttributeNameValuePair) {
attribute = (JSAttribute)element.getParent();
}
if (element instanceof JSAttribute) {
attribute = (JSAttribute)element;
}
if (attribute != null && navElement instanceof JSClass) {
final String type = attribute.getName();
if (DOCUMENTED_ATTRIBUTES.containsKey(type)) {
JSAttributeNameValuePair namePair = attribute.getValueByName("name");
if (namePair != null) {
return findNamedAttribute((JSClass)navElement, type, namePair.getSimpleValue());
}
}
}
if (element.getClass() == navElement.getClass()) {
return navElement;
}
return null;
}
@NotNull
public static PsiElement findTopLevelNavigationElement(JSQualifiedNamedElement element) {
if (element.getName() == null) return element;
final Ref<JSQualifiedNamedElement> withAsdoc = new Ref<>();
final PsiElement sourceElement =
JSPsiImplUtils.findTopLevelNavigatableElementWithSource(element, candidate -> {
if (withAsdoc.isNull()) {
ProjectFileIndex projectFileIndex = ProjectRootManager.getInstance(candidate.getProject()).getFileIndex();
String relPath = getAsDocRelativePath(candidate);
final PsiFile file = candidate.getContainingFile();
if (file == null) {
return;
}
VirtualFile containingFile = file.getVirtualFile();
if (containingFile == null || projectFileIndex.getClassRootForFile(containingFile) == null) {
return;
}
final List<OrderEntry> orderEntries = projectFileIndex.getOrderEntriesForFile(containingFile);
for (OrderEntry orderEntry : orderEntries) {
String[] roots = JavadocOrderRootType.getUrls(orderEntry);
if (PlatformDocumentationUtil.getHttpRoots(correctHttpRoots(roots), relPath) != null) {
withAsdoc.set(candidate);
}
}
}
});
if (sourceElement != null) {
return sourceElement;
}
if (!withAsdoc.isNull()) {
return withAsdoc.get();
}
return element;
}
@Override
@Nullable
public List<String> getUrlFor(PsiElement element, PsiElement originalElement) {
List<String> url = super.getUrlFor(element, originalElement);
if (url != null) {
return url;
}
final PsiElement elementToShowDoc = findElementToShowDoc(element);
final List<String> urls = findUrls(elementToShowDoc);
return urls != null && !urls.isEmpty() ? urls : null;
}
private static String makeLink(String input) {
input = input.trim();
final Matcher link = ourLinkPattern.matcher(input);
if (link.matches()) {
String href = link.group(1);
return "<a href=\"" + href + "\">" + link.group(2) + "</a>";
}
else {
if (StringUtil.containsAnyChar(input, SEE_PLAIN_TEXT_CHARS)) {
if (input.indexOf("\"") == input.length() - 1) {
input = input.substring(0, input.length() - 1); // asdoc may generate trailing quote
}
return input;
}
else {
input = StringUtil.escapeXml(input);
return "<a href=\"" + input + "\">" + input + "</a>";
}
}
}
private static String prettyPrint(String text) {
text = ourLabelSpan.matcher(text).replaceAll("<br><b>$1</b>");
for (Pattern pattern : ourStrip) {
text = pattern.matcher(text).replaceAll("");
}
text = replaceInPlainText(text, ourInsertBr, ourLinebreakInText);
text = replaceInPlainText(text, ourInsertBr, ourLinebreakAfterCode);
text = replaceInPlainText(text, ourInsertBr, ourLinebreakBeforeCode);
return text;
}
private static String replaceInPlainText(String input, String replacement, Pattern p) {
Matcher m = p.matcher(input);
boolean result = m.find();
int prevpos = 0;
boolean isInTag = false;
boolean isInPre = false;
if (result) {
StringBuffer sb = new StringBuffer();
do {
String s = input.substring(prevpos, m.start());
isInTag = endIsBetween(s, "<", ">", isInTag);
isInPre = endIsBetween(s, "<pre>", "</pre>", isInPre);
prevpos = m.end();
m.appendReplacement(sb, isInTag || isInPre ? "$0" : replacement);
result = m.find();
}
while (result);
m.appendTail(sb);
return sb.toString();
}
return input;
}
private static boolean endIsBetween(String s, String open, String close, boolean defaultValue) {
int openPos = s.lastIndexOf(open);
int closePos = s.lastIndexOf(close);
if (openPos == -1) {
return closePos == -1 && defaultValue;
}
else {
return closePos == -1 || closePos < openPos;
}
}
private static String getAsDocRelativePath(JSQualifiedNamedElement element) {
String qName = element.getQualifiedName();
if (qName == null) qName = "";
final String shortName = element instanceof JSClass ? StringUtil.getShortName(qName) : PACKAGE;
String packageName = StringUtil.getPackageName(qName);
packageName = packageName.replace('.', '/');
return packageName.length() > 0 ? packageName + "/" + shortName + HTML_EXTENSION : shortName + HTML_EXTENSION;
}
private static List<String> findUrlsForClass(JSQualifiedNamedElement aClass) {
String qName = aClass.getQualifiedName();
if (qName == null) {
return Collections.emptyList();
}
PsiFile file = aClass.getContainingFile();
if (!(file instanceof JSFile)) {
return Collections.emptyList();
}
final PsiFile containingFile = aClass.getContainingFile();
final VirtualFile virtualFile = containingFile.getVirtualFile();
if (virtualFile == null) {
return Collections.emptyList();
}
return findUrlForVirtualFile(containingFile.getProject(), virtualFile, getAsDocRelativePath(aClass));
}
private static List<String> findUrls(PsiElement element) {
JSQualifiedNamedElement clazz = findParentQualifiedElement(element);
if (clazz == null) {
return Collections.emptyList();
}
String anchor = null;
if (element instanceof JSFunction || element instanceof JSVariable) {
anchor = ((JSNamedElement)element).getName();
if (element instanceof JSFunction && !((JSFunction)element).isGetProperty() && !((JSFunction)element).isSetProperty()) {
anchor += "()";
}
}
else if (element instanceof JSAttributeNameValuePair) {
String type = ((JSAttribute)element.getParent()).getName();
final JSAttributeNameValuePair namePair = ((JSAttribute)element.getParent()).getValueByName("name");
if (namePair != null) {
anchor = DOCUMENTED_ATTRIBUTES.get(type) + namePair.getSimpleValue();
}
}
else if (element instanceof JSAttribute) {
final JSAttributeNameValuePair namePair = ((JSAttribute)element).getValueByName("name");
if (namePair != null) {
String type = ((JSAttribute)element).getName();
anchor = DOCUMENTED_ATTRIBUTES.get(type) + namePair.getSimpleValue();
}
}
List<String> urls = findUrlsForClass(clazz);
if (anchor != null) {
List<String> anchored = new ArrayList<>(urls.size());
for (String url : urls) {
anchored.add(url + "#" + anchor);
}
return anchored;
}
else {
return urls;
}
}
private static TextRange getRangeBetweenNested(String input, @Nullable TextRange within, Pattern openPattern, Pattern closePattern) {
Matcher open = openPattern.matcher(input);
Matcher close = closePattern.matcher(input);
if (!open.find(within != null ? within.getStartOffset() : 0)) {
return null;
}
final int contentsStart = open.end();
int nesting = 0;
int pos = contentsStart;
while (true) {
if ((within != null && !within.contains(pos)) || !close.find(pos)) {
return null;
}
int closePos = close.start();
if (open.find(pos) && open.start() < closePos) {
nesting++;
pos = open.end();
}
else {
if (nesting > 0) {
nesting--;
pos = close.end();
}
else {
return new TextRange(contentsStart, closePos);
}
}
}
}
private static boolean findIn(Matcher m, TextRange within) {
return m.find(within.getStartOffset()) && within.contains(m.start());
}
private static String getDisplayName(PsiElement element) {
if (element instanceof JSClass) {
return ((JSClass)element).getQualifiedName();
}
if (element instanceof JSFunction || element instanceof JSVariable) {
final PsiElement parent = JSResolveUtil.findParent(element);
String result;
if (parent instanceof JSClass) {
result = ((JSClass)parent).getQualifiedName() + "." + ((JSNamedElement)element).getName();
}
else {
result = ((JSQualifiedNamedElement)element).getQualifiedName();
}
if (element instanceof JSFunction && !((JSFunction)element).isGetProperty() && !((JSFunction)element).isSetProperty()) {
result += "()";
}
return result;
}
JSAttribute attribute = null;
if (element instanceof JSAttributeNameValuePair) {
attribute = (JSAttribute)element.getParent();
}
if (element instanceof JSAttribute) {
attribute = (JSAttribute)element;
}
if (attribute != null && DOCUMENTED_ATTRIBUTES.containsKey(attribute.getName())) {
final JSClass jsClass = PsiTreeUtil.getParentOfType(element, JSClass.class);
JSAttributeNameValuePair namePair = attribute.getValueByName("name");
if (jsClass != null && namePair != null) {
return attribute.getName() + " " + jsClass.getQualifiedName() + "." + namePair.getSimpleValue();
}
}
return null;
}
@NotNull
private static List<String> findUrlForVirtualFile(final Project project, final VirtualFile virtualFile, final String relPath) {
final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex();
Module module = fileIndex.getModuleForFile(virtualFile);
if (module == null) {
final VirtualFileSystem fs = virtualFile.getFileSystem();
if (fs instanceof JarFileSystem) {
final VirtualFile jar = ((JarFileSystem)fs).getVirtualFileForJar(virtualFile);
if (jar != null) {
module = fileIndex.getModuleForFile(jar);
}
}
}
if (module != null) {
String[] javadocPaths = JavaModuleExternalPaths.getInstance(module).getJavadocUrls();
List<String> httpRoots = PlatformDocumentationUtil.getHttpRoots(correctHttpRoots(javadocPaths), relPath);
if (httpRoots != null) return httpRoots;
}
final List<OrderEntry> orderEntries = fileIndex.getOrderEntriesForFile(virtualFile);
for (OrderEntry orderEntry : orderEntries) {
final String[] files = JavadocOrderRootType.getUrls(orderEntry);
final List<String> httpRoot = PlatformDocumentationUtil.getHttpRoots(correctHttpRoots(files), relPath);
if (httpRoot != null) return httpRoot;
}
return Collections.emptyList();
}
private static String[] correctHttpRoots(String [] roots) {
String[] result = roots.clone();
for (int i = 0; i < result.length; i++) {
if (result[i].startsWith("http://")) {
result[i] = StringUtil.trimEnd(StringUtil.trimEnd(result[i], "index.html"), "index.htm");
}
}
return result;
}
private static String getLinkToResolve(JSQualifiedNamedElement origin, String link) {
String originQname = origin.getQualifiedName();
if (link.length() == 0) {
return originQname;
}
else if (StringUtil.startsWithChar(link, '#')) {
if (origin instanceof JSClass) {
return originQname + "." + link.substring(1);
}
else {
String aPackage = StringUtil.getPackageName(originQname);
return aPackage + "." + link.substring(1);
}
}
else {
String linkFile = link.contains("#") ? link.substring(0, link.lastIndexOf('#')) : link;
String linkAnchor = link.contains("#") ? link.substring(link.lastIndexOf('#') + 1) : null;
final String qname;
if (StringUtil.endsWithIgnoreCase(linkFile, HTML_EXTENSION)) {
String prefix = StringUtil.getPackageName(originQname);
while (linkFile.startsWith("../")) {
linkFile = linkFile.substring("../".length());
prefix = StringUtil.getPackageName(prefix);
}
String linkFilename;
if (linkFile.endsWith(PACKAGE_FILE)) {
linkFilename = StringUtil.trimEnd(linkFile, PACKAGE_FILE);
linkFilename = StringUtil.trimEnd(linkFilename, "/");
}
else {
linkFilename = linkFile.substring(0, linkFile.lastIndexOf("."));
}
if (linkFilename.length() > 0) {
qname = (prefix.length() > 0 ? prefix + "." : prefix) + linkFilename.replaceAll("/", ".");
}
else {
qname = prefix;
}
}
else {
qname = linkFile;
}
return linkAnchor != null ? (qname.length() > 0 ? qname + "." : qname) + linkAnchor : qname;
}
}
@Nullable
private static JSQualifiedNamedElement findParentQualifiedElement(PsiElement element) {
if (element instanceof JSClass) {
return (JSClass)element;
}
if (element instanceof XmlComment) {
PsiElement parent = element.getParent();
if (parent instanceof XmlProlog) {
PsiElement e = parent.getNextSibling();
if (e instanceof XmlTag) {
return XmlBackedJSClassImpl.getContainingComponent((XmlElement)e);
}
}
}
if (element instanceof JSFunction || element instanceof JSVariable) {
final PsiElement parent = JSResolveUtil.findParent(element);
if (parent instanceof JSClass) {
return (JSClass)parent;
}
else if (parent instanceof JSFile || parent instanceof JSPackage) {
return (JSQualifiedNamedElement)element;
}
}
JSAttribute attribute = null;
if (element instanceof JSAttribute) {
attribute = (JSAttribute)element;
} else if (element instanceof JSAttributeNameValuePair) {
attribute = (JSAttribute)element.getParent();
}
if (attribute != null && DOCUMENTED_ATTRIBUTES.containsKey(attribute.getName())) {
final JSClass jsClass = PsiTreeUtil.getParentOfType(element, JSClass.class);
if (jsClass != null) {
return jsClass;
}
}
return null;
}
@Nullable
private static String getSeeAlsoLinkResolved(PsiElement originElement, String link) {
JSQualifiedNamedElement qualifiedElement = findParentQualifiedElement(originElement);
if (qualifiedElement == null) {
return null;
}
String linkToResolve = getLinkToResolve(qualifiedElement, link);
final PsiElement resolvedElement = getDocumentationElementForLinkStatic(originElement.getManager(), linkToResolve, originElement);
if (resolvedElement != null) {
return linkToResolve;
}
return null;
}
@Nullable
public PsiElement getDocumentationElementForLink(final PsiManager psiManager, String link, final PsiElement context) {
return getDocumentationElementForLinkStatic(psiManager, link, context);
}
@Nullable
private static PsiElement getDocumentationElementForLinkStatic(final PsiManager psiManager, String link, final PsiElement context) {
final int delimiterIndex = link.lastIndexOf(':');
final JavaScriptIndex index = JavaScriptIndex.getInstance(psiManager.getProject());
String attributeType = null;
String attributeName = null;
for (Map.Entry<String, String> e : DOCUMENTED_ATTRIBUTES.entrySet()) {
final String pattern = "." + e.getValue();
if (link.contains(pattern)) {
attributeType = e.getKey();
attributeName = StringUtil.substringAfter(link, pattern);
link = link.substring(0, link.indexOf(pattern));
break;
}
}
if (delimiterIndex != -1 && attributeType == null) {
return resolveDocumentLink(psiManager, link, delimiterIndex);
}
else if (attributeType != null) {
PsiElement clazz = ActionScriptClassResolver.findClassByQName(link, index, context != null ? ModuleUtilCore.findModuleForPsiElement(context) : null);
if (!(clazz instanceof JSClass)) {
return null;
}
return findNamedAttribute((JSClass)clazz, attributeType, attributeName);
}
else {
PsiElement clazz = ActionScriptClassResolver.findClassByQName(link, index, context != null ? ModuleUtilCore.findModuleForPsiElement(context) : null);
if (clazz == null && link.contains(".")) {
String qname = link.substring(0, link.lastIndexOf('.'));
clazz = ActionScriptClassResolver.findClassByQName(qname, index, context != null ? ModuleUtilCore.findModuleForPsiElement(context) : null);
if (clazz instanceof JSClass) {
JSClass jsClass = (JSClass)clazz;
String member = link.substring(link.lastIndexOf('.') + 1);
if (member.endsWith("()")) {
member = member.substring(0, member.length() - 2);
PsiElement result = findMethod(jsClass, member);
if (result == null) {
result = findProperty(jsClass, member); // user might refer to a property
}
return result;
}
else {
PsiElement result = jsClass.findFieldByName(member);
if (result == null) {
result = findProperty(jsClass, member);
}
if (result == null) {
result = findMethod(jsClass, member); // user might forget brackets
}
return result;
}
}
}
if (clazz instanceof JSVariable) {
return clazz;
}
if (link.endsWith("()")) {
link = link.substring(0, link.length() - 2);
clazz = ActionScriptClassResolver.findClassByQName(link, index, context != null ? ModuleUtilCore.findModuleForPsiElement(context) : null);
if (clazz instanceof JSFunction) {
return clazz;
}
}
if (clazz == null && context != null) {
final PsiReference[] references = new JSDocReferenceSet(context, link, 0, false).getReferences();
if (references.length > 0) {
final PsiElement resolve = references[references.length - 1].resolve();
if (resolve != null) return resolve;
}
}
return clazz;
}
}
@Nullable
private static JSAttributeNameValuePair findNamedAttribute(JSClass clazz, final String type, final String name) {
final Ref<JSAttributeNameValuePair> attribute = new Ref<>();
ActionScriptResolveUtil.processMetaAttributesForClass(clazz, new ActionScriptResolveUtil.MetaDataProcessor() {
public boolean process(@NotNull JSAttribute jsAttribute) {
if (type.equals(jsAttribute.getName())) {
final JSAttributeNameValuePair jsAttributeNameValuePair = jsAttribute.getValueByName("name");
if (jsAttributeNameValuePair != null && name.equals(jsAttributeNameValuePair.getSimpleValue())) {
attribute.set(jsAttributeNameValuePair);
return false;
}
}
return true;
}
public boolean handleOtherElement(PsiElement el, PsiElement context, @Nullable Ref<PsiElement> continuePassElement) {
return true;
}
});
return attribute.get();
}
private static PsiElement findProperty(JSClass jsClass, String name) {
PsiElement result = jsClass.findFunctionByNameAndKind(name, JSFunction.FunctionKind.GETTER);
if (result == null) {
result = jsClass.findFunctionByNameAndKind(name, JSFunction.FunctionKind.SETTER);
}
return result;
}
private static PsiElement findMethod(JSClass jsClass, String name) {
PsiElement result = jsClass.findFunctionByNameAndKind(name, JSFunction.FunctionKind.CONSTRUCTOR);
if (result == null) {
result = jsClass.findFunctionByNameAndKind(name, JSFunction.FunctionKind.SIMPLE);
}
return result;
}
@Nullable
@Override
public String tryGetSeeAlsoLink(final String remainingLineContent, PsiElement element) {
if (!remainingLineContent.contains(".") && !remainingLineContent.startsWith("#")) {
// first try to find class in the same package, then in default one
JSQualifiedNamedElement qualifiedElement = findParentQualifiedElement(element);
if (qualifiedElement != null) {
String qname = qualifiedElement.getQualifiedName();
String aPackage = qname.contains(".") ? qname.substring(0, qname.lastIndexOf('.') + 1) : "";
String resolvedLink = getSeeAlsoLinkResolved(element, aPackage + remainingLineContent);
if (resolvedLink != null) {
return resolvedLink;
}
}
}
return getSeeAlsoLinkResolved(element, remainingLineContent);
}
@NotNull
@Override
protected JSDocumentationBuilder createDocumentationBuilder(@NotNull PsiElement element, PsiElement _contextElement, boolean showNamedItem) {
return new FlexDocumentationBuilder(element, _contextElement, showNamedItem, this);
}
@Override
protected void appendParentInfo(PsiElement parent, StringBuilder builder, PsiNamedElement element) {
if (parent instanceof JSClass) {
builder.append(((JSClass)parent).getQualifiedName()).append("\n");
}
else if (parent instanceof JSPackageStatement) {
builder.append(((JSPackageStatement)parent).getQualifiedName()).append("\n");
}
else if (parent instanceof JSFile) {
if (parent.getContext() != null) {
final String mxmlPackage = ActionScriptResolveUtil.findPackageForMxml(parent);
if (mxmlPackage != null) {
builder.append(mxmlPackage).append(mxmlPackage.length() > 0 ? "." : "").append(parent.getContext().getContainingFile().getName())
.append("\n");
}
}
else {
boolean foundQualified = false;
if (element instanceof ActionScriptFunctionImpl && ((ActionScriptFunctionImpl)element).hasQualifiedName() ||
element instanceof ActionScriptVariableImpl && ((ActionScriptVariableImpl)element).hasQualifiedName()) {
final JSQualifiedName namespace = ((JSQualifiedNamedElement)element).getNamespace();
assert namespace != null : "null namespace of element having qualified name";
builder.append(namespace.getQualifiedName()).append("\n");
foundQualified = true;
}
if (!foundQualified) builder.append(parent.getContainingFile().getName()).append("\n");
}
}
}
}