Markdown Preview Swing Component

Answered

## What I am trying to create

A method I can call like `createMarkdownPreview(String markdownText)` which returns a `JComponent` so I can use it in a UI. I want to use IntelliJ's (and I want my plugin to be compatible with PyCharm) built-in markdown preview (without the 3 options, see below)

 

## Why

To display read-only documentation with code examples. I want to be able to format markdown automatically with little work.

## What I have found

https://github.com/JetBrains/intellij-community/blob/4361513598b1cfe7bcbf2a18d46ae4955dcdd96d/plugins/markdown/core/src/org/intellij/plugins/markdown/ui/preview/MarkdownPreviewFileEditor.java

That looks like what I want, but I need to know the dependency I need to get that to work. 

0
10 comments
Official comment

Hi Colin,

MarkdownPreviewFileEditor is not intented to be used as a standalone editor (it won't work without main editor).

You can use MarkdownJCEFHtmlPanel instead. You can use this code to create it:

fun createPreviewComponent(
  project: Project,
  @Language("Markdown")
  content: String,
  parentDisposable: Disposable
): JComponent {
  val file = LightVirtualFile("content.md", content)
  val panel = MarkdownJCEFHtmlPanel(project, file)
  Disposer.register(parentDisposable, panel)
  val html = runReadAction {
    MarkdownUtil.generateMarkdownHtml(file, content, project)
  }
  panel.setHtml(html, 0)
  return panel.component
}

Please note, that it is essential to dispose MarkdownJCEFHtmlPanel instance after use, otherwise it will create a substantial memory leak.

Ivan Posti Thanks for the reply, and the code sample. I had a few questions as well

* Is it acceptable to have multiple `MarkdownJCEFHtmlPanel` component, given the JCEF constraints and memory impacts?

* Can it be used as renderer (e.g. in a `JTable`) ?

0

Hi Brice,

It is OK to have multiple MarkdownJCEFHtmlPanel, however, I would not recommend to have a lot of them. You can think of a single panel instance as a tab in a browser. You would need to test your scenario and see if the memory usage is acceptable for you.

In theory, it can be used as a renderer. I would not recomend using it for this purpose, since renderers are expected to be relatively fast and stateless. If you really want to use it as a renderer I suggest to try using a single panel instance for all renderers.

 

1

Is there a way to add buttons (like the bottom 3 buttons?) 

Is there a way to add more icon buttons in (top right)?

My first instinct is to just add JBButtons under the panel, but this solution requires that I instantiate a bunch of markdown panels (in my case) which was previously suggested to be a bad idea. Any suggestions?

0

@IvanPosti

> You can think of a single panel instance as a tab in a browser.

Ok thank you for the metaphor, that convey well how the jcef panel works.

> I suggest to try using a single panel instance for all renderers.

I understood that as single instance renderer, I'd this what you meant.

Do you think that OSR would consumes less resources for the rendering propose?

I and my team are still undecided on jcef due to the constraint you mention.

By the way thank you for the feedback.

0

Collin Barber, The copy button is added internaly by the code processing code fences. Unfortunatelly, you won't be able to customize it's behaviour. If you are using this component in your custom view, I would suggest implementing your own version on MarkdownJCEFHtmlPanel, since the default one is quite limited in customizations. You can generate your Markdown HTML with the org.intellij.plugins.markdown.ui.preview.html.MarkdownUtil#generateMarkdownHtml, and then process it with Jsoup adding the elements you need.

If you want to customize the default Markdown view in IDE, you can substitute the default implementation with our own using the MarkdownHTMLPanelProvider. However, this API proved to be unstable and will be revised in the future.

2

Brice, Yes, it should be slightly ligther on resources than the non-OSR. Moreover, it is the prefered rendering mode, because it has much better integration with Swing (using non-OSR JCEF components will likely cause numerous problems, such as broken focus transfer, input, notifications...).

If you need to render Markdown in a few places exactly as the IDE does, you should use the MarkdownJCEFHtmlPanel.

If you want to show a lots of static Markdown fragments and don't need them to be interactive (e.g. no copy button for fences), you can use JTextPane (you can find the CSS styles used for the preview in org.intellij.plugins.markdown.ui.preview.PreviewLAFThemeStyles and in org/intellij/plugins/markdown/extensions/common/baseStyles/default.css.

1

Thanks for the tip! Using html/css has helped me solve a bunch of problems! I am having trouble listening for button interactions in my kotlin code though. ChatGPT has given me probably 10 different incorrect answers.

How can I execute a kotlin method when a user presses an html button?

0

@collin I never tried with the markdown preview jcef panel. But this is jcef integration stuff.

The code needs to register handlers, the code can register handler on the jcef client, depending on your strategy it might be:
* a request handler `CefRequestHandlerAdapter` it should intercept when link are clicked
* a _message_ handler `CefMessageRouterHandler`, those messages are sent via Javascript in the web page.

There's another one but I forgot which one.

Note the code should be very strict at disposing these handlers when the jcef panel is closed.

0

Collin Barber Please see the JBCefJSQuery:

  1. You create the instance of JBCefJSQuery
  2. Bind the query listener on the IDE side
  3. Load your page and inject the query into the page using executeJavaScript (JBCefJSQuery#inject)
  4. Bind your button listeners to invoke the function provided by the injected query (it looks a bit convoluted, but is actually quite easy to implement)

Tip: You can use a single listener for multiple events.

2

Please sign in to leave a comment.