/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Contributor(s):
*
* The Original Software is NetBeans. The Initial Developer of the Original
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2009 Sun
* Microsystems, Inc. All Rights Reserved.
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*/
package org.netbeans.modules.ruby;
import java.awt.Color;
import java.io.CharConversionException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.rmi.CORBA.Util;
import javax.swing.text.AttributeSet;
import javax.swing.text.StyleConstants;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.api.editor.settings.FontColorSettings;
import org.netbeans.modules.ruby.elements.Element;
import org.netbeans.api.lexer.Language;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.api.ruby.platform.RubyInstallation;
import org.netbeans.modules.csl.spi.GsfUtilities;
import org.netbeans.modules.ruby.elements.ClassElement;
import org.netbeans.modules.ruby.elements.MethodElement;
import org.netbeans.modules.ruby.lexer.RubyCommentTokenId;
import org.netbeans.modules.ruby.lexer.RubyTokenId;
import org.netbeans.spi.lexer.LanguageProvider;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.xml.XMLUtil;
/**
* Digest rdoc-formatter strings and format them as html,
* A bit hacky at the moment, but it tries to understand most
* of the conventions described here:
* http://rdoc.sourceforge.net/doc/index.html
* and produce reasonably similar HTML. It also relies on
* the RubyCommentLexer.
*
* @todo Handle definition-style labelled blocks (I only do tabular ones at this point)
* where you have double brackets: [[foo]]
* @todo Only recognize call-seqs if they are the first pre block in the comment AND
* there is no preceeding text!
* @todo Add italics around class names in the callseqs
* @todo Swing Text often breaks up symbols in tables where it's on the left side; try
* wrapping these in <nobr> tags. (Look at ClassMethods.paginate for example)
* @todo This is broken for the callseq handling of Kernel.raise. It includes the various
* overloaded methods in place.
* @todo When syntax highlighting potential ruby snippets, look for the common "=>" pattern
* and only attempt to tokenize the left hand side (and POSSIBLY) the right hand side)
* Look at the abbrev methods for example. It contains:
* <pre>
* # %w{ car cone }.abbrev #=> { "ca" => "car", "car" => "car",
* # "co" => "cone", "con" => cone",
* # "cone" => "cone" }
* </pre>
* Here I should tokenize the LHS and the RHS separately
*
* @author Tor Norbye
*/
class RDocFormatter {
private boolean inVerbatim;
private boolean inBulletedList;
private boolean inLabelledList;
private boolean inNumberedList;
private boolean inStopDoc;
/**
* Indicates whether we're within an explicit <code>:call-seq:</code> section.
*/
private boolean inCallSeq;
private List<String> code;
private boolean firstVerbatim = true;
private String seqName;
private boolean wroteSignature = false;
/**
* Lines within explicit <code>:call-seq:</code> sections.
*/
private final Set<String> callSeqLines = new HashSet<String>();
/** State during rdoc generation: in suppressed comments (#--) section */
private boolean noComment;
private final StringBuilder sb = new StringBuilder(500);
/** Creates a new instance of RDocFormatter */
public RDocFormatter() {
}
/** Set method name associated with this rdoc, if any. Will be used by
* the rdoc analyzer to highlight method calls in call-seqs, if any.
* (Call-seq are method signatures baked into the rdoc, usually from
* C comments in the standard library.)
*/
public void setSeqName(String seqName) {
this.seqName = seqName;
}
/** Return true if the formatted comment appears to have a signature that was processed */
public boolean wroteSignature() {
return wroteSignature;
}
public void appendLine(String text) {
if (text.equals("#--")) { // NOI18N
noComment = true;
return;
} else if (text.equals("#++")) { // NOI18N
noComment = false;
return;
} else if (text.startsWith(RDocAnalyzer.PARAM_HINT_ARG) ||
text.startsWith(RDocAnalyzer.PARAM_HINT_RETURN)) {
// Don't include param hints in the documentation.
// TODO: Try to include these correlated to the actual parameter list in the logical view.
return;
}
if (noComment) {
return;
}
if (text.startsWith("# ")) {
text = text.substring(2);
} else if (text.equals("#")) { // empty comment line
text = "";
// rdoc starting w/o space, e.g. #Something - stripping of # so that the lexer doesn't
// confuse it for a link (but #\n etc should be processed normally).
} else if (text.startsWith("#") && Character.isLetter(text.charAt(1))) {
text = text.substring(1);
}
// should be :call-seq: as per rdoc rules, but call-seq: seems to be used as well
if (text.trim().endsWith("call-seq:")) {
inCallSeq = true;
sb.append("\n<hr>\n");
return;
} else if (text.trim().length() == 0 && inCallSeq) {
// an empty line after :call-seq: ends it
inCallSeq = false;
}
process(text);
}
private void process(String text) {
if (text.indexOf(":stopdoc") != -1) {
inStopDoc = true;
return;
} else if (text.indexOf(":startdoc") != -1) {
inStopDoc = false;
}
if (inStopDoc) {
return;
}
if (inCallSeq) {
callSeqLines.add(text);
}
// Use the lexer! The following is naive since it will
// do something about URLs, multiplication (*), etc.
if (text.length() == 0) {
finishSection();
int n = sb.length();
if (sb.length() > 1 && sb.charAt(sb.length()-1) == '\n') {
if (GsfUtilities.endsWith(sb, "</pre>\n") || GsfUtilities.endsWith(sb, "</h1>\n") ||
GsfUtilities.endsWith(sb, "</h2>\n") || GsfUtilities.endsWith(sb, "</h3>\n") ||
GsfUtilities.endsWith(sb, "</h4>\n") || GsfUtilities.endsWith(sb, "</h5>\n") ||
GsfUtilities.endsWith(sb, "</ul>\n") || GsfUtilities.endsWith(sb, "</ol>\n") ||
GsfUtilities.endsWith(sb, "</table>\n") || GsfUtilities.endsWith(sb, "<hr>\n")) {
// No need for a separator
return;
}
}
if (sb.length() > 0) {
if (!(n > 4 && GsfUtilities.endsWith(sb, "<br>"))) {
sb.append("<br>");
}
sb.append("<br>");
}
return;
}
if (text.startsWith("* ") || text.startsWith("- ")) { // NOI18N
if (!inBulletedList) {
sb.append("<ul>\n"); // NOI18N
inBulletedList = true;
}
sb.append("<li>"); // NOI18N
appendTokenized(text.substring(text.indexOf(' ') + 1));
return;
} else if (text.matches("^[0-9]+\\.\\s*( .*)?")) {
if (!inNumberedList) {
sb.append("<ol>\n"); // NOI18N
inNumberedList = true;
}
sb.append("<li value=\"");
Matcher m = Pattern.compile("^([0-9]+)\\.\\s*( .*)?").matcher(text);
if (m.matches()) {
sb.append(m.group(1));
}
sb.append("\">"); // NOI18N
int index = text.indexOf(' ');
if (index != -1) {
appendTokenized(text.substring(index + 1));
}
return;
} else if (text.matches("^\\[[\\S]+\\]\\s*( .+)?")) { // NOI18N
// Labelled list: [+foo+] whatever
if (!inLabelledList) {
sb.append("<table>\n"); // NOI18N
inLabelledList = true;
} else {
sb.append("</td></tr>\n"); // NOI18N
}
sb.append("<tr><td valign=\"top\">"); // NOI18N
int index = text.indexOf("]");
appendTokenized(text.substring(1, index)); // label between []'s
sb.append("</td><td>");
appendTokenized(text.substring(index + 1));
return;
} else if (text.matches("^[\\S]+::\\s*( .*)?")) { // NOI18N
// Labelled list: foo::
if (!inLabelledList) {
sb.append("<table>\n"); // NOI18N
inLabelledList = true;
} else {
sb.append("</td></tr>\n"); // NOI18N
}
sb.append("<tr><td valign=\"top\">"); // NOI18N
int index = text.indexOf("::"); // NOI18N
appendTokenized(text.substring(0, index)); // label
sb.append("</td><td>");
appendTokenized(text.substring(index + 2));
return;
} else if (!inBulletedList && !inNumberedList && !inLabelledList &&
text.length() > 0 && Character.isWhitespace(text.charAt(0))) { // Indented text in list is in same paragraph
if (!inVerbatim) {
// Chomp off preceeding <br> to make output leaner
if (GsfUtilities.endsWith(sb, "<br>")) {
sb.setLength(sb.length()-4);
}
inVerbatim = true;
code = new ArrayList<String>();
}
appendTokenized(text);
return;
} else if (text.startsWith("=")) { // NOI18N
// Generate a heading
// Count ='s
int i = 0;
for (; i < text.length(); i++) {
if (text.charAt(i) != '=') {
break;
}
}
if (i <= 6) {
// Chomp off preceeding <br> to make output leaner
if (GsfUtilities.endsWith(sb, "<br>")) {
sb.setLength(sb.length()-4);
}
sb.append("<h"); // NOI18N
sb.append(Integer.toString(i));
sb.append(">"); // NOI18N
sb.append(text.substring(i));
sb.append("</h"); // NOI18N
sb.append(Integer.toString(i));
sb.append(">\n"); // NOI18N
return;
}
// Normal line with lots of ='s
appendTokenized(text);
return;
} else if (text.startsWith("#---") || (text.startsWith("---"))) { // NOI18N
// Generate a separator
// See if the line contains only -'s
int i = 1;
int n = text.length();
for (; i < n; i++) {
if (text.charAt(i) != '-') {
break;
}
}
if (i == n) {
sb.append("<hr>\n"); // NOI18N
return;
}
appendTokenized(text);
return;
} else {
if (text.startsWith("####")) {
// Generate a separator
// See if the line contains only #'s
int i = 1;
int n = text.length();
for (; i < n; i++) {
if (text.charAt(i) != '#') {
break;
}
}
if (i == n) {
sb.append("<hr>\n");
return;
}
} else if (inVerbatim) {
finishSection();
}
appendTokenized(text);
return;
}
}
private void appendTokenized(String text) {
if (inVerbatim) {
// We need to buffer up the text such that we can lex it as a unit
// (and determine when done with the section if it's code or regular text)
code.add(text);
return;
}
firstVerbatim = false;
appendTokenized(sb, text);
if (inVerbatim) {
sb.append("<br>");
} else {
sb.append(" "); // Ensure adjacent lines are separated by space.
}
}
private void appendTokenized(StringBuilder sb, String text) {
TokenHierarchy hi = TokenHierarchy.create(text, RubyCommentTokenId.language());
TokenSequence ts = hi.tokenSequence();
// If necessary move ts to the requested offset
int offset = 0;
ts.move(offset);
if (ts.moveNext()) {
do {
Token t = ts.token();
if ((t.id() == RubyCommentTokenId.COMMENT_TEXT) ||
(t.id() == RubyCommentTokenId.COMMENT_TODO)) {
try {
String s = t.text().toString();
s = XMLUtil.toElementContent(s);
if (s.indexOf("---") != -1) {
s = s.replace("---", "—");
}
sb.append(s);
} catch (CharConversionException cce) {
Exceptions.printStackTrace(cce);
}
} else if (t.id() == RubyCommentTokenId.COMMENT_HTMLTAG) {
String s = t.text().toString();
char c = s.charAt(0);
if (c == '+') {
sb.append("<tt>");
sb.append(s.substring(1, s.length() - 1));
sb.append("</tt>");
} else {
sb.append(s);
}
} else if (t.id() == RubyCommentTokenId.COMMENT_LINK) {
String s = t.text().toString();
sb.append("<a href=\"");
sb.append(s);
sb.append("\">");
if (s.startsWith("#")) {
s = s.substring(1); // Chop off leading #
// Method reference
// TODO - generate special URL for local methods here?
}
sb.append(s);
sb.append("</a>");
} else if (t.id() == RubyCommentTokenId.COMMENT_ITALIC) {
sb.append("<i>");
String s = t.text().toString();
char c = s.charAt(0);
if (c == '_') {
sb.append(s.substring(1, s.length() - 1));
} else {
sb.append(t.text());
}
sb.append("</i>");
} else if (t.id() == RubyCommentTokenId.COMMENT_BOLD) {
sb.append("<b>");
String s = t.text().toString();
char c = s.charAt(0);
if (c == '*') {
sb.append(s.substring(1, s.length() - 1));
} else {
sb.append(t.text());
}
sb.append("</b>");
} else if (t.id() == RubyCommentTokenId.COMMENT_RDOC) {
// Do nothing - swallow these so they don't show up in the html
}
} while (ts.moveNext());
}
}
private void finishSection() {
if (inVerbatim) {
boolean addHr = false;
if ((code != null) && (code.size() > 0)) {
if (formatAsRuby(code)) {
// Process code and format as Ruby
String html = getRubyHtml(code);
if (html != null) {
// <pre> tag is added as part of the rubyhtml (since it
// needs to pick up the background color from the syntax
// coloring settings)
sb.append(html);
} else {
sb.append("<pre>\n"); // NOI18N
// Some kind of error; normal append
for (String s : code) {
try {
sb.append(XMLUtil.toElementContent(s));
} catch (CharConversionException cce) {
Exceptions.printStackTrace(cce);
}
sb.append("<br>"); // NOI18N
}
sb.append("</pre>\n"); // NOI18N
}
} else {
sb.append("<pre>\n"); // NOI18N
if (isCallSeq(code)) {
String html = getCallSeqHtml(code);
sb.append(html);
addHr = true;
wroteSignature = true;
} else {
for (String s : code) {
appendTokenized(sb, s);
sb.append("<br>"); // NOI18N
}
}
sb.append("</pre>\n"); // NOI18N
}
code = null;
}
if (addHr) {
sb.append("<hr>\n"); // NOI18N
}
inVerbatim = false;
firstVerbatim = false;
}
if (inBulletedList) {
sb.append("</ul>\n"); // NOI18N
inBulletedList = false;
}
if (inNumberedList) {
sb.append("</ol>\n"); // NOI18N
inBulletedList = false;
}
if (inLabelledList) {
sb.append("</td></tr>\n</table>\n"); // NOI18N
inLabelledList = false;
}
}
public String toHtml() {
finishSection();
return sb.toString();
}
@SuppressWarnings("unchecked")
private String getRubyHtml(List<String> source) {
StringBuilder ruby = new StringBuilder(500);
for (String s : source) {
ruby.append(s);
ruby.append("\n"); // NOI18N
}
Language<?> language = RubyTokenId.language();
String mimeType = RubyInstallation.RUBY_MIME_TYPE;
if (ruby.indexOf(" <%") != -1) { // NOI18N
mimeType = "application/x-httpd-eruby"; // RHTML
Collection<LanguageProvider> providers = (Collection<LanguageProvider>) Lookup.getDefault().lookupAll(LanguageProvider.class);
for (LanguageProvider provider : providers) {
language = provider.findLanguage(mimeType);
if (language != null) {
break;
}
}
if (language == null) {
mimeType = RubyInstallation.RUBY_MIME_TYPE;
language = RubyTokenId.language();
}
} else if (source.get(0).trim().startsWith("<")) {
// Looks like markup (other than RHTML) - don't colorize it
// since we don't know how
return null;
}
StringBuilder buffer = new StringBuilder(1500);
boolean errors = appendSequence(buffer, ruby.toString(), language, mimeType, true);
// TODO: See
// link_to_unless_current
// in ActionView - it doesn't get highlighted right. Perhaps I should
// retry rendering with RHTML if I see errors in Ruby handling? (and it
// looks like it starts with HTML?)
// Another common pattern seems to be a Ruby call (which should be shown as
// Ruby) followed by a "=>" (indicating result) followed by something
// which should be treated as RHTML/HTML. Perhaps I can split my output
// processing?
return errors ? null : buffer.toString();
}
@SuppressWarnings("unchecked")
private boolean appendSequence(StringBuilder sb, String text,
Language<?> language, String mimeType, boolean addPre) {
// XXX is this getting called twice?
MimePath mimePath = MimePath.parse(mimeType);
Lookup lookup = MimeLookup.getLookup(mimePath);
FontColorSettings fcs = lookup.lookup(FontColorSettings.class);
if (addPre) {
sb.append("<pre style=\""); // NOI18N
AttributeSet attribs = fcs.getTokenFontColors("default"); // NOI18N
Color fg = (Color)attribs.getAttribute(StyleConstants.Foreground);
if (fg != null) {
sb.append("color:"); // NOI18N
sb.append(getHtmlColor(fg));
sb.append(";"); // NOI18N
}
Color bg = (Color)attribs.getAttribute(StyleConstants.Background);
// Only set the background for dark colors
if (bg != null && bg.getRed() < 128) {
sb.append("background:"); // NOI18N
sb.append(getHtmlColor(bg));
}
sb.append("\">\n"); // NOI18N
}
TokenHierarchy hi = TokenHierarchy.create(text, language);
TokenSequence ts = hi.tokenSequence();
int offset = 0;
ts.move(offset);
if (ts.moveNext()) {
do {
Token t = ts.token();
String tokenText = t.text().toString();
// TODO - make style classes instead of inlining everything as font!
String category = t.id().name();
String primaryCategory = t.id().primaryCategory();
if ("error".equals(primaryCategory)) { // NOI18N
// Abort: an error token means the output probably isn't
// code, or it's code or markup but in a different language
// than we're trying to process it as
return true;
}
AttributeSet attribs = fcs.getTokenFontColors(category);
String escapedText = tokenText;
try {
escapedText = XMLUtil.toElementContent(tokenText);
} catch (CharConversionException cce) {
Exceptions.printStackTrace(cce);
}
if (attribs == null) {
category = primaryCategory;
attribs = fcs.getTokenFontColors(category);
}
TokenSequence embedded = ts.embedded();
if (embedded != null) {
//embedded.languagePath().mimePath();
String embeddedMimeType = MimePath.parse(embedded.languagePath().mimePath()).getPath();
Color bg = null;
Color fg = null;
if (attribs != null) {
bg = (Color)attribs.getAttribute(StyleConstants.Background);
fg = (Color)attribs.getAttribute(StyleConstants.Foreground);
if (fg != null || bg != null) {
sb.append("<span style=\"");
if (bg != null) {
sb.append("background:"); // NOI18N
sb.append(getHtmlColor(bg));
sb.append(";");
}
if (fg != null) {
sb.append("color:"); // NOI18N
sb.append(getHtmlColor(fg));
}
sb.append("\">"); // NOI18N
}
}
appendSequence(sb, tokenText, embedded.language(), embeddedMimeType, false);
if (fg != null || bg != null) {
sb.append("</span>"); // NOI18N
}
continue;
}
if (attribs == null) {
sb.append(escapedText);
continue;
}
if (escapedText.indexOf('\n') != -1) {
escapedText = escapedText.replace("\n", "<br>"); // NOI18N
}
if (t.id() == RubyTokenId.WHITESPACE) {
sb.append(escapedText);
} else {
sb.append("<span style=\""); // NOI18N
Color fg = (Color)attribs.getAttribute(StyleConstants.Foreground);
if (fg != null) {
sb.append("color:"); // NOI18N
sb.append(getHtmlColor(fg));
sb.append(";"); // NOI18N
}
Color bg = (Color)attribs.getAttribute(StyleConstants.Background);
if (bg != null) {
sb.append("background:"); // NOI18N
sb.append(getHtmlColor(bg));
sb.append(";"); // NOI18N
}
Boolean b = (Boolean)attribs.getAttribute(StyleConstants.Bold);
if ((b != null) && b.booleanValue()) {
sb.append("font-weight:bold;"); // NOI18N
}
b = (Boolean)attribs.getAttribute(StyleConstants.Italic);
if ((b != null) && b.booleanValue()) {
sb.append("font-style:italic;"); // NOI18N
}
// TODO - underline, strikethrough, ... and FONTS!
sb.append("\">"); // NOI18N
sb.append(escapedText);
sb.append("</span>"); // NOI18N
}
} while (ts.moveNext());
}
if (addPre) {
sb.append("</pre>\n");
}
return false;
}
/**
* Call seq: try to format the call seq in a special
* way: bold the call-seq name, and also left justify all
*/
private String getCallSeqHtml(List<String> code) {
StringBuilder callSeqSb = new StringBuilder();
// First determine how much to truncate from the left hand side
int min = Integer.MAX_VALUE;
for (String s : code) {
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// TODO - make this work better for Tabs!
if (c != ' ') {
if (i < min) {
min = i;
}
break;
}
}
}
if (min == Integer.MAX_VALUE) {
min = 0;
}
for (String s : code) {
// Truncate shared left hand side
if (min > 0 && s.length() >= min) {
s = s.substring(min);
}
// Attempt
if (seqName != null) {
int index = s.indexOf(seqName);
if (index != -1 && (s.length() > index+seqName.length())) {
char c = s.charAt(index+seqName.length());
if (!(c == ' ' || c == '(' || c == '{' || c == '[')) {
index = -1;
}
}
if (index != -1) {
String lhs = s.substring(0,index);
String rhs = ""; // NOI18N
if (s.length() > index+seqName.length()) {
rhs = s.substring(index+seqName.length());
}
try {
callSeqSb.append(XMLUtil.toElementContent(lhs));
callSeqSb.append("<b>"); // NOI18N
callSeqSb.append(XMLUtil.toElementContent(seqName));
callSeqSb.append("</b>"); // NOI18N
callSeqSb.append(XMLUtil.toElementContent(rhs));
callSeqSb.append("<br>"); // NOI18N
} catch (CharConversionException cce) {
Exceptions.printStackTrace(cce);
}
continue;
}
}
appendTokenized(callSeqSb, s);
callSeqSb.append("<br>"); // NOI18N
}
return callSeqSb.toString();
}
public String getSignature(Element element) {
StringBuilder signature = new StringBuilder();
// TODO:
signature.append("<pre>");
if (element instanceof MethodElement) {
MethodElement executable = (MethodElement)element;
if (element.getIn() != null) {
String in = element.getIn();
signature.append("<i>");
signature.append(in);
signature.append("</i>");
signature.append("<br>");
}
// TODO - share this between Navigator implementation and here...
signature.append("<b>");
signature.append(element.getName());
signature.append("</b>");
Collection<String> parameters = executable.getParameters();
if ((parameters != null) && (parameters.size() > 0)) {
signature.append("(");
signature.append("<font color=\"#808080\">");
for (Iterator<String> it = parameters.iterator(); it.hasNext();) {
String ve = it.next();
// TODO - if I know types, list the type here instead. For now, just use the parameter name instead
signature.append(ve);
if (it.hasNext()) {
signature.append(", ");
}
}
signature.append("</font>");
signature.append(")");
}
} else if (element instanceof ClassElement) {
ClassElement clz = (ClassElement)element;
String name = element.getName();
final String fqn = clz.getFqn();
if (fqn != null && !name.equals(fqn)) {
signature.append("<i>");
signature.append(fqn);
signature.append("</i>");
signature.append("<br>");
}
signature.append("<b>");
signature.append(name);
signature.append("</b>");
} else {
signature.append(element.getName());
}
RubyType type = getElementType(element);
if (type != null && type.isKnown()) {
signature.append("<br>");
signature.append("<i>");
signature.append(NbBundle.getMessage(RDocFormatter.class, "InferredType"));
signature.append(" ");
signature.append(type.asString(", ", " " + NbBundle.getMessage(RDocFormatter.class, "Or") + " "));
signature.append("</i>");
}
signature.append("</pre>\n");
return signature.toString();
}
private RubyType getElementType(Element element) {
// best not to show the inferred type for 'new' at all as ATM we can't
// infer it correctly (it's inferred as Object, the return type of Class#new)
if (element instanceof MethodElement && "new".equals(element.getName())) {
return RubyType.unknown();
}
return element.getType();
}
private static String getHtmlColor(Color c) {
int r = c.getRed();
int g = c.getGreen();
int b = c.getBlue();
StringBuffer result = new StringBuffer();
result.append('#');
String rs = Integer.toHexString(r);
String gs = Integer.toHexString(g);
String bs = Integer.toHexString(b);
if (r < 0x10) {
result.append('0');
}
result.append(rs);
if (g < 0x10) {
result.append('0');
}
result.append(gs);
if (b < 0x10) {
result.append('0');
}
result.append(bs);
return result.toString();
}
private boolean isCallSeq(List<String> source) {
// in an explicit :call-seq:
if (callSeqLines.containsAll(source)) {
return true;
}
// See if it looks like a call seq - check the first line
// TODO: MAke the code be a List<String> instead!!
if (firstVerbatim) {
String first = source.get(0);
if (first.indexOf("=>") != -1 || first.indexOf("->") != -1) { // NOI18N
return true;
}
}
return false;
}
private boolean formatAsRuby(List<String> source) {
// Check for "---" to see if the preformatted text is a table
// Avoid looking at call-seq lines (first preformatted section, in case it contains => or ->
if (isCallSeq(source)) {
return false;
}
for (String s : source) {
// ASCII formatted tables such as the File.fnmatch
if (s.indexOf("---") != -1) { // NOI18N
return false;
}
if (s.indexOf(" | ") != -1) { // NOI18N
return false;
}
}
return true;
}
}