blob: 4e262090e732c6051c249df7060149136098933b [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2013, 2015 Tasktop Technologies and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* David Green - initial API and implementation
*******************************************************************************/
package org.eclipse.mylyn.wikitext.maven.internal;
import static java.text.MessageFormat.format;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.eclipse.mylyn.wikitext.maven.internal.SourceFileTraversal.Visitor;
import org.eclipse.mylyn.wikitext.parser.MarkupParser;
import org.eclipse.mylyn.wikitext.parser.builder.HtmlDocumentBuilder;
import org.eclipse.mylyn.wikitext.parser.markup.MarkupLanguage;
import org.eclipse.mylyn.wikitext.parser.util.MarkupToEclipseToc;
import org.eclipse.mylyn.wikitext.splitter.DefaultSplittingStrategy;
import org.eclipse.mylyn.wikitext.splitter.NoSplittingStrategy;
import org.eclipse.mylyn.wikitext.splitter.SplitOutlineItem;
import org.eclipse.mylyn.wikitext.splitter.SplittingHtmlDocumentBuilder;
import org.eclipse.mylyn.wikitext.splitter.SplittingMarkupToEclipseToc;
import org.eclipse.mylyn.wikitext.splitter.SplittingOutlineParser;
import org.eclipse.mylyn.wikitext.splitter.SplittingStrategy;
import org.eclipse.mylyn.wikitext.util.ServiceLocator;
import com.google.common.io.Files;
/**
* @goal eclipse-help
* @phase compile
*/
public class MarkupToEclipseHelpMojo extends AbstractMojo {
/**
* Output folder.
*
* @parameter expression="${project.build.directory}/generated-eclipse-help"
* @required
*/
protected File outputFolder;
/**
* Source folder.
*
* @parameter expression="${basedir}/src/main/docs"
* @required
*/
protected File sourceFolder;
/**
* The filename format to use when generating output filenames for HTML files. Defaults to {@code $1.html} where
* {@code $1} is the name of the source file without extension.
*
* @parameter
*/
protected String htmlFilenameFormat = "$1.html"; //$NON-NLS-1$
/**
* The filename format to use when generating output filenames for Eclipse help table of contents XML files.
* Defaults to {@code $1-toc.xml} where {@code $1} is the name of the source file without extension.
*
* @parameter
*/
protected String xmlFilenameFormat = "$1-toc.xml"; //$NON-NLS-1$
/**
* Specify the title of the output document. If unspecified, the title is the filename (without extension).
*
* @parameter
*/
protected String title;
/**
* The 'rel' value for HTML links. If specified the value is applied to all generated links. The default value is
* null.
*
* @parameter
*/
protected String linkRel;
/**
* Indicate if output should be generated to multiple output files (true/false). Default is false.
*
* @parameter
*/
protected boolean multipleOutputFiles = false;
/**
* @parameter expression="utf-8"
*/
private final String sourceEncoding = "utf-8";
/**
* Indicate if the output should be formatted (true/false). Default is false.
*
* @parameter
*/
protected boolean formatOutput = false;
/**
* Indicate if navigation links should be images (true/false). Only applicable for multi-file output. Default is
* false.
*
* @parameter
*/
protected boolean navigationImages = false;
/**
* If specified, the prefix is prepended to relative image urls.
*
* @parameter
*/
protected String prependImagePrefix = null;
/**
* @parameter
*/
protected boolean useInlineCssStyles = true;
/**
* @parameter
*/
protected boolean suppressBuiltInCssStyles = false;
/**
* Specify that hyperlinks to external resources (<a href) should use a target attribute to cause them to be
* opened in a seperate window or tab. The value specified becomes the value of the target attribute on anchors
* where the href is an absolute URL.
*
* @parameter
*/
protected String defaultAbsoluteLinkTarget;
/**
* Indicate if the builder should attempt to conform to strict XHTML rules. The default is false.
*
* @parameter
*/
protected boolean xhtmlStrict = false;
/**
* Indicate if the builder should emit a DTD doctype declaration. The default is true.
*
* @parameter
*/
protected boolean emitDoctype = true;
/**
* The doctype to use. Defaults to
* {@code <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">}
* .
*
* @parameter
*/
protected String htmlDoctype = null;
/**
* The copyright notice to include in generated output files.
*
* @parameter
*/
protected String copyrightNotice = null;
/**
* The list of CSS stylesheet URLs relative to the {@link #sourceFolder}.
*
* @parameter
*/
protected List<String> stylesheetUrls = new ArrayList<>();
/**
* the prefix to URLs in the toc.xml, typically the relative path from the plugin to the help files. For example, if
* the help file is in 'help/index.html' then the help prefix would be 'help'
*
* @parameter
*/
protected String helpPrefix;
/**
* Indicates the heading level at which anchors of the form {@code &lt;anchor id="additions"/&gt;} should be
* emitted. A level of 0 corresponds to the root of the document, and levels 1-6 correspond to heading levels h1,
* h2...h6.
* <p>
* The default level is 0 (the document root)
* </p>
*
* @parameter
*/
protected int tocAnchorLevel = 0;
/**
* Indicates whether an embedded table of contents is generated. When true, a table of contents is generated in each
* HTML page. Using CSS the table of contents can be positioned on the left hand side in a column, with portions of
* the table of contents expanded or collapsed.
* <p>
* Defaults to false.
* </p>
*
* @parameter
*/
protected boolean embeddedTableOfContents = false;
public void execute() throws MojoExecutionException, MojoFailureException {
try {
ensureOutputFolderExists();
ensureSourceFolderExists();
ServiceLocator serviceLocator = ServiceLocator.getInstance(MarkupToEclipseHelpMojo.class.getClassLoader());
Set<MarkupLanguage> markupLanguages = serviceLocator.getAllMarkupLanguages();
if (markupLanguages.isEmpty()) {
throw new MojoFailureException("No markup languages are available");
}
getLog().info(
format("Generating Eclipse help content from sources: {0} -> {1}", sourceFolder, outputFolder));
final FileToMarkupLanguage fileToMarkupLanguage = new FileToMarkupLanguage(markupLanguages);
SourceFileTraversal fileTraversal = new SourceFileTraversal(sourceFolder);
final AtomicInteger fileCount = new AtomicInteger();
fileTraversal.traverse(new Visitor() {
@Override
public void accept(String relativePath, File sourceFile) {
fileCount.incrementAndGet();
process(sourceFile, relativePath, fileToMarkupLanguage.get(sourceFile));
}
});
getLog().info(format("Processed {0} files", fileCount.get()));
} catch (BuildFailureException e) {
getLog().error(e.getMessage(), e);
throw new MojoFailureException(e.getMessage(), e.getCause());
}
}
protected void process(File sourceFile, String relativePath, MarkupLanguage markupLanguage) {
if (markupLanguage == null) {
copy(sourceFile, relativePath);
} else {
processMarkup(sourceFile, relativePath, markupLanguage);
}
}
private void copy(File sourceFile, String relativePath) {
File targetFolder = new File(outputFolder, relativePath);
ensureFolderExists("target folder", targetFolder, true);
File targetFile = new File(targetFolder, sourceFile.getName());
try {
Files.copy(sourceFile, targetFile);
} catch (IOException e) {
throw new BuildFailureException(
format("Cannot copy {0} to {1}: {2}", sourceFile, targetFile, e.getMessage()), e);
}
}
protected void processMarkup(File sourceFile, String relativePath, MarkupLanguage markupLanguage) {
getLog().info(format("Processing markup file: {0}", sourceFile));
String name = sourceFile.getName();
if (name.lastIndexOf('.') != -1) {
name = name.substring(0, name.lastIndexOf('.'));
}
File htmlOutputFile = computeHtmlFile(relativePath, name);
if (!htmlOutputFile.exists() || htmlOutputFile.lastModified() < sourceFile.lastModified()) {
String markupContent = readFully(sourceFile);
if (!htmlOutputFile.getParentFile().exists()) {
if (!htmlOutputFile.getParentFile().mkdirs()) {
throw new BuildFailureException(format("Cannot create folder {0}", htmlOutputFile.getParentFile()));
}
}
Writer writer = createWriter(htmlOutputFile);
try {
HtmlDocumentBuilder builder = createRootBuilder(writer, name, relativePath);
SplittingStrategy splittingStrategy = createSplittingStrategy();
SplittingOutlineParser outlineParser = createOutlineParser(markupLanguage, splittingStrategy);
SplitOutlineItem rootTocItem = outlineParser.parse(markupContent);
rootTocItem.setSplitTarget(htmlOutputFile.getName());
SplittingHtmlDocumentBuilder splittingBuilder = createSplittingBuilder(builder, rootTocItem,
htmlOutputFile, relativePath);
MarkupParser parser = new MarkupParser();
parser.setMarkupLanguage(markupLanguage);
parser.setBuilder(splittingBuilder);
parser.parse(markupContent);
createEclipseHelpToc(rootTocItem, sourceFile, relativePath, htmlOutputFile, name);
} finally {
close(writer, htmlOutputFile);
}
}
}
private void close(Writer writer, File file) {
try {
writer.close();
} catch (IOException e) {
throw new BuildFailureException(format("Cannot write to file {0}: {1}", file, e.getMessage()));
}
}
private void createEclipseHelpToc(SplitOutlineItem rootTocItem, File sourceFile, String relativePath,
File htmlOutputFile, String name) {
File tocOutputFile = computeTocFile(htmlOutputFile, name);
if (!tocOutputFile.exists() || tocOutputFile.lastModified() < sourceFile.lastModified()) {
Writer writer = createWriter(tocOutputFile);
try {
MarkupToEclipseToc toEclipseToc = createMarkupToEclipseToc(relativePath, htmlOutputFile, name);
String tocXml = toEclipseToc.createToc(rootTocItem);
writer.write(tocXml);
} catch (IOException e) {
throw new BuildFailureException(format("Cannot write to file {0}: {1}", tocOutputFile, e.getMessage()),
e);
} finally {
close(writer, tocOutputFile);
}
}
}
protected MarkupToEclipseToc createMarkupToEclipseToc(String relativePath, File htmlOutputFile, String name) {
MarkupToEclipseToc toEclipseToc = new SplittingMarkupToEclipseToc();
toEclipseToc.setBookTitle(title == null ? name : title);
toEclipseToc.setCopyrightNotice(copyrightNotice);
toEclipseToc.setAnchorLevel(tocAnchorLevel);
toEclipseToc.setHelpPrefix(calculateHelpPrefix(relativePath));
toEclipseToc.setHtmlFile(htmlOutputFile.getName());
return toEclipseToc;
}
protected String calculateHelpPrefix(String relativePath) {
String prefix = helpPrefix == null ? "" : helpPrefix;
if (relativePath.length() > 0) {
if (prefix.length() > 0) {
prefix += "/";
}
prefix += relativePath;
}
return prefix.length() == 0 ? null : prefix.replaceAll("\\\\", "/");
}
private File computeTocFile(File htmlFile, String name) {
return new File(htmlFile.getParentFile(), xmlFilenameFormat.replace("$1", name)); //$NON-NLS-1$
}
protected SplittingHtmlDocumentBuilder createSplittingBuilder(HtmlDocumentBuilder builder, SplitOutlineItem item,
File htmlOutputFile, String relativePath) {
SplittingHtmlDocumentBuilder splittingBuilder = new SplittingHtmlDocumentBuilder();
splittingBuilder.setRootBuilder(builder);
splittingBuilder.setOutline(item);
splittingBuilder.setEmbeddedTableOfContents(embeddedTableOfContents);
splittingBuilder.setRootFile(htmlOutputFile);
splittingBuilder.setNavigationImages(navigationImages);
splittingBuilder
.setNavigationImagePath(computeResourcePath(splittingBuilder.getNavigationImagePath(), relativePath));
splittingBuilder.setFormatting(formatOutput);
return splittingBuilder;
}
private SplittingOutlineParser createOutlineParser(MarkupLanguage markupLanguage,
SplittingStrategy splittingStrategy) {
SplittingOutlineParser outlineParser = new SplittingOutlineParser();
outlineParser.setMarkupLanguage(markupLanguage.clone());
outlineParser.setSplittingStrategy(splittingStrategy);
return outlineParser;
}
private SplittingStrategy createSplittingStrategy() {
SplittingStrategy splittingStrategy = multipleOutputFiles
? new DefaultSplittingStrategy()
: new NoSplittingStrategy();
return splittingStrategy;
}
protected HtmlDocumentBuilder createRootBuilder(Writer writer, String name, String relativePath) {
HtmlDocumentBuilder builder = new HtmlDocumentBuilder(writer, formatOutput);
builder.setTitle(title == null ? name : title);
builder.setEmitDtd(emitDoctype);
if (emitDoctype && htmlDoctype != null) {
builder.setHtmlDtd(htmlDoctype);
}
builder.setUseInlineStyles(useInlineCssStyles);
builder.setSuppressBuiltInStyles(suppressBuiltInCssStyles);
builder.setLinkRel(linkRel);
builder.setDefaultAbsoluteLinkTarget(defaultAbsoluteLinkTarget);
builder.setPrependImagePrefix(prependImagePrefix);
builder.setXhtmlStrict(xhtmlStrict);
builder.setCopyrightNotice(copyrightNotice);
builder.setHtmlFilenameFormat(htmlFilenameFormat);
configureStylesheets(builder, relativePath);
return builder;
}
protected void configureStylesheets(HtmlDocumentBuilder builder, String relativePath) {
for (String cssStylesheetUrl : stylesheetUrls) {
builder.addCssStylesheet(
new HtmlDocumentBuilder.Stylesheet(computeResourcePath(cssStylesheetUrl, relativePath)));
}
}
protected String computeResourcePath(String resourcePath, String relativePath) {
if (resourcePath.startsWith("/") || isAbsoluteUri(resourcePath)) {
return resourcePath;
}
String path = resourcePath;
String prefix = relativePath.replaceAll("[^\\\\/]+", "..").replace('\\', '/');
if (prefix.length() > 0) {
if (!resourcePath.startsWith("/")) {
prefix += '/';
}
path = prefix + resourcePath;
}
return path;
}
private boolean isAbsoluteUri(String resourcePath) {
try {
return new URI(resourcePath).getScheme() != null;
} catch (URISyntaxException e) {
throw new BuildFailureException(format("\"{0}\" is not a valid URI", resourcePath), e);
}
}
private Writer createWriter(File outputFile) {
Writer writer;
try {
writer = new OutputStreamWriter(new BufferedOutputStream(new FileOutputStream(outputFile)), "utf-8");
} catch (IOException e) {
throw new BuildFailureException(format("Cannot write to file {0}: {1}", outputFile, e.getMessage()), e);
}
return writer;
}
protected File computeHtmlFile(final String relativePath, String name) {
File parent = outputFolder;
if (relativePath.length() > 0) {
parent = new File(parent, relativePath);
}
return new File(parent, htmlFilenameFormat.replace("$1", name)); //$NON-NLS-1$
}
protected void ensureSourceFolderExists() {
ensureFolderExists("Source folder", sourceFolder, false);
}
protected void ensureOutputFolderExists() {
ensureFolderExists("Output folder", outputFolder, true);
}
protected void ensureFolderExists(String name, File folder, boolean createIfMissing) {
if (folder.exists()) {
if (!folder.isDirectory()) {
throw new BuildFailureException(format("{0} exists but is not a folder: {1}", name, folder));
}
return;
}
if (!createIfMissing) {
throw new BuildFailureException(format("{0} does not exist: {1}", name, folder));
}
if (!folder.mkdirs()) {
throw new BuildFailureException(format("Cannot create {0}: {1}", name, folder));
}
}
protected String readFully(File inputFile) {
try {
return Files.toString(inputFile, Charset.forName(sourceEncoding));
} catch (IOException e) {
throw new BuildFailureException(format("Cannot read source file {0}: {1}", inputFile, e.getMessage()), e);
}
}
}