package org.rubypeople.rdt.internal.corext.util; import java.io.IOException; import java.util.Collection; import java.util.Collections; import org.eclipse.core.runtime.Path; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Region; import org.jruby.Ruby; import org.jruby.RubyString; import org.jruby.ast.CommentNode; import org.jruby.lexer.yacc.ISourcePosition; import org.jruby.lexer.yacc.SyntaxException; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.util.KCode; import org.rubypeople.rdt.core.IMember; import org.rubypeople.rdt.core.IRubyElement; import org.rubypeople.rdt.core.ISourceRange; import org.rubypeople.rdt.core.RubyCore; import org.rubypeople.rdt.core.RubyModelException; import org.rubypeople.rdt.internal.core.parser.RubyParser; import org.rubypeople.rdt.internal.ui.text.HTMLPrinter; import org.rubypeople.rdt.ui.RubyElementLabels; public class RDocUtil { private final static long LABEL_FLAGS= RubyElementLabels.ALL_FULLY_QUALIFIED | RubyElementLabels.M_PARAMETER_NAMES | RubyElementLabels.USE_RESOLVED; private final static long LOCAL_VARIABLE_FLAGS= LABEL_FLAGS & ~RubyElementLabels.F_FULLY_QUALIFIED | RubyElementLabels.F_POST_QUALIFIED; private static Ruby fgRuby; private static String fgRdocScriptPath; private RDocUtil() {} public static String getDocumentation(IRubyElement element) { if (element instanceof IMember) { return getContents((IMember)element); } return ""; } public static String getHTMLDocumentation(IRubyElement element) { return getHTMLDocumentation(getDocumentation(element)); } private static String getContents(IMember member) { String src = ""; int elementOffset = -1; try { src = member.getRubyScript().getSource(); ISourceRange range = member.getSourceRange(); if (range == null) return null; elementOffset = range.getOffset(); } catch (RubyModelException e) { return null; } Collection<CommentNode> comments = getComments(src); if (member.isType(IRubyElement.TYPE) || member.isType(IRubyElement.METHOD)) { return getPrecedingComment(comments, elementOffset, src); } // for variables try to get the following comment, if it's null grab the leading/preceding comment if (member.isType(IRubyElement.CLASS_VAR) || member.isType(IRubyElement.INSTANCE_VAR) || member.isType(IRubyElement.LOCAL_VARIABLE) || member.isType(IRubyElement.CONSTANT)) { String comment = getFollowingComment(comments, elementOffset, src); if (comment != null) return comment; return getPrecedingComment(comments, elementOffset, src); } return getFollowingComment(comments, elementOffset, src); } /** * Grabs and merges together all comment nodes which immediately precede the elementStart offset. * @param comments * @param elementStart * @param src * @return a combined string of all immediately preceding comments */ private static String getPrecedingComment(Collection<CommentNode> comments, int elementStart, String src) { if (comments == null || comments.isEmpty()) return null; for (CommentNode comment : comments) { ISourcePosition pos = comment.getPosition(); if (pos.getEndOffset() > elementStart) continue; String between = src.substring(pos.getEndOffset(), elementStart); if (between.trim().length() > 0) continue; // if there's anything but whitespace between (\n\r\t ), move to next comment String preceding = getPrecedingComment(comments, pos.getStartOffset(), src); if (preceding == null) { preceding = removePrecedingHashes(comment.getContent()); } else { preceding += "\n" + removePrecedingHashes(comment.getContent()); } return preceding; } return null; } /** * Grabs the comment from any comment node that follows this element (has to be on the same line) * @param comments * @param elementStart * @param src * @return */ private static String getFollowingComment(Collection<CommentNode> comments, int elementStart, String src) { if (comments == null || comments.isEmpty()) return null; for (CommentNode comment : comments) { ISourcePosition pos = comment.getPosition(); if (pos.getStartOffset() < elementStart) continue; String between = src.substring(elementStart, pos.getStartOffset()); if (between.contains("\n")) continue; // if there's a newline between the positions - it's not on same line String com = comment.getContent(); if (com != null && com.length() > 0) return removePrecedingHashes(com); } return null; } /** * Trims the string and drops the beginning hash mark (#) * @param comment * @return */ private static String removePrecedingHashes(String comment) { return comment.trim().substring(1); } public static String getHTMLDocumentation(String docs) { if (docs == null) return null; try { docs = removeUnecessaryIndent(docs); String script = "$KCODE = 'utf8'\n" + "require 'rdoc/markup/simple_markup'\n" + "require 'rdoc/markup/simple_markup/to_html'\n" + "p = SM::SimpleMarkup.new\n"; String script2 = "require 'rdoc/markup/simple_markup'\n" + "require 'rdoc/markup/simple_markup/to_html'\n" + "h = SM::ToHtml.new\n"; Ruby ruby = getJRubyInstance(); RubyString blah = RubyString.newUnicodeString(ruby, docs); ruby.setCurrentDirectory(getRDocScriptPath()); ruby.setKCode(KCode.UTF8); IRubyObject p = ruby.evalScriptlet(script); IRubyObject html = ruby.evalScriptlet(script2); IRubyObject output = p.callMethod(ruby.getCurrentContext(), "convert", new IRubyObject[] {blah, html}); docs = output.asString().getUnicodeValue(); } catch (Exception e) { // ignore } return docs; } private static String removeUnecessaryIndent(String docs) { int count = 0; String[] lines = docs.split("\n"); if (lines == null || lines.length == 0) return docs; String tmp = lines[0]; if (tmp != null && tmp.length() > 0) { while(tmp.charAt(0) == ' ') { count++; if (tmp.length() == 1) break; tmp = tmp.substring(1); } } StringBuffer modified = new StringBuffer(); for (int i = 0; i < lines.length; i++) { String line = lines[i]; if (line.length() > count) { if (line.substring(0, count).trim().length() == 0) { line = line.substring(count); } } modified.append(line); modified.append("\n"); } modified.deleteCharAt(modified.length() - 1); // remove last newline return modified.toString(); } private static Ruby getJRubyInstance() { if (fgRuby == null) fgRuby = Ruby.newInstance(); return fgRuby; } private static String getRDocScriptPath() throws IOException { if (fgRdocScriptPath == null) { // Copy over all the rdoc files RubyCore.copyToStateLocation(RubyCore.getPlugin(), new Path("ruby").append("rdoc").append("markup").append("simple_markup").append("fragments.rb")); RubyCore.copyToStateLocation(RubyCore.getPlugin(), new Path("ruby").append("rdoc").append("markup").append("simple_markup").append("inline.rb")); RubyCore.copyToStateLocation(RubyCore.getPlugin(), new Path("ruby").append("rdoc").append("markup").append("simple_markup").append("lines.rb")); RubyCore.copyToStateLocation(RubyCore.getPlugin(), new Path("ruby").append("rdoc").append("markup").append("simple_markup").append("to_html.rb")); RubyCore.copyToStateLocation(RubyCore.getPlugin(), new Path("ruby").append("rdoc").append("markup").append("simple_markup.rb")); RubyCore.copyToStateLocation(RubyCore.getPlugin(), new Path("ruby").append("cgi.rb")); RubyCore.copyToStateLocation(RubyCore.getPlugin(), new Path("ruby").append("delegate.rb")); RubyCore.copyToStateLocation(RubyCore.getPlugin(), new Path("ruby").append("English.rb")); RubyCore.copyToStateLocation(RubyCore.getPlugin(), new Path("ruby").append("metadata.rb")); RubyCore.copyToStateLocation(RubyCore.getPlugin(), new Path("ruby").append("rdoc.rb")); // Now point at the directory that they're in fgRdocScriptPath = RubyCore.getPlugin().getStateLocation().append("ruby").toPortableString(); } return fgRdocScriptPath; } public static IRegion getDocumentationRegion(IMember member) { if (!(member.isType(IRubyElement.TYPE) || member.isType(IRubyElement.METHOD))) return null; String src = ""; int elementOffset = -1; try { src = member.getRubyScript().getSource(); elementOffset = member.getSourceRange().getOffset(); } catch (RubyModelException e) { return null; } Collection<CommentNode> comments = getComments(src); return getPrecedingCommentRegion(comments, elementOffset, src); } private static Collection<CommentNode> getComments(String src) { try { RubyParser parser = new RubyParser(); return parser.parse(src).getCommentNodes(); // parse so we can grab the comment nodes } catch (SyntaxException e) { // ignore } catch (Exception e) { RubyCore.log(e); } return Collections.emptyList(); } private static IRegion getPrecedingCommentRegion(Collection<CommentNode> comments, int elementStart, String src) { if (comments == null || comments.isEmpty()) return null; for (CommentNode comment : comments) { ISourcePosition pos = comment.getPosition(); if (pos.getEndOffset() > elementStart) continue; String between = src.substring(pos.getEndOffset(), elementStart); if (between.trim().length() > 0) continue; // if there's anything but whitespace between (\n\r\t ), move to next comment IRegion preceding = getPrecedingCommentRegion(comments, pos.getStartOffset(), src); if (preceding == null) { preceding = new Region(pos.getStartOffset(), pos.getEndOffset() - pos.getStartOffset()); } else { preceding = new Region(preceding.getOffset(), pos.getEndOffset() - preceding.getOffset()); } return preceding; } return null; } public static String getHTMLDocumentation(IRubyElement[] result) { StringBuffer buffer= new StringBuffer(); int nResults= result.length; if (nResults == 0) return null; boolean hasContents= false; if (nResults > 1) { // TODO Create links for each of these? for (int i= 0; i < result.length; i++) { HTMLPrinter.startBulletList(buffer); IRubyElement curr= result[i]; if (curr instanceof IMember || curr.getElementType() == IRubyElement.LOCAL_VARIABLE) { HTMLPrinter.addBullet(buffer, getInfoText(curr)); hasContents= true; } HTMLPrinter.endBulletList(buffer); } } else { IRubyElement curr= result[0]; if (curr instanceof IMember) { IMember member= (IMember) curr; String contents = RDocUtil.getHTMLDocumentation(member); if (contents != null) { HTMLPrinter.addSmallHeader(buffer, getInfoText(member)); HTMLPrinter.addParagraph(buffer, contents); } hasContents= true; } else if (curr != null && curr.getElementType() == IRubyElement.LOCAL_VARIABLE) { HTMLPrinter.addSmallHeader(buffer, getInfoText(curr)); hasContents= true; } } if (!hasContents) return null; if (buffer.length() > 0) { // HTMLPrinter.insertPageProlog(buffer, 0, getStyleSheet()); // HTMLPrinter.addPageEpilog(buffer); return buffer.toString(); } return null; } private static String getInfoText(IRubyElement member) { long flags= member.getElementType() == IRubyElement.LOCAL_VARIABLE ? LOCAL_VARIABLE_FLAGS : LABEL_FLAGS; String label= RubyElementLabels.getElementLabel(member, flags); StringBuffer buf= new StringBuffer(); for (int i= 0; i < label.length(); i++) { char ch= label.charAt(i); if (ch == '<') { buf.append("<"); //$NON-NLS-1$ } else if (ch == '>') { buf.append(">"); //$NON-NLS-1$ } else { buf.append(ch); } } return buf.toString(); } }