You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@netbeans.apache.org by GitBox <gi...@apache.org> on 2019/01/07 20:25:39 UTC

[GitHub] vieiro closed pull request #8: Donation 3 - HTML to AsciiDoc Conversion

vieiro closed pull request #8: Donation 3 - HTML to AsciiDoc Conversion
URL: https://github.com/apache/incubator-netbeans-tools/pull/8
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/.gitignore b/.gitignore
index be539e7..08e92ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,6 @@
 /wiki-convert/target/
 *.swp
 *.swo
+/html-convert/target/**
+/html-convert/tutorials-asciidoc/**
+/html-convert/external-links.txt
diff --git a/html-convert/README.md b/html-convert/README.md
new file mode 100644
index 0000000..94e8b38
--- /dev/null
+++ b/html-convert/README.md
@@ -0,0 +1,13 @@
+# html-convert
+
+This tool reads tutorials in HTML format from the 3rd Oracle donation and converts these to the AsciiDoc format.
+
+## Getting started
+
+1. Download and extract the third Oracle donation zip file. This will generate a "netbeans-docs.zip" file.
+2. Extract the "netbeans-docs.zip" zip file somewhere in a directory "X". This will generate the "X/docs" folder.
+3. Run `mvn package exec:java X`, where "X" is the directory where you extracted the docs zip file.
+4. Open the `tutorials-asciidoc` directory to see the results.
+5. See the generated "external-links.txt" file to see referenced external links.
+
+NOTE: This tool is expected to be run once, after that manual revision of generated files should be done.
diff --git a/html-convert/nbactions.xml b/html-convert/nbactions.xml
new file mode 100644
index 0000000..24261f4
--- /dev/null
+++ b/html-convert/nbactions.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+<actions>
+        <action>
+            <actionName>run</actionName>
+            <packagings>
+                <packaging>jar</packaging>
+            </packagings>
+            <goals>
+                <goal>process-classes</goal>
+                <goal>exec:java</goal>
+            </goals>
+            <properties>
+                <exec.args>C:\Users\avieiro\Downloads\NETBEANS\</exec.args>
+                <exec.executable>java</exec.executable>
+            </properties>
+        </action>
+        <action>
+            <actionName>debug</actionName>
+            <packagings>
+                <packaging>jar</packaging>
+            </packagings>
+            <goals>
+                <goal>process-classes</goal>
+                <goal>org.codehaus.mojo:exec-maven-plugin:1.5.0:exec</goal>
+            </goals>
+            <properties>
+                <exec.args>-agentlib:jdwp=transport=dt_socket,server=n,address=${jpda.address} -classpath %classpath org.netbeans.tools.tutorials.HTMLConverter C:\Users\avieiro\Downloads\NETBEANS</exec.args>
+                <exec.executable>java</exec.executable>
+                <jpda.listen>true</jpda.listen>
+            </properties>
+        </action>
+        <action>
+            <actionName>profile</actionName>
+            <packagings>
+                <packaging>jar</packaging>
+            </packagings>
+            <goals>
+                <goal>process-classes</goal>
+                <goal>org.codehaus.mojo:exec-maven-plugin:1.5.0:exec</goal>
+            </goals>
+            <properties>
+                <exec.args>-classpath %classpath org.netbeans.tools.tutorials.HTMLConverter C:\Users\avieiro\Downloads\NETBEANS</exec.args>
+                <exec.executable>java</exec.executable>
+            </properties>
+        </action>
+    </actions>
diff --git a/html-convert/pom.xml b/html-convert/pom.xml
new file mode 100644
index 0000000..d076ce1
--- /dev/null
+++ b/html-convert/pom.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.netbeans.tools</groupId>
+    <artifactId>html-convert</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.eclipse.mylyn.docs</groupId>
+            <artifactId>org.eclipse.mylyn.wikitext</artifactId>
+            <version>3.0.20</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.mylyn.docs</groupId>
+            <artifactId>org.eclipse.mylyn.wikitext.mediawiki</artifactId>
+            <version>3.0.20</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.mylyn.docs</groupId>
+            <artifactId>org.eclipse.mylyn.wikitext.asciidoc</artifactId>
+            <version>3.0.20</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.mylyn.docs</groupId>
+            <artifactId>org.eclipse.mylyn.wikitext.html</artifactId>
+            <version>3.0.20</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.mylyn.docs</groupId>
+            <artifactId>org.eclipse.mylyn.wikitext.markdown</artifactId>
+            <version>3.0.20</version>
+        </dependency>
+        <dependency>
+            <groupId>org.asciidoctor</groupId>
+            <artifactId>asciidoctorj</artifactId>
+            <version>1.5.6</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.spullara.mustache.java</groupId>
+            <artifactId>compiler</artifactId>
+            <version>0.9.5</version>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>exec-maven-plugin</artifactId>
+                <version>1.5.0</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>java</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <mainClass>org.netbeans.tools.tutorials.HTMLConverter</mainClass>
+                    <arguments>
+                        <argument>C:\Users\avieiro\Downloads\NETBEANS</argument>
+                    </arguments>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/html-convert/src/main/java/org/netbeans/tools/tutorials/AsciidocPostProcessor.java b/html-convert/src/main/java/org/netbeans/tools/tutorials/AsciidocPostProcessor.java
new file mode 100644
index 0000000..aa0abc4
--- /dev/null
+++ b/html-convert/src/main/java/org/netbeans/tools/tutorials/AsciidocPostProcessor.java
@@ -0,0 +1,240 @@
+/*
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+ */
+package org.netbeans.tools.tutorials;
+
+import com.github.mustachejava.DefaultMustacheFactory;
+import com.github.mustachejava.Mustache;
+import com.github.mustachejava.MustacheFactory;
+import com.github.mustachejava.functions.BundleFunctions;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ResourceBundle;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class AsciidocPostProcessor {
+
+    private static final Logger LOG = Logger.getLogger(AsciidocPostProcessor.class.getName());
+
+    private static enum ContentSectionState {
+        BEFORE_CONTENT_SECTION,
+        INSIDE_CONTENT_SECTION,
+        AFTER_CONTENT_SECTION
+    };
+
+    private static Pattern TITLE_PATTERN = Pattern.compile("^= (.*)$");
+
+    private static Pattern EN_CONTENTS_PATTERN = Pattern.compile("^.*Contents.*");
+
+    private static boolean isContentsHeader(String line) {
+        return line.contains("*Content")
+                || line.contains("目录")
+                || line.contains("目次")
+                || line.contains("Conteúdo")
+                || line.contains("Содержание");
+    }
+
+    /**
+     * Scans a generated AsciiDoc file and remove the "*Contents*" section with
+     * all links in it. Because the contents section is being replaced by a
+     * table of contents 'toc'.
+     *
+     * @param file
+     * @param titles
+     * @throws IOException
+     */
+    private static void cleanUpAndGetTitle(File file, HashMap<File, String> titles) throws IOException {
+        File temporaryFile = new File(file.getParentFile(), file.getName() + ".tmp");
+        ContentSectionState state = ContentSectionState.BEFORE_CONTENT_SECTION;
+        String title = null;
+
+        try (BufferedReader input = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8));
+                PrintWriter output = new PrintWriter(new OutputStreamWriter(new FileOutputStream(temporaryFile), StandardCharsets.UTF_8))) {
+
+            do {
+                String line = input.readLine();
+                if (line == null) {
+                    break;
+                }
+                if (title == null) {
+                    Matcher m = TITLE_PATTERN.matcher(line);
+                    if (m.matches()) {
+                        title = m.group(1);
+                    }
+                }
+                switch (state) {
+                    case BEFORE_CONTENT_SECTION:
+                        if (isContentsHeader(line)) {
+                            state = ContentSectionState.INSIDE_CONTENT_SECTION;
+                            break;
+                        }
+                        output.println(line);
+                        break;
+                    case INSIDE_CONTENT_SECTION:
+                        if (line.startsWith("* <")
+                                || line.startsWith("* link:")) {
+                            // Ignore bullet lists with cross references or external links
+                        } else {
+                            output.println(line);
+                        }
+                        if (line.startsWith("=")) {
+                            state = ContentSectionState.AFTER_CONTENT_SECTION;
+                        }
+                        break;
+                    case AFTER_CONTENT_SECTION:
+                        output.println(line);
+                        break;
+                }
+            } while (true);
+
+        }
+
+        Files.move(temporaryFile.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+
+        if (title != null) {
+            titles.put(file, title);
+        }
+    }
+
+    static Map<File, String> removeContentSetcion(File dest) throws Exception {
+        LOG.log(Level.INFO, "Removing \"Content\" section of files...");
+
+        // Retrieve the list of asciidoc files
+        List<File> asciidocFiles = Files.find(dest.toPath(), 999,
+                (p, bfa) -> bfa.isRegularFile()).map(Path::toFile).filter((f) -> f.getName().endsWith(".asciidoc")).collect(Collectors.toList());
+
+        // Remove the 'Contents' section and fetch titles
+        HashMap<File, String> titles = new HashMap<>();
+        for (File file : asciidocFiles) {
+            cleanUpAndGetTitle(file, titles);
+        }
+
+        return titles;
+    }
+
+    /**
+     * Generates index.asciidoc, index_ja.asciidoc, index_pt_BR.asciidoc, etc.,
+     * in the nested directories, each index file has a list of links to
+     * asciidoc documents in the directory. Asciidoc documents are sorted by
+     * title, attending to the proper Locale collation rules.
+     *
+     * @param dest The destination directory.
+     * @param titles
+     * @throws IOException
+     */
+    static void generateIndexes(File dest, Map<File, String> titles) throws IOException {
+        LOG.info("Generating index.asciidoc (and translations) on all directories...");
+        /*
+        Compute the list of directories under 'dest'
+         */
+        List<File> directories = Files.find(dest.toPath(), 999,
+                (p, bfa) -> bfa.isDirectory()
+        ).map((p) -> p.toFile()).collect(Collectors.toList());
+
+        /*
+        A filter that selects documents in english (i.e., without _ja, _pt_BR, etc. suffixes).
+         */
+        FileFilter englishTutorialsFilter = (f) -> f.isFile() && Language.getLanguage(f) == Language.DEFAULT;
+
+        MustacheFactory factory = new DefaultMustacheFactory();
+        Mustache indexMustache = factory.compile("org/netbeans/tools/tutorials/index-template.mustache");
+        Mustache sectionMustache = factory.compile("org/netbeans/tools/tutorials/section-template.mustache");
+
+        /*
+        Iterate over all nexted directories...
+         */
+        for (File directory : directories) {
+            if ("images".equals(directory.getName())) {
+                continue;
+            }
+
+            HashMap<Language, List<File>> filesByLanguage = new HashMap<>();
+            /*
+            Compute the files in english
+             */
+            File[] tutorialsEnglish = directory.listFiles(englishTutorialsFilter);
+            for (File english : tutorialsEnglish) {
+
+                List<File> englishFiles = filesByLanguage.get(Language.DEFAULT);
+                if (englishFiles == null) {
+                    englishFiles = new ArrayList<File>();
+                    filesByLanguage.put(Language.DEFAULT, englishFiles);
+                }
+                englishFiles.add(english);
+                /*
+                And retrieve the list of translations of the english file.
+                 */
+                HashMap<Language, File> translations = Language.getTranslations(english);
+                for (Map.Entry<Language, File> translation : translations.entrySet()) {
+                    List<File> languageFiles = filesByLanguage.get(translation.getKey());
+                    if (languageFiles == null) {
+                        languageFiles = new ArrayList<>();
+                        filesByLanguage.put(translation.getKey(), languageFiles);
+                    }
+                    languageFiles.add(translation.getValue());
+                }
+            }
+
+            for (Map.Entry<Language, List<File>> entry : filesByLanguage.entrySet()) {
+                Language language = entry.getKey();
+                if (language == Language.UNKNOWN) {
+                    continue;
+                }
+                ResourceBundle bundle = ResourceBundle.getBundle("org.netbeans.tools.tutorials.TutorialsBundle", language.locale);
+                String directoryTitle = bundle.getString( directory.getName() + ".title");
+                if (directoryTitle == null) {
+                    throw new IllegalArgumentException("Please add a title for directory '" + directory.getName() + "' in locale " + language.locale);
+                }
+                LocalizedTutorialSection section = new LocalizedTutorialSection(language, directoryTitle);
+                section.addAll(entry.getValue());
+                section.sort(titles);
+
+                // Generate the index
+                String name = "index" + language.extension;
+                File output = new File(directory, name);
+                try (Writer indexOutput = new OutputStreamWriter(new FileOutputStream(output), StandardCharsets.UTF_8)) {
+                    indexMustache.execute(indexOutput, section);
+                }
+                
+                // Also generate section.asciidoc (section_ja.asciidoc, etc.)
+                // This will be in a sidebar for all tutorials in this section
+                String sectionSidebarName = "section" + language.extension;
+                File sectionSidebarFile = new File(directory, sectionSidebarName);
+                try (Writer indexOutput = new OutputStreamWriter(new FileOutputStream(sectionSidebarFile), StandardCharsets.UTF_8)) {
+                    sectionMustache.execute(indexOutput, section);
+                }
+                
+            }
+
+
+        }
+    }
+
+}
diff --git a/html-convert/src/main/java/org/netbeans/tools/tutorials/CustomAsciiDocDocumentBuilder.java b/html-convert/src/main/java/org/netbeans/tools/tutorials/CustomAsciiDocDocumentBuilder.java
new file mode 100644
index 0000000..6c49a98
--- /dev/null
+++ b/html-convert/src/main/java/org/netbeans/tools/tutorials/CustomAsciiDocDocumentBuilder.java
@@ -0,0 +1,462 @@
+/*
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+ */
+package org.netbeans.tools.tutorials;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import org.eclipse.mylyn.wikitext.asciidoc.internal.AsciiDocDocumentBuilder;
+import org.eclipse.mylyn.wikitext.parser.Attributes;
+import org.eclipse.mylyn.wikitext.parser.DocumentBuilder;
+import org.eclipse.mylyn.wikitext.parser.ImageAttributes;
+import org.eclipse.mylyn.wikitext.parser.LinkAttributes;
+import org.eclipse.mylyn.wikitext.parser.builder.AbstractMarkupDocumentBuilder;
+
+/**
+ * An AsciiDocDocumentBuilder.
+ *
+ * @author Antonio Vieiro <vi...@apache.org>
+ */
+public class CustomAsciiDocDocumentBuilder extends AsciiDocDocumentBuilder {
+
+    /**
+     * Base class for asciidoc delimited blocks.
+     */
+    class AsciiDocContentBlock extends AbstractMarkupDocumentBuilder.NewlineDelimitedBlock {
+
+        protected String prefix;
+        protected String suffix;
+
+        AsciiDocContentBlock(DocumentBuilder.BlockType blockType, String prefix, String suffix) {
+            this(blockType, prefix, suffix, 1, 1);
+        }
+
+        AsciiDocContentBlock(DocumentBuilder.BlockType blockType, String prefix, String suffix, int leadingNewlines, int trailingNewlines) {
+            super(blockType, leadingNewlines, trailingNewlines);
+            this.prefix = prefix;
+            this.suffix = suffix;
+        }
+
+        AsciiDocContentBlock(String prefix, String suffix, int leadingNewlines, int trailingNewlines) {
+            this(null, prefix, suffix, leadingNewlines, trailingNewlines);
+        }
+
+        @Override
+        public void write(int c) throws IOException {
+            CustomAsciiDocDocumentBuilder.this.emitContent(c);
+        }
+
+        @Override
+        public void write(String s) throws IOException {
+            CustomAsciiDocDocumentBuilder.this.emitContent(s);
+        }
+
+        @Override
+        public void open() throws IOException {
+            super.open();
+            pushWriter(new StringWriter());
+        }
+
+        @Override
+        public void close() throws IOException {
+            Writer thisContent = popWriter();
+            String content = thisContent.toString();
+            if (content.length() > 0) {
+                emitContent(content);
+            }
+            super.close();
+        }
+
+        protected void emitContent(final String content) throws IOException {
+            CustomAsciiDocDocumentBuilder.this.emitContent(prefix);
+            CustomAsciiDocDocumentBuilder.this.emitContent(content);
+            CustomAsciiDocDocumentBuilder.this.emitContent(suffix);
+        }
+
+    }
+
+    /**
+     * Handles links. Links with images are handled properly, copying images
+     * from the image directory close to the document.
+     */
+    private class LinkBlock extends AsciiDocContentBlock {
+
+        final LinkAttributes attributes;
+
+        LinkBlock(LinkAttributes attributes) {
+            super("", "", 0, 0);
+            this.attributes = attributes;
+        }
+
+        @Override
+        protected void emitContent(String content) throws IOException {
+            String href = attributes.getHref();
+            if (href == null) {
+                if (attributes.getId() != null) {
+                    super.emitContent("[[" + attributes.getId() + "]]\n");
+                    return;
+                }
+                LOG.log(Level.WARNING, "Empty href: {0}", href);
+            }
+            href = href == null ? "" : href;
+            href = copyImageIfRequired(href, false);
+
+            if (href.startsWith("http")) {
+                externalLinks.addExternalLink(href, CustomAsciiDocDocumentBuilder.this.relativePathToTutorialFile);
+            }
+
+            if (content.contains("image:")) {
+                // Hande links with images properly, using a image with a "link" attribute
+                // content is something like "image:images/whatever-small.png[]" (small image)
+                // href is something like "images/whatever.png" (larger image)
+                // This must be transformed (https://stackoverflow.com/questions/34299474/using-an-image-as-a-link-in-asciidoc)
+                // to
+                // image:whatever-small.png[link="whatever.png"]
+                String smallPart = content.substring(6);
+                int i = smallPart.indexOf('[');
+                smallPart = i == -1 ? smallPart : smallPart.substring(0, i);
+                StringBuilder sb = new StringBuilder();
+                sb.append("[.feature]\n");
+                sb.append("--\n");
+                sb.append("image");
+                sb.append(smallPart);
+                sb.append("[role=\"left\", link=\"").append(href).append("\"]\n");
+                sb.append("--\n");
+                CustomAsciiDocDocumentBuilder.this.emitContent(sb.toString());
+            } else {
+                // link::http://url.com[label]
+                CustomAsciiDocDocumentBuilder.this.emitContent("link:"); //$NON-NLS-1$
+                CustomAsciiDocDocumentBuilder.this.emitContent(href);
+                CustomAsciiDocDocumentBuilder.this.emitContent("[+");
+                if (content != null) {
+                    CustomAsciiDocDocumentBuilder.this.emitContent(content);
+                }
+                CustomAsciiDocDocumentBuilder.this.emitContent("+]");
+            }
+        }
+
+    }
+
+    /**
+     * A header-1 block.
+     */
+    class AsciiDocMainHeaderBlock extends AsciiDocContentBlock {
+
+        public AsciiDocMainHeaderBlock() {
+            super("", "", 2, 2);
+        }
+
+        @Override
+        protected void emitContent(String content) throws IOException {
+            String trimmedContent = content.replaceAll("\n", " ");
+            super.emitContent("= " + trimmedContent + "\n");
+            super.emitContent(":jbake-type: tutorial\n");
+            super.emitContent(":jbake-tags: tutorials \n");
+            super.emitContent(":jbake-status: published\n");
+            super.emitContent(":syntax: true\n");
+            super.emitContent(":source-highlighter: pygments\n");
+            super.emitContent(":toc: left\n");
+            super.emitContent(":toc-title:\n");
+            super.emitContent(":description: " + trimmedContent + " - Apache NetBeans\n");
+            super.emitContent(":keywords: Apache NetBeans, Tutorials, " + trimmedContent + "");
+            super.emitContent("\n");
+        }
+
+    }
+
+    private static final String headerPrefix(int level) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < level; i++) {
+            sb.append("=");
+        }
+        sb.append(" ");
+        return sb.toString();
+    }
+
+    class AsciiDocHeaderBlock extends AsciiDocContentBlock {
+
+        private Attributes attributes;
+        private int level;
+
+        public AsciiDocHeaderBlock(int level, Attributes attributes) {
+            super("", "", 2, 2);
+            this.attributes = attributes;
+            this.level = level;
+        }
+
+        @Override
+        protected void emitContent(String content) throws IOException {
+            super.emitContent("\n");
+            if (attributes != null && attributes.getId() != null) {
+                super.emitContent("[[" + attributes.getId() + "]]\n");
+            }
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < level; i++) {
+                sb.append("=");
+            }
+            sb.append(" ");
+            sb.append(content.replaceAll("\n", " "));
+            sb.append("\n\n");
+            super.emitContent(sb.toString());
+        }
+    }
+
+    /**
+     * An inline code block.
+     */
+    class AsciiDocInlinePreformatted extends AsciiDocContentBlock {
+
+        String language;
+
+        AsciiDocInlinePreformatted(String language, CustomAsciiDocDocumentBuilder documentBuilder) {
+            super(BlockType.CODE, " ``", "`` ", 0, 0);
+            this.language = language;
+        }
+
+    }
+    
+    private static final Pattern RUBY_PATTERN=Pattern.compile("^require '.*", Pattern.MULTILINE);
+    private static final Pattern C_PATTERN = Pattern.compile("^#include.*", Pattern.MULTILINE);
+    private static final Pattern SHELL_PATTERN = Pattern.compile("^\\$ ", Pattern.MULTILINE);
+
+    /**
+     * Generates a
+     * <pre> block.
+     */
+    class AsciiDocPreformatted extends AsciiDocContentBlock {
+
+        String language;
+
+        AsciiDocPreformatted(String language) {
+            super(BlockType.PREFORMATTED, "", "", 1, 1);
+            this.language = language;
+        }
+
+        @Override
+        protected void emitContent(String content) throws IOException {
+            if (language == null) {
+                // Use "java" as a default language for tutorials
+                language = "java";
+                if (content.contains("<?xml") || content.contains("</")) {
+                    language = "xml";
+                }
+                if (content.contains("<div") || content.contains("<p>")) {
+                    language = "html";
+                }
+                if (C_PATTERN.matcher(content).find()) {
+                    language = "c";
+                }
+                if (RUBY_PATTERN.matcher(content).find()) {
+                    language = "ruby";
+                }
+                if (SHELL_PATTERN.matcher(content).find()) {
+                    language = "shell";
+                }
+                if (content.contains("<?php")) {
+                    language = "php";
+                }
+                if (content.contains("$.") || content.contains("function (")) {
+                    language = "javascript";
+                }
+            }
+            // [label](http://url.com) or
+            // [label](http://url.com "title")
+            CustomAsciiDocDocumentBuilder.this.emitContent("\n[source," + language + "]\n");
+            CustomAsciiDocDocumentBuilder.this.emitContent("----\n");
+            CustomAsciiDocDocumentBuilder.this.emitContent(content.startsWith("\n") ? content : "\n" + content);
+            CustomAsciiDocDocumentBuilder.this.emitContent("\n----\n");
+        }
+    }
+    
+    private static Logger LOG = Logger.getLogger(CustomAsciiDocDocumentBuilder.class.getName());
+
+    private File topDirectory;
+    private File imageDirectory;
+    private File outputDirectory;
+    private File imageDestDirectory;
+    private ExternalLinksMap externalLinks;
+    private Language language;
+    private String relativePathToTutorialsRoot;
+    private File outputFile;
+    private String relativePathToTutorialFile;
+    
+    public CustomAsciiDocDocumentBuilder(File topDirectory, File imageDirectory, File outputFile, BufferedWriter output, ExternalLinksMap externalLinks) {
+        super(output);
+        this.topDirectory = topDirectory;
+        this.imageDirectory = imageDirectory;
+        this.outputFile = outputFile;
+        this.outputDirectory = outputFile.getParentFile();
+        this.language = Language.getLanguage(outputFile);
+        imageDestDirectory = new File(outputDirectory, "images");
+        imageDestDirectory.mkdirs();
+        this.externalLinks = externalLinks;
+        relativePathToTutorialsRoot = outputDirectory.toURI().relativize( topDirectory.toURI()).getPath();
+        relativePathToTutorialFile = topDirectory.toURI().relativize( outputFile.toURI() ).getPath();
+        
+    }
+
+    /**
+     * Responsible for handling headers.
+     *
+     * @param level
+     * @param attributes
+     * @return
+     */
+    @Override
+    protected Block computeHeading(int level, Attributes attributes) {
+        if (level == 1) {
+            return new AsciiDocMainHeaderBlock();
+        }
+        //return super.computeHeading(level, attributes);
+        return new AsciiDocHeaderBlock(level, attributes);
+    }
+
+    @Override
+    protected Block computeSpan(SpanType type, Attributes attributes) {
+        switch (type) {
+            case MARK:
+                throw new IllegalStateException("Mark");
+            case MONOSPACE:
+                return new AsciiDocInlinePreformatted(null, this);
+            case LINK:
+                if (attributes instanceof LinkAttributes) {
+                    LinkAttributes linkAttributes = (LinkAttributes) attributes;
+                    if (linkAttributes.getHref() != null) {
+                        if (linkAttributes.getHref().startsWith("#")) {
+                            return new AsciiDocContentBlock("<<" + linkAttributes.getHref().substring(1) + ",", ">>", 0, 0);
+                            /* This is an internal link */
+                        }
+                        return new LinkBlock((LinkAttributes) attributes);
+                    } else if (linkAttributes.getId() != null) {
+                        return new AsciiDocContentBlock("[[", "]]", 0, 1);
+                    }
+                }
+                return new AsciiDocContentBlock("", "", 0, 0);
+            case DELETED:
+                return new AsciiDocContentBlock("[.line-through]#", "#", 0, 0);
+            case UNDERLINED:
+                return new AsciiDocContentBlock("[.underline]#", "#", 0, 0);
+            default:
+                return super.computeSpan(type, attributes);
+        }
+    }
+
+    @Override
+    public void link(Attributes attributes, String hrefOrHashName, String text) {
+        super.link(attributes, hrefOrHashName, text);
+    }
+
+    @Override
+    public void entityReference(String entity) {
+        super.entityReference(entity);
+    }
+
+    @Override
+    protected Block
+            computeBlock(BlockType type, Attributes attributes) {
+        switch (type) {
+            case CODE:
+            case PREFORMATTED:
+                String language = null;
+
+                if (attributes.getCssClass() != null) {
+                    if (attributes.getCssClass().equals("source-java")) {
+                        language = "java";
+
+                    } else if (attributes.getCssClass().equals("source-xml")) {
+                        language = "xml";
+
+                    } else if (attributes.getCssClass().equals("source-properties")) {
+                        language = "yaml";
+
+                    }
+                }
+                return new AsciiDocPreformatted(language);
+            default:
+                return super.computeBlock(type, attributes);
+        }
+    }
+
+    @Override
+    public void image(Attributes attributes, String url) {
+        url = copyImageIfRequired(url, true);
+
+        assertOpenBlock();
+        try {
+            currentBlock.write(computeImage(attributes, url));
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private String computeImage(Attributes attributes, String url) {
+        // image:/path/to/img.jpg[] or
+        // image:path/to/img.jpg[alt text]
+        String altText = null;
+        String title = null;
+        if (attributes instanceof ImageAttributes) {
+            ImageAttributes imageAttr = (ImageAttributes) attributes;
+            altText = imageAttr.getAlt();
+        }
+        if (!Strings.isNullOrEmpty(attributes.getTitle())) {
+            title = "title=\"" + attributes.getTitle().replaceAll("\n", " ").replaceAll("\r", " ") + '"'; //$NON-NLS-1$
+        }
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("image::"); //$NON-NLS-1$
+        sb.append(Strings.nullToEmpty(url));
+        sb.append("["); //$NON-NLS-1$
+        sb.append(Joiner.on(", ").skipNulls().join(altText, title)); //$NON-NLS-1$
+        sb.append("]"); //$NON-NLS-1$
+        return sb.toString();
+    }
+
+    private String copyImageIfRequired(String url, boolean warnMissingImages) {
+        if (url.contains("images_www/articles/")) {
+            File imageFile = new File(imageDirectory, url.replaceAll("^.*images_www/articles/", "/"));
+            File copiedImageFile = new File(imageDestDirectory, imageFile.getName());
+            if (imageFile.exists()) {
+                if (!copiedImageFile.exists()) {
+                    try {
+                        Files.copy(imageFile.toPath(), copiedImageFile.toPath());
+                        url = imageDestDirectory.getName() + "/" + imageFile.getName();
+                    } catch (IOException ex) {
+                        Logger.getLogger(CustomAsciiDocDocumentBuilder.class.getName()).log(Level.SEVERE, null, ex);
+                    }
+                } else {
+                    url = imageDestDirectory.getName() + "/" + imageFile.getName();
+                }
+            }
+        } else if (warnMissingImages) {
+            LOG.log(Level.WARNING, "Image not found: {0} in file {1}", new Object[]{url, CustomAsciiDocDocumentBuilder.this.outputFile.getAbsolutePath()});
+        }
+        return url;
+    }
+}
diff --git a/html-convert/src/main/java/org/netbeans/tools/tutorials/CustomAsciiDocDocumentBuilderWithoutTables.java b/html-convert/src/main/java/org/netbeans/tools/tutorials/CustomAsciiDocDocumentBuilderWithoutTables.java
new file mode 100644
index 0000000..bc701c2
--- /dev/null
+++ b/html-convert/src/main/java/org/netbeans/tools/tutorials/CustomAsciiDocDocumentBuilderWithoutTables.java
@@ -0,0 +1,47 @@
+/*
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+ */
+package org.netbeans.tools.tutorials;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import org.eclipse.mylyn.wikitext.parser.Attributes;
+
+/**
+ *
+ */
+public class CustomAsciiDocDocumentBuilderWithoutTables extends CustomAsciiDocDocumentBuilder {
+
+    public CustomAsciiDocDocumentBuilderWithoutTables(File topDirectory, File imageDirectory, File outputFile, BufferedWriter output, ExternalLinksMap externalLinks) {
+        super(topDirectory, imageDirectory, outputFile, output, externalLinks);
+    }
+
+    @Override
+    protected Block computeBlock(BlockType type, Attributes attributes) {
+        switch(type) {
+            case TABLE:
+            case TABLE_CELL_HEADER:
+            case TABLE_CELL_NORMAL:
+            case TABLE_ROW:
+                return new AsciiDocContentBlock("", "", 0, 0);
+        }
+        return super.computeBlock(type, attributes); //To change body of generated methods, choose Tools | Templates.
+    }
+
+
+}
diff --git a/html-convert/src/main/java/org/netbeans/tools/tutorials/ExternalLinksMap.java b/html-convert/src/main/java/org/netbeans/tools/tutorials/ExternalLinksMap.java
new file mode 100644
index 0000000..3e19585
--- /dev/null
+++ b/html-convert/src/main/java/org/netbeans/tools/tutorials/ExternalLinksMap.java
@@ -0,0 +1,81 @@
+/*
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+ */
+package org.netbeans.tools.tutorials;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Maps external links (String, href) to File's referencing it.
+ */
+public class ExternalLinksMap {
+    
+    /** Maps "href" to tutorials. */
+    private TreeMap<String, TreeSet<String>> hrefToFile;
+    /** Maps domain names ("bits.netbeans.org", for instance) to hrefs */
+    private TreeMap<String, TreeSet<String>> domainToHref;
+    
+    public ExternalLinksMap() {
+        hrefToFile = new TreeMap<>();
+        domainToHref = new TreeMap<>();
+    }
+    
+    public void addExternalLink(String href, String tutorial) {
+        try {
+            URL url = new URL(href);
+            TreeSet<String> hrefs = domainToHref.get(url.getHost());
+            if (hrefs == null) {
+                hrefs = new TreeSet<>();
+                domainToHref.put(url.getHost(), hrefs);
+            }
+            hrefs.add(href);
+            
+            TreeSet<String> tutorials = hrefToFile.get(href);
+            if (tutorials == null) {
+                tutorials = new TreeSet<>();
+                hrefToFile.put(href, tutorials);
+            }
+            tutorials.add(tutorial);
+        } catch (MalformedURLException ex) {
+            Logger.getLogger(ExternalLinksMap.class.getName()).log(Level.SEVERE, null, ex);
+        }
+    }
+    
+    public Set<String> getDomains() {
+        return domainToHref.keySet();
+    }
+    
+    public Set<String> getHrefs(String domain) {
+        return domainToHref.get(domain);
+    }
+    
+    public Set<String> getTutorials(String href) {
+        return hrefToFile.get(href);
+    }
+    
+    @Override
+    public String toString() {
+        return domainToHref.toString() + " " + hrefToFile.toString();
+    }
+}
diff --git a/html-convert/src/main/java/org/netbeans/tools/tutorials/HTMLConverter.java b/html-convert/src/main/java/org/netbeans/tools/tutorials/HTMLConverter.java
new file mode 100644
index 0000000..d80ef20
--- /dev/null
+++ b/html-convert/src/main/java/org/netbeans/tools/tutorials/HTMLConverter.java
@@ -0,0 +1,276 @@
+/*
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+ */
+package org.netbeans.tools.tutorials;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.Transformer;
+import java.io.*;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.SimpleDateFormat;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import org.eclipse.mylyn.wikitext.parser.HtmlParser;
+import org.xml.sax.InputSource;
+
+/**
+ * Converts HTML files in a source directory to ASCIIDOC files in a destination
+ * directory.
+ *
+ * @author Antonio Vieiro <vi...@apache.org>
+ */
+public class HTMLConverter {
+
+    private static final String APACHE_LICENSE_HEADER = ""
+            + "\n"
+            + "    Licensed to the Apache Software Foundation (ASF) under one\n"
+            + "    or more contributor license agreements.  See the NOTICE file\n"
+            + "    distributed with this work for additional information\n"
+            + "    regarding copyright ownership.  The ASF licenses this file\n"
+            + "    to you under the Apache License, Version 2.0 (the\n"
+            + "    \"License\"); you may not use this file except in compliance\n"
+            + "    with the License.  You may obtain a copy of the License at\n"
+            + "\n"
+            + "      http://www.apache.org/licenses/LICENSE-2.0\n"
+            + "\n"
+            + "    Unless required by applicable law or agreed to in writing,\n"
+            + "    software distributed under the License is distributed on an\n"
+            + "    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n"
+            + "    KIND, either express or implied.  See the License for the\n"
+            + "    specific language governing permissions and limitations\n"
+            + "    under the License.\n\n";
+
+    private static final Logger LOG = Logger.getLogger(HTMLConverter.class.getName());
+    private static Transformer transformer;
+    private static DocumentBuilderFactory documentBuilderFactory;
+
+    private static void convert(File docsTutorialsDocs, File docsTutorialsImages, File dest, ExternalLinksMap externalLinks) throws Exception {
+        LOG.log(Level.INFO, "Converting tutorials from {0} to {1}", new Object[]{docsTutorialsDocs.getAbsolutePath(), dest.getAbsolutePath()});
+
+        List<File> html_files = Files.find(docsTutorialsDocs.toPath(), 999,
+                (p, bfa) -> bfa.isRegularFile()).map(Path::toFile).filter((f) -> f.getName().endsWith(".html")).collect(Collectors.toList());
+
+        URI baseDirectory = docsTutorialsDocs.toURI();
+        int fileCount = 0;
+        boolean debug=false;
+        for (File htmlFile : html_files) {
+            if (debug) {
+                if (! htmlFile.getName().equals("annotations.html")) {
+                    continue;
+                }
+            }
+            String relativePath = baseDirectory.relativize(htmlFile.toURI()).getPath().replaceAll("\\.html", ".asciidoc");
+            File asciidoc = new File(dest, relativePath);
+            convertHTMLToAsciiDoc(dest, htmlFile, docsTutorialsImages, asciidoc, externalLinks);
+            fileCount++;
+        }
+        LOG.log(Level.INFO, "Converted {0} tutorials.", fileCount);
+    }
+    
+    private static void convertTrails(File docsTutorialsTrailsDirectory, File docsTutorialsImages, File dest, ExternalLinksMap externalLinks) throws Exception {
+         LOG.log(Level.INFO, "Converting trails {0} to {1}", new Object[]{docsTutorialsTrailsDirectory.getAbsolutePath(), dest.getAbsolutePath()});
+
+        List<File> html_files = Files.find(docsTutorialsTrailsDirectory.toPath(), 999,
+                (p, bfa) -> bfa.isRegularFile()).map(Path::toFile).filter((f) -> f.getName().endsWith(".html")).collect(Collectors.toList());
+
+        URI baseDirectory = docsTutorialsTrailsDirectory.toURI();
+        int fileCount = 0;
+        boolean debug=false;
+        for (File htmlFile : html_files) {
+            if (debug) {
+                if (! htmlFile.getName().equals("annotations.html")) {
+                    continue;
+                }
+            }
+            String relativePath = baseDirectory.relativize(htmlFile.toURI()).getPath().replaceAll("\\.html", ".asciidoc");
+            File asciidoc = new File(dest, relativePath);
+            convertHTMLToAsciiDocWithoutTables(dest, htmlFile, docsTutorialsImages, asciidoc, externalLinks);
+            fileCount++;
+        }
+        LOG.log(Level.INFO, "Converted {0} trails.", fileCount);       
+    }
+
+    private static String asciidocHeader = null;
+
+    private static String getAsciidocHeader() {
+        if (asciidocHeader == null) {
+            StringBuilder sb = new StringBuilder();
+            String[] lines = APACHE_LICENSE_HEADER.split("\n");
+            for (String line : lines) {
+                sb.append("// ").append(line).append("\n");
+            }
+            sb.append("//\n\n");
+            asciidocHeader = sb.toString();
+        }
+        return asciidocHeader;
+    }
+
+    private static ThreadLocal<SimpleDateFormat> sdf = null;
+
+    private static final synchronized SimpleDateFormat getSimpleDateFormat() {
+        if (sdf == null) {
+            SimpleDateFormat d = new SimpleDateFormat("yyyy-MM-dd");
+            sdf = new ThreadLocal<>();
+            sdf.set(d);
+        }
+        return sdf.get();
+    }
+
+    /**
+     * Converts the given HTML file to AsciiDoc format.
+     * @param inputHTMLFile The input file.
+     * @param imageDirectory The directory where images are to be found.
+     * @param outputAsciidocFile The output asciidoc file.
+     * @param externalLinks A map used to store external links detected in the HTML file.
+     * @throws Exception on error.
+     */
+    private static void convertHTMLToAsciiDoc(File topDirectory, File inputHTMLFile, File imageDirectory, File outputAsciidocFile, ExternalLinksMap externalLinks) throws Exception {
+        if (! outputAsciidocFile.getParentFile().exists()) {
+            if (! outputAsciidocFile.getParentFile().mkdirs()) {
+                throw new IOException(String.format("Cannot create directory '%s'", outputAsciidocFile.getParent()));
+            }
+        }
+        try (BufferedWriter output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputAsciidocFile), "utf-8"), 16 * 1024);
+                BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(inputHTMLFile), "utf-8"))) {
+            output.write(getAsciidocHeader());
+
+            CustomAsciiDocDocumentBuilder asciidocBuilder = new CustomAsciiDocDocumentBuilder(topDirectory, imageDirectory, outputAsciidocFile, output, externalLinks);
+            InputSource source = new InputSource(reader);
+            HtmlParser.instance().parse(source, asciidocBuilder);
+            // HtmlParser.instanceWithHtmlCleanupRules().parse(source, asciidocBuilder);
+        }
+    }
+    
+    /**
+     * Converts the given HTML file to AsciiDoc format, ignoring tables completely.
+     * @param inputHTMLFile The input file.
+     * @param imageDirectory The directory where images are to be found.
+     * @param outputAsciidocFile The output asciidoc file.
+     * @param externalLinks A map used to store external links detected in the HTML file.
+     * @throws Exception on error.
+     */
+    private static void convertHTMLToAsciiDocWithoutTables(File topDirectory, File inputHTMLFile, File imageDirectory, File outputAsciidocFile, ExternalLinksMap externalLinks) throws Exception {
+        if (! outputAsciidocFile.getParentFile().exists()) {
+            if (! outputAsciidocFile.getParentFile().mkdirs()) {
+                throw new IOException(String.format("Cannot create directory '%s'", outputAsciidocFile.getParent()));
+            }
+        }
+        try (BufferedWriter output = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputAsciidocFile), "utf-8"), 16 * 1024);
+                BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(inputHTMLFile), "utf-8"))) {
+            output.write(getAsciidocHeader());
+
+            CustomAsciiDocDocumentBuilderWithoutTables asciidocBuilder = new CustomAsciiDocDocumentBuilderWithoutTables(topDirectory, imageDirectory, outputAsciidocFile, output, externalLinks);
+            InputSource source = new InputSource(reader);
+            HtmlParser.instance().parse(source, asciidocBuilder);
+        }
+    }
+    
+    private static void checkDirectoryExists(String message, File dir) throws Exception {
+        String error = null;
+        if (!dir.exists()) {
+            error = String.format("%s '%s' does not exist.", message, dir.getAbsolutePath());
+        }
+        if (error == null && !dir.isDirectory()) {
+            error = String.format("%s '%s' is not a directory.", message, dir.getAbsolutePath());
+        }
+        if (error == null && !dir.canRead()) {
+            error = String.format("%s '%s' is not readable.", message, dir.getAbsolutePath());
+        }
+        if (error != null) {
+            throw new IllegalArgumentException(error);
+        }
+    }
+
+    private static void usage() {
+
+        System.err.println("Use: java " + HTMLConverter.class
+                .getName() + " 3rd-donation-docs-directory");
+        System.err.println("   Where '3rd-donation-docs-directory' is the 'docs' directory of the third Oracle donation");
+        System.exit(1);
+    }
+
+    public static void main(String[] args) throws Exception {
+        if (args.length != 1) {
+            usage();
+        }
+
+        File docsDirectory = new File(new File(args[0]), "docs");
+        checkDirectoryExists("Incorrect 'docs' donation directory ", docsDirectory);
+        
+        File docsTutorialsDirectory = new File(docsDirectory, "tutorials");
+        checkDirectoryExists("Incorrect 'docs/tutorials' donation directory ", docsTutorialsDirectory);
+
+        File docsTutorialsImagesDirectory = new File(docsTutorialsDirectory, "images");
+        checkDirectoryExists("Incorrect 'docs/tutorials/images' donation directory ", docsTutorialsImagesDirectory);
+
+        File docsTutorialsDocsDirectory = new File(docsTutorialsDirectory, "docs");
+        checkDirectoryExists("Incorrect 'docs/tutorials/docs' donation directory ", docsTutorialsDocsDirectory);
+
+        File docsTutorialsTrailsDirectory = new File(docsTutorialsDirectory, "trails");
+        checkDirectoryExists("Incorrect 'docs/tutorials/trails' donation directory", docsTutorialsTrailsDirectory);
+
+        File currentDirectory = new File(System.getProperty("user.dir"));
+        File dest = new File(currentDirectory, "tutorials-asciidoc");
+
+        if (!dest.exists()) {
+            if (!dest.mkdirs()) {
+                throw new IllegalStateException("Cannot create directory " + dest.getAbsolutePath());
+            }
+        }
+
+        checkDirectoryExists("Output directory", dest);
+        if (!dest.canWrite()) {
+            throw new IllegalStateException("Cannot write to " + dest.getAbsolutePath());
+        }
+
+        ExternalLinksMap externalLinks = new ExternalLinksMap();
+
+        convert(docsTutorialsDocsDirectory, docsTutorialsImagesDirectory, dest, externalLinks);
+        
+        convertTrails(docsTutorialsTrailsDirectory, docsTutorialsImagesDirectory, dest, externalLinks);
+
+        LOG.info("Generating 'external-links.txt' with list of external links...");
+        
+        try ( PrintWriter ef = new PrintWriter(new FileWriter("external-links.yml"))) {
+            for (String domain : externalLinks.getDomains()) {
+                ef.format("- domain: \"%s\"%n", domain);
+                ef.format("  links:%n");
+                for (String href : externalLinks.getHrefs(domain)) {
+                    ef.format("    link: \"%s\"%n", href);
+                    ef.format("    used-at:%n");
+                    for (String tutorial : externalLinks.getTutorials(href)) {
+                        ef.format("      - \"%s\"%n", tutorial);
+                    }
+                }
+            }
+        }
+
+        /* Remove a hand-made "content" section, that is not replaced by the asciidoc 'toc' stuff */
+        Map<File, String> titles = AsciidocPostProcessor.removeContentSetcion(dest);
+        
+        /* Generate some "index.asciidoc" files with the list of tutorials on each directory. */
+        AsciidocPostProcessor.generateIndexes(dest, titles);
+        
+    }
+
+}
diff --git a/html-convert/src/main/java/org/netbeans/tools/tutorials/Language.java b/html-convert/src/main/java/org/netbeans/tools/tutorials/Language.java
new file mode 100644
index 0000000..bf4421b
--- /dev/null
+++ b/html-convert/src/main/java/org/netbeans/tools/tutorials/Language.java
@@ -0,0 +1,66 @@
+/*
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+ */
+package org.netbeans.tools.tutorials;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Locale;
+
+/**
+ *
+ * @author avieiro
+ */
+public enum Language {
+    UNKNOWN("", Locale.getDefault(), "English"), DEFAULT(".asciidoc", Locale.ENGLISH, "English"), PORTUGUESE("_pt_BR.asciidoc", new Locale("pt_BR"), "Português brasileiro"), CHINESE("_zh_CN.asciidoc", new Locale("zh_CN"), "中文"), JAPANESE("_ja.asciidoc", new Locale("ja"), "日本人"), RUSSIAN("_ru.asciidoc", new Locale("ru"), "русский"), CATALAN("_ca.asciidoc", new Locale("ca_ES"), "Català");
+    public final String extension;
+    public final Locale locale;
+    public final String title;
+    /* Array of non-default languages.*/
+    public static final Language[] FOREIGN_LANGUAGES = {RUSSIAN, JAPANESE, CHINESE, PORTUGUESE, CATALAN};
+
+    Language(String extension, Locale locale, String title) {
+        this.extension = extension;
+        this.locale = locale;
+        this.title = title;
+    }
+
+    public static Language getLanguage(File file) {
+        String name = file.getName().toLowerCase();
+        for (Language language : FOREIGN_LANGUAGES) {
+            if (name.endsWith(language.extension.toLowerCase())) {
+                return language;
+            }
+        }
+        return name.endsWith(DEFAULT.extension.toLowerCase()) ? DEFAULT : UNKNOWN;
+    }
+
+    public static HashMap<Language, File> getTranslations(File file) {
+        File parentDirectory = file.getParentFile();
+        String prefix = file.getName().replace(".asciidoc", "");
+        HashMap<Language, File> translations = new HashMap<>();
+        for (Language l : FOREIGN_LANGUAGES) {
+            File translationFile = new File(parentDirectory, prefix + l.extension);
+            if (translationFile.exists()) {
+                translations.put(l, translationFile);
+            }
+        }
+        return translations;
+    }
+    
+}
diff --git a/html-convert/src/main/java/org/netbeans/tools/tutorials/LocalizedTutorialSection.java b/html-convert/src/main/java/org/netbeans/tools/tutorials/LocalizedTutorialSection.java
new file mode 100644
index 0000000..c58124f
--- /dev/null
+++ b/html-convert/src/main/java/org/netbeans/tools/tutorials/LocalizedTutorialSection.java
@@ -0,0 +1,91 @@
+/*
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+ */
+package org.netbeans.tools.tutorials;
+
+import java.io.File;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *
+ * @author avieiro
+ */
+public class LocalizedTutorialSection {
+    
+    public static final String URL_KEY = "url";
+    public static final String TITLE_KEY = "title";
+    private Language language;
+    private ArrayList<File> files;
+    private ArrayList<HashMap<String, String>> details;
+    private String title;
+    
+    public LocalizedTutorialSection(Language language, String title) {
+        this.language = language;
+        this.files = new ArrayList<>();
+        this.details = new ArrayList<>();
+        this.title = title;
+    }
+
+    public void add(File file) {
+        this.files.add(file);
+    }
+
+    public void addAll(List<File> files) {
+        this.files.addAll(files);
+    }
+
+    public void sort(Map<File, String> fileTitles) {
+        ArrayList<File> sortedFiles = new ArrayList<>(this.files);
+        Collator collator = Collator.getInstance(language.locale);
+        sortedFiles.sort((file1, file2) -> {
+            String title1 = fileTitles.get(file1);
+            String title2 = fileTitles.get(file2);
+            title1 = title1 == null ? file1.getName() : title1;
+            title2 = title2 == null ? file2.getName() : title2;
+            return collator.compare(title1, title2);
+        });
+        this.files = sortedFiles;
+        details.clear();
+        for (File file : files) {
+            HashMap<String, String> detail = new HashMap<String, String>();
+            details.add(detail);
+            detail.put(URL_KEY, file.getName().replaceAll(".asciidoc", ".html"));
+            detail.put(TITLE_KEY, fileTitles.get(file));
+        }
+    }
+
+    public Language getLanguage() {
+        return language;
+    }
+
+    public ArrayList<File> getFiles() {
+        return files;
+    }
+
+    public ArrayList<HashMap<String, String>> getDetails() {
+        return details;
+    }
+    
+    public String getTitle() {
+        return title;
+    }
+}
diff --git a/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle.properties b/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle.properties
new file mode 100644
index 0000000..912e3ac
--- /dev/null
+++ b/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle.properties
@@ -0,0 +1,31 @@
+#    Licensed to the Apache Software Foundation (ASF) under one
+#    or more contributor license agreements.  See the NOTICE file
+#    distributed with this work for additional information
+#    regarding copyright ownership.  The ASF licenses this file
+#    to you under the Apache License, Version 2.0 (the
+#    "License"); you may not use this file except in compliance
+#    with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing,
+#    software distributed under the License is distributed on an
+#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#    KIND, either express or implied.  See the License for the
+#    specific language governing permissions and limitations
+#    under the License.
+tutorials-asciidoc.title=NetBeans Tutorials
+cnd.title=C and C++ Tutorials
+ide.title=NetBeans IDE Tutorials
+java.title=Java Tutorials
+javaee.title=JavaEE Tutorials
+ecommerce.title=e-Commerce Tutorials
+javame.title=JavaME Tutorials
+php.title=PHP Tutorials
+web.title=Web Technologies Tutorials
+webclient.title=HTML5 Tutorials
+websvc.title=Web Service Tutorials
+
+
+
+
diff --git a/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_es_CA.properties b/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_es_CA.properties
new file mode 100644
index 0000000..912e3ac
--- /dev/null
+++ b/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_es_CA.properties
@@ -0,0 +1,31 @@
+#    Licensed to the Apache Software Foundation (ASF) under one
+#    or more contributor license agreements.  See the NOTICE file
+#    distributed with this work for additional information
+#    regarding copyright ownership.  The ASF licenses this file
+#    to you under the Apache License, Version 2.0 (the
+#    "License"); you may not use this file except in compliance
+#    with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing,
+#    software distributed under the License is distributed on an
+#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#    KIND, either express or implied.  See the License for the
+#    specific language governing permissions and limitations
+#    under the License.
+tutorials-asciidoc.title=NetBeans Tutorials
+cnd.title=C and C++ Tutorials
+ide.title=NetBeans IDE Tutorials
+java.title=Java Tutorials
+javaee.title=JavaEE Tutorials
+ecommerce.title=e-Commerce Tutorials
+javame.title=JavaME Tutorials
+php.title=PHP Tutorials
+web.title=Web Technologies Tutorials
+webclient.title=HTML5 Tutorials
+websvc.title=Web Service Tutorials
+
+
+
+
diff --git a/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_ja.properties b/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_ja.properties
new file mode 100644
index 0000000..912e3ac
--- /dev/null
+++ b/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_ja.properties
@@ -0,0 +1,31 @@
+#    Licensed to the Apache Software Foundation (ASF) under one
+#    or more contributor license agreements.  See the NOTICE file
+#    distributed with this work for additional information
+#    regarding copyright ownership.  The ASF licenses this file
+#    to you under the Apache License, Version 2.0 (the
+#    "License"); you may not use this file except in compliance
+#    with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing,
+#    software distributed under the License is distributed on an
+#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#    KIND, either express or implied.  See the License for the
+#    specific language governing permissions and limitations
+#    under the License.
+tutorials-asciidoc.title=NetBeans Tutorials
+cnd.title=C and C++ Tutorials
+ide.title=NetBeans IDE Tutorials
+java.title=Java Tutorials
+javaee.title=JavaEE Tutorials
+ecommerce.title=e-Commerce Tutorials
+javame.title=JavaME Tutorials
+php.title=PHP Tutorials
+web.title=Web Technologies Tutorials
+webclient.title=HTML5 Tutorials
+websvc.title=Web Service Tutorials
+
+
+
+
diff --git a/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_pt_BR.properties b/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_pt_BR.properties
new file mode 100644
index 0000000..912e3ac
--- /dev/null
+++ b/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_pt_BR.properties
@@ -0,0 +1,31 @@
+#    Licensed to the Apache Software Foundation (ASF) under one
+#    or more contributor license agreements.  See the NOTICE file
+#    distributed with this work for additional information
+#    regarding copyright ownership.  The ASF licenses this file
+#    to you under the Apache License, Version 2.0 (the
+#    "License"); you may not use this file except in compliance
+#    with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing,
+#    software distributed under the License is distributed on an
+#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#    KIND, either express or implied.  See the License for the
+#    specific language governing permissions and limitations
+#    under the License.
+tutorials-asciidoc.title=NetBeans Tutorials
+cnd.title=C and C++ Tutorials
+ide.title=NetBeans IDE Tutorials
+java.title=Java Tutorials
+javaee.title=JavaEE Tutorials
+ecommerce.title=e-Commerce Tutorials
+javame.title=JavaME Tutorials
+php.title=PHP Tutorials
+web.title=Web Technologies Tutorials
+webclient.title=HTML5 Tutorials
+websvc.title=Web Service Tutorials
+
+
+
+
diff --git a/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_ru.properties b/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_ru.properties
new file mode 100644
index 0000000..912e3ac
--- /dev/null
+++ b/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_ru.properties
@@ -0,0 +1,31 @@
+#    Licensed to the Apache Software Foundation (ASF) under one
+#    or more contributor license agreements.  See the NOTICE file
+#    distributed with this work for additional information
+#    regarding copyright ownership.  The ASF licenses this file
+#    to you under the Apache License, Version 2.0 (the
+#    "License"); you may not use this file except in compliance
+#    with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing,
+#    software distributed under the License is distributed on an
+#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#    KIND, either express or implied.  See the License for the
+#    specific language governing permissions and limitations
+#    under the License.
+tutorials-asciidoc.title=NetBeans Tutorials
+cnd.title=C and C++ Tutorials
+ide.title=NetBeans IDE Tutorials
+java.title=Java Tutorials
+javaee.title=JavaEE Tutorials
+ecommerce.title=e-Commerce Tutorials
+javame.title=JavaME Tutorials
+php.title=PHP Tutorials
+web.title=Web Technologies Tutorials
+webclient.title=HTML5 Tutorials
+websvc.title=Web Service Tutorials
+
+
+
+
diff --git a/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_zh_CN.properties b/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_zh_CN.properties
new file mode 100644
index 0000000..912e3ac
--- /dev/null
+++ b/html-convert/src/main/resources/org/netbeans/tools/tutorials/TutorialsBundle_zh_CN.properties
@@ -0,0 +1,31 @@
+#    Licensed to the Apache Software Foundation (ASF) under one
+#    or more contributor license agreements.  See the NOTICE file
+#    distributed with this work for additional information
+#    regarding copyright ownership.  The ASF licenses this file
+#    to you under the Apache License, Version 2.0 (the
+#    "License"); you may not use this file except in compliance
+#    with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing,
+#    software distributed under the License is distributed on an
+#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#    KIND, either express or implied.  See the License for the
+#    specific language governing permissions and limitations
+#    under the License.
+tutorials-asciidoc.title=NetBeans Tutorials
+cnd.title=C and C++ Tutorials
+ide.title=NetBeans IDE Tutorials
+java.title=Java Tutorials
+javaee.title=JavaEE Tutorials
+ecommerce.title=e-Commerce Tutorials
+javame.title=JavaME Tutorials
+php.title=PHP Tutorials
+web.title=Web Technologies Tutorials
+webclient.title=HTML5 Tutorials
+websvc.title=Web Service Tutorials
+
+
+
+
diff --git a/html-convert/src/main/resources/org/netbeans/tools/tutorials/index-template.mustache b/html-convert/src/main/resources/org/netbeans/tools/tutorials/index-template.mustache
new file mode 100644
index 0000000..649f96e
--- /dev/null
+++ b/html-convert/src/main/resources/org/netbeans/tools/tutorials/index-template.mustache
@@ -0,0 +1,33 @@
+// 
+//     Licensed to the Apache Software Foundation (ASF) under one
+//     or more contributor license agreements.  See the NOTICE file
+//     distributed with this work for additional information
+//     regarding copyright ownership.  The ASF licenses this file
+//     to you under the Apache License, Version 2.0 (the
+//     "License"); you may not use this file except in compliance
+//     with the License.  You may obtain a copy of the License at
+// 
+//       http://www.apache.org/licenses/LICENSE-2.0
+// 
+//     Unless required by applicable law or agreed to in writing,
+//     software distributed under the License is distributed on an
+//     "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+//     KIND, either express or implied.  See the License for the
+//     specific language governing permissions and limitations
+//     under the License.
+//
+
+= {{title}}
+:jbake-type: tutorial
+:jbake-tags: tutorials
+:jbake-status: published
+:toc: left
+:toc-title:
+:description: {{title}}
+
+{{#details}}
+- link:{{url}}[{{title}}]
+{{/details}}
+
+
+
diff --git a/html-convert/src/main/resources/org/netbeans/tools/tutorials/section-template.mustache b/html-convert/src/main/resources/org/netbeans/tools/tutorials/section-template.mustache
new file mode 100644
index 0000000..5ea7713
--- /dev/null
+++ b/html-convert/src/main/resources/org/netbeans/tools/tutorials/section-template.mustache
@@ -0,0 +1,27 @@
+// 
+//     Licensed to the Apache Software Foundation (ASF) under one
+//     or more contributor license agreements.  See the NOTICE file
+//     distributed with this work for additional information
+//     regarding copyright ownership.  The ASF licenses this file
+//     to you under the Apache License, Version 2.0 (the
+//     "License"); you may not use this file except in compliance
+//     with the License.  You may obtain a copy of the License at
+// 
+//       http://www.apache.org/licenses/LICENSE-2.0
+// 
+//     Unless required by applicable law or agreed to in writing,
+//     software distributed under the License is distributed on an
+//     "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+//     KIND, either express or implied.  See the License for the
+//     specific language governing permissions and limitations
+//     under the License.
+//
+
+.{{title}}
+************************************************
+{{#details}}
+- link:{{url}}[{{title}}]
+{{/details}}
+************************************************
+
+


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@netbeans.apache.org
For additional commands, e-mail: notifications-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists