package org.jetbrains.dokka import com.google.inject.Inject import com.google.inject.name.Named import org.jetbrains.dokka.Utilities.impliedPlatformsName import java.util.* enum class ListKind { Ordered, Unordered } private class ListState(val kind: ListKind, var size: Int = 1) { fun getTagAndIncrement() = when (kind) { ListKind.Ordered -> "${size++}. " else -> "* " } } private val TWO_LINE_BREAKS = System.lineSeparator() + System.lineSeparator() open class MarkdownOutputBuilder(to: StringBuilder, location: Location, generator: NodeLocationAwareGenerator, languageService: LanguageService, extension: String, impliedPlatforms: List) : StructuredOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) { private val listStack = ArrayDeque() protected var inTableCell = false protected var inCodeBlock = false private var lastTableCellStart = -1 private var maxBackticksInCodeBlock = 0 private fun appendNewline() { while (to.endsWith(' ')) { to.setLength(to.length - 1) } to.appendln() } private fun ensureNewline() { if (inTableCell && listStack.isEmpty()) { if (to.length != lastTableCellStart && !to.endsWith("
")) { to.append("
") } } else { if (!endsWithNewline()) { appendNewline() } } } private fun endsWithNewline(): Boolean { var index = to.length - 1 while (index > 0) { val c = to[index] if (c != ' ') { return c == '\n' } index-- } return false } override fun ensureParagraph() { if (!to.endsWith(TWO_LINE_BREAKS)) { if (!to.endsWith('\n')) { appendNewline() } appendNewline() } } override fun appendBreadcrumbSeparator() { to.append(" / ") } private val backTickFindingRegex = """(`+)""".toRegex() override fun appendText(text: String) { if (inCodeBlock) { to.append(text) val backTicks = backTickFindingRegex.findAll(text) val longestBackTickRun = backTicks.map { it.value.length }.max() ?: 0 maxBackticksInCodeBlock = maxBackticksInCodeBlock.coerceAtLeast(longestBackTickRun) } else { if (text == "\n" && inTableCell) { to.append(" ") } else { to.append(text.htmlEscape()) } } } override fun appendCode(body: () -> Unit) { inCodeBlock = true val codeBlockStart = to.length maxBackticksInCodeBlock = 0 wrapIfNotEmpty("`", "`", body, checkEndsWith = true) if (maxBackticksInCodeBlock > 0) { val extraBackticks = "`".repeat(maxBackticksInCodeBlock) to.insert(codeBlockStart, extraBackticks) to.append(extraBackticks) } inCodeBlock = false } override fun appendUnorderedList(body: () -> Unit) { listStack.push(ListState(ListKind.Unordered)) body() listStack.pop() ensureNewline() } override fun appendOrderedList(body: () -> Unit) { listStack.push(ListState(ListKind.Ordered)) body() listStack.pop() ensureNewline() } override fun appendListItem(body: () -> Unit) { ensureNewline() to.append(listStack.peek()?.getTagAndIncrement()) body() ensureNewline() } override fun appendStrong(body: () -> Unit) = wrap("**", "**", body) override fun appendEmphasis(body: () -> Unit) = wrap("*", "*", body) override fun appendStrikethrough(body: () -> Unit) = wrap("~~", "~~", body) override fun appendLink(href: String, body: () -> Unit) { if (inCodeBlock) { wrap("`[`", "`]($href)`", body) } else { wrap("[", "]($href)", body) } } override fun appendLine() { if (inTableCell) { to.append("
") } else { appendNewline() } } override fun appendAnchor(anchor: String) { // no anchors in Markdown } override fun appendParagraph(body: () -> Unit) { if (inTableCell) { ensureNewline() body() } else if (listStack.isNotEmpty()) { body() ensureNewline() } else { ensureParagraph() body() ensureParagraph() } } override fun appendHeader(level: Int, body: () -> Unit) { ensureParagraph() to.append("${"#".repeat(level)} ") body() ensureParagraph() } override fun appendBlockCode(language: String, body: () -> Unit) { inCodeBlock = true ensureParagraph() to.appendln(if (language.isEmpty()) "```" else "``` $language") body() ensureNewline() to.appendln("```") appendLine() inCodeBlock = false } override fun appendTable(vararg columns: String, body: () -> Unit) { ensureParagraph() body() ensureParagraph() } override fun appendTableBody(body: () -> Unit) { body() } override fun appendTableRow(body: () -> Unit) { to.append("|") body() appendNewline() } override fun appendTableCell(body: () -> Unit) { to.append(" ") inTableCell = true lastTableCellStart = to.length body() inTableCell = false to.append(" |") } override fun appendNonBreakingSpace() { if (inCodeBlock) { to.append(" ") } else { to.append(" ") } } } open class MarkdownFormatService(generator: NodeLocationAwareGenerator, signatureGenerator: LanguageService, linkExtension: String, val impliedPlatforms: List) : StructuredFormatService(generator, signatureGenerator, "md", linkExtension) { @Inject constructor(generator: NodeLocationAwareGenerator, signatureGenerator: LanguageService, @Named(impliedPlatformsName) impliedPlatforms: List): this(generator, signatureGenerator, "md", impliedPlatforms) override fun createOutputBuilder(to: StringBuilder, location: Location): FormattedOutputBuilder = MarkdownOutputBuilder(to, location, generator, languageService, extension, impliedPlatforms) }