de.unkrig.commons.doclet.html.Html Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of de-unkrig-commons Show documentation
Show all versions of de-unkrig-commons Show documentation
A versatile Java(TM) library that implements many useful container and utility classes.
/*
* de.unkrig.commons.doclet - Writing doclets made easy
*
* Copyright (c) 2015, Arno Unkrig
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
* following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the
* following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package de.unkrig.commons.doclet.html;
import java.net.URL;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.ConstructorDoc;
import com.sun.javadoc.Doc;
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.Parameter;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Tag;
import com.sun.javadoc.Type;
import de.unkrig.commons.doclet.Docs;
import de.unkrig.commons.doclet.Tags;
import de.unkrig.commons.lang.AssertionUtil;
import de.unkrig.commons.lang.protocol.Longjump;
import de.unkrig.commons.nullanalysis.NotNull;
import de.unkrig.commons.nullanalysis.Nullable;
/**
* Helper functionality in the context of doclets and HTML.
*/
public
class Html {
static { AssertionUtil.enableAssertionsForThisClass(); }
/**
* When generating HTML from JAVADOC, this interface is used to generate links to JAVA elements.
*/
public
interface LinkMaker {
/**
* Generates an "href" that refers from the HTML page on which {@code from} is described to the HTML page (and
* possibly the anchor) that describes {@code to}.
*
* @return {@code null} if the bare label should be displayed instead of a link
* @throws Longjump A link href could be determined, but for some reason it was forbidden to link there
*/
@Nullable String
makeHref(Doc from, Doc to, RootDoc rootDoc) throws Longjump;
/**
* Generates the "default label" for the link that refers from the HTML page on which {@code from} is described
* to the place where {@code to} is described.
*/
String
makeDefaultLabel(Doc from, Doc to, RootDoc rootDoc) throws Longjump;
}
/**
* Implements the strategy of the standard JAVADOC doclet.
* Hrefs are generated as follows:
*
* - Field, constructor or method of same class:
* - {@code "#toField"}
* - {@code "#ToClass(java.lang.String)"}
* - {@code "#toMethod(java.lang.String)"}
* - Class, field, constructor or method in external package:
* - {@code "http://external.url/to/package/ToClass"}
* - {@code "http://external.url/to/package/ToClass#toField"}
* - {@code "http://external.url/to/package/ToClass#ToClass(java.lang.String)"}
* - {@code "http://external.url/to/package/ToClass#toMethod(java.lang.String)"}
* - Class, field, constructor or method in same package:
* - {@code "ToClass"}
* - {@code "ToClass#toField"}
* - {@code "ToClass#ToClass(String)"}
* - {@code "ToClass#toMethod(String)"}
* - Class, field, constructor or method in different (but "included") package:
* - {@code "../../to/package/ToClass"}
* - {@code "../../to/package/ToClass#toField"}
* - {@code "../../to/package/ToClass#ToClass(String)"}
* - {@code "../../to/package/ToClass#toMethod(String)"}
* - Class, field or method in non-included package:
* - {@code null}
*
* Default labels are generated as follows:
*
* - Field, constructor or method of same class:
* - {@code "toField"}
* - {@code "ToClass(java.lang.String)"}
* - {@code "toMethod(java.lang.String)"}
* - Class, or field, constructor or method in different class:
* - {@code ToClass}
* - {@code ToClass.toField}
* - {@code ToClass(java.lang.String)}
* - {@code ToClass.toMethod(java.lang.String)}
*
*/
public static final LinkMaker
STANDARD_LINK_MAKER = new LinkMaker() {
@Override @Nullable public String
makeHref(Doc from, Doc to, RootDoc rootDoc) {
if (to == from && !(to instanceof ClassDoc)) return null;
if (!to.isIncluded()) return null;
PackageDoc toPackage = Docs.packageScope(to);
assert toPackage != null;
PackageDoc fromPackage = Docs.packageScope(from);
StringBuilder href = new StringBuilder();
if (fromPackage != null) { // if (toPackage != fromPackage) {
for (@SuppressWarnings("unused") String component : fromPackage.name().split("\\.")) {
href.append("../");
}
}
href.append(toPackage.name().replace('.', '/')).append('/');
ClassDoc toClass = Docs.classScope(to);
if (toClass == null) {
href.append("index.html");
} else {
href.append(toClass.name()).append(".html");
}
href.append(Html.fragmentIdentifier(to));
return href.toString();
}
@Override public String
makeDefaultLabel(Doc from, Doc to, RootDoc rootDoc) {
if (!(to instanceof MemberDoc)) return to.name();
MemberDoc toMember = (MemberDoc) to;
String label = (
toMember.containingClass() == from || (
from instanceof MemberDoc
&& toMember.containingClass() == ((MemberDoc) from).containingClass()
)
? ""
: toMember.containingClass().name() + '.'
);
if (to.isField()) {
return label + to.name();
} else
if (to.isConstructor()) {
ConstructorDoc toConstructorDoc = (ConstructorDoc) to;
return (
label
+ toConstructorDoc.containingClass().name()
+ this.prettyPrintParameterList(toConstructorDoc)
);
} else
if (to.isMethod()) {
MethodDoc toMethodDoc = (MethodDoc) to;
return label + to.name() + this.prettyPrintParameterList(toMethodDoc);
} else
{
throw new IllegalArgumentException(String.valueOf(to));
}
}
private String
prettyPrintParameterList(ExecutableMemberDoc executableMemberDoc) {
StringBuilder result = new StringBuilder().append('(');
for (int i = 0; i < executableMemberDoc.parameters().length; i++) {
Parameter parameter = executableMemberDoc.parameters()[i];
if (i > 0) {
result.append(", ");
}
Type pt = parameter.type();
if (pt.isPrimitive()) {
result.append(pt.toString());
continue;
}
// Show erasure type, not type variable name.
ClassDoc cd = pt.asClassDoc();
assert cd != null : parameter;
result.append(cd.name());
}
result.append(')');
return result.toString();
}
};
/**
* Generates the "fragment" identifier for the given {@code doc}.
*
* - {@link FieldDoc}:
* - {@code "#fieldName"}
* - {@link MethodDoc}:
* - {@code "#methodName(java.lang.String,int)"}
* - Other:
* - {@code ""}
*
*/
private static String
fragmentIdentifier(Doc doc) {
if (doc.isField()) return '#' + doc.name();
if (doc.isConstructor()) {
ConstructorDoc constructorDoc = (ConstructorDoc) doc;
return (
'#'
+ constructorDoc.containingClass().name()
+ Html.parameterListForFragmentIdentifier(constructorDoc)
);
}
if (doc.isMethod()) {
MethodDoc methodDoc = (MethodDoc) doc;
return '#' + doc.name() + Html.parameterListForFragmentIdentifier(methodDoc);
}
return "";
}
private static String
parameterListForFragmentIdentifier(ExecutableMemberDoc executableMemberDoc) {
StringBuilder result = new StringBuilder().append('(');
for (int i = 0; i < executableMemberDoc.parameters().length; i++) {
Parameter parameter = executableMemberDoc.parameters()[i];
if (i > 0) result.append(", ");
result.append(parameter.type().qualifiedTypeName());
}
return result.append(')').toString();
}
/**
* See the
* documentation of the '-linkoffline' option of the JAVADOC tool.
*/
public static final
class ExternalJavadocsLinkMaker implements LinkMaker {
private final Map externalJavadocs;
private final LinkMaker delegate;
public
ExternalJavadocsLinkMaker(Map externalJavadocs, LinkMaker delegate) {
this.externalJavadocs = externalJavadocs;
this.delegate = delegate;
}
@Override @Nullable public String
makeHref(Doc from, Doc to, RootDoc rootDoc) throws Longjump {
PackageDoc toPackage = Docs.packageScope(to);
assert toPackage != null;
URL url = this.externalJavadocs.get(toPackage.name());
if (url == null) return this.delegate.makeHref(from, to, rootDoc);
ClassDoc toClass = Docs.classScope(to);
if (toClass == null) return url.toString() + '/' + toPackage.name().replace('.', '/') + "/index.html";
return url + toClass.qualifiedName().replace('.', '/') + ".html" + Html.fragmentIdentifier(to);
}
@Override public String
makeDefaultLabel(Doc from, Doc to, RootDoc rootDoc) throws Longjump {
PackageDoc toPackage = Docs.packageScope(to);
assert toPackage != null;
if (this.externalJavadocs.containsKey(toPackage.name())) {
return Html.STANDARD_LINK_MAKER.makeDefaultLabel(from, to, rootDoc);
}
return this.delegate.makeDefaultLabel(from, to, rootDoc);
}
}
private static final Pattern
DOC_TAG = Pattern.compile("\\{(@[^\\s}]+)(?:\\s+([^\\s}][^}]*))?\\}", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
private final LinkMaker linkMaker;
public
Html(LinkMaker linkMaker) { this.linkMaker = linkMaker; }
/**
* Expands inline tags to HTML. Inline tags, as of Java 8, are:
*
* {@code text}
* {@docRoot}
* {@inheritDoc}
* {@link package.class#member label}
* {@linkplain package.class#member label}
* {@literal text}
* {@value package.class#field}
*
* Only part of these are currently acceptable for the transformation into HTML.
*/
public String
fromTags(Tag[] tags, Doc ref, RootDoc rootDoc) throws Longjump {
StringBuilder sb = new StringBuilder();
for (Tag tag : tags) {
String tagText = tag.text();
// It is not clearly documented, but "Tag.text()" appears to return an EMPTY STRING when there is no
// argument - not NULL, as you'd possibly expect.
if (tagText.isEmpty()) tagText = null;
sb.append(this.expandTag(ref, rootDoc, tag.name(), tagText));
}
return sb.toString();
}
/**
* Converts JAVADOC markup into HTML.
*
* @param ref The 'current element'; relevant to resolve relative references
* @param rootDoc Used to resolve absolute references and to print errors and warnings
*/
public String
fromJavadocText(String s, Doc ref, RootDoc rootDoc) throws Longjump {
// Expand inline tags. Inline tags, as of Java 8, are:
// {@code text}
// {@docRoot}
// {@inheritDoc}
// {@link package.class#member label}
// {@linkplain package.class#member label}
// {@literal text}
// {@value package.class#field}
// Only part of these are currently acceptable for the transformation into HTML.
INLINE_TAGS: {
Matcher m = Html.DOC_TAG.matcher(s);
if (!m.find()) break INLINE_TAGS; // Short-circuit iff no inline tag found.
StringBuffer sb = new StringBuffer();
do {
String tagName = m.group(1).intern(); // E.g. "@code".
String argument = m.group(2);
String replacement = this.expandTag(ref, rootDoc, tagName, argument);
m.appendReplacement(sb, Matcher.quoteReplacement(replacement));
} while (m.find());
return m.appendTail(sb).toString();
}
return s;
}
/**
* Expands a tag to HTML text. Supported tags are:
*
* - Text
* - Expands to the literal text
* {@code
text}
* - Expands to the text, in monospace font and with HTML entities escaped
* {@value
field-ref}
* - Expands to the constant initializer value of the designated field
* {@link
ref [ text ] }
* - Expands to a link (in monospace font) to the designated ref
* {@linkplain
ref [ text ] }
* - Like
{@ link}
, but in default font
*
*
* Subclasses may override this method to expand more than these tags.
*
*
* @param argument The text between the tag name and the closing brace, not including any leading space, or
* {@code null} iff there is no argument
*/
protected String
expandTag(Doc ref, RootDoc rootDoc, String tagName, @Nullable String argument) throws Longjump {
if ("Text".equals(tagName)) {
if (argument == null) return "";
// Text tags contain UNIX line breaks ("\n").
// DOC comments appear to be "String.trim()"med, i.e. leading and trailing spaces and line breaks are
// removed:
//
// /**
// *
// * foo => "\n\n foo\n" => "foo"
// *
// */
// From continuation lines, any leading " *\**" is removed:
//
// /**
// * one
// ***** two
// */ => "one\n ***** two" => "one\n two"
// Notice that the standard JDK JAVADOC DOCLET treats continuation lines WITHOUT a leading blank as a
// masked line break:
//
// /**
// * one
// *two => "one\n *two" => "onetwo"
// */
for (int idx = argument.indexOf('\n'); idx != -1; idx = argument.indexOf('\n', idx)) {
if (idx == argument.length() - 1) {
// This case should not occur, as, as described above, JAVADOC silently trims texts.
argument = argument.substring(0, idx) + Html.LINE_SEPARATOR;
break;
}
char c = argument.charAt(idx + 1);
if (c == '\n') {
// "Short" line (" *").
argument = argument.substring(0, idx) + Html.LINE_SEPARATOR + argument.substring(idx + 1);
idx += Html.LINE_SEPARATOR.length();
} else
if (c == ' ') {
// "Normal" continuation line (" * two").
argument = argument.substring(0, idx) + Html.LINE_SEPARATOR + argument.substring(idx + 2);
idx += Html.LINE_SEPARATOR.length();
} else
{
// Masked line break (" *two").
argument = argument.substring(0, idx) + argument.substring(idx + 1);
}
}
return argument;
}
if ("@code".equals(tagName)) {
if (argument == null) {
rootDoc.printError(ref.position(), "Argument missing for '{@code ...}' tag");
return "";
}
argument = Html.escapeSgmlEntities(argument);
return "" + argument + "
";
}
if ("@value".equals(tagName)) {
if (argument == null) {
rootDoc.printError(ref.position(), "Argument missing for '{@value ...}' tag");
return "";
}
Doc doc = argument.length() == 0 ? ref : Docs.findDoc(ref, argument, rootDoc);
if (doc == null) {
rootDoc.printError(ref.position(), "Field '" + argument + "' not found");
return argument;
}
if (!(doc instanceof FieldDoc)) {
rootDoc.printError(doc.position(), "'" + argument + "' does not designate a field");
return argument;
}
Object cv = ((FieldDoc) doc).constantValue();
if (cv == null) {
rootDoc.printError(
doc.position(),
"Field '" + argument + "' does not have a constant value"
);
return argument;
}
return this.makeLink(ref, doc, true, cv.toString(), null, rootDoc);
}
if ("@link".equals(tagName) || "@linkplain".equals(tagName)) {
if (argument == null) {
rootDoc.printError(ref.position(), "Argument missing for '{@link ...}' tag");
return "";
}
// Parse the argument of the {@link} tag, e.g. "MyClass#meth(int,java.lang.String) TEXT".
Matcher m = Pattern.compile("([^\\(\\s]*(?:\\([^\\)]*\\))?)(?:\\s+(.*))?").matcher(argument);
if (!m.matches()) throw new AssertionError("Regex does not match");
@NotNull final String targetSpec = m.group(1);
@Nullable final String label = m.group(2);
return this.makeLink(ref, targetSpec, "@linkplain".equals(tagName), label, rootDoc);
}
rootDoc.printError(ref.position(), (
"Inline tag '{"
+ tagName
+ "}' is not supported; you could "
+ "(A) remove it from the text, or "
+ "(B) improve 'Html.expandTag()' to transform it into nice HTML (if that is "
+ "reasonably possible)"
));
return "{" + tagName + (argument == null ? "" : " " + argument) + "}";
}
/**
* @param href A link like "{@code ../../pkg/MyClass#myMethod(java.lang.String)}"
* @param from The {@link ClassDoc} to which this link is relative to
* @return The package, class, field or method that the {@code href} designates
*/
public static Doc
hrefToDoc(String href, RootDoc rootDoc, ClassDoc from) throws Longjump {
String prefix = href.startsWith("#") ? from.qualifiedName() : from.containingPackage().name() + '.';
while (href.startsWith("../")) {
prefix = prefix.substring(0, prefix.lastIndexOf('.', prefix.length() - 2) + 1);
href = href.substring(3);
}
Doc result = Docs.findDoc(rootDoc, prefix + href.replace('/', '.'), rootDoc);
if (result == null) {
// It is a link to an "external javadoc", so leave it as is.
throw new Longjump();
}
return result;
}
/**
* Resolves a 'target specification' as in the "@link" tag.
* Example return values are:
*
* - {@code #myMethod}
* - {@code myMethod(java.lang.String)}
* - (method in same class)
* - {@code pkg.MyClass#myMethod(String)}
* - {@code MyClass.myMethod(java.lang.String)}
* - ("included" class)
*
* - {@code java.net.Socket#close()}
* -
* {@code
* Socket.close()}
*
* - (class is not "included", but is contained in an "external package")
* - {@code org.apache.tools.ant.Task#execute()}
* - {@code org.apache.tools.ant.Task.execute()}
* - (class is neither "included" nor documented in an "external package")
*
*
* @param from The package, class or member currently being documented, or the rootDoc
* @param to E.g. "{@code pkg.MyClass#myMethod(String)}"
* @param plain Whether this is a "{@code @linkplain}"
* @param label The (optional) label to display in the link
*/
public String
makeLink(Doc from, final String to, boolean plain, @Nullable String label, RootDoc rootDoc) throws Longjump {
Doc to2 = Docs.findDoc(from, to, rootDoc);
if (to2 == null) {
rootDoc.printError(from.position(), "Cannot resolve target \"" + to + "\" relative to \"" + from + "\"");
return "{@link " + to + (label == null ? "}" : ' ' + label + '}');
}
return this.makeLink(from, to2, plain, label, null, rootDoc);
}
/**
* @param plain Whether this is a "{@code @plainlink}"
* @param target The value of the (optional) 'target="..."' attribute of the HTML anchor
* @return An HTML snippet like "{@code THE-LABEL}"
*/
public String
makeLink(
Doc from,
Doc to,
boolean plain,
@Nullable String label,
@Nullable String target,
RootDoc rootDoc
) throws Longjump {
if (label == null) label = this.linkMaker.makeDefaultLabel(from, to, rootDoc);
if (!plain) label = "" + label + "
";
String href = this.linkMaker.makeHref(from, to, rootDoc);
if (href == null) return label;
return (
""
+ label
+ ""
);
}
/**
* Replaces "{@code <}", "{@code >}" and "{@code &}".
*/
public static String
escapeSgmlEntities(String text) {
text = Html.AMPERSAND.matcher(text).replaceAll("&");
text = Html.LESS_THAN.matcher(text).replaceAll("<");
text = Html.GREATER_THAN.matcher(text).replaceAll(">");
return text;
}
private static final Pattern AMPERSAND = Pattern.compile("&");
private static final Pattern LESS_THAN = Pattern.compile("<");
private static final Pattern GREATER_THAN = Pattern.compile(">");
/**
* Verifies that the named block tag exists at most once, replaces line breaks with spaces, and convert
* its text to HTML.
*
* @return {@code null} iff the tag does not exist
*/
@Nullable public String
optionalTag(Doc doc, String tagName, RootDoc rootDoc) throws Longjump {
String s = Tags.optionalTag(doc, tagName, rootDoc);
if (s == null) return null;
return this.fromJavadocText(s, doc, rootDoc);
}
/**
* Verifies that the named block tag exists at most once, replaces line breaks with spaces, and convert
* its text to HTML.
*
* @return defaulT iff the tag does not exist
*/
public String
optionalTag(Doc doc, String tagName, String defaulT, RootDoc rootDoc) throws Longjump {
String s = Tags.optionalTag(doc, tagName, defaulT, rootDoc);
return this.fromJavadocText(s, doc, rootDoc);
}
/**
* Generates HTML markup for the given {@code doc} in the context of {@code ref}.
*/
public String
generateFor(Doc doc, RootDoc rootDoc) throws Longjump {
// Generate HTML text from the doc's inline tags.
String htmlText = this.fromTags(doc.inlineTags(), doc, rootDoc);
// Append the "See also" list.
Tag[] seeTags = doc.tags("@see");
if (seeTags.length == 0) return htmlText;
StringBuilder sb = new StringBuilder(htmlText).append("- See also:
");
for (Tag seeTag : seeTags) {
try {
Doc to = Docs.findDoc(doc, seeTag.text(), rootDoc);
if (to == null) {
rootDoc.printError(doc.position(), "Cannot resolve '" + seeTag.text() + "'");
continue;
}
sb.append("").append(this.linkMaker.makeDefaultLabel(doc, to, rootDoc)).append("
");
} catch (Longjump e) {}
}
sb.append("
");
return sb.toString();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy