MarkdownWriter.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.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * This class provides a writer which allows to link in deferred defined
 * contents.
 *
 * @author <a href="mailto:gene@ctan.org">Gerd Neugebauer</a>
 */
public class MarkdownWriter extends Writer {

    /**
     * This class is a {@link StringWriter} packaged as {@link Value}.
     */
    private class Literal extends StringWriter implements Value {

        /**
         * {@inheritDoc}
         *
         * @see org.ctan.markdown.MarkdownWriter.Value#addSelfIfNotEmpty(java.util
         *     .List)
         */
        @Override
        public void addSelfIfNotEmpty(List<Value> list) {

            if (getBuffer().length() > 0) {
                list.add(this);
            }
        }

        /**
         * {@inheritDoc}
         *
         * @see org.ctan.markdown.MarkdownWriter.Value#put(java.io.Writer)
         */
        @Override
        public void flush(Writer out) throws IOException {

            out.write(toString());
        }

        /**
         * {@inheritDoc}
         *
         * @see org.ctan.markdown.MarkdownWriter.Value#fush(java.io.Writer)
         */
        @Override
        public boolean put(Writer w) throws IOException {

            w.write(toString());
            return true;
        }
    }

    /**
     * This class represents an undefined reference.
     */
    private class Reference implements Value {

        /**
         * The field <code>value</code> contains the reference key.
         */
        protected String key;

        /**
         * This is the constructor for <code>Value</code>.
         *
         * @param key the reference key
         */
        public Reference(String key) {

            this.key = key;
        }

        /**
         * {@inheritDoc}
         *
         * @see org.ctan.markdown.MarkdownWriter.Value#addSelfIfNotEmpty(java.util
         *     .List)
         */
        @Override
        public void addSelfIfNotEmpty(List<Value> list) {

        }

        /**
         * {@inheritDoc}
         *
         * @see org.ctan.markdown.MarkdownWriter.Value#put(java.io.Writer)
         */
        @Override
        public void flush(Writer out) throws IOException {

            if (!put(out)) {
                out.write(key);
            }
        }

        /**
         * {@inheritDoc}
         *
         * @see org.ctan.markdown.MarkdownWriter.Value#flush(java.io.Writer)
         */
        @Override
        public boolean put(Writer w) throws IOException {

            var ref = reference.get(key);
            if (ref != null) {
                w.write(ref[0]);
                return true;
            }
            return false;
        }

        /**
         * {@inheritDoc}
         *
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {

            return "${" + key + "}";
        }
    }

    /**
     * This class represents an undefined reference.
     */
    private class ReferenceText extends Reference {

        /**
         * This is the constructor for <code>Value</code>.
         *
         * @param key the reference key
         */
        public ReferenceText(String key) {

            super(key);
        }

        /**
         * {@inheritDoc}
         *
         * @see org.ctan.markdown.MarkdownWriter.Value#put(java.io.Writer)
         */
        @Override
        public void flush(Writer out) throws IOException {

            if (!put(out)) {
                out.write(key);
            }
        }

        /**
         * {@inheritDoc}
         *
         * @see org.ctan.markdown.MarkdownWriter.Value#flush(java.io.Writer)
         */
        @Override
        public boolean put(Writer w) throws IOException {

            var ref = reference.get(key);
            if (ref != null && ref[1] != null) {
                w.write(ref[1] != null ? ref[1] : "");
                return true;
            }
            return false;
        }

        /**
         * {@inheritDoc}
         *
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {

            return "$[" + key + "]";
        }
    }

    /**
     * This interface describes a value for the stack.
     */
    private interface Value {

        /**
         * This method optionally adds the value to the list.
         *
         * @param list the list
         */
        void addSelfIfNotEmpty(List<Value> list);

        /**
         * This method writes out it's contents.
         *
         * @param out the writer
         *
         * @throws IOException in case of an I/O error
         */
        void flush(Writer out) throws IOException;

        /**
         * This method tries to write out its contents. If it encounters
         * incomplete information is stops and reports a failure.
         *
         * @param out the writer
         *
         * @return {@code true} iff the contents has been written
         *
         * @throws IOException in case of an I/O error
         */
        boolean put(Writer out) throws IOException;
    }

    /**
     * The field <code>literal</code> contains the current buffer for literal
     * data.
     */
    private Literal literal;

    /**
     * The field <code>out</code> contains the current output writer.
     */
    private Writer out;

    /**
     * The field <code>reference</code> contains the collected references.
     */
    private Map<String, String[]> reference = new HashMap<String, String[]>();

    /**
     * The field <code>values</code> contains the list of deferred values to be
     * written later.
     */
    private List<Value> values;

    /**
     * The field <code>writer</code> contains the target writer.
     */
    private final Writer writer;

    /**
     * This is the constructor for <code>MarkdownWriter</code>.
     *
     * @param out the target writer
     */
    @SuppressFBWarnings(value = "EI_EXPOSE_REP2")
    public MarkdownWriter(Writer out) {

        this.out = out;
        this.writer = out;
        this.literal = new Literal();
        this.values = new ArrayList<Value>();
        this.values.add(this.literal);
    }

    /**
     * {@inheritDoc}
     *
     * @see java.io.Writer#close()
     */
    @Override
    public void close() throws IOException {

        for (Value v : values) {
            v.flush(writer);
        }
        if (out instanceof Value) {
            ((Value) out).flush(writer);
        }
        values = null;
        writer.flush();
    }

    /**
     * This method stores a definition for a reference and flushes the output as
     * much as possible.
     *
     * @param key the key
     * @param url the URL
     * @param text the text
     *
     * @throws IOException in case of an I/O error
     */
    public void defineReference(String key, String url, String text)
        throws IOException {

        reference.put(key, new String[]{url, text});
        flush();
    }

    /**
     * {@inheritDoc}
     *
     * @see java.io.Writer#flush()
     */
    @Override
    public void flush() throws IOException {

        while (!values.isEmpty() && !values.get(0).put(writer)) {
            values.remove(0);
        }
    }

    /**
     * The method <code>getReference</code> provides means to retrieve a
     * reference.
     *
     * @param key the key
     * @return the value of the reference or {@code null} for none
     */
    public String[] getReference(String key) {

        return reference.get(key);
    }

    /**
     * This method recognizes a reference.
     *
     * @param key the key
     * @param index the index
     *
     * @return {@code true} iff
     *
     * @throws IOException in case of an I/O error
     */
    private boolean reference(String key, int index) throws IOException {

        String[] value = reference.get(key);
        if (value != null) {
            write(value[index]);
            return false;
        }
        if (out instanceof Literal) {
            ((Literal) out).addSelfIfNotEmpty(values);
        }
        out = new Literal();
        return true;
    }

    /**
     * {@inheritDoc}
     *
     * @see java.io.Writer#write(char[], int, int)
     */
    @Override
    public void write(char[] cbuf, int off, int len) throws IOException {

        out.write(cbuf, off, len);
    }

    /**
     * {@inheritDoc}
     *
     * @see java.io.BufferedWriter#write(int)
     */
    @Override
    public void write(int c) throws IOException {

        out.write(c);
    }

    /**
     * {@inheritDoc}
     *
     * @see java.io.Writer#write(java.lang.String)
     */
    @Override
    public void write(String str) throws IOException {

        out.write(str);
    }

    /**
     * This method writes a character to out and escapes HTML entities.
     *
     * @param c the character code
     *
     * @throws IOException in case of an I/O error
     */
    public void writeEscaped(int c) throws IOException {

        switch (c) {
            case '&':
                write("&amp;");
                break;
            case '<':
                write("&lt;");
                break;
            case '>':
                write("&gt;");
                break;
            default:
                write(c);
        }
    }

    /**
     * This method places a reference on the output.
     *
     * @param key the key
     *
     * @throws IOException in case of an I/O error
     */
    public void writeReference(String key) throws IOException {

        if (reference(key, 0)) {
            values.add(new Reference(key));
        }
    }

    /**
     * This method places a reference text on the output.
     *
     * @param key the key
     *
     * @throws IOException in case of an I/O error
     */
    public void writeReferenceText(String key) throws IOException {

        if (reference(key, 1)) {
            values.add(new ReferenceText(key));
        }
    }
}