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}