blob: 8f0fbdbbf30acebe047572e92f8f2ef03b250473 [file] [log] [blame]
/*
* Copyright 2006 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.doctool;
import com.google.doctool.LinkResolver.ExtraClassResolver;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.ConstructorDoc;
import com.sun.javadoc.Doc;
import com.sun.javadoc.DocErrorReporter;
import com.sun.javadoc.ExecutableMemberDoc;
import com.sun.javadoc.FieldDoc;
import com.sun.javadoc.MemberDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.PackageDoc;
import com.sun.javadoc.ParamTag;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.ProgramElementDoc;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.SeeTag;
import com.sun.javadoc.SourcePosition;
import com.sun.javadoc.Tag;
import com.sun.javadoc.Type;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Stack;
/**
* Generates XML from Javadoc source, with particular idioms to make it possible
* to translate into either expository doc or API doc.
*/
public class Booklet {
private static final String OPT_BKCODE = "-bkcode";
private static final String OPT_BKDOCPKG = "-bkdocpkg";
private static final String OPT_BKOUT = "-bkout";
private static Booklet sBooklet;
public static void main(String[] args) {
// Strip off our arguments at the beginning.
//
com.sun.tools.javadoc.Main.execute(args);
}
public static int optionLength(String option) {
if (option.equals(OPT_BKOUT)) {
return 2;
} else if (option.equals(OPT_BKDOCPKG)) {
return 2;
} else if (option.equals(OPT_BKCODE)) {
return 1;
}
return 0;
}
public static String slurpSource(SourcePosition position) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(position.file()));
for (int i = 0, n = position.line() - 1; i < n; ++i) {
br.readLine();
}
StringBuffer lines = new StringBuffer();
String line = br.readLine();
int braceDepth = 0;
int indent = -1;
boolean seenSemiColonOrBrace = false;
while (line != null) {
if (indent == -1) {
for (indent = 0; Character.isWhitespace(line.charAt(indent)); ++indent) {
// just accumulate
}
}
if (line.length() >= indent) {
line = line.substring(indent);
}
lines.append(line + "\n");
for (int i = 0, n = line.length(); i < n; ++i) {
char c = line.charAt(i);
if (c == '{') {
seenSemiColonOrBrace = true;
++braceDepth;
} else if (c == '}') {
--braceDepth;
} else if (c == ';') {
seenSemiColonOrBrace = true;
}
}
if (braceDepth > 0 || !seenSemiColonOrBrace) {
line = br.readLine();
} else {
break;
}
}
String code = lines.toString();
return code;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return "";
}
public static boolean start(RootDoc rootDoc) {
getBooklet().process(rootDoc);
return true;
}
public static boolean validOptions(String[][] options,
DocErrorReporter reporter) {
return getBooklet().analyzeOptions(options, reporter);
}
private static Booklet getBooklet() {
if (sBooklet == null) {
sBooklet = new Booklet();
}
return sBooklet;
}
private String outputPath;
private HashSet packagesToGenerate;
private RootDoc initialRootDoc;
private String rootDocId;
private boolean showCode;
private HashSet standardTagKinds = new HashSet();
private Stack tagStack = new Stack();
private PrintWriter pw;
public Booklet() {
// Set up standard tags (to ignore during tag processing)
//
standardTagKinds.add("@see");
standardTagKinds.add("@serial");
standardTagKinds.add("@throws");
standardTagKinds.add("@param");
standardTagKinds.add("@id");
}
private boolean analyzeOptions(String[][] options, DocErrorReporter reporter) {
for (int i = 0, n = options.length; i < n; ++i) {
if (options[i][0].equals(OPT_BKOUT)) {
outputPath = options[i][1];
} else if (options[i][0].equals(OPT_BKDOCPKG)) {
String[] packages = options[i][1].split(";");
packagesToGenerate = new HashSet();
for (int packageIndex = 0; packageIndex < packages.length; ++packageIndex) {
packagesToGenerate.add(packages[packageIndex]);
}
} else if (options[i][0].equals(OPT_BKCODE)) {
showCode = true;
}
}
if (outputPath == null) {
reporter.printError("You must specify an output directory with "
+ OPT_BKOUT);
return false;
}
return true;
}
private void begin(String tag) {
pw.print("<" + tag + ">");
tagStack.push(tag);
}
private void begin(String tag, String attr, String value) {
pw.print("<" + tag + " " + attr + "='" + value + "'>");
tagStack.push(tag);
}
private void beginCDATA() {
pw.print("<![CDATA[");
}
private void beginEndln(String tag) {
pw.println("<" + tag + "/>");
}
private void beginln(String tag) {
pw.println();
begin(tag);
}
private void beginln(String tag, String attr, String value) {
pw.println();
begin(tag, attr, value);
}
private void emitDescription(ClassDoc enclosing, Doc forWhat,
Tag[] leadInline, Tag[] descInline) {
emitJRELink(enclosing, forWhat);
beginln("lead");
processTags(leadInline);
endln();
beginln("description");
processTags(descInline);
endln();
}
private void emitIdentity(String id, String name) {
beginln("id");
text(id);
endln();
beginln("name");
text(name);
endln();
}
private void emitJRELink(ClassDoc enclosing, Doc doc) {
String jreLink = "http://java.sun.com/j2se/1.5.0/docs/api/";
if (doc instanceof ClassDoc) {
ClassDoc classDoc = (ClassDoc) doc;
String pkg = classDoc.containingPackage().name();
if (!pkg.startsWith("java.")) {
return;
}
String clazz = classDoc.name();
jreLink += pkg.replace('.', '/') + "/";
jreLink += clazz;
jreLink += ".html";
} else if (doc instanceof ExecutableMemberDoc) {
ExecutableMemberDoc execMemberDoc = (ExecutableMemberDoc) doc;
String pkg = enclosing.containingPackage().name();
if (!pkg.startsWith("java.")) {
return;
}
String clazz = enclosing.name();
String method = execMemberDoc.name();
String sig = execMemberDoc.signature();
jreLink += pkg.replace('.', '/') + "/";
jreLink += clazz;
jreLink += ".html";
jreLink += "#";
jreLink += method;
jreLink += sig;
} else if (doc instanceof PackageDoc) {
String pkg = doc.name();
if (!pkg.startsWith("java.")) {
return;
}
jreLink += pkg.replace('.', '/') + "/package-summary.html";
} else if (doc instanceof FieldDoc) {
FieldDoc fieldDoc = (FieldDoc) doc;
String pkg = enclosing.containingPackage().name();
if (!pkg.startsWith("java.")) {
return;
}
String clazz = fieldDoc.containingClass().name();
String field = fieldDoc.name();
jreLink += pkg.replace('.', '/') + "/";
jreLink += clazz;
jreLink += ".html";
jreLink += "#";
jreLink += field;
}
// Add the link.
//
beginln("jre");
text(jreLink);
endln();
}
private void emitLocation(Doc doc) {
Doc parent = getParentDoc(doc);
if (parent != null) {
beginln("location");
emitLocationLink(parent);
endln();
}
}
private void emitLocationLink(Doc doc) {
// Intentionally reverses the order.
//
String myId;
String myTitle;
if (doc instanceof MemberDoc) {
MemberDoc memberDoc = (MemberDoc) doc;
myId = getId(memberDoc);
myTitle = memberDoc.name();
} else if (doc instanceof ClassDoc) {
ClassDoc classDoc = (ClassDoc) doc;
myId = getId(classDoc);
myTitle = classDoc.name();
} else if (doc instanceof PackageDoc) {
PackageDoc pkgDoc = (PackageDoc) doc;
myId = getId(pkgDoc);
myTitle = pkgDoc.name();
} else if (doc instanceof RootDoc) {
myId = rootDocId;
myTitle = initialRootDoc.name();
} else {
throw new IllegalStateException(
"Expected only a member, type, or package");
}
Doc parent = getParentDoc(doc);
if (parent != null) {
emitLocationLink(parent);
}
beginln("link", "ref", myId);
Tag[] titleTag = doc.tags("@title");
if (titleTag.length > 0) {
myTitle = titleTag[0].text().trim();
}
if (myTitle == null || myTitle.length() == 0) {
myTitle = "[NO TITLE]";
}
text(myTitle);
endln();
}
private void emitModifiers(ProgramElementDoc doc) {
if (doc.isPrivate()) {
beginEndln("isPrivate");
} else if (doc.isProtected()) {
beginEndln("isProtected");
} else if (doc.isPublic()) {
beginEndln("isPublic");
} else if (doc.isPackagePrivate()) {
beginEndln("isPackagePrivate");
}
if (doc.isStatic()) {
beginEndln("isStatic");
}
if (doc.isFinal()) {
beginEndln("isFinal");
}
if (doc instanceof MethodDoc) {
MethodDoc methodDoc = (MethodDoc) doc;
if (methodDoc.isAbstract()) {
beginEndln("isAbstract");
}
if (methodDoc.isSynchronized()) {
beginEndln("isSynchronized");
}
}
}
private void emitOutOfLineTags(Tag[] tags) {
beginln("tags");
processTags(tags);
endln();
}
private void emitType(Type type) {
ClassDoc typeAsClass = type.asClassDoc();
if (typeAsClass != null) {
begin("type", "ref", getId(typeAsClass));
} else {
begin("type");
}
String typeName = type.typeName();
String dims = type.dimension();
text(typeName + dims);
end();
}
private void end() {
pw.print("</" + tagStack.pop() + ">");
}
private void endCDATA() {
pw.print("]]>");
}
private void endln() {
end();
pw.println();
}
private MethodDoc findMatchingInterfaceMethodDoc(ClassDoc[] interfaces,
MethodDoc methodDoc) {
if (interfaces != null) {
// Look through the methods on superInterface for a matching methodDoc.
//
for (int intfIndex = 0; intfIndex < interfaces.length; ++intfIndex) {
ClassDoc currentIntfDoc = interfaces[intfIndex];
MethodDoc[] intfMethodDocs = currentIntfDoc.methods();
for (int methodIndex = 0; methodIndex < intfMethodDocs.length; ++methodIndex) {
MethodDoc intfMethodDoc = intfMethodDocs[methodIndex];
String methodDocName = methodDoc.name();
String intfMethodDocName = intfMethodDoc.name();
if (methodDocName.equals(intfMethodDocName)) {
if (methodDoc.signature().equals(intfMethodDoc.signature())) {
// It's a match!
//
return intfMethodDoc;
}
}
}
// Try the superinterfaces of this interface.
//
MethodDoc foundMethodDoc = findMatchingInterfaceMethodDoc(
currentIntfDoc.interfaces(), methodDoc);
if (foundMethodDoc != null) {
return foundMethodDoc;
}
}
}
// Just didn't find it anywhere. Must not be based on an implemented
// interface.
//
return null;
}
private ExtraClassResolver getExtraClassResolver(Tag tag) {
if (tag.holder() instanceof PackageDoc) {
return new ExtraClassResolver() {
public ClassDoc findClass(String className) {
return initialRootDoc.classNamed(className);
}
};
}
return null;
}
private String getId(ClassDoc classDoc) {
return classDoc.qualifiedName();
}
private String getId(ExecutableMemberDoc memberDoc) {
// Use the mangled name to look up a unique id (based on its hashCode).
//
String clazz = memberDoc.containingClass().qualifiedName();
String id = clazz + "#" + memberDoc.name() + memberDoc.signature();
return id;
}
private String getId(FieldDoc fieldDoc) {
String clazz = fieldDoc.containingClass().qualifiedName();
String id = clazz + "#" + fieldDoc.name();
return id;
}
private String getId(MemberDoc memberDoc) {
if (memberDoc.isMethod()) {
return getId((MethodDoc) memberDoc);
} else if (memberDoc.isConstructor()) {
return getId((ConstructorDoc) memberDoc);
} else if (memberDoc.isField()) {
return getId((FieldDoc) memberDoc);
} else {
throw new RuntimeException("Unknown member type");
}
}
private String getId(PackageDoc packageDoc) {
return packageDoc.name();
}
private Doc getParentDoc(Doc doc) {
if (doc instanceof MemberDoc) {
MemberDoc memberDoc = (MemberDoc) doc;
return memberDoc.containingClass();
} else if (doc instanceof ClassDoc) {
ClassDoc classDoc = (ClassDoc) doc;
Doc enclosingClass = classDoc.containingClass();
if (enclosingClass != null) {
return enclosingClass;
} else {
return classDoc.containingPackage();
}
} else if (doc instanceof PackageDoc) {
return initialRootDoc;
} else if (doc instanceof RootDoc) {
return null;
} else {
throw new IllegalStateException(
"Expected only a member, type, or package");
}
}
private boolean looksSynthesized(ExecutableMemberDoc memberDoc) {
SourcePosition memberPos = memberDoc.position();
int memberLine = memberPos.line();
SourcePosition classPos = memberDoc.containingClass().position();
int classLine = classPos.line();
if (memberLine == classLine) {
return true;
} else {
return false;
}
}
private void process(ClassDoc enclosing, ClassDoc classDoc) {
// Make sure it isn't a @skip-ped topic.
//
if (classDoc.tags("@skip").length > 0) {
// This one is explicitly skipped right now.
//
return;
}
if (classDoc.isInterface()) {
beginln("interface");
} else {
beginln("class");
}
emitIdentity(getId(classDoc), classDoc.name());
emitLocation(classDoc);
emitDescription(enclosing, classDoc, classDoc.firstSentenceTags(),
classDoc.inlineTags());
emitOutOfLineTags(classDoc.tags());
emitModifiers(classDoc);
ClassDoc superclassDoc = classDoc.superclass();
if (superclassDoc != null) {
beginln("superclass", "ref", getId(superclassDoc));
text(superclassDoc.name());
endln();
}
ClassDoc[] superinterfacesDoc = classDoc.interfaces();
for (int i = 0; i < superinterfacesDoc.length; i++) {
ClassDoc superinterfaceDoc = superinterfacesDoc[i];
beginln("superinterface", "ref", getId(superinterfaceDoc));
text(superinterfaceDoc.name());
endln();
}
ClassDoc[] cda = classDoc.innerClasses();
for (int i = 0; i < cda.length; i++) {
process(classDoc, cda[i]);
}
FieldDoc[] fda = classDoc.fields();
for (int i = 0; i < fda.length; i++) {
process(classDoc, fda[i]);
}
ConstructorDoc[] ctorDocs = classDoc.constructors();
for (int i = 0; i < ctorDocs.length; i++) {
process(classDoc, ctorDocs[i]);
}
MethodDoc[] methods = classDoc.methods();
for (int i = 0; i < methods.length; i++) {
process(classDoc, methods[i]);
}
endln();
}
private void process(ClassDoc enclosing, ExecutableMemberDoc memberDoc) {
if (looksSynthesized(memberDoc)) {
// Skip it.
//
return;
}
// Make sure it isn't a @skip-ped member.
//
if (memberDoc.tags("@skip").length > 0) {
// This one is explicitly skipped right now.
//
return;
}
if (memberDoc instanceof MethodDoc) {
beginln("method");
emitIdentity(getId(memberDoc), memberDoc.name());
emitLocation(memberDoc);
// If this method is not explicitly documented, use the best inherited
// one.
//
String rawComment = memberDoc.getRawCommentText();
if (rawComment.length() == 0) {
// Switch out the member doc being used.
//
MethodDoc methodDoc = (MethodDoc) memberDoc;
MethodDoc superMethodDoc = methodDoc.overriddenMethod();
if (superMethodDoc == null) {
ClassDoc classDocToTry = methodDoc.containingClass();
while (classDocToTry != null) {
// See if this is a method from an interface.
// If so, borrow its description.
//
superMethodDoc = findMatchingInterfaceMethodDoc(
classDocToTry.interfaces(), methodDoc);
if (superMethodDoc != null) {
break;
}
classDocToTry = classDocToTry.superclass();
}
}
if (superMethodDoc != null) {
// Borrow the description from the superclass/superinterface.
//
memberDoc = superMethodDoc;
}
}
} else if (memberDoc instanceof ConstructorDoc) {
beginln("constructor");
emitIdentity(getId(memberDoc), memberDoc.containingClass().name());
emitLocation(memberDoc);
} else {
throw new IllegalStateException("What kind of executable member is this?");
}
emitDescription(enclosing, memberDoc, memberDoc.firstSentenceTags(),
memberDoc.inlineTags());
emitOutOfLineTags(memberDoc.tags());
emitModifiers(memberDoc);
begin("flatSignature");
text(memberDoc.flatSignature());
end();
// Return type if it's a method
//
if (memberDoc instanceof MethodDoc) {
emitType(((MethodDoc) memberDoc).returnType());
}
// Parameters
//
beginln("params");
Parameter[] pda = memberDoc.parameters();
for (int i = 0; i < pda.length; i++) {
Parameter pd = pda[i];
begin("param");
emitType(pd.type());
begin("name");
text(pd.name());
end();
end();
}
endln();
// Exceptions thrown
//
ClassDoc[] tea = memberDoc.thrownExceptions();
if (tea.length > 0) {
beginln("throws");
for (int i = 0; i < tea.length; ++i) {
ClassDoc te = tea[i];
beginln("throw", "ref", getId(te));
text(te.name());
endln();
}
endln();
}
// Maybe show code
//
if (showCode) {
SourcePosition pos = memberDoc.position();
if (pos != null) {
beginln("code");
String source = slurpSource(pos);
begin("pre", "class", "code");
beginCDATA();
text(source);
endCDATA();
endln();
endln();
}
}
endln();
}
private void process(ClassDoc enclosing, FieldDoc fieldDoc) {
// Make sure it isn't @skip-ped.
//
if (fieldDoc.tags("@skip").length > 0) {
// This one is explicitly skipped right now.
//
return;
}
String commentText = fieldDoc.commentText();
if (fieldDoc.isPrivate()
&& (commentText == null || commentText.length() == 0)) {
return;
}
beginln("field");
emitIdentity(fieldDoc.qualifiedName(), fieldDoc.name());
emitLocation(fieldDoc);
emitDescription(enclosing, fieldDoc, fieldDoc.firstSentenceTags(),
fieldDoc.inlineTags());
emitOutOfLineTags(fieldDoc.tags());
emitModifiers(fieldDoc);
emitType(fieldDoc.type());
endln();
}
private void process(PackageDoc packageDoc) {
beginln("package");
emitIdentity(packageDoc.name(), packageDoc.name());
emitLocation(packageDoc);
emitDescription(null, packageDoc, packageDoc.firstSentenceTags(),
packageDoc.inlineTags());
emitOutOfLineTags(packageDoc.tags());
// Top-level classes
//
ClassDoc[] cda = packageDoc.allClasses();
for (int i = 0; i < cda.length; i++) {
ClassDoc cd = cda[i];
// Make sure we have source.
//
SourcePosition p = cd.position();
if (p == null || p.line() == 0) {
// Skip this since it isn't ours (otherwise we would have source).
//
continue;
}
if (cd.containingClass() == null) {
process(cd, cda[i]);
} else {
// Not a top-level class.
//
cd = cda[i];
}
}
endln();
}
private void process(RootDoc rootDoc) {
try {
initialRootDoc = rootDoc;
File outputFile = new File(outputPath);
// Ignore result since the next line will fail if the directory doesn't
// exist.
outputFile.getParentFile().mkdirs();
FileWriter fw = new FileWriter(outputFile);
pw = new PrintWriter(fw, true);
beginln("booklet");
rootDocId = "";
String title = "";
Tag[] idTags = rootDoc.tags("@id");
if (idTags.length > 0) {
rootDocId = idTags[0].text();
} else {
initialRootDoc.printWarning("Expecting @id in an overview html doc; see -overview");
}
Tag[] titleTags = rootDoc.tags("@title");
if (titleTags.length > 0) {
title = titleTags[0].text();
} else {
initialRootDoc.printWarning("Expecting @title in an overview html doc; see -overview");
}
emitIdentity(rootDocId, title);
emitLocation(rootDoc);
emitDescription(null, rootDoc, rootDoc.firstSentenceTags(),
rootDoc.inlineTags());
emitOutOfLineTags(rootDoc.tags());
// Create a list of the packages to iterate over.
//
HashSet packageNames = new HashSet();
ClassDoc[] cda = initialRootDoc.classes();
for (int i = 0; i < cda.length; i++) {
ClassDoc cd = cda[i];
// Only top-level classes matter.
//
if (cd.containingClass() == null) {
packageNames.add(cd.containingPackage().name());
}
}
// Packages
//
for (Iterator iter = packageNames.iterator(); iter.hasNext();) {
String packageName = (String) iter.next();
// Only process this package if either no "docpkg" is set, or it is
// included.
//
if (packagesToGenerate == null
|| packagesToGenerate.contains(packageName)) {
PackageDoc pd = initialRootDoc.packageNamed(packageName);
process(pd);
}
}
endln();
} catch (Exception e) {
e.printStackTrace();
initialRootDoc.printError("Caught exception: " + e.toString());
}
}
private void processSeeTag(SeeTag seeTag) {
String ref = null;
ClassDoc cd = null;
PackageDoc pd = null;
MemberDoc md = null;
String title = null;
// Check for HTML links
if (seeTag.text().startsWith("<")) {
// TODO: ignore for now
return;
}
// Ordered: most-specific to least-specific
if (null != (md = seeTag.referencedMember())) {
ref = getId(md);
} else if (null != (cd = seeTag.referencedClass())) {
ref = getId(cd);
// See if the target has a title.
//
Tag[] titleTag = cd.tags("@title");
if (titleTag.length > 0) {
title = titleTag[0].text().trim();
if (title.length() == 0) {
title = null;
}
}
} else if (null != (pd = seeTag.referencedPackage())) {
ref = getId(pd);
}
String label = seeTag.label();
// If there is a label, use it.
if (label == null || label.trim().length() == 0) {
// If there isn't a label, see if the @see target has a @title.
//
if (title != null) {
label = title;
} else {
label = seeTag.text();
if (label.endsWith(".")) {
label = label.substring(0, label.length() - 1);
}
// Rip off all but the last interesting part to prevent fully-qualified
// names everywhere.
//
int last1 = label.lastIndexOf('.');
int last2 = label.lastIndexOf('#');
if (last2 > last1) {
// Use the class name plus the member name.
//
label = label.substring(last1 + 1).replace('#', '.');
} else if (last1 != -1) {
label = label.substring(last1 + 1);
}
if (label.charAt(0) == '.') {
// Started with "#" so remove the dot.
//
label = label.substring(1);
}
}
}
if (ref != null) {
begin("link", "ref", ref);
text(label != null ? label.trim() : "");
end();
} else {
initialRootDoc.printWarning(seeTag.position(),
"Unverifiable cross-reference to '" + seeTag.text() + "'");
// The link probably won't work, but emit it anyway.
begin("link");
text(label != null ? label.trim() : "");
end();
}
}
private void processTags(Tag[] tags) {
for (int i = 0; i < tags.length; i++) {
Tag tag = tags[i];
String tagKind = tag.kind();
if (tagKind.equals("Text")) {
text(tag.text());
} else if (tagKind.equals("@see")) {
processSeeTag((SeeTag) tag);
} else if (tagKind.equals("@param")) {
ParamTag paramTag = (ParamTag) tag;
beginln("param");
begin("name");
text(paramTag.parameterName());
end();
begin("description");
processTags(paramTag.inlineTags());
end();
endln();
} else if (tagKind.equals("@example")) {
ExtraClassResolver extraClassResolver = getExtraClassResolver(tag);
SourcePosition pos = LinkResolver.resolveLink(tag, extraClassResolver);
String source = slurpSource(pos);
begin("pre", "class", "code");
beginCDATA();
text(source);
endCDATA();
endln();
} else if (tagKind.equals("@gwt.include")) {
String contents = ResourceIncluder.getResourceFromClasspathScrubbedForHTML(tag);
begin("pre", "class", "code");
text(contents);
endln();
} else if (!standardTagKinds.contains(tag.name())) {
// Custom tag; pass it along other tag.
//
String tagName = tag.name().substring(1);
begin(tagName);
processTags(tag.inlineTags());
end();
}
}
}
private void text(String s) {
pw.print(s);
}
}