Skip to content

Commit

Permalink
Use value classes for all markdown nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
Oleg Baskakov authored and obask committed Mar 27, 2024
1 parent 1bc717b commit 428ee36
Show file tree
Hide file tree
Showing 16 changed files with 407 additions and 13,290 deletions.
296 changes: 197 additions & 99 deletions markdown/core/api/core.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,62 +1,121 @@
package org.jetbrains.jewel.markdown

import org.commonmark.node.Node
import org.jetbrains.jewel.markdown.MarkdownBlock.BlockQuote
import org.jetbrains.jewel.markdown.MarkdownBlock.CodeBlock
import org.jetbrains.jewel.markdown.MarkdownBlock.CustomBlock
import org.jetbrains.jewel.markdown.MarkdownBlock.Heading
import org.jetbrains.jewel.markdown.MarkdownBlock.HtmlBlock
import org.jetbrains.jewel.markdown.MarkdownBlock.LinkReferenceDefinition
import org.jetbrains.jewel.markdown.MarkdownBlock.ListBlock
import org.jetbrains.jewel.markdown.MarkdownBlock.ListItem
import org.jetbrains.jewel.markdown.MarkdownBlock.Paragraph
import org.jetbrains.jewel.markdown.MarkdownBlock.ThematicBreak
import org.commonmark.node.Block as CMBlock
import org.commonmark.node.BlockQuote as CMBlockQuote
import org.commonmark.node.BulletList as CMBulletList
import org.commonmark.node.CustomBlock as CMCustomBlock
import org.commonmark.node.FencedCodeBlock as CMFencedCodeBlock
import org.commonmark.node.Heading as CMHeading
import org.commonmark.node.HtmlBlock as CMHtmlBlock
import org.commonmark.node.IndentedCodeBlock as CMIndentedCodeBlock
import org.commonmark.node.LinkReferenceDefinition as CMLinkReferenceDefinition
import org.commonmark.node.ListItem as CMListItem
import org.commonmark.node.OrderedList as CMOrderedList
import org.commonmark.node.Paragraph as CMParagraph
import org.commonmark.node.ThematicBreak as CMThematicBreak

public sealed interface MarkdownBlock {

public data class BlockQuote(val content: List<MarkdownBlock>) : MarkdownBlock
public val value: CMBlock

public interface CustomBlock : MarkdownBlock
@JvmInline
public value class BlockQuote(override val value: CMBlockQuote) : MarkdownBlock

public sealed interface CodeBlock : MarkdownBlock {
@JvmInline
public value class FencedCodeBlock(override val value: CMFencedCodeBlock) : CodeBlock

public val content: String
@JvmInline
public value class IndentedCodeBlock(override val value: CMIndentedCodeBlock) : CodeBlock
}

public data class IndentedCodeBlock(
override val content: String,
) : CodeBlock
@JvmInline
public value class CustomBlock(override val value: CMCustomBlock) : MarkdownBlock

public data class FencedCodeBlock(
override val content: String,
val mimeType: MimeType?,
) : CodeBlock
}
@JvmInline
public value class Heading(override val value: CMHeading) : MarkdownBlock, BlockWithInlineMarkdown {

public data class Heading(
override val inlineContent: List<InlineMarkdown>,
val level: Int,
) : MarkdownBlock, BlockWithInlineMarkdown
public val level: Int
get() = value.level
}

public data class HtmlBlock(val content: String) : MarkdownBlock
@JvmInline
public value class HtmlBlock(override val value: CMHtmlBlock) : MarkdownBlock

public sealed interface ListBlock : MarkdownBlock {
@JvmInline
public value class BulletList(override val value: CMBulletList) : MarkdownBlock

public val items: List<ListItem>
public val isTight: Boolean

public data class BulletList(
override val items: List<ListItem>,
override val isTight: Boolean,
val bulletMarker: Char,
) : ListBlock

public data class OrderedList(
override val items: List<ListItem>,
override val isTight: Boolean,
val startFrom: Int,
val delimiter: Char,
) : ListBlock
@JvmInline
public value class OrderedList(override val value: CMOrderedList) : MarkdownBlock
}

public data class ListItem(
val content: List<MarkdownBlock>,
) : MarkdownBlock
@JvmInline
public value class ListItem(override val value: CMListItem) : MarkdownBlock

public data class LinkReferenceDefinition(val payload: Node) : MarkdownBlock {
override val value: CMBlock = object : CMCustomBlock() {}
}

public object ThematicBreak : MarkdownBlock
@JvmInline
public value class ThematicBreak(override val value: CMThematicBreak) : MarkdownBlock

public data class Paragraph(override val inlineContent: List<InlineMarkdown>) :
MarkdownBlock, BlockWithInlineMarkdown
@JvmInline
public value class Paragraph(override val value: CMParagraph) : MarkdownBlock, BlockWithInlineMarkdown

public val children: Iterator<MarkdownBlock>
get() = object : Iterator<MarkdownBlock> {
var current = this@MarkdownBlock.value.firstChild

override fun hasNext(): Boolean = current != null

override fun next(): MarkdownBlock =
if (hasNext()) {
current.toMarkdownBlock().also {
current = current.next
}
} else {
throw NoSuchElementException()
}
}
}

public interface BlockWithInlineMarkdown {

public val inlineContent: List<InlineMarkdown>
get() = buildList {
var child = (this@BlockWithInlineMarkdown as MarkdownBlock).value.firstChild

while (child != null) {
add(child.toInlineNode())
child = child.next
}
}
}

public fun Node.toMarkdownBlock(): MarkdownBlock = when (this) {
is CMParagraph -> Paragraph(this)
is CMHeading -> Heading(this)
is CMHtmlBlock -> HtmlBlock(this)
is CMBlockQuote -> BlockQuote(this)
is CMFencedCodeBlock -> CodeBlock.FencedCodeBlock(this)
is CMIndentedCodeBlock -> CodeBlock.IndentedCodeBlock(this)
is CMBulletList -> ListBlock.BulletList(this)
is CMOrderedList -> ListBlock.OrderedList(this)
is CMListItem -> ListItem(this)
is CMThematicBreak -> ThematicBreak(this)
is CMCustomBlock -> CustomBlock(this)
is CMLinkReferenceDefinition -> LinkReferenceDefinition(this)
else -> error("Unexpected block $this")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.jetbrains.jewel.markdown

public interface WithInlineMarkdown {
public val inlineContent: InlineMarkdown
}
Original file line number Diff line number Diff line change
@@ -1,39 +1,25 @@
package org.jetbrains.jewel.markdown.processing

import org.commonmark.node.BlockQuote
import org.commonmark.node.BulletList
import org.commonmark.node.Code
import org.commonmark.node.CustomBlock
import org.commonmark.node.Document
import org.commonmark.node.Emphasis
import org.commonmark.node.FencedCodeBlock
import org.commonmark.node.HardLineBreak
import org.commonmark.node.Heading
import org.commonmark.node.HtmlBlock
import org.commonmark.node.HtmlInline
import org.commonmark.node.Image
import org.commonmark.node.IndentedCodeBlock
import org.commonmark.node.Link
import org.commonmark.node.ListBlock
import org.commonmark.node.ListItem
import org.commonmark.node.Node
import org.commonmark.node.OrderedList
import org.commonmark.node.Paragraph
import org.commonmark.node.SoftLineBreak
import org.commonmark.node.StrongEmphasis
import org.commonmark.node.Text
import org.commonmark.node.ThematicBreak
import org.commonmark.parser.Parser
import org.commonmark.renderer.text.TextContentRenderer
import org.intellij.lang.annotations.Language
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.markdown.InlineMarkdown
import org.jetbrains.jewel.markdown.MarkdownBlock
import org.jetbrains.jewel.markdown.MarkdownBlock.CodeBlock
import org.jetbrains.jewel.markdown.MimeType
import org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension
import org.jetbrains.jewel.markdown.rendering.DefaultInlineMarkdownRenderer
import org.jetbrains.jewel.markdown.toInlineNode
import org.jetbrains.jewel.markdown.toMarkdownBlock

@ExperimentalJewelApi
public class MarkdownProcessor(private val extensions: List<MarkdownProcessorExtension> = emptyList()) {
Expand Down Expand Up @@ -80,78 +66,9 @@ public class MarkdownProcessor(private val extensions: List<MarkdownProcessorExt
val document =
commonMarkParser.parse(rawMarkdown) as? Document
?: error("This doesn't look like a Markdown document")

return processChildren(document)
}

private fun Node.tryProcessMarkdownBlock(): MarkdownBlock? =
// Non-Block children are ignored
when (this) {
is BlockQuote -> toMarkdownBlockQuote()
is Heading -> toMarkdownHeadingOrNull()
is Paragraph -> toMarkdownParagraphOrNull()
is FencedCodeBlock -> toMarkdownCodeBlockOrNull()
is IndentedCodeBlock -> toMarkdownCodeBlockOrNull()
is BulletList -> toMarkdownListOrNull()
is OrderedList -> toMarkdownListOrNull()
is ThematicBreak -> MarkdownBlock.ThematicBreak
is HtmlBlock -> toMarkdownHtmlBlockOrNull()
is CustomBlock -> {
extensions.find { it.processorExtension.canProcess(this) }
?.processorExtension?.processMarkdownBlock(this, this@MarkdownProcessor)
}
// TODO: add support for LinkReferenceDefinition
else -> null
}

private fun BlockQuote.toMarkdownBlockQuote(): MarkdownBlock.BlockQuote =
MarkdownBlock.BlockQuote(processChildren(this))

private fun Heading.toMarkdownHeadingOrNull(): MarkdownBlock.Heading =
MarkdownBlock.Heading(contentsAsInlineMarkdown(), level)

private fun Paragraph.toMarkdownParagraphOrNull(): MarkdownBlock.Paragraph {
val inlineMarkdown = contentsAsInlineMarkdown()

// if (inlineMarkdown.isEmpty()) return null
return MarkdownBlock.Paragraph(inlineMarkdown)
}

private fun FencedCodeBlock.toMarkdownCodeBlockOrNull(): CodeBlock.FencedCodeBlock =
CodeBlock.FencedCodeBlock(
literal.trimEnd('\n'),
MimeType.Known.fromMarkdownLanguageName(info),
)

private fun IndentedCodeBlock.toMarkdownCodeBlockOrNull(): CodeBlock.IndentedCodeBlock =
CodeBlock.IndentedCodeBlock(literal.trimEnd('\n'))

private fun BulletList.toMarkdownListOrNull(): MarkdownBlock.ListBlock.BulletList? {
val children = processListItems()
if (children.isEmpty()) return null

return org.jetbrains.jewel.markdown.MarkdownBlock.ListBlock.BulletList(children, isTight, bulletMarker)
}

private fun OrderedList.toMarkdownListOrNull(): MarkdownBlock.ListBlock.OrderedList? {
val children = processListItems()
if (children.isEmpty()) return null

return MarkdownBlock.ListBlock.OrderedList(children, isTight, startNumber, delimiter)
}

private fun ListBlock.processListItems() = buildList {
forEachChild { child ->
if (child !is ListItem) return@forEachChild
add(MarkdownBlock.ListItem(processChildren(child)))
}
}

public fun processChildren(node: Node): List<MarkdownBlock> = buildList {
node.forEachChild { child ->
val parsedBlock = child.tryProcessMarkdownBlock()
if (parsedBlock != null) {
this.add(parsedBlock)
return buildList {
document.forEachChild { child ->
add(child.toMarkdownBlock())
}
}
}
Expand All @@ -165,17 +82,6 @@ public class MarkdownProcessor(private val extensions: List<MarkdownProcessorExt
}
}

private fun HtmlBlock.toMarkdownHtmlBlockOrNull(): MarkdownBlock.HtmlBlock? {
if (literal.isBlank()) return null
return MarkdownBlock.HtmlBlock(content = literal.trimEnd('\n'))
}

private fun Node.contentsAsInlineMarkdown() = buildList {
forEachChild {
add(it.toInlineNode())
}
}

private fun StringBuilder.appendInlineMarkdownFrom(
node: Node,
allowLinks: Boolean = true,
Expand Down
Loading

0 comments on commit 428ee36

Please sign in to comment.