How do I implement Rouge syntax highlighting in Rails?
Asked Answered
S

1

15

There are a bunch of tutorials floating around, but they seem to be incomplete or not fully current or don't fully work for me.

This is what I have done.

Gemfile:

gem 'rouge'
gem 'redcarpet'

Then I created a config/initializer/rouge.rb:

require 'rouge/plugins/redcarpet'

Then I created a file called app/assets/stylesheets/rouge.css.erb

<%= Rouge::Themes::Github.render(:scope => '.highlight') %>

Then in my app/helpers/application_helper.rb, I added this:

module ApplicationHelper
  class HTML < Redcarpet::Render::HTML
    include Rouge::Plugins::Redcarpet

    def block_code(code, language)
      Rouge.highlight(code, language || 'text', 'html')
    end
  end

  def markdown(text)
    render_options = {
      filter_html: true,
      hard_wrap: true,
      link_attributes: { rel: 'nofollow' }
    }
    renderer = HTML.new(render_options)

    extensions = {
      autolink: true,
      fenced_code_blocks: true,
      lax_spacing: true,
      no_intra_emphasis: true,
      strikethrough: true,
      superscript: true
    }
    Redcarpet::Markdown.new(renderer, extensions).render(text).html_safe
  end
end

Then in my show.html.erb, I did this:

<%= markdown(@question.body) %>

But that literally does not work. It outputs my ruby code snippet like this:

unformatted-ruby-snippet

How do I get this snippet of code to be formatted like Github? Or even just the first step being to be formatted any at all, then how do I tweak the formatting?

I don't see a stylesheet included in the source of the page, so I don't know which styles to tweak for what I want.

Edit 1

Or even when I do this:

            <div class="highlight">
              <%= @question.test_suite %>
            </div>

It renders like this:

another-ruby-snippet

Edit 2

I attempted BoraMa's suggestion and I got output that looks like this:

enter image description here

Edit 3

I made a modification to BoraMa's answer as follows.

In my block_code method, I call highlight as follows:

Rouge.highlight(code, 'ruby', 'html')

Then in my view I do this:

<%= raw rouge_markdown(<<-'EOF'
                def rouge_me
                  puts "this is a #{'test'} for rouge"
                end
                EOF
                ) %>

Then that produces this:

enter image description here

Note I am referring to the code snippet at the bottom of the screenshot.

However, the text at the top is generated with this:

          <pre class="highlight ruby">
            <%= rouge_markdown(@question.body) %>
          </pre>

And it is rendered as is shown in the screenshot.

Edit 4

After removing the <div class="highlight">, I see this:

enter image description here

Aka....nothing is being rendered at all.

Once I add raw to my view...aka <%= raw rouge_markdown(@question.body) %>

The view renders this:

enter image description here

Edit 5

Here is the content for various @question objects:

[1] pry(#<#<Class:0x007fc041b97ce8>>)> @question.body
=> "5.times do\r\n   puts \"Herro Rerl!\"\r\nend"
[1] pry(#<#<Class:0x007fc041b97ce8>>)> @question.body
=> "puts \"Hello World version 9\"\r\nputs \"This comes after version 8.\"\r\nputs \"This comes after version 7.\"\r\nputs \"This comes after version 6.\"\r\nputs \"This comes after version 5.\"\r\nputs \"This comes after version 4.\"\r\nputs \"This comes after version 3.\"\r\nputs \"This comes after version 2.\"\r\nputs \"This definitely comes after version 1.\""

[1] pry(#<#<Class:0x007fc041b97ce8>>)> @question.body
=> "def convert_relation(invited_gender, relation)\r\n case invited_gender\r\n \twhen \"male\"\r\n  \tcase relation\r\n      when \"daughter\", \"son\" then \"dad\"\r\n      when \"mom\", \"dad\" then \"son\"\r\n      when \"grandfather\", \"grandmother\" then \"grandson\"\r\n      when \"sister\", \"brother\" then \"brother\"\r\n      when \"wife\" then \"husband\"\r\n      when \"husband\" then \"husband\"\r\n    end\r\n  when \"female\"\r\n  \tcase relation\r\n      when \"daughter\", \"son\" then \"mom\"\r\n      when \"mom\", \"dad\" then \"daughter\"\r\n      when \"grandfather\", \"grandmother\" then \"granddaughter\"\r\n      when \"sister\", \"brother\" then \"sister\"\r\n      when \"wife\" then \"wife\"\r\n      when \"husband\" then \"wife\"\r\n    end\r\n  end\r\nend\r\n\r\nputs convert_relation(\"male\", \"wife\")"
Sammie answered 7/6, 2016 at 0:14 Comment(0)
C
14

The original question indicated (in the solution attempted) that markdown would be used in the highlighted questions but it turned out not to be the case. So this answer is split to two distinct sections, one for highlighting pure code without markdown, the other one for markdown text with code.

A) You want to highlight pure code (no Markdown involved)

In this case, and according to the README, all you need to highlight the code with Rouge is a lexer and a formatter. Since the highlighted text will be displayed on a web page, you need the HTML formatter. For the lexer, you need to know the language the code is in beforehand (or you may try guessing it from the source code itself but it does not seem to be very reliable for small code snippets).

You can create a simple helper method for the highlighting:

module RougeHelper
  def rouge(text, language)
    formatter = Rouge::Formatters::HTML.new(css_class: 'highlight')
    lexer = Rouge::Lexer.find(language)
    formatter.format(lexer.lex(text))
  end
end

Then in the template, simply call this helper with a text to highlight and the language:

<%= raw rouge("def rouge_me\n  puts 'hey!'\nend", "ruby") %>

Which will render:

To get a list of all languages that Rouge supports and their corresponding names that should be passed to the rouge helper, you can use the following code. The code gets all defined lexers from Rouge and shows their tags (i.e. the names Rouge recognizes them with):

Rouge::Lexer.all.map(&:tag).sort
# => ["actionscript", "apache", "apiblueprint", "applescript", ..., "xml", "yaml"]

You can (and probably should) use this list when showing users the languages to choose from in the selection box. Note that each lexer also has the title and desc methods defined that will give you a human-readable name and a short description of each of them. You might want to use this info to be shown to the user, too.

Note: you should get rid of the initializer, the custom HTML class and the div wrapped around the rouge helper call (all of these you have in your original attempt). The only thing you need besides the code above is the CSS rules, which you have already correctly included in the web page.

B) The highlighted text is a Markdown text with code blocks

A couple of changes from your attempt to make it work:

  1. The initializer is not needed, you can remove it I think (but if you don't want to require all the files later in the helper, I guess you can leave it).

  2. Remove the block_code method from the helper class, the same is already done by including the markdown plugin.

  3. Remove the <div class="highlight"> wrapper div from your template and just use the helper in it. Rouge adds its own wrapper with the "highlight" class and another div seems to confuse it.

Try the following helper code. BTW, I moved the code from ApplicationHelper to a separate RougeHelper (but that is not a required change):

module RougeHelper
  require 'redcarpet'
  require 'rouge'
  require 'rouge/plugins/redcarpet'

  class HTML < Redcarpet::Render::HTML
    include Rouge::Plugins::Redcarpet
  end

  def rouge_markdown(text)
    render_options = {
        filter_html: true,
        hard_wrap: true,
        link_attributes: { rel: 'nofollow' }
    }
    renderer = HTML.new(render_options)

    extensions = {
        autolink: true,
        fenced_code_blocks: true,
        lax_spacing: true,
        no_intra_emphasis: true,
        strikethrough: true,
        superscript: true
    }
    markdown = Redcarpet::Markdown.new(renderer, extensions)
    markdown.render(text)
  end
end

Then, in the template, I tried to highlight a test ruby code:

<%= raw rouge_markdown(<<-'EOF'
```ruby
def rouge_me
  puts "this is a #{'test'} for rouge"
end
```
EOF
) %>

Note that I needed to specify the language manually, which made me use the 3 backticks way to delimit the code instead of space indentation. I have no clue why the code language autodetection did not work here, perhaps it's a too short code.

In the end this rendered the colors for me nicely:

Chaetopod answered 9/6, 2016 at 9:9 Comment(20)
I tried what you suggested but it didn't work for me. Please see the question updated with the results of my experiment.Sammie
I added another update to the question based on some more things I have tried in the vein of your answer.Sammie
I noticed one more important difference that made the code not work for you. You wrapped the rouge helper in a div with the highlight class. You don't need to do that, rouge itself wraps the code in a pre with this class and the div seems to confuse it. I updated the answer (see point 3.)Barbie
I have updated the question with more details about what your suggestions have resulted in. Refresh plz.Sammie
I don't know why your Edit 4 behaves like that but I think I know why Edit 2 did not work. Please make sure you have no indentation when you test the code with <<-EOF because otherwise markdown will interpret this as verbatim code. If that still won't work, please add versions of the gems, mine are: redcarpet (3.3.4) and rouge (1.11.0).Barbie
Ok...so when I get rid of the indentations, the version with backticks renders like the screenshot I have in 'Edit 3'. However, that doesn't quite help me when it comes to displaying the value I have stored in @question.body. That is still outputting as text or something else. I am using redcarpet 3.3.4 & rouge 1.11.0. How can I get my @question.body to display highlighted?Sammie
Can you paste what exactly is in the @question.body when you test it?Barbie
I have pasted it above. Look at the screenshots. It is different things depending on the value of @question. The first instance is in the original question (before any of the edits). There the output is a method called def convert_relation(params1, params2). In another instance, I also have @question.test_suite for which the values are printed above too in Edit 1.Sammie
I have seen those screenshots but can you paste the raw textual content so that I can test it too?Barbie
Refresh question, I added the content for 3 different @question.body objects.Sammie
I must say I am very confused now, none of those question bodies are markdown! Then of course it won't work. Sorry, but is simply highlighting code snippets what you actually want (i.e. with no markdown involved)? If so, the answer would be quite different (you'd need no redcarpet plugin and no custom HTML class). If the questions were meant to be markdown, the code snippets could be wrapped by the 3 backticks and the highlighting, as posted above in the answer, would work correctly.Barbie
Well I never said that it was markdown in the first place. That's why I put the raw data there so there is no ambiguity. So I guess my fundamental question is, the user will store that text (as raw text) in that column. How do I then take that raw text and format it in the style they specified in my @question.language field? Would I have to manually wrap the @question.body form input with backticks and the language name on input so that it shows up with those backticks when passed to Rouge? Or is there something else I can do with Rouge that doesn't require that?Sammie
I just want to get syntax highlight working, from some raw text they have passed in to a form field and an option they have chosen from a different form field.Sammie
Originally, you put only screenshots of the wrongly rendered text which still leave ambiguity as of whether the original raw text is markdown or not. And the code that you originally attempted very clearly references markdown input. Never mind, I should have asked explicitly to clarify things. Now, all the failed attempts to solve your issue make sense. Please see the updated answer, I added a new section for your scenario.Barbie
Perfect! The first half of this answer is exactly what I was looking for. Thanks much. I know it took us a while to get there, but we finally did. PHEW! Thanks much. You deserve this bounty.Sammie
@BoraMa I do want to use markdown/redcarpet and rouge together. I followed the steps in the second section - thnx! - but I don't get coloring. What Rails version do you use here? I am in Rails5.Biometrics
I had problem with highlight JavaScript, because in code block I wrote language JavaScript, but should write javascript lowercaseTarkany
I tried A) and it renders plain text (no error, but not formatted code). Any ideas?Rejoin
@user5783745 Not sure, do you use raw when showing the output in a template?Barbie
@BoraMa I have tried with and without. I asked here in case that helpsRejoin

© 2022 - 2024 — McMap. All rights reserved.