1 package org.jetbrains.dokka 2 3 import com.google.inject.Inject 4 import com.google.inject.name.Named 5 import org.jetbrains.dokka.Utilities.impliedPlatformsName 6 import java.util.* 7 8 enum class ListKind { 9 Ordered, 10 Unordered 11 } 12 13 private class ListState(val kind: ListKind, var size: Int = 1) { getTagAndIncrementnull14 fun getTagAndIncrement() = when (kind) { 15 ListKind.Ordered -> "${size++}. " 16 else -> "* " 17 } 18 } 19 20 private val TWO_LINE_BREAKS = System.lineSeparator() + System.lineSeparator() 21 22 open class MarkdownOutputBuilder(to: StringBuilder, 23 location: Location, 24 generator: NodeLocationAwareGenerator, 25 languageService: LanguageService, 26 extension: String, 27 impliedPlatforms: List<String>) 28 : StructuredOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) 29 { 30 private val listStack = ArrayDeque<ListState>() 31 protected var inTableCell = false 32 protected var inCodeBlock = false 33 private var lastTableCellStart = -1 34 private var maxBackticksInCodeBlock = 0 35 appendNewlinenull36 private fun appendNewline() { 37 while (to.endsWith(' ')) { 38 to.setLength(to.length - 1) 39 } 40 to.appendln() 41 } 42 ensureNewlinenull43 private fun ensureNewline() { 44 if (inTableCell && listStack.isEmpty()) { 45 if (to.length != lastTableCellStart && !to.endsWith("<br>")) { 46 to.append("<br>") 47 } 48 } 49 else { 50 if (!endsWithNewline()) { 51 appendNewline() 52 } 53 } 54 } 55 endsWithNewlinenull56 private fun endsWithNewline(): Boolean { 57 var index = to.length - 1 58 while (index > 0) { 59 val c = to[index] 60 if (c != ' ') { 61 return c == '\n' 62 } 63 index-- 64 } 65 return false 66 } 67 ensureParagraphnull68 override fun ensureParagraph() { 69 if (!to.endsWith(TWO_LINE_BREAKS)) { 70 if (!to.endsWith('\n')) { 71 appendNewline() 72 } 73 appendNewline() 74 } 75 } appendBreadcrumbSeparatornull76 override fun appendBreadcrumbSeparator() { 77 to.append(" / ") 78 } 79 80 private val backTickFindingRegex = """(`+)""".toRegex() 81 appendTextnull82 override fun appendText(text: String) { 83 if (inCodeBlock) { 84 to.append(text) 85 val backTicks = backTickFindingRegex.findAll(text) 86 val longestBackTickRun = backTicks.map { it.value.length }.max() ?: 0 87 maxBackticksInCodeBlock = maxBackticksInCodeBlock.coerceAtLeast(longestBackTickRun) 88 } 89 else { 90 if (text == "\n" && inTableCell) { 91 to.append(" ") 92 } else { 93 to.append(text.htmlEscape()) 94 } 95 } 96 } 97 appendCodenull98 override fun appendCode(body: () -> Unit) { 99 inCodeBlock = true 100 val codeBlockStart = to.length 101 maxBackticksInCodeBlock = 0 102 103 wrapIfNotEmpty("`", "`", body, checkEndsWith = true) 104 105 if (maxBackticksInCodeBlock > 0) { 106 val extraBackticks = "`".repeat(maxBackticksInCodeBlock) 107 to.insert(codeBlockStart, extraBackticks) 108 to.append(extraBackticks) 109 } 110 111 inCodeBlock = false 112 } 113 appendUnorderedListnull114 override fun appendUnorderedList(body: () -> Unit) { 115 listStack.push(ListState(ListKind.Unordered)) 116 body() 117 listStack.pop() 118 ensureNewline() 119 } 120 appendOrderedListnull121 override fun appendOrderedList(body: () -> Unit) { 122 listStack.push(ListState(ListKind.Ordered)) 123 body() 124 listStack.pop() 125 ensureNewline() 126 } 127 appendListItemnull128 override fun appendListItem(body: () -> Unit) { 129 ensureNewline() 130 to.append(listStack.peek()?.getTagAndIncrement()) 131 body() 132 ensureNewline() 133 } 134 appendStrongnull135 override fun appendStrong(body: () -> Unit) = wrap("**", "**", body) 136 override fun appendEmphasis(body: () -> Unit) = wrap("*", "*", body) 137 override fun appendStrikethrough(body: () -> Unit) = wrap("~~", "~~", body) 138 139 override fun appendLink(href: String, body: () -> Unit) { 140 if (inCodeBlock) { 141 wrap("`[`", "`]($href)`", body) 142 } 143 else { 144 wrap("[", "]($href)", body) 145 } 146 } 147 appendLinenull148 override fun appendLine() { 149 if (inTableCell) { 150 to.append("<br>") 151 } 152 else { 153 appendNewline() 154 } 155 } 156 appendAnchornull157 override fun appendAnchor(anchor: String) { 158 // no anchors in Markdown 159 } 160 appendParagraphnull161 override fun appendParagraph(body: () -> Unit) { 162 if (inTableCell) { 163 ensureNewline() 164 body() 165 } else if (listStack.isNotEmpty()) { 166 body() 167 ensureNewline() 168 } else { 169 ensureParagraph() 170 body() 171 ensureParagraph() 172 } 173 } 174 appendHeadernull175 override fun appendHeader(level: Int, body: () -> Unit) { 176 ensureParagraph() 177 to.append("${"#".repeat(level)} ") 178 body() 179 ensureParagraph() 180 } 181 appendBlockCodenull182 override fun appendBlockCode(language: String, body: () -> Unit) { 183 inCodeBlock = true 184 ensureParagraph() 185 to.appendln(if (language.isEmpty()) "```" else "``` $language") 186 body() 187 ensureNewline() 188 to.appendln("```") 189 appendLine() 190 inCodeBlock = false 191 } 192 appendTablenull193 override fun appendTable(vararg columns: String, body: () -> Unit) { 194 ensureParagraph() 195 body() 196 ensureParagraph() 197 } 198 appendTableBodynull199 override fun appendTableBody(body: () -> Unit) { 200 body() 201 } 202 appendTableRownull203 override fun appendTableRow(body: () -> Unit) { 204 to.append("|") 205 body() 206 appendNewline() 207 } 208 appendTableCellnull209 override fun appendTableCell(body: () -> Unit) { 210 to.append(" ") 211 inTableCell = true 212 lastTableCellStart = to.length 213 body() 214 inTableCell = false 215 to.append(" |") 216 } 217 appendNonBreakingSpacenull218 override fun appendNonBreakingSpace() { 219 if (inCodeBlock) { 220 to.append(" ") 221 } 222 else { 223 to.append(" ") 224 } 225 } 226 } 227 228 open class MarkdownFormatService(generator: NodeLocationAwareGenerator, 229 signatureGenerator: LanguageService, 230 linkExtension: String, 231 val impliedPlatforms: List<String>) 232 : StructuredFormatService(generator, signatureGenerator, "md", linkExtension) { 233 @Inject constructor(generator: NodeLocationAwareGenerator, 234 signatureGenerator: LanguageService, 235 @Named(impliedPlatformsName) impliedPlatforms: List<String>): this(generator, signatureGenerator, "md", impliedPlatforms) 236 createOutputBuildernull237 override fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder = 238 MarkdownOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) 239 } 240