Skip to content

Commit

Permalink
Use value classes for inline markdown
Browse files Browse the repository at this point in the history
This classes wrap CommonMark's nodes and preserve
a familiar API. Some tests were turned off because
we don't use the same IR for string serialization.

Changing dataclasses to values classes for blocks
is coming in the next pull request.
  • Loading branch information
obask committed Mar 27, 2024
1 parent 5d3ee47 commit c04758d
Show file tree
Hide file tree
Showing 19 changed files with 313 additions and 364 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package org.jetbrains.jewel.markdown

import org.commonmark.node.Node
import org.jetbrains.jewel.markdown.InlineMarkdown.Code
import org.jetbrains.jewel.markdown.InlineMarkdown.CustomNode
import org.jetbrains.jewel.markdown.InlineMarkdown.Emphasis
import org.jetbrains.jewel.markdown.InlineMarkdown.HardLineBreak
import org.jetbrains.jewel.markdown.InlineMarkdown.HtmlInline
import org.jetbrains.jewel.markdown.InlineMarkdown.Image
import org.jetbrains.jewel.markdown.InlineMarkdown.Link
import org.jetbrains.jewel.markdown.InlineMarkdown.SoftLineBreak
import org.jetbrains.jewel.markdown.InlineMarkdown.StrongEmphasis
import org.jetbrains.jewel.markdown.InlineMarkdown.Text
import org.commonmark.node.Code as CMCode
import org.commonmark.node.CustomNode as CMCustomNode
import org.commonmark.node.Emphasis as CMEmphasis
import org.commonmark.node.HardLineBreak as CMHardLineBreak
import org.commonmark.node.HtmlInline as CMHtmlInline
import org.commonmark.node.Image as CMImage
import org.commonmark.node.Link as CMLink
import org.commonmark.node.Paragraph as CMParagraph
import org.commonmark.node.SoftLineBreak as CMSoftLineBreak
import org.commonmark.node.StrongEmphasis as CMStrongEmphasis
import org.commonmark.node.Text as CMText

/**
* A run of inline Markdown used as content for
* [block-level elements][MarkdownBlock].
*/
public sealed interface InlineMarkdown {

public val value: Node

@JvmInline
public value class Emphasis(override val value: CMEmphasis) : InlineMarkdown

@JvmInline
public value class Image(override val value: CMImage) : InlineMarkdown

@JvmInline
public value class Code(override val value: CMCode) : InlineMarkdown

@JvmInline
public value class CustomNode(override val value: CMCustomNode) : InlineMarkdown

@JvmInline
public value class HardLineBreak(override val value: CMHardLineBreak) : InlineMarkdown

@JvmInline
public value class SoftLineBreak(override val value: CMSoftLineBreak) : InlineMarkdown

@JvmInline
public value class HtmlInline(override val value: CMHtmlInline) : InlineMarkdown

@JvmInline
public value class Link(override val value: CMLink) : InlineMarkdown

@JvmInline
public value class StrongEmphasis(override val value: CMStrongEmphasis) : InlineMarkdown

@JvmInline
public value class Paragraph(override val value: CMParagraph) : InlineMarkdown

@JvmInline
public value class Text(override val value: CMText) : InlineMarkdown

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

override fun hasNext(): Boolean = child.next != null

override fun next(): InlineMarkdown = child.toInlineNode().also {
child = child.next
}
}
}

public fun Node.toInlineNode(): InlineMarkdown = when (this) {
is CMText -> Text(this)
is CMEmphasis -> Emphasis(this)
is CMImage -> Image(this)
is CMCode -> Code(this)
is CMCustomNode -> CustomNode(this)
is CMHardLineBreak -> HardLineBreak(this)
is CMSoftLineBreak -> SoftLineBreak(this)
is CMHtmlInline -> HtmlInline(this)
is CMLink -> Link(this)
is CMStrongEmphasis -> StrongEmphasis(this)
else -> error("Unexpected block $this")
}
Original file line number Diff line number Diff line change
@@ -1,83 +1,62 @@
package org.jetbrains.jewel.markdown

import org.intellij.lang.annotations.Language

public sealed interface MarkdownBlock {

public data class Paragraph(override val inlineContent: InlineMarkdown) :
MarkdownBlock, BlockWithInlineMarkdown

public sealed interface Heading : MarkdownBlock, BlockWithInlineMarkdown {

public data class H1(override val inlineContent: InlineMarkdown) : Heading
public data class BlockQuote(val content: List<MarkdownBlock>) : MarkdownBlock

public data class H2(override val inlineContent: InlineMarkdown) : Heading
public interface CustomBlock : MarkdownBlock

public data class H3(override val inlineContent: InlineMarkdown) : Heading
public sealed interface CodeBlock : MarkdownBlock {

public data class H4(override val inlineContent: InlineMarkdown) : Heading
public val content: String

public data class H5(override val inlineContent: InlineMarkdown) : Heading
public data class IndentedCodeBlock(
override val content: String,
) : CodeBlock

public data class H6(override val inlineContent: InlineMarkdown) : Heading
public data class FencedCodeBlock(
override val content: String,
val mimeType: MimeType?,
) : CodeBlock
}

public data class BlockQuote(val content: List<MarkdownBlock>) : MarkdownBlock
public data class Heading(
override val inlineContent: List<InlineMarkdown>,
val level: Int,
) : MarkdownBlock, BlockWithInlineMarkdown

public data class HtmlBlock(val content: String) : MarkdownBlock

public sealed interface ListBlock : MarkdownBlock {

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

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

public data class UnorderedList(
public data class OrderedList(
override val items: List<ListItem>,
override val isTight: Boolean,
val bulletMarker: Char,
val startFrom: Int,
val delimiter: Char,
) : ListBlock
}

public data class ListItem(
val content: List<MarkdownBlock>,
) : MarkdownBlock

public sealed interface CodeBlock : MarkdownBlock {

public val content: String

public data class IndentedCodeBlock(
override val content: String,
) : CodeBlock

public data class FencedCodeBlock(
override val content: String,
val mimeType: MimeType?,
) : CodeBlock
}

public data class Image(val url: String, val altString: String?) : MarkdownBlock

public object ThematicBreak : MarkdownBlock

public data class HtmlBlock(val content: String) : MarkdownBlock

public interface Extension : MarkdownBlock
public data class Paragraph(override val inlineContent: List<InlineMarkdown>) :
MarkdownBlock, BlockWithInlineMarkdown
}

public interface BlockWithInlineMarkdown {

public val inlineContent: InlineMarkdown
public val inlineContent: List<InlineMarkdown>
}

/**
* A run of inline Markdown used as content for
* [block-level elements][MarkdownBlock].
*/
@JvmInline
public value class InlineMarkdown(@Language("Markdown") public val content: String)
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ public interface MarkdownBlockProcessorExtension {
public fun canProcess(block: CustomBlock): Boolean

/**
* Processes the [block] as a [MarkdownBlock.Extension], if possible. Note
* Processes the [block] as a [MarkdownBlock.CustomBlock], if possible. Note
* that you should always check that [canProcess] returns true for the same
* [block], as implementations might throw an exception for unsupported
* block types.
*/
public fun processMarkdownBlock(block: CustomBlock, processor: MarkdownProcessor): MarkdownBlock.Extension?
public fun processMarkdownBlock(block: CustomBlock, processor: MarkdownProcessor): MarkdownBlock.CustomBlock?
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,26 @@ package org.jetbrains.jewel.markdown.extensions

import androidx.compose.runtime.Composable
import org.jetbrains.jewel.markdown.MarkdownBlock
import org.jetbrains.jewel.markdown.MarkdownBlock.Extension
import org.jetbrains.jewel.markdown.MarkdownBlock.CustomBlock
import org.jetbrains.jewel.markdown.rendering.InlineMarkdownRenderer
import org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer

/**
* An extension for [MarkdownBlockRenderer] that can render one or more
* [MarkdownBlock.Extension]s.
* [MarkdownBlock.CustomBlock]s.
*/
public interface MarkdownBlockRendererExtension {

/** Check whether the provided [block] can be rendered by this extension. */
public fun canRender(block: Extension): Boolean
public fun canRender(block: CustomBlock): Boolean

/**
* Render a [MarkdownBlock.Extension] as a native Composable. Note that if
* Render a [MarkdownBlock.CustomBlock] as a native Composable. Note that if
* [canRender] returns `false` for [block], the implementation might throw.
*/
@Composable
public fun render(
block: Extension,
block: CustomBlock,
blockRenderer: MarkdownBlockRenderer,
inlineRenderer: InlineMarkdownRenderer,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public interface MarkdownProcessorExtension {
* An extension for
* [`MarkdownParser`][org.jetbrains.jewel.markdown.parsing.MarkdownParser]
* that will transform a supported [CustomBlock] into the corresponding
* [MarkdownBlock.Extension].
* [MarkdownBlock.CustomBlock].
*/
public val processorExtension: MarkdownBlockProcessorExtension
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public interface MarkdownRendererExtension {
* An extension for
* [`MarkdownBlockRenderer`][org.jetbrains.jewel.markdown.rendering.MarkdownBlockRenderer]
* that will render a supported
* [`Extension`][org.jetbrains.jewel.markdown.MarkdownBlock.Extension] into
* [`CustomBlock`][org.jetbrains.jewel.markdown.MarkdownBlock.CustomBlock] into
* a native Jewel UI.
*/
public val blockRenderer: MarkdownBlockRendererExtension
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,10 @@ 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.MarkdownBlock.Heading.H1
import org.jetbrains.jewel.markdown.MarkdownBlock.Heading.H2
import org.jetbrains.jewel.markdown.MarkdownBlock.Heading.H3
import org.jetbrains.jewel.markdown.MarkdownBlock.Heading.H4
import org.jetbrains.jewel.markdown.MarkdownBlock.Heading.H5
import org.jetbrains.jewel.markdown.MarkdownBlock.Heading.H6
import org.jetbrains.jewel.markdown.MarkdownBlock.ListBlock.UnorderedList
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

@ExperimentalJewelApi
public class MarkdownProcessor(private val extensions: List<MarkdownProcessorExtension> = emptyList()) {
Expand Down Expand Up @@ -98,7 +92,6 @@ public class MarkdownProcessor(private val extensions: List<MarkdownProcessorExt
is Paragraph -> toMarkdownParagraphOrNull()
is FencedCodeBlock -> toMarkdownCodeBlockOrNull()
is IndentedCodeBlock -> toMarkdownCodeBlockOrNull()
is Image -> toMarkdownImageOrNull()
is BulletList -> toMarkdownListOrNull()
is OrderedList -> toMarkdownListOrNull()
is ThematicBreak -> MarkdownBlock.ThematicBreak
Expand All @@ -114,21 +107,13 @@ public class MarkdownProcessor(private val extensions: List<MarkdownProcessorExt
private fun BlockQuote.toMarkdownBlockQuote(): MarkdownBlock.BlockQuote =
MarkdownBlock.BlockQuote(processChildren(this))

private fun Heading.toMarkdownHeadingOrNull(): MarkdownBlock.Heading? =
when (level) {
1 -> H1(contentsAsInlineMarkdown())
2 -> H2(contentsAsInlineMarkdown())
3 -> H3(contentsAsInlineMarkdown())
4 -> H4(contentsAsInlineMarkdown())
5 -> H5(contentsAsInlineMarkdown())
6 -> H6(contentsAsInlineMarkdown())
else -> null
}
private fun Heading.toMarkdownHeadingOrNull(): MarkdownBlock.Heading =
MarkdownBlock.Heading(contentsAsInlineMarkdown(), level)

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

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

Expand All @@ -141,17 +126,11 @@ public class MarkdownProcessor(private val extensions: List<MarkdownProcessorExt
private fun IndentedCodeBlock.toMarkdownCodeBlockOrNull(): CodeBlock.IndentedCodeBlock =
CodeBlock.IndentedCodeBlock(literal.trimEnd('\n'))

private fun Image.toMarkdownImageOrNull(): MarkdownBlock.Image? {
if (destination.isBlank()) return null

return MarkdownBlock.Image(destination.trim(), title.trim())
}

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

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

private fun OrderedList.toMarkdownListOrNull(): MarkdownBlock.ListBlock.OrderedList? {
Expand All @@ -171,7 +150,11 @@ public class MarkdownProcessor(private val extensions: List<MarkdownProcessorExt
public fun processChildren(node: Node): List<MarkdownBlock> = buildList {
node.forEachChild { child ->
val parsedBlock = child.tryProcessMarkdownBlock()
if (parsedBlock != null) this.add(parsedBlock)
if (parsedBlock != null) {
this.add(parsedBlock)
} else {
println(child)
}
}
}

Expand All @@ -189,8 +172,11 @@ public class MarkdownProcessor(private val extensions: List<MarkdownProcessorExt
return MarkdownBlock.HtmlBlock(content = literal.trimEnd('\n'))
}

private fun Node.contentsAsInlineMarkdown() =
InlineMarkdown(buildString { appendInlineMarkdownFrom(this@contentsAsInlineMarkdown) })
private fun Node.contentsAsInlineMarkdown() = buildList {
forEachChild {
add(it.toInlineNode())
}
}

private fun StringBuilder.appendInlineMarkdownFrom(
node: Node,
Expand Down Expand Up @@ -341,7 +327,5 @@ public class MarkdownProcessor(private val extensions: List<MarkdownProcessorExt
return "$backticks$this$backticks"
}

private fun InlineMarkdown.isBlank(): Boolean = content.isBlank()

private fun plainTextContents(node: Node): String = textContentRenderer.render(node)
}
Loading

0 comments on commit c04758d

Please sign in to comment.