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("&");
break;
case '<':
write("<");
break;
case '>':
write(">");
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));
}
}
}