platform/jewel/markdown/README.md
[!IMPORTANT] The Jewel Markdown renderer is currently considered experimental. Its API and implementations may change at any time, and no guarantees are made for binary and source compatibility. It might also have bugs and missing features.
Adds the ability to render Markdown as native Compose UI.
Currently supports the CommonMark 0.31.2 specs.
Additional supported Markdown, via extensions:
extension-gfm-alertsextension-autolinkextension-gfm-tablesOn the roadmap, but not currently supported — in no particular order:
Not supported, and not on the roadmap:
The Jewel Markdown renderer is designed to be run in a project that already has a jewel-int-ui-standalone-* or
jewel-ide-laf-bridge-* dependency. The core module doesn't contain any styling, and you're supposed to use either
the jewel-markdown-int-ui-standalone-styling-* or jewel-markdown-ide-laf-bridge-styling-* instead. They will carry
the necessary dependencies. Make sure to use an artifact with the matching IJP version to what you already have in your
project.
[!CAUTION] Don't use the standalone artifact in an IDE plugin, and don't use the bridge artifact in a standalone project!
If you want to use extensions, you also need to add them alongside the jewel-markdown-core:
dependencies {
implementation(libs.jewel.standalone)
implementation(libs.jewel.markdown.intUiStandaloneStyling)
implementation(libs.jewel.markdown.extensions.gfm.alerts) // Optional
// Et cetera...
}
The process that leads to rendering Markdown in a native UI is two-pass.
The first pass is an upfront rendering that pre-processes blocks into MarkdownBlocks, that contain nested blocks and/
or InlineMarkdown. It's recommended to run this outside of the composition, since it has no dependencies on it, and
it can be slow, depending on the amount of Markdown to process.
// Somewhere outside of composition — e.g., in a viewmodel or service
val processor = MarkdownProcessor()
val rawMarkdown = "..."
val markdownBlocks: List<MarkdownBlock> = processor.processMarkdownDocument(rawMarkdown)
Once you have your list of MarkdownBlocks, you can do the second step in the composition: render a series of
MarkdownBlocks into native Jewel UI. The easiest way is by using
the Markdown composable:
// We recommend having the Provide call at the theme level
ProvideMarkdownStyling {
Markdown(markdownBlocks)
}
For your convenience, Jewel also provides a variant of the Markdown composable that accepts a string instead of the
list of blocks; this is an easier-to-use API, but comes with the compromise of running the processing in the composition
and is as such only advisable for small, never-changing runs of Markdown.
For large Markdown documents, you should use the LazyMarkdown composable, which is backed by a LazyColumn instead of
a regular Column. This should provide better performances, but it is not recommended for small documents, as its
overhead is not justifiable.
If you're using the Markdown functionality to generate a real-time preview of some raw Markdown in an editor, we
recommend enabling the editorMode in the MarkdownProcessor, and using a LazyMarkdown composable.
A MarkdownProcessor in editor mode is optimized for small, incremental edits, such as the ones that happen when the
user is typing. The processor, in this mode, tries to figure out which block(s) have been modified, and only recomputes
those.
[!CAUTION] Never share an instance of
MarkdownProcessorin editor mode across multiple places! Editor mode is a stateful mode that caches its computations, and using it in multiple places will bust the cache and leave you with a worse performance.
By default, the processor will ignore any kind of Markdown it doesn't support. To support additional features, such as
ones found in GitHub Flavored Markdown, you can use extensions. If you don't specify any extension, the processor will
be restricted to the CommonMark specs as supported by
commonmark-java.
[!NOTE] Images are not supported yet, even if they are part of the CommonMark specs. See https://github.com/JetBrains/Jewel/issues/472 for status updates.
Extensions are composed of two parts: a parsing and a rendering part. The two parts need to be passed to the
MarkdownProcessor and MarkdownBlockRenderer, respectively. For example, in a standalone project:
// Where the parsing happens...
val parsingExtensions: List<MarkdownProcessorExtension> = listOf(/*...*/)
val processor = MarkdownProcessor(parsingExtensions)
// Where the rendering happens...
val blockRenderer = remember(markdownStyling, isDark) {
val rendererExtensions = listOf<MarkdownRendererExtension>(/*...*/)
if (isDark) {
MarkdownBlockRenderer.dark(rendererExtensions)
} else {
MarkdownBlockRenderer.light(rendererExtensions)
}
}
It is strongly recommended to use the corresponding set of rendering extensions as the ones used for parsing, otherwise the custom blocks will be parsed but not rendered.
Note that you should create InlineMarkdownRenderers with the same list of extensions that was used to build the
block renderer. The MarkdownBlockRenderer factory functions take care of this for you, but if you want to provide your
own inline renderer, this is something to be careful about.
You can see this in action running the Standalone sample, and selecting Markdown from the top-left menu.
The following image shows the Jewel Markdown renderer displaying the Jewel readme.