MarkdownCli.java

/*
 * Copyright © 2014-2025 The CTAN Team and individual authors
 *
 * This file is distributed under the 3-clause BSD license.
 * See file LICENSE for details.
 */
package org.ctan.markup.markdown;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

import lombok.NonNull;

/**
 * This class provides the command line interface for the markdown renderer.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
public class MarkdownCli {

    /**
     * The field <code>EXIT_ERROR</code> contains the exit code for errors.
     */
    private static final int EXIT_ERROR = -1;

    /**
     * The field <code>EXIT_OK</code> contains the exit code for success.
     */
    private static final int EXIT_OK = 0;

    /**
     * The field <code>SYNOPSIS</code> contains the -h text.
     */
    private static final String SYNOPSIS =
        "java -jar [....jar] [-h] [-t title] [-s] [-i] "
            + "[[-f] infile] [-o outfile]";

    /**
     * This method provides the command line interface.
     *
     * @param argv the command line arguments
     */
    public static int cli(String[] argv) {

        return new MarkdownCli().run(argv);
    }

    /**
     * This method provides the command line interface. It invokes System.exit()
     * at the end.
     *
     * @param argv the command line arguments
     */
    public static void main(String[] argv) {

        System.exit(cli(argv));
    }

    /**
     * The field <code>in</code> contains the input file name or {@code null}
     * for stdin.
     */
    private String in = null;

    /**
     * The field <code>out</code> contains the output file name or {@code null}
     * for stdout.
     */
    private String out = null;

    /**
     * The field <code>css</code> contains the list of CSS files to include.
     */
    private List<String> css = new ArrayList<>();

    /**
     * The field <code>standalone</code> contains the indicator that a
     * stand-alone HTML file should be produced.
     */
    private boolean standalone = true;

    /**
     * The field <code>title</code> contains the HTML title.
     */
    private String title = "";

    /**
     * This method parses the command line arguments.
     *
     * @param argv the command line arguments
     */
    private boolean parseCli(String[] argv) {

        for (var i = 0; i < argv.length; i++) {
            var arg = argv[i];
            if ("-i".equals(arg) || "--input".startsWith(arg)
                || "-f".equals(arg) || "--file".startsWith(arg)) {
                if (++i >= argv.length) {
                    throw new IllegalArgumentException(arg);
                }
                in = argv[i];
            } else if ("-o".equals(arg) || "--output".startsWith(arg)) {
                if (++i >= argv.length) {
                    throw new IllegalArgumentException(arg);
                }
                out = argv[i];
            } else if ("-s".equals(arg) || "--standalone".startsWith(arg)) {
                standalone = !standalone;
            } else if ("-c".equals(arg) || "--css".startsWith(arg)) {
                if (++i >= argv.length) {
                    throw new IllegalArgumentException(arg);
                }
                css.add(argv[i]);
            } else if ("-t".equals(arg) || "--title".startsWith(arg)) {
                if (++i >= argv.length) {
                    throw new IllegalArgumentException(arg);
                }
                title = argv[i];
            } else if (arg.startsWith("-")) {
                System.err.println(SYNOPSIS);
                return true;
            } else {
                in = arg;
            }
        }
        return false;
    }

    /**
     * This method performs all actions required.
     *
     * @throws IOException in case of an I/O error.
     */
    private void run() throws IOException {

        if (in != null && out == null) {
            out = in.replaceAll("(\\.md)$", "") + ".html";
        }
        try (Reader r = new BufferedReader(in != null
            ? new FileReader(in, StandardCharsets.UTF_8)
            : new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
            try (Writer w = new BufferedWriter(out != null
                ? new FileWriter(out, StandardCharsets.UTF_8)
                : new OutputStreamWriter(System.out, StandardCharsets.UTF_8))) {
                run(r, w);
            }
        }
    }

    /**
     * The method <code>run</code> reads the source and writes the target.
     *
     * @param r the reader
     * @param w the writer
     * @throws IOException in case of an error
     */
    private void run(Reader r, Writer w) throws IOException {

        if (standalone) {
            w.write("<html>\n<head>\n<title>");
            w.write(title);
            w.write("</title>\n");
            for (String c : css) {
                w.write(
                    "<link rel='stylesheet' type='text/css' media='all' "
                        + "href='");
                w.write(c);
                w.write("' />\n");
            }
            w.write("\n</head>\n<body>\n");
        }
        new MarkdownRenderer(r, "").render(w);
        if (standalone) {
            w.write("\n</body>\n</html>\n");
        }
        w.flush();
    }

    /**
     * This method contains the command line processor with handling of
     * Exceptions.
     *
     * @param argv the command line arguments
     *
     * @return the exit code
     */
    public int run(@NonNull String[] argv) {

        try {
            if (!parseCli(argv)) {
                run();
            }
            return EXIT_OK;
        } catch (IOException e) {
            System.err.println(
                "*** " + e.getMessage());
            return EXIT_ERROR;
        } catch (IllegalArgumentException e) {
            System.err.println(
                "*** Missing argument for parameter " + e.getMessage());
            return EXIT_ERROR;
        }
    }
}