/******************************************************************************* * Copyright (c) 2013, 2014 Obeo. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Obeo - initial API and implementation *******************************************************************************/ package org.eclipse.emf.compare.doc; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.charset.Charset; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.mylyn.internal.wikitext.core.parser.builder.DefaultSplittingStrategy; import org.eclipse.mylyn.internal.wikitext.core.parser.builder.NoSplittingStrategy; import org.eclipse.mylyn.internal.wikitext.core.parser.builder.SplitOutlineItem; import org.eclipse.mylyn.internal.wikitext.core.parser.builder.SplittingHtmlDocumentBuilder; import org.eclipse.mylyn.internal.wikitext.core.parser.builder.SplittingOutlineParser; import org.eclipse.mylyn.internal.wikitext.core.parser.builder.SplittingStrategy; import org.eclipse.mylyn.internal.wikitext.core.validation.StandaloneMarkupValidator; import org.eclipse.mylyn.wikitext.core.parser.MarkupParser; import org.eclipse.mylyn.wikitext.core.parser.builder.HtmlDocumentBuilder; import org.eclipse.mylyn.wikitext.core.parser.markup.MarkupLanguage; import org.eclipse.mylyn.wikitext.core.parser.outline.OutlineItem; import org.eclipse.mylyn.wikitext.core.parser.util.MarkupToEclipseToc; import org.eclipse.mylyn.wikitext.core.util.XmlStreamWriter; import org.eclipse.mylyn.wikitext.core.validation.ValidationProblem; import org.eclipse.mylyn.wikitext.core.validation.ValidationProblem.Severity; /** * @author <a href="mailto:mikael.barbero@obeo.fr">Mikael Barbero</a> */ public class WikiTextToHTML { private static final FileSystem DEFAULT_FS = FileSystems.getDefault(); private static final boolean FORMAT_OUTPUT = true; private static final Charset UTF_8 = Charset.forName("UTF-8"); private static final String MEDIA_WIKI = "MediaWiki"; private MarkupLanguage markupLanguage; private boolean navigationImages = false; private String title; private boolean emitDoctype = false; private boolean multipleOutputFiles; private String copyrightNotice; private boolean xhtmlStrict; private String prependImagePrefix; private String defaultAbsoluteLinkTarget; private String linkRel; private boolean suppressBuiltInCssStyles; private boolean useInlineCssStyles; private String htmlDoctype; private List<Stylesheet> stylesheets; private List<Stylesheet> helpStylesheets = new ArrayList<>(); private List<Stylesheet> websiteStylesheets = new ArrayList<>(); private PrimaryTOCWriter primaryTOCWriter = new PrimaryTOCWriter(); private Path sourceFolder; private List<Path> foldersToCopy; private Path targetRootFolder; private Path targetWebsiteFolder; private Path targetHelpFolder; private boolean genEclipseHelp; private boolean genWebsite; private String version; private String projectName; private String projectId; private String anchorId; public static void main(String[] args) throws Exception { WikiTextToHTML wikiTextToHTML = new WikiTextToHTML(); wikiTextToHTML.run(args); } public void run(String[] args) throws Exception { processCommandLineArgs(args); if (targetRootFolder == null) { System.err.println("Error: unable to find -location argument"); usage(); System.exit(1); } if (version == null || "".equals(version.trim())) { System.err.println("Error: unable to find -version argument"); usage(); System.exit(1); } if (!genEclipseHelp && !genWebsite) { System.err.println("Error: you must at least provide a -eclipsehelp or a -website option"); usage(); System.exit(1); } if (projectName == null || "".equals(projectName)) { System.err.println("Error: unable to find -projectname argument"); usage(); System.exit(1); } if (genEclipseHelp && (projectId == null || "".equals(projectId.trim()))) { System.err.println("Error: -projectid is mandatory when using -eclipsehelp"); usage(); System.exit(1); } if (!genEclipseHelp && anchorId != null && !anchorId.trim().equals("")) { System.err.println("Error: -anchorid can be used only with -eclipsehelp"); usage(); System.exit(1); } markupLanguage = new CustomMediaWikiLanguage(); markupLanguage.setInternalLinkPattern("{0}"); Stylesheet ss1 = new Stylesheet(); ss1.setUrl("/help/topic/" + projectId + "/help/resources/bootstrap.css"); helpStylesheets.add(ss1); Stylesheet ss2 = new Stylesheet(); ss2.setUrl("/help/topic/" + projectId + "/help/resources/custom.css"); helpStylesheets.add(ss2); ss1 = new Stylesheet(); ss1.setUrl("resources/bootstrap.css"); websiteStylesheets.add(ss1); ss2 = new Stylesheet(); ss2.setUrl("resources/custom.css"); websiteStylesheets.add(ss2); sourceFolder = DEFAULT_FS.getPath("src"); foldersToCopy = new ArrayList<>(); foldersToCopy.add(targetRootFolder.resolve(sourceFolder).resolve("images")); foldersToCopy.add(targetRootFolder.resolve(sourceFolder).resolve("resources")); final Path resolvedTargetHelpFolder = targetRootFolder.resolve(targetHelpFolder); if (genEclipseHelp) { if (Files.exists(resolvedTargetHelpFolder)) { System.out.println( "Deleting " + resolvedTargetHelpFolder + " before regenerating Eclipse help"); removeRecursiveContent(resolvedTargetHelpFolder); } primaryTOCWriter.startPrimaryTOC(targetHelpFolder.resolve("index.html"), projectName + " Documentation", anchorId); } final PathMatcher mediawikiPattern = DEFAULT_FS.getPathMatcher("glob:**/*.mediawiki"); Files.walkFileTree(targetRootFolder.resolve(sourceFolder), new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path markupPath, BasicFileAttributes attrs) throws IOException { if (mediawikiPattern.matches(markupPath)) { if ("~javadoc.mediawiki".equals(markupPath.getFileName().toString())) { processJavadoc(sourceFolder, targetWebsiteFolder, targetHelpFolder, markupPath); } else { processFile(sourceFolder, targetWebsiteFolder, targetHelpFolder, markupPath); } } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { System.err.println("Failed to visit " + file); exc.printStackTrace(); return FileVisitResult.CONTINUE; } /** * {@inheritDoc} * * @see java.nio.file.SimpleFileVisitor#preVisitDirectory(java.lang.Object, * java.nio.file.attribute.BasicFileAttributes) */ @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { if (genEclipseHelp && !dir.equals(targetRootFolder.resolve(sourceFolder)) && !foldersToCopy.contains(dir)) { if (dir.resolve("index.mediawiki").toFile().exists()) { primaryTOCWriter.startTopic(getTitle(dir), dir.resolve("index.html")); } else { primaryTOCWriter.startTopic(getTitle(dir), null); } } return FileVisitResult.CONTINUE; } /** * {@inheritDoc} * * @see java.nio.file.SimpleFileVisitor#postVisitDirectory(java.lang.Object, java.io.IOException) */ @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { if (genEclipseHelp && !dir.equals(targetRootFolder.resolve(sourceFolder)) && !foldersToCopy.contains(dir)) { primaryTOCWriter.endTopic(); } return FileVisitResult.CONTINUE; } }); if (genEclipseHelp) { primaryTOCWriter.endPrimaryTOC(); writeStringToFile(primaryTOCWriter.getPrimaryTOCContent(), resolvedTargetHelpFolder.resolve("toc.xml")); writeStringToFile(primaryTOCWriter.getPluginContent(), targetRootFolder.resolve("plugin.xml")); } for (Path folder : foldersToCopy) { if (genWebsite) copy(folder, targetRootFolder.resolve(targetWebsiteFolder).resolve(folder.getFileName()), "glob:**/*"); if (genEclipseHelp) copy(folder, resolvedTargetHelpFolder.resolve(folder.getFileName()), "glob:**/*"); } } /** * */ private void usage() { System.out.println( "Usage: wikiTextToHTML -projectname \"Name of the Project\" -projectid org.eclipse.emf.compare.doc -location path -version version [-eclipsehelp path] [-website path] [-anchorId path/id]"); } private void processCommandLineArgs(String[] args) throws Exception { if (args == null) throw new Exception("No argument provided"); for (int i = 0; i < args.length; i++) { String option = args[i]; String arg = ""; if (i == args.length - 1 || args[i + 1].startsWith("-")) {//$NON-NLS-1$ // do nothgin } else { arg = args[++i]; } if (option.equalsIgnoreCase("-location")) { //$NON-NLS-1$ targetRootFolder = DEFAULT_FS.getPath(arg); } if (option.equalsIgnoreCase("-eclipsehelp")) { //$NON-NLS-1$ genEclipseHelp = true; targetHelpFolder = DEFAULT_FS.getPath(arg); } if (option.equalsIgnoreCase("-website")) { //$NON-NLS-1$ genWebsite = true; targetWebsiteFolder = DEFAULT_FS.getPath(arg); } if (option.equalsIgnoreCase("-version")) { //$NON-NLS-1$ version = arg.trim(); } if (option.equalsIgnoreCase("-projectname")) { //$NON-NLS-1$ projectName = arg.trim(); } if (option.equalsIgnoreCase("-projectid")) { //$NON-NLS-1$ projectId = arg.trim(); } if (option.equalsIgnoreCase("-anchorid")) { //$NON-NLS-1$ anchorId = arg.trim(); } } if (targetHelpFolder.isAbsolute()) { targetHelpFolder = targetRootFolder.relativize(targetHelpFolder); } if (targetWebsiteFolder.isAbsolute()) { targetWebsiteFolder = targetRootFolder.relativize(targetWebsiteFolder); } } private String getTitle(Path path) { String filename = path.getFileName().toString(); int lastIndexOf = filename.lastIndexOf('.'); if (lastIndexOf >= 0) { filename = filename.substring(0, lastIndexOf); } String[] split = filename.split("-"); StringBuilder sb = new StringBuilder(); for (int i = 0; i < split.length; i++) { String str = split[i].trim(); if (str.length() > 0) { if (i == 0) { char firstChar = str.charAt(0); sb.append(Character.toUpperCase(firstChar)); sb.append(str.substring(1)); } else { sb.append(str); } sb.append(' '); } } return sb.toString().trim(); } private void processJavadoc(final Path sourceFolder, final Path targetWebsiteFolder, final Path targetHelpFolder, Path markupPath) throws IOException, FileNotFoundException, UnsupportedEncodingException { System.out.println("Processing " + markupPath); Path relativeMarkupPath = targetRootFolder.resolve(sourceFolder).relativize(markupPath); Path relativeTOCPath = relativeMarkupPath.getParent().resolve("toc-javadoc.xml"); Path targetTOC = targetHelpFolder.resolve(relativeTOCPath); Path targetHelp = targetHelpFolder.resolve(changeFilename(relativeMarkupPath, ".html")); if (genEclipseHelp) { mkdirs(targetRootFolder.resolve(targetTOC)); mkdirs(targetRootFolder.resolve(targetHelp)); } // for eclipse help if (genEclipseHelp) { final PathMatcher indexPattern = DEFAULT_FS.getPathMatcher("glob:**/index.mediawiki"); if (!indexPattern.matches(markupPath)) { primaryTOCWriter.startTopic("Reference", null); primaryTOCWriter.createLink(relativeTOCPath); primaryTOCWriter.endTopic(); } } } private void processFile(final Path sourceFolder, final Path targetWebsiteFolder, final Path targetHelpFolder, Path markupPath) throws IOException, FileNotFoundException, UnsupportedEncodingException { System.out.println("Processing " + markupPath); Path relativeMarkupPath = targetRootFolder.resolve(sourceFolder).relativize(markupPath); Path targetHTML = targetWebsiteFolder.resolve(changeFilename(relativeMarkupPath, ".html")); Path relativeTOCPath = changeFilename(relativeMarkupPath, "toc-", ".xml"); Path targetTOC = targetHelpFolder.resolve(relativeTOCPath); Path targetHelp = targetHelpFolder.resolve(changeFilename(relativeMarkupPath, ".html")); if (genWebsite) { mkdirs(targetRootFolder.resolve(targetHTML)); } if (genEclipseHelp) { mkdirs(targetRootFolder.resolve(targetTOC)); mkdirs(targetRootFolder.resolve(targetHelp)); } String markupContent = new String(Files.readAllBytes(markupPath), UTF_8); final String markupContentWithTOC; if ("index.mediawiki".equals(markupPath.getFileName().toString())) { markupContentWithTOC = markupContent.replaceFirst("=(.*)=", "=" + projectName + " — $1=\n\nVersion " + version + "\n\n") + "\n\nVersion " + version; } else { Path relativeToRoot = targetHTML.getParent() .relativize(targetWebsiteFolder.resolve("index.html")); markupContentWithTOC = markupContent.replaceFirst("=(.*)=", "=" + projectName + " — $1=\n\nVersion " + version + "\n\n__TOC__\n\n") + "\n\nPart of [" + relativeToRoot + " " + projectName + " Documentation]" + "\n\nVersion " + version; } if (performValidation(markupPath, markupContent)) { // for website if (genWebsite) { stylesheets = websiteStylesheets; genHTML(getTitle(targetHTML), markupContentWithTOC, targetRootFolder.resolve(targetHTML)); } // for eclipse help if (genEclipseHelp) { stylesheets = helpStylesheets; genHTML(getTitle(targetHTML), markupContent, targetRootFolder.resolve(targetHelp)); final PathMatcher indexPattern = DEFAULT_FS.getPathMatcher("glob:**/index.mediawiki"); if (!indexPattern.matches(markupPath)) { genTOC(getTitle(targetHelp), markupContent, targetRootFolder.resolve(targetTOC), targetHelp); primaryTOCWriter.startTopic(getTitle(targetHelp), targetHelp); primaryTOCWriter.createLink(relativeTOCPath); primaryTOCWriter.endTopic(); } } } else { System.exit(1); } } private void mkdirs(Path file) { File parentFile = file.getParent().toFile(); if (!parentFile.exists()) { file.getParent().toFile().mkdirs(); } } private static void removeRecursiveContent(final Path path) throws IOException { Files.walkFileTree(path, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { // try to delete the file anyway, even if its attributes // could not be read, since delete-only access is // theoretically possible Files.delete(file); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { if (exc == null) { if (!dir.equals(path)) { Files.delete(dir); } return FileVisitResult.CONTINUE; } else { // directory iteration failed; propagate exception throw exc; } } }); } private Path changeFilename(Path source, String newFileExtension) { return changeFilename(source, "", newFileExtension); } private Path changeFilename(Path source, String prefix, String newFileExtension) { String filename = source.getFileName().toString(); int lastIndexOf = filename.lastIndexOf("."); String newFileName = prefix + filename.substring(0, lastIndexOf) + newFileExtension; return source.resolveSibling(newFileName); } private void genTOC(String name, String markupContent, Path targetToc, Path targetHTML) throws IOException, UnsupportedEncodingException, FileNotFoundException { MarkupToEclipseToc toEclipseToc = new MarkupToEclipseToc() { public String createToc(OutlineItem root) { StringWriter out = new StringWriter(8096); XmlStreamWriter writer = createXmlStreamWriter(out); writer.writeStartDocument("utf-8", "1.0"); //$NON-NLS-1$ //$NON-NLS-2$ if (copyrightNotice != null) { writer.writeComment(copyrightNotice); } Method method = null; try { method = MarkupToEclipseToc.class.getDeclaredMethod("emitToc", XmlStreamWriter.class, List.class); method.setAccessible(true); } catch (NoSuchMethodException | SecurityException | IllegalArgumentException e) { throw new RuntimeException(e); } if (root.getChildren().size() == 1 && root.getChildren().get(0).getLevel() == 1) { OutlineItem innerRoot = root.getChildren().get(0); writer.writeStartElement("toc"); //$NON-NLS-1$ writer.writeAttribute("topic", getHtmlFile() + "#" + innerRoot.getId()); //$NON-NLS-1$ writer.writeAttribute("label", innerRoot.getLabel()); //$NON-NLS-1$ try { method.invoke(this, writer, innerRoot.getChildren()); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new RuntimeException(e); } } else { writer.writeStartElement("toc"); //$NON-NLS-1$ writer.writeAttribute("topic", getHtmlFile()); //$NON-NLS-1$ writer.writeAttribute("label", root.getLabel()); //$NON-NLS-1$ try { method.invoke(this, writer, root.getChildren()); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new RuntimeException(e); } } writer.writeEndElement(); // toc writer.writeEndDocument(); writer.close(); return out.toString(); } }; toEclipseToc.setMarkupLanguage(markupLanguage.clone()); toEclipseToc.setHtmlFile(targetHTML.toString()); toEclipseToc.setBookTitle(name); String tocContents = toEclipseToc.parse(markupContent); writeStringToFile(tocContents, targetToc); } private void writeStringToFile(String content, Path path) throws UnsupportedEncodingException, FileNotFoundException, IOException { try (Writer writer = new OutputStreamWriter( new BufferedOutputStream(new FileOutputStream(path.toFile())), "UTF-8")) { writer.write(content); } } private void genHTML(String name, String markupContent, Path htmlOutputFile) throws IOException, FileNotFoundException { try (Writer writer = new OutputStreamWriter( new BufferedOutputStream(new FileOutputStream(htmlOutputFile.toFile())), UTF_8)) { HtmlDocumentBuilder builder = new HtmlDocumentBuilder(writer, FORMAT_OUTPUT); for (Stylesheet stylesheet : stylesheets) { HtmlDocumentBuilder.Stylesheet builderStylesheet; if (stylesheet.url != null) { if (stylesheets == websiteStylesheets) { Path stylesheetPath = DEFAULT_FS.getPath(stylesheet.url); Path targetStylesheetPath = targetRootFolder .resolve(targetWebsiteFolder.resolve(stylesheetPath)); Path relativeStylesheetPath = htmlOutputFile.getParent() .relativize(targetStylesheetPath); builderStylesheet = new HtmlDocumentBuilder.Stylesheet( relativeStylesheetPath.toString()); } else { builderStylesheet = new HtmlDocumentBuilder.Stylesheet(stylesheet.url); } } else { builderStylesheet = new HtmlDocumentBuilder.Stylesheet(stylesheet.file); } builder.addCssStylesheet(builderStylesheet); if (!stylesheet.attributes.isEmpty()) { for (Map.Entry<String, String> attr : stylesheet.attributes.entrySet()) { builderStylesheet.getAttributes().put(attr.getKey(), attr.getValue()); } } } builder.setTitle(title == null ? name.toString() : 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); SplittingStrategy splittingStrategy = multipleOutputFiles ? new DefaultSplittingStrategy() : new NoSplittingStrategy(); SplittingOutlineParser outlineParser = new SplittingOutlineParser(); outlineParser.setMarkupLanguage(markupLanguage.clone()); outlineParser.setSplittingStrategy(splittingStrategy); SplitOutlineItem item = outlineParser.parse(markupContent); item.setSplitTarget(htmlOutputFile.toFile().getName()); SplittingHtmlDocumentBuilder splittingBuilder = new SplittingHtmlDocumentBuilder(); splittingBuilder.setRootBuilder(builder); splittingBuilder.setOutline(item); splittingBuilder.setRootFile(htmlOutputFile.toFile()); splittingBuilder.setNavigationImages(navigationImages); splittingBuilder.setFormatting(FORMAT_OUTPUT); MarkupParser parser = new MarkupParser(); parser.setMarkupLanguage(markupLanguage); parser.setBuilder(splittingBuilder); parser.parse(markupContent); } } private void copy(final Path sourceFolder, final Path targetFolder, String pattern) throws IOException { final PathMatcher imageMatcher = DEFAULT_FS.getPathMatcher(pattern); Files.walkFileTree(sourceFolder, new SimpleFileVisitor<Path>() { @Override public FileVisitResult visitFile(Path sourcePath, BasicFileAttributes attrs) throws IOException { if (imageMatcher.matches(sourcePath)) { String targetFile = sourcePath.toString().replace(sourceFolder.toString(), targetFolder.toString()); Path targetPath = DEFAULT_FS.getPath(targetFile); targetPath.getParent().toFile().mkdirs(); Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING); } return FileVisitResult.CONTINUE; } }); } /** * Returns true if valid (may have warning). * * @param source * @param markupContent * @return */ private boolean performValidation(Path source, String markupContent) { StandaloneMarkupValidator markupValidator = StandaloneMarkupValidator.getValidator(MEDIA_WIKI); List<ValidationProblem> problems = markupValidator.validate(markupContent); int errorCount = 0; for (ValidationProblem problem : problems) { String messageLevel = problem.getSeverity().name(); if (problem.getSeverity() == Severity.ERROR) { errorCount++; } System.out.println(String.format("%s: %s:%s %s", messageLevel, source.toString(), //$NON-NLS-1$ problem.getOffset(), problem.getMessage())); } return errorCount == 0; } public static class Stylesheet { private File file; private String url; private final Map<String, String> attributes = new HashMap<>(); public File getFile() { return file; } public void setFile(File file) { this.file = file; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public void addConfiguredAttribute(Attribute attribute) { attributes.put(attribute.getName(), attribute.getValue()); } } public static class Attribute { private String name; private String value; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } } }