org.eclipse.jetty.docs.JavadocIncludeExtension Maven / Gradle / Ivy
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.docs;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import org.asciidoctor.Asciidoctor;
import org.asciidoctor.ast.Document;
import org.asciidoctor.extension.IncludeProcessor;
import org.asciidoctor.extension.PreprocessorReader;
import org.asciidoctor.jruby.extension.spi.ExtensionRegistry;
import org.xml.sax.InputSource;
/**
* Asciidoctor include extension that includes into
* the document the output produced by an XSL transformation of
* parts of the javadoc of a file.
* Example usage in an Asciidoc page:
*
* include::javadoc[file=Source.java,xsl=source.xsl,tags=docs]
*
* Available configuration parameters are:
*
* - file
* - Mandatory, specifies the file to read the javadoc from, relative to the root of the Jetty Project source.
* - xsl
* - Mandatory, specifies the XSL file to use to transform the javadoc, relative to the root of the documentation source.
* - tags
* - Optional, specifies the name of the tagged regions of the javadoc to include.
* - replace
* - Optional, specifies a comma-separated pair where the first element is a regular
* expression and the second is the string replacement, applied to each included line.
*
* An example javadoc could be:
*
* /**
* * <p>Class description.</p>
* * <!-- tag::docs -->
* * <p>Parameters</p>
* * <table>
* * <tr>
* * <td>param</td>
* * <td>value</td>
* * </tr>
* * </table>
* * <!-- end::docs -->
* */
* public class A
* {
* }
*
* The javadoc lines included in the tagged region "docs" (between {@code tag::docs} and {@code end::docs})
* will be stripped of the asterisk at the beginning of the line and wrapped
* into a {@code <root>} element, so that it becomes a well-formed XML document.
* Each line of the XML document is then passed through the regular expression specified by the {@code replace}
* parameter (if any), and then transformed using the XSL file specified by the {@code xsl} parameter,
* which should produce a valid Asciidoc block which is then included in the Asciidoc documentation page.
*/
public class JavadocIncludeExtension implements ExtensionRegistry
{
@Override
public void register(Asciidoctor asciidoctor)
{
asciidoctor.javaExtensionRegistry().includeProcessor(JavadocIncludeExtension.JavadocIncludeProcessor.class);
}
public static class JavadocIncludeProcessor extends IncludeProcessor
{
private static final Pattern JAVADOC_INITIAL_ASTERISK = Pattern.compile("^\\s*\\*\\s*(.*)$");
private static final Pattern JAVADOC_INLINE_CODE = Pattern.compile("\\{@code ([^\\}]+)\\}");
@Override
public boolean handles(String target)
{
return "javadoc".equals(target);
}
@Override
public void process(Document document, PreprocessorReader reader, String target, Map attributes)
{
try
{
// Document attributes are converted by Asciidoctor to lowercase.
Path jettyDocsPath = Path.of((String)document.getAttribute("project-basedir"));
Path jettyRoot = jettyDocsPath.resolve("../..").normalize();
String file = (String)attributes.get("file");
if (file == null)
throw new IllegalArgumentException("Missing 'file' attribute");
Path filePath = jettyRoot.resolve(file.trim());
String xsl = (String)attributes.get("xsl");
if (xsl == null)
throw new IllegalArgumentException("Missing 'xsl' attribute");
Path xslPath = jettyDocsPath.resolve(xsl.trim());
List tagList = new ArrayList<>();
String tags = (String)attributes.get("tags");
if (tags != null)
{
for (String tag : tags.split(","))
{
tag = tag.trim();
boolean exclude = tag.startsWith("!");
if (exclude)
tag = tag.substring(1);
if (tag.isEmpty())
throw new IllegalArgumentException("Invalid tag in 'tags' attribute: " + tags);
tagList.add(new Tag(tag, exclude));
}
}
String replace = (String)attributes.get("replace");
List contentLines = new ArrayList<>();
contentLines.add("");
Iterator lines = Files.lines(filePath, StandardCharsets.UTF_8).iterator();
Deque tagStack = new ArrayDeque<>();
while (lines.hasNext())
{
String line = lines.next();
// Strip the initial Javadoc asterisk.
Matcher matcher = JAVADOC_INITIAL_ASTERISK.matcher(line);
if (matcher.matches())
line = matcher.group(1);
// Convert {@code X} into X
line = JAVADOC_INLINE_CODE.matcher(line).replaceAll("$1
");
boolean keepLine = tagList.isEmpty() || tagList.stream().allMatch(tag -> tag.exclude);
if (tagStack.isEmpty())
{
for (Tag tag : tagList)
{
if (line.contains("tag::" + tag.name))
tagStack.push(tag);
}
}
else
{
Tag currentTag = tagStack.peek();
keepLine = !currentTag.exclude;
if (line.contains("end::" + currentTag.name))
{
tagStack.pop();
keepLine = false;
}
}
if (keepLine)
{
if (replace == null)
contentLines.add(line);
else
contentLines.addAll(replace(line, replace));
}
}
contentLines.add(" ");
String content = String.join("\n", contentLines);
// Run the XML stylesheet over the remaining lines.
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
org.w3c.dom.Document xml = builder.parse(new InputSource(new StringReader(content)));
Transformer transformer = TransformerFactory.newInstance().newTransformer(new StreamSource(xslPath.toFile()));
StringWriter output = new StringWriter(content.length());
transformer.transform(new DOMSource(xml), new StreamResult(output));
String asciidoc = output.toString();
asciidoc = Arrays.stream(asciidoc.split("\n")).map(String::stripLeading).collect(Collectors.joining("\n"));
reader.pushInclude(asciidoc, "javadoc", target, 1, attributes);
}
catch (Throwable x)
{
reader.pushInclude(x.toString(), "javadoc", target, 1, attributes);
x.printStackTrace();
}
}
private List replace(String line, String replace)
{
// Format is: (regexp,replacement).
String[] parts = replace.split(",");
String regExp = parts[0];
String replacement = parts[1].replace("\\n", "\n");
return List.of(line.replaceAll(regExp, replacement).split("\n"));
}
private static class Tag
{
private final String name;
private final boolean exclude;
private Tag(String name, boolean exclude)
{
this.name = name;
this.exclude = exclude;
}
}
}
}