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.