CommonMarkPageBaker.java

/**************************************************************************
 *
 * Copyright (c) 2019-2020 Yawg project contributors.
 *
 **************************************************************************/

package com.varmateo.yawg.commonmark;

import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Function;
import java.util.regex.Pattern;

import io.vavr.Lazy;
import io.vavr.collection.List;
import io.vavr.control.Try;
import org.commonmark.Extension;
import org.commonmark.ext.front.matter.YamlFrontMatterExtension;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;

import com.varmateo.yawg.api.Result;
import com.varmateo.yawg.spi.PageBakeResult;
import com.varmateo.yawg.spi.PageBaker;
import com.varmateo.yawg.spi.PageContext;
import com.varmateo.yawg.spi.Template;
import com.varmateo.yawg.spi.TemplateContext;
import com.varmateo.yawg.util.FileUtils;
import com.varmateo.yawg.util.PageBakeResults;
import com.varmateo.yawg.util.Results;


/**
 * A <code>Baker</code> that translates text files in Markdown format
 * into HTML files.
 */
public final class CommonMarkPageBaker
        implements PageBaker {


    private static final String NAME = "markdown";

    private static final Pattern RE_ADOC = Pattern.compile(".*\\.md$");

    private static final String TARGET_EXTENSION = ".html";

    private final Lazy<Parser> _markdownParser =
            Lazy.of(this::createMarkdownParser);

    private final Lazy<HtmlRenderer> _htmlRenderer =
            Lazy.of(this::createHtmlRenderer);

    private final Lazy<CommonMarkTemplateContextFactory> _templateContextFactory =
            Lazy.of(this::createTemplateContextFactory);


    private CommonMarkPageBaker() {
        // Nothing to do.
    }


    /**
     *
     */
    public static PageBaker create() {

        return new CommonMarkPageBaker();
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public String shortName() {

        return NAME;
    }


    /**
     * Checks if the given file name has one of the known extensions.
     *
     * <p>The following extensions will be allowed:</p>
     *
     * <ul>
     *   <li>.md</li>
     * </ul>
     *
     * @return True if the given file name has one of the allowed
     * extensions.
     */
    @Override
    public boolean isBakeable(final Path path) {

        return FileUtils.isNameMatch(path, RE_ADOC);
    }


    /**
     * Converts the given text file in Asciidoc format into an HTML
     * file.
     *
     * <p>The target directory must already exist. Otherwise an
     * exception will be thrown.</p>
     *
     * @param sourcePath The file to be baked.
     *
     * @param context Provides the template for generating the target
     * document. If no template is provided, then the default
     * AsciidoctorJ document generator will be used.
     *
     * @param targetDir The directory where the source file will be
     * copied to.
     *
     * @return A result signaling success of failure.
     */
    @Override
    public PageBakeResult bake(
            final Path sourcePath,
            final PageContext context,
            final Path targetDir) {

        final Try<Void> result = doBake(sourcePath, context, targetDir);

        return PageBakeResults.fromTry(result);
    }


    /**
     *
     */
    private Try<Void> doBake(
            final Path sourcePath,
            final PageContext context,
            final Path targetDir) {

        final Path targetPath = getTargetPath(sourcePath, targetDir);
        final Optional<Template> template = context.templateFor(sourcePath);
        final Try<Void> result;

        if ( template.isPresent() ) {
            result = doBakeWithTemplate(
                    sourcePath,
                    context,
                    targetDir,
                    targetPath,
                    template.get());
        } else {
            result = doBakeWithoutTemplate(
                    sourcePath,
                    targetPath);
        }

        return result;
    }


    /**
     *
     */
    private Path getTargetPath(
            final Path sourcePath,
            final Path targetDir) {

        final String sourceBasename = FileUtils.basename(sourcePath);
        final String targetName = sourceBasename + TARGET_EXTENSION;

        return targetDir.resolve(targetName);
    }


    /**
     *
     */
    private Try<Void> doBakeWithoutTemplate(
            final Path sourcePath,
            final Path targetPath) {

        return renderBody(sourcePath)
                .flatMap(body -> renderContentAndSave(sourcePath, targetPath, body));
    }


    private Try<String> renderBody(final Path sourcePath) {

        final Try<Node> document = FileUtils.safeReadFrom(
                sourcePath,
                reader -> _markdownParser.get().parseReader(reader));
        final Try<String> body = document
                .map(doc -> _htmlRenderer.get().render(doc));

        return body
                .recoverWith(CommonMarkPageBakerException.commonMarkFailureTry(sourcePath));
    }


    private Try<Void> renderContentAndSave(
            final Path sourcePath,
            final Path targetPath,
            final String body) {

        final String contentTemplate = ""
                + "<!DOCTYPE html>%n"
                + "<html><body>%s</body></html>";
        final String content = String.format(contentTemplate, body);
        final Try<Void> result = FileUtils.safeWriteTo(
                targetPath,
                writer -> writer.write(content));

        return result
                .recoverWith(CommonMarkPageBakerException.commonMarkFailureTry(sourcePath));
    }


    /**
     *
     */
    private Try<Void> doBakeWithTemplate(
            final Path sourcePath,
            final PageContext context,
            final Path targetDir,
            final Path targetPath,
            final Template template) {

        final Try<TemplateContext> templateContext = _templateContextFactory.get().build(
                sourcePath, targetDir, targetPath, context);

        return templateContext.flatMap(processTemplate(sourcePath, targetPath, template));
    }


    private Function<TemplateContext, Try<Void>> processTemplate(
            final Path sourcePath,
            final Path targetPath,
            final Template template) {

        return (TemplateContext templateContext) -> {
            final Try<Result<Void>> result = FileUtils.safeWriteWith(
                    targetPath,
                    writer -> template.process(templateContext, writer));

            return result
                    .recoverWith(CommonMarkPageBakerException.templateFailureTry(sourcePath))
                    .flatMap(Results::toTry);
        };
    }


    private Parser createMarkdownParser() {

        final Extension frontMatterExtension = YamlFrontMatterExtension.create();
        final List<Extension> extensions = List.of(frontMatterExtension);

        return Parser.builder()
                .extensions(extensions)
                .build();
    }


    private HtmlRenderer createHtmlRenderer() {

        return HtmlRenderer.builder().build();
    }


    private CommonMarkTemplateContextFactory createTemplateContextFactory() {

        return new CommonMarkTemplateContextFactory(_markdownParser.get(), _htmlRenderer.get());
    }


}