All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.unkrig.commons.doclet.html.Html Maven / Gradle / Ivy

Go to download

A versatile Java(TM) library that implements many useful container and utility classes.

There is a newer version: 1.1.12
Show newest version

/*
 * 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