Previewing Jekyll posts with Marked

If you use both Marked and Jekyll (or Octopress), you may want to preview your posts accurately in Marked before publishing. The first obvious step is to link your screen.css file as a Custom Style:

Marked Style preferences screenshot
Marked Style preferences

Depending on your theme, you may need to copy the CSS1 and edit a few things, including any custom @font-face fonts used. I find the easiest way to handle fonts in Custom Styles is to base64 encode them and embed them directly in the CSS. The following bash function — added to your .bash_profile and run on the command line with a .woff file as the argument, will put the correct code for the embedded font into your clipboard:

function 64font() {
	openssl base64 -in $1 | awk -v ext="${1#*.}" '{ str1=str1 $0 }END{ print "src:url(\"data:font/"ext";base64,"str1"\")  format(\"woff\");" }'|pbcopy
	echo "$1 encoded as font and copied to clipboard"
}

You’ll also want to replace any rules in your CSS that specifically reference containers that won’t exist in Marked. For example, my layout uses #main .post_body to specify styles for the main post. You can remove these with a search and replace, or use #wrapper instead. While not required, you can also remove any layout-related CSS that doesn’t apply to the main post body.

Next, turn on Strip YAML Headers under Behavior preferences to remove all of the post data at the top of the post for clean rendering.

You’ll probably want to set up Kramdown or Maruku as your custom processor, but it won’t convert any Liquid tags for you by default. Octopress comes with an image_tag plugin that you can use with Jekyll (or just use Octopress), as well as a gist_tag plugin, among others. The sample Custom Processor below can be added to Marked to convert those tags and then handle the Markdown conversion.

To add the custom processor to Marked, save the script below to your local drive, open Behavior preferences, check the Custom Processor box, add the full path to the script and hit Save in the Custom Processor box. Open a Jekyll post in Marked. If all goes well, you should be seeing a preview with img and gist tags replaced with HTML img tags and Gist embed script tags that will render in Marked.

Marked behavior preferences screenshot
Marked behavior preferences

The script — by default — requires that Kramdown be installed in /usr/bin/kramdown. You can do this with sudo gem install kramdown. If you’re using RVM, you might need to use sudo rvm do gem install kramdown. You can always substitute Maruku, MultiMarkdown or any other processor in the last line of the script2.

Note that the script may take some customization. It adds “..” before the substituted image path, which makes the assumption that images exist in your source folder at a level equal to your _drafts or _posts folder. Modify as needed.

You may also want to add/adjust style substitutions. Right now if it detects an alignleft or alignright class in an img tag, it adds the styles I personally use for those classes to the output <img> tag.

Here’s the script, provided as an example for your own exploration of Marked custom scripts.

jekyllpre.rbraw
#!/usr/bin/ruby
# encoding: utf-8
# Version 1
# 
# Example custom processor for use with Marked <http://markedapp.com> and Jekyll _posts
# It's geared toward my personal set of plugins and tags, but you'll get the idea.
#   It turns
# {% img alignright /images/heythere.jpg 100 100 "Hey there" "hi" %}
#   into
# <img src="../images/heythere.jpg" alt="Hey there" class="alignright" title="hi" />
#
# replaces alignleft and alignright classes with appropriate style attribute
# ---
# Replaces {% gist XXXXX filename.rb %} with appropriate script tag
#
# Replace various other OctoPress, Jekyll and custom tags
#
# Processes final output with /usr/bin/kramdown (install kramdown as system gem: `sudo gem install kramdown`)
#
# Be sure to run *without* stripping YAML headers in Marked Behavior preferences.

# full path to image folder
full_image_path = "/Users/ttscoff/Sites/dev/bt/source/"

require "rubygems"
require "rubypants"
require "shellwords"

def class_exists?(class_name)
  klass = Module.const_get(class_name)
  return klass.is_a?(Class)
rescue NameError
  return false
end

if class_exists? 'Encoding'
  Encoding.default_external = Encoding::UTF_8 if Encoding.respond_to?('default_external')
  Encoding.default_internal = Encoding::UTF_8 if Encoding.respond_to?('default_internal')
end

begin
  content = STDIN.read.force_encoding('utf-8')
rescue
  content = STDIN.read
end

parts = content.split(/^---\s*$/)

# Read YAML headers as needed before cutting them out
post_title = parts[1].match(/^title:\s+(\!\s*)?["']?(.*?)["']?\s*$/i)[2].strip

# Remove YAML
# content.sub!(/^---.*?---\s*$/m,'')
content = parts[2]

# Fenced code
content.gsub!(/^```(\w+)?\s*\n(.*?)\n```/m,"<pre><code class=\"\\1\">\\2</code></pre>")

# Update image urls to point to absolute file path
content.gsub!(/([\("])\/uploads\/(\d+\/.*?)([ \)"])/,"\\1#{full_image_path}\\2\\3")

# Process image Liquid tags
content.gsub!(/\{% img (.*?) %\}/) {|img|
  if img =~ /\{% img (\S.*\s+)?(https?:\/\/\S+|\/\S+|\S+\/\s+)(\s+\d+\s+\d+)?(\s+.+)? %\}/i
    classes = $1.strip if $1
    src = $2
    size = $3
    title = $4

    if /(?:"|')([^"']+)?(?:"|')\s+(?:"|')([^"']+)?(?:"|')/ =~ title
      title  = $1
      alt    = $2
    else
      alt    = title.gsub!(/"/, '&#34;') if title
    end
    classes.gsub!(/"/, '') if classes
  end

  style = %Q{ style="float:right;margin:0 0 10px 10px"} if classes =~ /alignright/
  style = %Q{ style="float:left;margin:0 10px 10px 0"} if classes =~ /alignleft/

  %Q{<img src="#{File.join(full_image_path,src)}" alt="#{alt}" class="#{classes}" title="#{title}"#{style} />}
}

# Process gist tags
content.gsub!(/\{% gist(.*?) %\}/) {|gist|
    if parts = gist.match(/\{% gist ([\d]*) (.*?)?%\}/)
      gist_id = parts[1].strip
      file = parts[2].nil? ? '' : "?file-#{parts[2].strip}"
      %Q{<script src="https://gist.github.com/#{gist_id}.js#{file}"></script>}
    else
      ""
    end
}

# Replace YouTube tags with a placeholder block
# <http://brettterpstra.com/2013/01/20/jekyll-tag-plugin-for-responsive-youtube-video-embeds/>
content.gsub!(/\{% youtube (\S+) ((\d+) (\d+) )?%\}/) {|vid|
  id = $1
  width, height = $2.nil? ? [640, 480] : [$3, $4] # width:#{width}px;height:#{height}px;
  style = "position:relative;padding-bottom:56.25%;padding-top:30px;height:0;overflow:hidden;background:#666;"
  %Q{<div class="bt-video-container" style="#{style}"><h3 style="text-align:center;margin-top:25%;">YouTube Video</h3></div>}
}

# HTML5 semantic pullquote plugin
content.gsub!(/\{% pullquote(.*?) %\}(.*)\{% endpullquote %\}/m) {|q|
  quoteblock = $2
  if quoteblock =~ /\{"\s*(.+?)\s*"\}/m
    quote = RubyPants.new($1).to_html
    "<span class='pullquote' data-pullquote='#{quote}'>#{quoteblock.gsub(/\{"\s*|\s*"\}/, '')}</span>"
  else
    quoteblock
  end
}

# Custom downloads manager plugin shiv
content.gsub!(/\{% download(.*?) %\}/,%Q{<div class="download"><h4>A download</h4><p class="dl_icon"><a href="#"><img src="/Volumes/Raptor/Users/ttscoff/Sites/dev/octopress/source/images/serviceicon.jpg"></a></p><div class="dl_body"><p class="dl_link"><a href="#">Download this download</a></p><p class="dl_description">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p><p class="dl_updated">Updated Today</p><p class="dl_info"><a href="#" title="More information on this download">More info&hellip;</a></p></div></div>})

content = "## #{post_title}\n\n#{content}"

puts %x{echo #{Shellwords.escape(content)}|/usr/bin/kramdown}

Marked makes a great tool for Jekyll/Octopress bloggers. You can find more information on it at markedapp.com.

  1. If you normally compress your CSS, render it out without compression to ease editing. With compass you can use the -s nested argument for nice results.

  2. The next version of Marked will simplify this process, allowing you to use this script as a custom preprocessor, and then set up a separate custom processor or use the default renderer. Still bugfixing, though.

Brett Terpstra

Brett is a writer and developer living in Minnesota, USA. You can follow him as ttscoff on Twitter, GitHub, and Mastodon. Keep up with this blog by subscribing in your favorite news reader.

This content is supported by readers like you.