work in progress on rendering epydoc docstrings to HTML

This commit is contained in:
Dmitry Jemerov
2011-04-07 14:07:26 +02:00
parent 0e221ca277
commit 2a79e8ea20
5 changed files with 166 additions and 81 deletions
@@ -1,6 +1,7 @@
package com.jetbrains.python.documentation;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.xml.util.XmlTagUtilBase;
import org.jetbrains.annotations.Nullable;
/**
@@ -11,6 +12,13 @@ public class EpydocString extends StructuredDocString {
super(docstringText, "@");
}
@Override
public String getDescription() {
final String html = inlineMarkupToHTML(myDescription);
assert html != null;
return html;
}
@Override
@Nullable
public String getReturnType() {
@@ -46,39 +54,101 @@ public class EpydocString extends StructuredDocString {
@Nullable
private static String convertInlineMarkup(String s, boolean toHTML) {
if (s == null) return null;
StringBuilder resultBuilder = new StringBuilder();
appendWithMarkup(s, resultBuilder, toHTML);
return resultBuilder.toString();
MarkupConverter converter = toHTML ? new HTMLConverter() : new MarkupConverter();
converter.appendWithMarkup(s);
return converter.result();
}
private static void appendWithMarkup(String s, StringBuilder resultBuilder, boolean toHTML) {
int pos = 0;
while(true) {
int bracePos = s.indexOf('{', pos);
if (bracePos < 1) break;
char prevChar = s.charAt(bracePos-1);
if (prevChar >= 'A' && prevChar <= 'Z') {
resultBuilder.append(s.substring(pos, bracePos - 1));
int rbracePos = findMatchingEndBrace(s, bracePos);
if (rbracePos < 0) {
pos = bracePos + 1;
break;
}
final String inlineMarkupContent = s.substring(bracePos + 1, rbracePos);
if (toHTML) {
appendInlineMarkup(resultBuilder, prevChar, inlineMarkupContent);
private static class MarkupConverter {
protected final StringBuilder myResult = new StringBuilder();
public void appendWithMarkup(String s) {
int pos = 0;
while(true) {
int bracePos = s.indexOf('{', pos);
if (bracePos < 1) break;
char prevChar = s.charAt(bracePos-1);
if (prevChar >= 'A' && prevChar <= 'Z') {
appendText(s.substring(pos, bracePos - 1));
int rbracePos = findMatchingEndBrace(s, bracePos);
if (rbracePos < 0) {
pos = bracePos + 1;
break;
}
final String inlineMarkupContent = s.substring(bracePos + 1, rbracePos);
appendMarkup(prevChar, inlineMarkupContent);
pos = rbracePos + 1;
}
else {
resultBuilder.append(inlineMarkupContent);
appendText(s.substring(pos, bracePos + 1));
pos = bracePos+1;
}
pos = rbracePos + 1;
}
else {
resultBuilder.append(StringUtil.escapeXml(joinLines(s.substring(pos, bracePos + 1), true)));
pos = bracePos+1;
appendText(s.substring(pos));
}
protected void appendText(String text) {
myResult.append(text);
}
protected void appendMarkup(char markupChar, String markupContent) {
myResult.append(markupContent);
}
public String result() {
return myResult.toString();
}
}
private static class HTMLConverter extends MarkupConverter {
@Override
protected void appendText(String text) {
myResult.append(joinLines(XmlTagUtilBase.escapeString(text, false), true));
}
@Override
protected void appendMarkup(char markupChar, String markupContent) {
if (markupChar == 'U') {
appendLink(markupContent);
return;
}
switch (markupChar) {
case 'I':
appendTagPair(markupContent, "i");
break;
case 'B':
appendTagPair(markupContent, "b");
break;
case 'C':
appendTagPair(markupContent, "code");
break;
default:
myResult.append(StringUtil.escapeXml(markupContent));
break;
}
}
resultBuilder.append(StringUtil.escapeXml(joinLines(s.substring(pos), true)));
private void appendTagPair(String markupContent, final String tagName) {
myResult.append("<").append(tagName).append(">");
appendWithMarkup(markupContent);
myResult.append("</").append(tagName).append(">");
}
private void appendLink(String markupContent) {
String linkText = StringUtil.escapeXml(markupContent);
String linkUrl = linkText;
int pos = markupContent.indexOf('<');
if (pos >= 0 && markupContent.endsWith(">")) {
linkText = StringUtil.escapeXml(markupContent.substring(0, pos).trim());
linkUrl = joinLines(StringUtil.escapeXml(markupContent.substring(pos + 1, markupContent.length() - 1)), false);
}
myResult.append("<a href=\"");
if (!linkUrl.matches("[a-z]+:.+")) {
myResult.append("http://");
}
myResult.append(linkUrl).append("\">").append(linkText).append("</a>");
}
}
private static int findMatchingEndBrace(String s, int bracePos) {
@@ -94,57 +164,23 @@ public class EpydocString extends StructuredDocString {
return -1;
}
private static void appendInlineMarkup(StringBuilder resultBuilder, char markupChar, String markupContent) {
if (markupChar == 'U') {
appendLink(resultBuilder, markupContent);
return;
}
switch (markupChar) {
case 'I':
appendTagPair(resultBuilder, markupContent, "i");
break;
case 'B':
appendTagPair(resultBuilder, markupContent, "b");
break;
case 'C':
appendTagPair(resultBuilder, markupContent, "pre");
break;
default:
resultBuilder.append(StringUtil.escapeXml(markupContent));
break;
}
}
private static void appendTagPair(StringBuilder resultBuilder, String markupContent, final String tagName) {
resultBuilder.append("<").append(tagName).append(">");
appendWithMarkup(markupContent, resultBuilder, true);
resultBuilder.append("</").append(tagName).append(">");
}
private static void appendLink(StringBuilder resultBuilder, String markupContent) {
String linkText = StringUtil.escapeXml(markupContent);
String linkUrl = linkText;
int pos = markupContent.indexOf('<');
if (pos >= 0 && markupContent.endsWith(">")) {
linkText = StringUtil.escapeXml(markupContent.substring(0, pos).trim());
linkUrl = StringUtil.escapeXml(joinLines(markupContent.substring(pos + 1, markupContent.length() - 1), false));
}
resultBuilder.append("<a href=\"");
if (!linkUrl.matches("[a-z]+:.+")) {
resultBuilder.append("http://");
}
resultBuilder.append(linkUrl).append("\">").append(linkText).append("</a>");
}
private static String joinLines(String s, boolean addSpace) {
while(true) {
int lineBreakStart = s.indexOf('\n');
if (lineBreakStart < 0) break;
int lineBreakEnd = lineBreakStart+1;
while(lineBreakEnd < s.length() && s.charAt(lineBreakEnd) == ' ') {
int blankLines = 0;
while(lineBreakEnd < s.length() && (s.charAt(lineBreakEnd) == ' ' || s.charAt(lineBreakEnd) == '\n')) {
if (s.charAt(lineBreakEnd) == '\n') blankLines++;
lineBreakEnd++;
}
s = s.substring(0, lineBreakStart) + (addSpace ? " " : "") + s.substring(lineBreakEnd);
if (addSpace) {
String separator = blankLines > 0 ? "<p>" : " ";
s = s.substring(0, lineBreakStart) + separator + s.substring(lineBreakEnd);
}
else {
s = s.substring(0, lineBreakStart) + s.substring(lineBreakEnd);
}
}
return s;
}
@@ -16,6 +16,10 @@ import com.intellij.util.xmlb.XmlSerializerUtil;
public class PyDocumentationSettings implements PersistentStateComponent<PyDocumentationSettings> {
public String myDocStringFormat = DocStringFormat.PLAIN;
public boolean isEpydocFormat() {
return DocStringFormat.EPYDOC.equals(myDocStringFormat);
}
public static PyDocumentationSettings getInstance(Project project) {
return ServiceManager.getService(project, PyDocumentationSettings.class);
}
@@ -45,10 +45,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -181,8 +178,14 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider impl
return cat;
}
private static @NotNull ChainIterable<String> combUpDocString(Project project, @NotNull String docstring) {
ChainIterable<String> cat = new ChainIterable<String>();
private static @NotNull List<String> combUpDocString(Project project, @NotNull String docstring) {
PyDocumentationSettings documentationSettings = PyDocumentationSettings.getInstance(project);
List<String> result = new ArrayList<String>();
if (documentationSettings.isEpydocFormat()) {
final EpydocString epydocString = new EpydocString(docstring);
result.add(formatStructuredDocString(epydocString));
return result;
}
// detect common indentation
String[] lines = LineTokenizer.tokenize(docstring, false);
Pattern spaces_pat = Pattern.compile("^\\s+");
@@ -217,7 +220,7 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider impl
for (String line : lines) {
if (is_first && spaces_pat.matcher(line).matches()) continue; // ignore all initial whitespace
if (is_first) is_first = false;
else cat.add(BR);
else result.add(BR);
int leadingTabs = 0;
while (leadingTabs < line.length() && line.charAt(leadingTabs) == '\t') {
leadingTabs++;
@@ -225,9 +228,22 @@ public class PythonDocumentationProvider extends QuickDocumentationProvider impl
if (leadingTabs > 0) {
line = StringUtil.repeatSymbol(' ', tabSize * leadingTabs) + line.substring(leadingTabs);
}
cat.add(combUp(line));
result.add(combUp(line));
}
return cat;
return result;
}
private static String formatStructuredDocString(StructuredDocString docString) {
StringBuilder result = new StringBuilder(docString.getDescription());
final List<String> parameters = docString.getParameters();
if (parameters.size() > 0) {
result.append("<br><b>Parameters:</b><br>");
for (String parameter : parameters) {
result.append("<b>").append(parameter).append("</b>: ").append(docString.getParamDescription(parameter)).append("<br>");
}
}
return result.toString();
}
// provides ctrl+Q doc
@@ -1,16 +1,20 @@
package com.jetbrains.python.documentation;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.intellij.openapi.util.text.LineTokenizer;
import com.intellij.openapi.util.text.StringUtil;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
/**
* @author yole
*/
public abstract class StructuredDocString {
protected final String myDescription;
protected final List<String> myParameters = Lists.newArrayList();
protected final Map<String, String> mySimpleTagValues = Maps.newHashMap();
protected final Map<String, Map<String, String>> myArgTagValues = Maps.newHashMap();
@@ -24,13 +28,22 @@ public abstract class StructuredDocString {
protected StructuredDocString(String docstringText, String tagPrefix) {
final String[] lines = LineTokenizer.tokenize(docstringText, false);
int i = 0;
StringBuilder descBuilder = new StringBuilder();
while (i < lines.length) {
String line = lines[i].trim();
if (line.startsWith(tagPrefix)) {
i = parseTag(lines, i, tagPrefix);
}
else {
descBuilder.append(line).append("\n");
}
i++;
}
myDescription = descBuilder.toString();
}
public String getDescription() {
return myDescription;
}
private int parseTag(String[] lines, int index, String tagPrefix) {
@@ -57,6 +70,9 @@ public abstract class StructuredDocString {
myArgTagValues.put(tagName, argValues);
}
argValues.put(arg, value);
if (tagName.equals("param")) {
myParameters.add(arg);
}
}
return index;
}
@@ -72,6 +88,10 @@ public abstract class StructuredDocString {
return argValues == null ? null : argValues.get(argName);
}
public List<String> getParameters() {
return myParameters;
}
@Nullable
public abstract String getReturnType();
@@ -1,12 +1,14 @@
package com.jetbrains.python;
import com.intellij.testFramework.UsefulTestCase;
import com.jetbrains.python.documentation.EpydocString;
import junit.framework.TestCase;
import java.util.List;
/**
* @author yole
*/
public class EpydocStringTest extends TestCase {
public class EpydocStringTest extends UsefulTestCase {
public void testTagValue() {
EpydocString docString = new EpydocString("@rtype: C{str}");
assertEquals("C{str}", docString.getTagValue("rtype"));
@@ -50,6 +52,9 @@ public class EpydocStringTest extends TestCase {
" @param *args: arguments passed to function\n" +
" @param **kwargs: keyword arguments passed to C{function}\n" +
" \"\"\"");
final List<String> params = docString.getParameters();
assertOrderedEquals(params, "euid", "egid", "function", "*args", "**kwargs");
assertEquals("effective UID used to call the function.", docString.getParamDescription("euid"));
assertEquals("effective GID used to call the function.", docString.getParamDescription("egid"));
assertEquals("arguments passed to function", docString.getParamDescription("args"));
@@ -60,7 +65,7 @@ public class EpydocStringTest extends TestCase {
}
public void testCodeToHTML() {
assertEquals("<pre>my_dict={1:2, 3:4}</pre>", EpydocString.inlineMarkupToHTML("C{my_dict={1:2, 3:4}}"));
assertEquals("<code>my_dict={1:2, 3:4}</code>", EpydocString.inlineMarkupToHTML("C{my_dict={1:2, 3:4}}"));
}
public void testUrlToHTML() {
@@ -77,4 +82,8 @@ public class EpydocStringTest extends TestCase {
" it may span} multiple lines."));
}
public void testParagraph() {
assertEquals("foo<p>bar", EpydocString.inlineMarkupToHTML("foo\n\nbar"));
}
}