001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.marshall.markdown;
018
019import java.io.*;
020
021import org.apache.juneau.marshall.serializer.*;
022
023/**
024 * Low-level Markdown writer providing helper methods for producing Markdown output.
025 *
026 * <p>
027 * Extends {@link SerializerWriter} to provide Markdown-specific output helpers for headings,
028 * tables, lists, and inline formatting.
029 *
030 * <h5 class='section'>Notes:</h5><ul>
031 *    <li class='warn'>This class is not thread safe and is typically discarded after one use.
032 * </ul>
033 *
034 */
035@SuppressWarnings({
036   "java:S110", // Inheritance depth acceptable for serializer writer hierarchy
037   "resource" // Writer lifecycle managed by caller
038})
039public class MarkdownWriter extends SerializerWriter<MarkdownWriter> {
040
041   /**
042    * Constructor.
043    *
044    * @param out The underlying writer.
045    * @param useWhitespace Whether to use whitespace formatting.
046    * @param maxIndent The maximum indentation level.
047    * @param trimStrings Whether to trim strings.
048    * @param uriResolver The URI resolver for resolving relative URIs.
049    */
050   public MarkdownWriter(Writer out, boolean useWhitespace, int maxIndent, boolean trimStrings, org.apache.juneau.marshall.UriResolver uriResolver) {
051      super(out, useWhitespace, maxIndent, trimStrings, '\'', uriResolver);
052   }
053
054   /**
055    * Writes a Markdown heading at the specified level.
056    *
057    * @param level The heading level (1-6).
058    * @param text The heading text.
059    *    <br>Can be <jk>null</jk> (written as an empty string).
060    * @return This object.
061    * @throws IOException Thrown by underlying stream.
062    */
063   public MarkdownWriter heading(int level, String text) throws IOException {
064      var l = Math.min(6, Math.max(1, level));
065      for (var i = 0; i < l; i++)
066         append('#');
067      append(' ');
068      append(escapeText(text));
069      append('\n');
070      return this;
071   }
072
073   /**
074    * Writes a Markdown table header row.
075    *
076    * @param columns The column names.
077    *    <br>Individual column names can be <jk>null</jk> (written as an empty cell).
078    * @return This object.
079    * @throws IOException Thrown by underlying stream.
080    */
081   public MarkdownWriter tableHeader(String... columns) throws IOException {
082      append('|');
083      for (var col : columns) {
084         append(' ');
085         append(escapeCell(col));
086         append(" |");
087      }
088      append('\n');
089      return this;
090   }
091
092   /**
093    * Writes a Markdown table separator row.
094    *
095    * @param numColumns The number of columns.
096    * @return This object.
097    * @throws IOException Thrown by underlying stream.
098    */
099   public MarkdownWriter tableSeparator(int numColumns) throws IOException {
100      append('|');
101      for (var i = 0; i < numColumns; i++)
102         append("---|");
103      append('\n');
104      return this;
105   }
106
107   /**
108    * Writes a Markdown table data row.
109    *
110    * @param values The cell values.
111    *    <br>Individual values can be <jk>null</jk> (written as an empty cell).
112    * @return This object.
113    * @throws IOException Thrown by underlying stream.
114    */
115   public MarkdownWriter tableRow(String... values) throws IOException {
116      append('|');
117      for (var val : values) {
118         append(' ');
119         append(val == null ? "" : val);
120         append(" |");
121      }
122      append('\n');
123      return this;
124   }
125
126   /**
127    * Writes a Markdown bulleted list item.
128    *
129    * @param depth The indentation depth (0 = no indent).
130    * @param text The item text.
131    *    <br>Can be <jk>null</jk> (written as an empty string).
132    * @return This object.
133    * @throws IOException Thrown by underlying stream.
134    */
135   public MarkdownWriter bulletItem(int depth, String text) throws IOException {
136      for (var i = 0; i < depth; i++)
137         append("  ");
138      append("- ");
139      append(text == null ? "" : text);
140      append('\n');
141      return this;
142   }
143
144   /**
145    * Writes a Markdown horizontal rule.
146    *
147    * @return This object.
148    * @throws IOException Thrown by underlying stream.
149    */
150   public MarkdownWriter horizontalRule() throws IOException {
151      append("---");
152      append('\n');
153      return this;
154   }
155
156   /**
157    * Writes a blank line.
158    *
159    * @return This object.
160    * @throws IOException Thrown by underlying stream.
161    */
162   public MarkdownWriter blankLine() throws IOException {
163      append('\n');
164      return this;
165   }
166
167   /**
168    * Writes plain text with Markdown escaping.
169    *
170    * @param text The text to write.
171    *    <br>Can be <jk>null</jk> (written as an empty string).
172    * @return This object.
173    * @throws IOException Thrown by underlying stream.
174    */
175   public MarkdownWriter text(String text) throws IOException {
176      append(escapeText(text));
177      return this;
178   }
179
180   /**
181    * Writes text wrapped in backticks (inline code).
182    *
183    * @param text The text to render as code.
184    *    <br>Can be <jk>null</jk> (written as an empty string).
185    * @return This object.
186    * @throws IOException Thrown by underlying stream.
187    */
188   public MarkdownWriter code(String text) throws IOException {
189      append('`');
190      append(text == null ? "" : text);
191      append('`');
192      return this;
193   }
194
195   /**
196    * Writes bold text.
197    *
198    * @param text The text to render bold.
199    *    <br>Can be <jk>null</jk> (written as an empty string).
200    * @return This object.
201    * @throws IOException Thrown by underlying stream.
202    */
203   public MarkdownWriter bold(String text) throws IOException {
204      append("**");
205      append(escapeText(text));
206      append("**");
207      return this;
208   }
209
210   /**
211    * Writes italic text.
212    *
213    * @param text The text to render italic.
214    *    <br>Can be <jk>null</jk> (written as an empty string).
215    * @return This object.
216    * @throws IOException Thrown by underlying stream.
217    */
218   public MarkdownWriter italic(String text) throws IOException {
219      append('*');
220      append(escapeText(text));
221      append('*');
222      return this;
223   }
224
225   /**
226    * Escapes a string for use inside a Markdown table cell.
227    *
228    * <p>
229    * Escapes pipe characters and backslashes. Replaces newlines with {@code <br>}.
230    *
231    * @param text The raw text.
232    *    <br>Can be <jk>null</jk> (returns an empty string).
233    * @return The escaped text suitable for a table cell.  Never <jk>null</jk>.
234    */
235   public static String escapeCell(String text) {
236      if (text == null)
237         return "";
238      var sb = new StringBuilder(text.length() + 4);
239      for (var i = 0; i < text.length(); i++) {
240         var c = text.charAt(i);
241         switch (c) {
242            case '\\' -> sb.append("\\\\");
243            case '|' -> sb.append("\\|");
244            case '\n' -> sb.append("<br>");
245            case '\r' -> { /* skip carriage return */ }
246            default -> sb.append(c);
247         }
248      }
249      return sb.toString();
250   }
251
252   /**
253    * Escapes a string for use in general Markdown text.
254    *
255    * <p>
256    * Escapes backslashes and leading Markdown special characters.
257    *
258    * @param text The raw text.
259    *    <br>Can be <jk>null</jk> (returns an empty string).
260    * @return The escaped text.  Never <jk>null</jk>.
261    */
262   public static String escapeText(String text) {
263      if (text == null)
264         return "";
265      var sb = new StringBuilder(text.length() + 4);
266      for (var i = 0; i < text.length(); i++) {
267         var c = text.charAt(i);
268         if (c == '\\') {
269            sb.append("\\\\");
270         } else {
271            sb.append(c);
272         }
273      }
274      return sb.toString();
275   }
276}