As I was thinking of a way to start writing my first blog post, I weighed in on some of the options for integrating a WYSIWYG editor in order to easily create and edit rich content. I used a few different ones on previous projects, but lately I'm leaning heavily towards markdown. Considering I'm using administrate, there's a great gem to add markdown field support for it. The setup is very easy so I won't cover it here.

What I want to achieve:
1. Write my posts in markdown
2. Save that to the database and transform it to HTML
3. Give the HTML content to the React component as a prop
4. Render the page

Here's how React would render the final HTML:

import React from 'react';

const PostBody = ({ content }) => (
  <div className="post-body" dangerouslySetInnerHTML={{ __html: content }} />
);

It's the backends job to provide the properly formatted HTML to the frontend and for that I'll be using Redcarpet

# models/post.rb

class Post < ApplicationRecord
  before_save :convert_markdown_to_html, if: -> { md_content_changed? }

  def convert_markdown_to_html
    self.html_content = MarkdownParser.render(md_content)
  end
end

And the parser would look something like this:

# services/markdown_parser.rb

require 'redcarpet'

class MarkdownParser
  OPTIONS = {
    filter_html:     true,
    hard_wrap:       true,
    link_attributes: { rel: 'nofollow', target: "_blank" },
    space_after_headers: true,
  }

  EXTENSIONS = {
    autolink:           true,
    superscript:        true,
    disable_indented_code_blocks: true,
    fenced_code_blocks: true
  }

  def self.render(markdown = '')
    parser.render(markdown)
  end

  def self.parser
    renderer = HTML.new(OPTIONS)
    Redcarpet::Markdown.new(renderer, EXTENSIONS)
  end
end

class HTML < Redcarpet::Render::HTML
  # we'll get back to this part later
end

The problem

So far so good! But how do we get these nice looking code samples from above?

One of the options is Rouge which is pretty sweet and comes with a redcarpet integration, but I didn't like the themes so I decided to try out PrismJS

After a lot of effort, I wasn't able to get Prism working on the dynamic content React renders, and thus the reason for this blog post.

If you've already checked out React Prism examples you'll notice that you need to escape your code with template string literals (``). However, that's impossible to do when the content is coming from some kind of CMS and the code blocks could be anywhere in the final html.

The solution

We have to use Prism on the server so it tokenizes the code samples before they reach the frontend. But that's a bit tricky (read hacky) to do when we're using Ruby on the server instead of Node.

Hard, but not impossible. Rails already comes with ExecJS and all we need is a supported runtime like Node.js and we're good to go.

But first we need to get some control over how code blocks are parsed with redcarpet, and we do that by overriding the code_block method of Redcarpet's HTML renderer - the part of the MarkdownParser mentioned above.

# services/markdown_parser.rb
# ...

class HTML < Redcarpet::Render::HTML
  def block_code(code, language)
    Prism.new(code, language).highlight
  end
end

block_code accepts the code that's between triple backticks we use in markdown to format a code block and the language that we can add exactly after the opening backticks, eg.: ```ruby
We'll pass those along to the Prism service which is responsible for doing the syntax highlighting.

ExecJS is not capable of requiring commonjs modules, so we'll have to leave the land of ES6 imports and make do with what we have.

Since we can't import PrismJS directly, we need to download the javascript straight from the website. It's not actually that bad, since you can choose the languages you want to be usable by default. I saved the source in app/assets/javascripts/prism.js. Hello asset pipeline.. oh how webpacker has spoiled me.

# services/prism.rb

class Prism
  attr_accessor :code, :language

  def initialize(code, language)
    @code = code
    @language = language
  end

  def highlight
    highlighted = prism.eval("Prism.highlight(code, Prism.languages.#{language})")

    "<pre class=\"language-#{language}\"><code class=\"language-#{language}\">#{highlighted}</code></pre>".html_safe
  end

  def prism
    @prism ||= ExecJS.compile(prism_source)
  end

  def prism_source
    @prism_source ||= Rails.cache.fetch('prism', expires_in: 30.days) do
      File.read(Rails.root.join('app/assets/javascripts/prism.js'))
    end
  end
end

We'll cache the results of prism_source so we don't read the file on every call. Calling theprism intance method, we compile the source and store the javascript context in which we can run/evaluate expressions. That's what we do inside the highlight function, where we highlight the code passed from the MarkdownParser for the specified language. We then wrap that code in pre and code tags and call html_safe to get the final result.

The solution is by no means ideal, but clearly working as you can see from this blog post.