Markdown渲染(全栈教程同款)

写在前面

一直觉得全栈教程里面的教程样式特别好看
终于,实现了,以后就拿这个样式对自己的博客进行渲染了

准备工作

  1. 创建一个只含有Post的model的项目 在命令行中运行 rails new markdown_demo cd markdown_demo rails g scaffold Post title:string content:text rake db:migrate
  2. 删掉scaffold提供的css代码 找到app/assets/stylesheets/scaffolds.scss 并把其中的css代码全部删掉
  3. 配置路由 在routs.rb中
    root "posts#index"
    

配置前端编辑器SimpleMDE

SimpleMDE

1. 在app/view/layout/application.html.erb中的head引入simpleMDE的js与css

html
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>

如图:

2. 在posts.coffee中填入
simplemde = null

cleanupSimpleMDE = ->
    if simplemde?
        simplemde.toTextArea()
        simplemde = null

$(window).on 'popstate', cleanupSimpleMDE
$(document).on 'turbolinks:before-visit', cleanupSimpleMDE

$(document).on "turbolinks:load", ->
    simplemde = new SimpleMDE()

注意:

  • 这里主要其作用的就是最后一句new SimpleMDE(),它的原理就是寻找页面上第一个textarea然后对其进行美化并加工成一个Markdown编辑器
  • 其它的代码都是为了防止其与turbolinks冲突而存在的
3. 现在可以去看效果了(localhost:3000)

点击New Post,就可以看到如下效果


然后创建一篇包含mardown语法的博文,看看
发现并没有效果
因为这个只是前端编辑器的美化,并没有涉及到markdown渲染的内容
下面就来写渲染的内容

markdown解析与渲染

1. 添加RedcarpetRouge 这两个gem

在gemfile中添加

gem 'redcarpet'
gem 'rouge'

然后bundle install并重启rails s

2. 在app/views/posts/show.html.erb中
<p id="notice"><%= notice %></p>

+++ <div class="post-container">
        <p>
            <strong>Title:</strong>
            <%= @post.title %>
        </p>

        <p>
          <strong>Content:</strong>
---       <%= @post.content %>
+++      <div class="markdown">
+++          <%= markdown(@post.content) %>
+++      </div>
        </p>

        <%= link_to 'Edit', edit_post_path(@post) %> |
        <%= link_to 'Back', posts_path %>
+++ </div>

如图:


注意这句

<%= markdown(@post.content) %>

这个markdown()是我们马上要写的一个helper方法

3. 在app/helpers/application_helper.rb中写
module ApplicationHelper

    def markdown(text)
        renderer_options = {
            hard_wrap: true,
            filter_html: true
        }

        markdown_options = {
            autolink: true,
            no_intra_emphasis: true,
            fenced_code_blocks: true,
            lax_html_blocks: true,
            strikethrough: true,
            superscript: true
        }

        renderer = HTMLwithRouge.new(renderer_options)
        Redcarpet::Markdown.new(renderer, markdown_options).render(text).html_safe
    end

    class HTMLwithRouge < Redcarpet::Render::HTML

        INDENT = " " * 2

        def block_code(code, metadata)
            language, filename = metadata.split(":") if metadata

            lexer = find_lexer_with(language)

            formatter = Rouge::Formatters::HTML.new
            formatter2 = Rouge::Formatters::HTMLTable.new(formatter, opts={})

            rows = []
            rows << %(<div class="code-block">)
            if filename
                rows << %(#{INDENT}<div class="code-header">)
                rows << %(#{INDENT * 2}<span>#(filename)</span>)
                rows << %(#{INDENT}</div>)
            end
            rows << %(#{INDENT}<div class="code-body">)
            rows << %(#{INDENT * 2}#{formatter2.format(lexer.lex(code))})
            rows << %(#{INDENT}</div>)
            rows << %(</div>)
            rows.join("\n")
        end

        def find_lexer_with(language)
            downcase_language = language.try(:downcase)
            case downcase_language
            when "rb", "ruby"
                lexer = DiffRuby
            when "html"
                lexer = DiffHTML
            when "erb"
                lexer = DiffErb
            when "js", "javascript"
                lexer = DiffJS
            else
                lexer = Rouge::Lexer.find(downcase_language) || lexer = Rouge::Lexers::PlainText
            end
        end
    end

    class DiffRuby < Rouge::Lexers::Ruby
        prepend :root do
            rule(/^\+.*$\n?/, Generic::Inserted)
            rule(/^-+.*$\n?/, Generic::Deleted)
        end
    end

    class DiffHTML < Rouge::Lexers::HTML
        prepend :root do
            rule(/^\+.*$\n?/, Generic::Inserted)
            rule(/^-+.*$\n?/, Generic::Deleted)
        end
    end

    class DiffErb < Rouge::Lexers::ERB
        prepend :root do
            rule(/^\+.*$\n?/, Generic::Inserted)
            rule(/^-+.*$\n?/, Generic::Deleted)
        end
    end

    class DiffJS < Rouge::Lexers::Javascript
        prepend :root do
            rule(/^\+.*$\n?/, Generic::Inserted)
            rule(/^-+.*$\n?/, Generic::Deleted)
        end
    end

    class DiffCoffee < Rouge::Lexers::Coffeescript
        prepend :root do
            rule(/^\+.*$\n?/, Generic::Inserted)
            rule(/^-+.*$\n?/, Generic::Deleted)
        end
    end

end
4. 现在可以打开项目

找一个带有markdown语法的post#show页面看看
localhost:3000/posts/1
可得到类似下图的样式

配置markdown装潢的css代码

在posts.scss中贴入如下代码

.code-block { 
  background-color: #efefef;
  padding: 7px 7px 7px 10px;
  border: 1px solid #ddd;
  -moz-box-shadow: 3px 3px rgba(0,0,0,0.1);
  -webkit-box-shadow: 3px 3px rgba(0,0,0,0.1);
  box-shadow: 3px 3px rgba(0,0,0,0.1);
  margin: 20px 0 20px 0;
  overflow: hidden;
}

code {
  font-family:'Bitstream Vera Sans Mono','Courier', monospace;
}

.rouge-code .c { color: #586E75 } /* Comment */
.rouge-code .err { color: #93A1A1 } /* Error */
.rouge-code .g { color: #93A1A1 } /* Generic */
.rouge-code .k { color: #859900 } /* Keyword */
.rouge-code .l { color: #93A1A1 } /* Literal */
.rouge-code .n { color: #93A1A1 } /* Name */
.rouge-code .o { color: #859900 } /* Operator */
.rouge-code .x { color: #CB4B16 } /* Other */
.rouge-code .p { color: #93A1A1 } /* Punctuation */
.rouge-code .cm { color: #586E75 } /* Comment.Multiline */
.rouge-code .cp { color: #859900 } /* Comment.Preproc */
.rouge-code .c1 { color: #586E75 } /* Comment.Single */
.rouge-code .cs { color: #859900 } /* Comment.Special */
.rouge-code .gd { color: #2AA198 } /* Generic.Deleted */
.rouge-code .ge { color: #93A1A1; font-style: italic } /* Generic.Emph */
.rouge-code .gr { color: #DC322F } /* Generic.Error */
.rouge-code .gh { color: #CB4B16 } /* Generic.Heading */
.rouge-code .gi { color: #859900 } /* Generic.Inserted */
.rouge-code .go { color: #93A1A1 } /* Generic.Output */
.rouge-code .gp { color: #93A1A1 } /* Generic.Prompt */
.rouge-code .gs { color: #93A1A1; font-weight: bold } /* Generic.Strong */
.rouge-code .gu { color: #CB4B16 } /* Generic.Subheading */
.rouge-code .gt { color: #93A1A1 } /* Generic.Traceback */
.rouge-code .kc { color: #CB4B16 } /* Keyword.Constant */
.rouge-code .kd { color: #268BD2 } /* Keyword.Declaration */
.rouge-code .kn { color: #859900 } /* Keyword.Namespace */
.rouge-code .kp { color: #859900 } /* Keyword.Pseudo */
.rouge-code .kr { color: #268BD2 } /* Keyword.Reserved */
.rouge-code .kt { color: #DC322F } /* Keyword.Type */
.rouge-code .ld { color: #93A1A1 } /* Literal.Date */
.rouge-code .m { color: #2AA198 } /* Literal.Number */
.rouge-code .s { color: #2AA198 } /* Literal.String */
.rouge-code .na { color: #93A1A1 } /* Name.Attribute */
.rouge-code .nb { color: #B58900 } /* Name.Builtin */
.rouge-code .nc { color: #268BD2 } /* Name.Class */
.rouge-code .no { color: #CB4B16 } /* Name.Constant */
.rouge-code .nd { color: #268BD2 } /* Name.Decorator */
.rouge-code .ni { color: #CB4B16 } /* Name.Entity */
.rouge-code .ne { color: #CB4B16 } /* Name.Exception */
.rouge-code .nf { color: #268BD2 } /* Name.Function */
.rouge-code .nl { color: #93A1A1 } /* Name.Label */
.rouge-code .nn { color: #93A1A1 } /* Name.Namespace */
.rouge-code .nx { color: #555 } /* Name.Other */
.rouge-code .py { color: #93A1A1 } /* Name.Property */
.rouge-code .nt { color: #268BD2 } /* Name.Tag */
.rouge-code .nv { color: #268BD2 } /* Name.Variable */
.rouge-code .ow { color: #859900 } /* Operator.Word */
.rouge-code .w { color: #93A1A1 } /* Text.Whitespace */
.rouge-code .mf { color: #2AA198 } /* Literal.Number.Float */
.rouge-code .mh { color: #2AA198 } /* Literal.Number.Hex */
.rouge-code .mi { color: #2AA198 } /* Literal.Number.Integer */
.rouge-code .mo { color: #2AA198 } /* Literal.Number.Oct */
.rouge-code .sb { color: #586E75 } /* Literal.String.Backtick */
.rouge-code .sc { color: #2AA198 } /* Literal.String.Char */
.rouge-code .sd { color: #93A1A1 } /* Literal.String.Doc */
.rouge-code .s2 { color: #2AA198 } /* Literal.String.Double */
.rouge-code .se { color: #CB4B16 } /* Literal.String.Escape */
.rouge-code .sh { color: #93A1A1 } /* Literal.String.Heredoc */
.rouge-code .si { color: #2AA198 } /* Literal.String.Interpol */
.rouge-code .sx { color: #2AA198 } /* Literal.String.Other */
.rouge-code .sr { color: #DC322F } /* Literal.String.Regex */
.rouge-code .s1 { color: #2AA198 } /* Literal.String.Single */
.rouge-code .ss { color: #2AA198 } /* Literal.String.Symbol */
.rouge-code .bp { color: #268BD2 } /* Name.Builtin.Pseudo */
.rouge-code .vc { color: #268BD2 } /* Name.Variable.Class */
.rouge-code .vg { color: #268BD2 } /* Name.Variable.Global */
.rouge-code .vi { color: #268BD2 } /* Name.Variable.Instance */
.rouge-code .il { color: #2AA198 } /* Literal.Number.Integer.Long */


.post-container {
    position: relative;
    display: block;
    margin: 20px auto;
    background-color: #fff;
    color: #333;
    width: 960px;
    min-height: 400px;
    box-shadow: 1px 2px 4px 0 rgba(0,0,0,0.25);
    padding: 30px 60px;
    border-radius: 1px;
}


.markdown {
    font-family: "PT Serif", Georgia, Times, "Times New Roman", serif !important;
}


.markdown .code-block {
    background-color: #ffffcc;
    font-family: Menlo, Monaco, "Courier New", monospace;
    font-size: 12px;
    // background-color: #F5F5F5;
    border: 0;
    padding: 5px;
    color: #444;
    overflow: auto;
    border-radius: 0;
    margin: 0;
}

.markdown .code-block {
    margin: 20px 0;
    padding: 0;

    text-overflow: ellipsis;
    word-wrap: break-word;
    font-family: "PT Serif", Georgia, Times, "Times New Roman", serif !important;
}

.markdown .code-block .code-header {
    background-color: #e6e6e6;
    color: #666;
    font: 90%/2.25 Monaco, Menlo, Consolas, "Courier New", monospace;
    text-indent: 10.5px;
    text-shadow: 0 1px 0 rgba(255,255,255,0.9);
    -moz-border-radius: 0.25em 0.25em 0 0;
    -webkit-border-radius: 0.25em;
    border-radius: 0.25em 0.25em 0 0;
    -moz-box-shadow: inset 0 0 0 1px #d9d9d9;
    -webkit-box-shadow: inset 0 0 0 1px #d9d9d9;
    box-shadow: inset 0 0 0 1px #d9d9d9;
    font-size: 14px;
}

.rouge-code {
    padding-left: 10px;
}


.markdown h1, h2 {
    border-bottom: 2px solid #eee;
}

.markdown h3 {
    border-bottom: 1px solid #eee;
}

.markdown img {
    border: 1px solid rgba(50,50,50,0.32);
    -webkit-box-shadow: 0 3px 6px 0 rgba(50,50,50,0.32);
    -moz-box-shadow: 0 3px 6px 0 rgba(50,50,50,0.32);
    box-shadow: 0 3px 6px 0 rgba(50,50,50,0.32);

    vertical-align: top;
    max-width: 100%;
    width: auto;
}

.markdown .code-block .code-body pre {
    background-color: #ffffcc;
    font-family: Menlo, Monaco, "Courier New", monospace;
    // font-size: 16px;
    // background-color: #F5F5F5;
    border: 0;
    padding: 5px;
    color: #444;
    overflow: auto;
    border-radius: 0;
    margin: 0;
}

.markdown>*:not(.code-block) code {
    background-color: #ececec;
    color: #d14;
    font-size: 85%;
    text-shadow: 0 1px 0 rgba(255,255,255,0.9);
    border: 1px solid #d9d9d9;
    padding: 0.15em 0.3em;
    border-radius: 4px;
}

然后刷新show页面,得到了如下的效果,是不是跟全栈营的教程很像

这还不算完,全栈营教程格式有两个我们很喜欢的样式
一个是现实代码文件地址的样式
一个是现实+++与—--的样式的代码

其实刚才在代码中已经实现了这两个功能
我们只需要给这篇博客加点代码看看

点击edit,加入如下内容


然后,更新重新查看show页面

有没有,已经与教程的样式没有什么差别是吧~
大功告成

参考资料:

  • RailCast的课程主要学习Redcarpet(markdown解析)+pygments(代码高亮)的配合使用
  • redcarpet_filename_extension学习如何从metadata中识别file_name 代码
  • rouge-json-diff-lexer-example如何给lexer加入识别diff语法的功能
  • Solarized Light Pygments CSS发现全栈营使用的这个代码主题,不过,这个是针对pygments写的主题,因为我想加入diff的功能,但又不会python,所以弃用pygemtns,使用纯ruby写的一个rouge,再加上,我喜欢代码框里要显示行数,就采用rouge里的table渲染器,就对css做了相应的修改,同时也抄了一些全栈教程的css,最终形成一些列自己的css

其它可以参考学习的gem

客户端解决方案

自己写的全栈项目学习计时器

Catetimer有兴趣可以试一试