Skip to content

Commit

Permalink
Add equals/hashcode to inline Markdown entities, too
Browse files Browse the repository at this point in the history
Tests have been updated to match the new behaviour, that's closer to the
CommonMark one. Now we don't carry unparsed inline Markdown anymore, but
rather we fully parse it in advance, so rendering it later is easier and
doesn't require running parsing in the UI anymore.

This is a tradeoff in memory usage for speed of rendering, and it makes
sense in the context of most Markdown text being static and only parsed
once, then kept on screen.

Inline extensions are now possible, but not tested. This makes #325
possible, since the extension now can be moved to its own module.

Note: please don't look too much at the test changes. They're _verbose_,
and are only there to ensure CommonMark specs compliance.
  • Loading branch information
rock3r committed Jul 19, 2024
1 parent 2b4d0c8 commit 1697dfe
Show file tree
Hide file tree
Showing 21 changed files with 4,044 additions and 2,713 deletions.
284 changes: 94 additions & 190 deletions markdown/core/api/core.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,96 +1,80 @@
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.SoftLineBreak as CMSoftLineBreak
import org.commonmark.node.StrongEmphasis as CMStrongEmphasis
import org.commonmark.node.Text as CMText
import org.jetbrains.jewel.foundation.GenerateDataFunctions

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

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

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

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

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

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

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

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

@JvmInline
public value class SoftLineBreak(override val nativeNode: CMSoftLineBreak) : InlineMarkdown
@GenerateDataFunctions
public class Code(override val content: String) : InlineMarkdown, WithTextContent

public interface CustomNode : InlineMarkdown {
/**
* If this custom node has a text-based representation, this function
* should return it. Otherwise, it should return null.
*/
public fun contentOrNull(): String? = null
}

@JvmInline
public value class StrongEmphasis(override val nativeNode: CMStrongEmphasis) : InlineMarkdown
@GenerateDataFunctions
public class Emphasis(
public val delimiter: String,
override val inlineContent: List<InlineMarkdown>,
) : InlineMarkdown, WithInlineMarkdown {
public constructor(
delimiter: String,
vararg inlineContent: InlineMarkdown,
) : this(delimiter, inlineContent.toList())
}

@JvmInline
public value class Text(override val nativeNode: CMText) : InlineMarkdown
public data object HardLineBreak : InlineMarkdown

@GenerateDataFunctions
public class HtmlInline(override val content: String) : InlineMarkdown, WithTextContent

@GenerateDataFunctions
public class Image(
public val source: String,
public val alt: String,
public val title: String?,
override val inlineContent: List<InlineMarkdown>,
) : InlineMarkdown, WithInlineMarkdown {
public constructor(
source: String,
alt: String,
title: String?,
vararg inlineContent: InlineMarkdown,
) : this(source, alt, title, inlineContent.toList())
}

public val children: Iterable<InlineMarkdown>
get() =
object : Iterable<InlineMarkdown> {
override fun iterator(): Iterator<InlineMarkdown> =
object : Iterator<InlineMarkdown> {
var current = this@InlineMarkdown.nativeNode.firstChild
@GenerateDataFunctions
public class Link(
public val destination: String,
public val title: String?,
override val inlineContent: List<InlineMarkdown>,
) : InlineMarkdown, WithInlineMarkdown {
public constructor(
destination: String,
title: String?,
vararg inlineContent: InlineMarkdown,
) : this(destination, title, inlineContent.toList())
}

override fun hasNext(): Boolean = current != null
public data object SoftLineBreak : InlineMarkdown

@GenerateDataFunctions
public class StrongEmphasis(
public val delimiter: String,
override val inlineContent: List<InlineMarkdown>,
) : InlineMarkdown, WithInlineMarkdown {
public constructor(
delimiter: String,
vararg inlineContent: InlineMarkdown,
) : this(delimiter, inlineContent.toList())
}

override fun next(): InlineMarkdown =
if (hasNext()) {
current.toInlineNode().also {
current = current.next
}
} else {
throw NoSuchElementException()
}
}
}
@GenerateDataFunctions
public class Text(override val content: String) : InlineMarkdown, WithTextContent
}

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

import org.commonmark.node.Block
import org.jetbrains.jewel.foundation.GenerateDataFunctions
import org.jetbrains.jewel.foundation.InternalJewelApi

public sealed interface MarkdownBlock {

@GenerateDataFunctions
public class BlockQuote(public val children: List<MarkdownBlock>) : MarkdownBlock
public class BlockQuote(public val children: List<MarkdownBlock>) : MarkdownBlock {
public constructor(vararg children: MarkdownBlock) : this(children.toList())
}

public sealed interface CodeBlock : MarkdownBlock {
public val content: String
Expand All @@ -28,7 +27,9 @@ public sealed interface MarkdownBlock {
public class Heading(
override val inlineContent: List<InlineMarkdown>,
public val level: Int,
) : MarkdownBlock, BlockWithInlineMarkdown
) : MarkdownBlock, WithInlineMarkdown {
public constructor(level: Int, vararg inlineContent: InlineMarkdown) : this(inlineContent.toList(), level)
}

@GenerateDataFunctions
public class HtmlBlock(public val content: String) : MarkdownBlock
Expand All @@ -43,47 +44,40 @@ public sealed interface MarkdownBlock {
override val isTight: Boolean,
public val startFrom: Int,
public val delimiter: String,
) : ListBlock
) : ListBlock {
public constructor(
isTight: Boolean,
startFrom: Int,
delimiter: String,
vararg children: ListItem,
) : this(children.toList(), isTight, startFrom, delimiter)
}

@GenerateDataFunctions
public class UnorderedList(
override val children: List<ListItem>,
override val isTight: Boolean,
public val marker: String,
) : ListBlock
) : ListBlock {
public constructor(
isTight: Boolean,
marker: String,
vararg children: ListItem,
) : this(children.toList(), isTight, marker)
}
}

@GenerateDataFunctions
public class ListItem(public val children: List<MarkdownBlock>) : MarkdownBlock
public class ListItem(public val children: List<MarkdownBlock>) : MarkdownBlock {
public constructor(vararg children: MarkdownBlock) : this(children.toList())
}

public data object ThematicBreak : MarkdownBlock

@GenerateDataFunctions
public class Paragraph(
override val inlineContent: List<InlineMarkdown>,
) : MarkdownBlock, BlockWithInlineMarkdown
}

public interface BlockWithInlineMarkdown {
public val inlineContent: Iterable<InlineMarkdown>
}

@InternalJewelApi
public fun Block.readInlineContent(): Iterable<InlineMarkdown> =
object : Iterable<InlineMarkdown> {
override fun iterator(): Iterator<InlineMarkdown> =
object : Iterator<InlineMarkdown> {
var current = this@readInlineContent.firstChild

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

override fun next(): InlineMarkdown =
if (hasNext()) {
current.toInlineNode().also {
current = current.next
}
} else {
throw NoSuchElementException()
}
}
) : MarkdownBlock, WithInlineMarkdown {
public constructor(vararg inlineContent: InlineMarkdown) : this(inlineContent.toList())
}
}
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: List<InlineMarkdown>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.jetbrains.jewel.markdown

public interface WithTextContent {
public val content: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.jetbrains.jewel.markdown.extensions

import org.commonmark.node.CustomNode
import org.jetbrains.jewel.markdown.InlineMarkdown
import org.jetbrains.jewel.markdown.processing.MarkdownProcessor

public interface MarkdownInlineProcessorExtension {
/**
* Returns true if the [node] can be processed by this extension instance.
*
* @param node The [CustomNode] to parse
*/
public fun canProcess(node: CustomNode): Boolean

/**
* Processes the [node] as a [InlineMarkdown.CustomNode], if possible. Note
* that you should always check that [canProcess] returns true for the same
* [node], as implementations might throw an exception for unsupported node
* types.
*/
public fun processInlineMarkdown(
node: CustomNode,
processor: MarkdownProcessor,
): InlineMarkdown.CustomNode?
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,58 @@
package org.jetbrains.jewel.markdown.extensions

import org.commonmark.node.CustomBlock
import org.commonmark.parser.Parser.ParserExtension
import org.commonmark.renderer.text.TextContentRenderer.TextContentRendererExtension
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.markdown.MarkdownBlock

/** An extension for the Jewel Markdown processing engine. */
@ExperimentalJewelApi
public interface MarkdownProcessorExtension {
/**
* A CommonMark [ParserExtension] that will be used to parse the extended
* syntax represented by this extension instance. Null in the case where
* parsing is already handled by an existing [org.commonmark.parser.Parser].
* syntax represented by this extension instance.
*
* Can be null if all required processing is already handled by an existing
* [org.commonmark.parser.Parser].
*/
public val parserExtension: ParserExtension?
get() = null

/**
* A CommonMark [TextContentRendererExtension] that will be used to render
* the text content of the CommonMark [CustomBlock] produced by the
* [parserExtension]. Null in the case where rendering is already
* handled by an existing [org.commonmark.renderer.Renderer].
* the text content of the CommonMark [org.commonmark.node.CustomBlock]
* produced by the [parserExtension].
*
* Can be null if all required processing is already handled by an existing
* [org.commonmark.renderer.Renderer].
*/
public val textRendererExtension: TextContentRendererExtension?
get() = null

/**
* An extension for
* [`MarkdownParser`][org.jetbrains.jewel.markdown.parsing.MarkdownParser]
* that will transform a supported [CustomBlock] into the corresponding
* [MarkdownBlock.CustomBlock]. Null in the case where processing
* is already be handled by [org.jetbrains.jewel.markdown.processing.MarkdownProcessor]
* or another [org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension].
* [`MarkdownProcessor`][org.jetbrains.jewel.markdown.processing.MarkdownProcessor]
* that will transform a supported [org.commonmark.node.CustomBlock] into
* the corresponding
* [org.jetbrains.jewel.markdown.MarkdownBlock.CustomBlock].
*
* Can be null if all required processing is already handled by
* [org.jetbrains.jewel.markdown.processing.MarkdownProcessor] or another
* [org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension].
*/
public val processorExtension: MarkdownBlockProcessorExtension?
public val blockProcessorExtension: MarkdownBlockProcessorExtension?
get() = null

/**
* An extension for
* [`MarkdownProcessor`][org.jetbrains.jewel.markdown.processing.MarkdownProcessor]
* that will transform a supported [org.commonmark.node.CustomNode] into
* the corresponding
* [org.jetbrains.jewel.markdown.InlineMarkdown.CustomNode].
*
* Can be null if all required processing is already handled by
* [org.jetbrains.jewel.markdown.processing.MarkdownProcessor] or another
* [org.jetbrains.jewel.markdown.extensions.MarkdownProcessorExtension].
*/
public val inlineProcessorExtension: MarkdownInlineProcessorExtension?
get() = null
}
Loading

0 comments on commit 1697dfe

Please sign in to comment.