How to extend redcarpet to support auto linking user mentions? - ruby

On my rails3 application I want to use redcarpet to handle user's posts and the user comment section. As such I'd like to extend redcarpet to support turning #username into a link to a user on my site. I know redcarpet is written in C but is there anyway easy way to extend it in ruby? How hard would it be to write it in C? Should I just do this outside of redcarpet?
Also I'm intrested in some other extensions of redcarpet that would be shorthand for linking to other models in my app. I'm not sure the syntax yet but I'm guessing it would be similar to how github handles linking to issues.

I found it pretty easy to extend redcarpet's parser in Ruby for my rails 3 app. It wasn't scary at all.
First, start by deriving a class from Redcarpet's HTML renderer and override the preprocess method as recommended in the docs. In Rails 3.2 and Rails 4, this file can go anywhere and you don't need to require it. I use a 'services' folder to hold code like this.
# app/services/my_flavored_markdown.rb
class MyFlavoredMarkdown < Redcarpet::Render::HTML
def preprocess(text)
text
end
end
Next step is to add methods that do text substitutions you want. Here I use regex to wrap text that looks like "#mention" in an HTML span tag with a css class 'mention'.
# app/services/my_flavored_markdown.rb
class MyFlavoredMarkdown < Redcarpet::Render::HTML
def preprocess(text)
wrap_mentions(text)
end
def wrap_mentions(text)
text.gsub! /(^|\s)(#\w+)/ do
"#{$1}<span class='mention'>#{$2}</span>"
end
text
end
end
You could just as easily look up a user's profile page and wrap the #mention in an anchor tag instead. In my case, I also made methods for emoticons and hashtags that worked the same way and chained the methods together.
The last step is to add a helper that accepts some text, creates an instance of your Redcarpet-derived class, passes the text into that for processing, and returns the html result.
# app/helpers/application_helper.rb
def flavored_markdown_to_html(text)
renderer = MyFlavoredMarkdown.new()
# These options might be helpful but are not required
options = {
safe_links_only: true,
no_intra_emphasis: true,
autolink: true
}
Redcarpet::Markdown.new(renderer, options).render(text)
}
In your views you can call it like this:
<%= flavored_markdown_to_html("This is something worth #mentioning") %>
The output would then be:
This is something worth <span class='mention'>#mentioning</span>.

I once tried to extend redcarpet, but found it very difficult. If there are no other dependencies on redcarpet I'd recommend you try rpeg-markdown which is a (somewhat outdated) Ruby gem providing bindings to the excellent peg-markdown.
peg-markdown is a markdown interpreter written as a formal grammar. This means that it is very easy to extend it with own syntax. I've successfully extended peg-markdown for my own projects (see my fork here) and I found it to be much simpler than fiddling with redcarpet's custom parser code.
I also found peg-markdown to have fewer bugs.
The Ruby bindings may have to be made current by updating the git submodule. (I'm planning to submit a pull request to update rpeg-markdown to the latest version of peg-markdown.)

Related

How can you access page properties (YAML front matter) within a converter plugin

I'm writing a converter plugin for Jekyll and need access to some of the page header (YAML front matter) properties. Only the content is passed to the main converter method and does not seem possible to access the context.
Example:
module Jekyll
class UpcaseConverter < Converter
safe true
priority :low
def matches(ext)
ext =~ /^\.upcase$/i
end
def output_ext(ext)
".html"
end
def convert(content)
###########
#
# Its here that I need access to the content page header data
#
#
###########
content.upcase
end
end
end
Any ideas how I can access the page header data within a converter plugin?
Based on the Jekyll source code, it is not possible to retrieve the YAML front matter in a converter.
I see two solutions that could work depending on your situation.
Your file extension could be descriptive enough to provide the information you would have included in the front matter. It looks like the Converter plugin was designed to be this basic.
If modifying Jekyll is an option, you could change the Convertible.transform method to send the front matter to Converter.convert. The Converters that come with Jekyll would have to be modified as well. Fork it on GitHub and see if others like the idea. Here's where to start: https://github.com/mojombo/jekyll/blob/cb1a2d1818770ca5088818a73860198b8ccca27a/lib/jekyll/convertible.rb#L49
Good luck.
devnull, I ran into a similar situation and I figured a way of doing it.
In the converter, I registered a pre-render hook to pull YAML into a variable, so that in the actual convert method, I have access to the info I just pulled. Also, another post_render hook is needed to remove that piece of info since this should be a per-post data.
A side note. I found that the convert will be called twice, once for use in the html <meta> tag, once for the actual content. The hook will be only invoked for the second case, not the first. You may need to guard you convert function.
Another side note. I think having YAML in the converter is not unreasonable. Just like in pandoc where you can specify bibliography file in the YAML section and do other fine tuning, people should be given freedom to customize a single post using YAML, too.
def initialize(config)
super(config)
Jekyll::Hooks.register :posts, :pre_render do |post|
if matches(post.data["ext"])
# extract per post metadata, including those in YAML
#myconfig["meta"] = post.data
# you may need the path to the post: post.path
end
end
Jekyll::Hooks.register :posts, :post_render do |post|
if matches(post.data["ext"])
# remove per post metadata
#myconfig.delete("meta")
end
end
end
def convert(content)
return content unless #myconfig["meta"]
# actual conversion goes here
end

How to render Mustache partials in Sinatra while using ERB as the primary tempate engine

I have a sinatra app that is mostly using erb for templates, but I am adding some mustache partials for blocks of html that need to be rendered on both the server and client side. I have a "views" directory where I'm keeping all my templates, including the mustache templates I'm adding. For example, the structure looks something like this:
views/
index.html.erb
_wingding.html.erb
_widget.html.mustache
Let's say I'm rendering index.html.erb using this endpoint:
get '/' do
erb :index
end
And inside that template, I want to render both of the above partials [UPDATE: it turns out this partial method is not built into sinatra, but is included via a gem (see my answer for details), but it still relies on the main sinatra rendering component, so the problem here still stands.]. So index.html.erb contains:
<%= partial :wingding %>
<%= partial :widget %>
The erb partial (wingding) renders fine, but the mustache partial (widget) does not. It causes the application to throw an exception:
Template engine not found: mustache
I have included the 'mustache' gem in the project. I tried 'require mustache/sinatra' and register Mustache::Sinatra. Both of these statements worked, but didn't solve the problem above. Any idea how to tell sinatra about mustache so that it can render mustache partials?
I'm setting my views directory by including this configuration:
configure do
set :views, File.join(File.dirname(__FILE__), 'views')
end
Version numbers:
sinatra 1.3.1
mustache 0.99.4
ruby 1.9.3
So it turns out the gem listed in the solution in my other answer caches output, meaning you can't use it to include the same partial with different locals in the same request, which makes it worthless for my purposes. So here's another (admittedly hackish) solution that I've settled on. I created a helper that will just deliver the contents of a given view:
helpers do
def template_contents(path)
File.open("#{settings.views}/#{path}") { |f| f.read }
end
end
Then, I just do a regular Mustache#render:
<%= Mustache.render(template_contents('_widget.html.mustache'), { ... }) %>
This works great for my use case.
I figured this out. First, I must note something I didn't realize when I posted the question. partial is not actually built into sinatra. It is being included in my app as part of the sinatra-more gem (discontinued, most of it's functionality is now in a project called padrino, but still using sinatra-more in this app). The partial method is included like this:
require 'sinatra_more/render_plugin'
module Sinatra
register SinatraMore::RenderPlugin
end
Anyway, that just adds the partial method (along with some other rendering helpers) but this method falls back on the built-in sinatra rendering code, so this fact is independent of the actual issue at hand in this question -- that sinatra does not by default recognize the mustache template engine. Turns out someone created a super simple gem to make it work, sinatra-mustache. You simply include the gem and require the library:
require 'sinatra/mustache'
That's it -- it just works! No additional configuration necessary. If you're curious how it works, the source code is pretty simple. It's by no means a one-liner to register a new template engine with sinatra (even saying "register" is a misnomer -- you basically have to implement it), but it's still fairly simple.
UPDATE: This gem caches the output for a given request/template, so you can't use it a second time with different locals. I could see cases where that's okay, but that makes it worthless for my use case.

i18n markdown files in Rails 3 views

I am currently working through Michael Hartl's Rails Tutorial while experimenting with some other things not covered in the book. After completing Chapter 5, where the static pages are created, I decided change the view code to HAML, internationalize the pages, and put the static content into separate (non-partial) Markdown files, using the RDiscount gem to render them. For example:
app/views/static_pages/about.html.haml
- provide(:title, t('.about_us'))
:markdown
#{render file: "static_pages/about.#{params[:locale]}.md"}
Under the static_pages directory, I have Markdown files like about.en.md, about.it.md, about.ja.md etc, so interpolating in the :locale parameter is what determines which language Markdown file gets rendered.
My questions are:
The static_pages directory is a bit crowded with Markdown files, so are there any sensible default/best practice locations (perhaps outside of the app directory) to keep these Markdown files, where they could be presumably be edited by people who don't need to know about the inner workings of the app?
What better ways are there to implement rendering multi-lingual Markdown files in views? My use of :locale and the double string-interpolation seems inelegant.
Is there a way to change this code so that I can pass Ruby variables into the Markdown file? I know I can, for example, use a #{language} variable in the Markdown by just changing about.en.md into a HAML partial (_about.en.html.haml) and change the code to look something like:
app/views/static_pages/about.html.haml
- provide(:title, t('.about_us'))
:markdown
#{render "about.#{params[:locale]}", language: 'Markdown!'}
But, is there a way to do this without changing the Markdown file into another type of file? If such a way exists, is it recommended/feasible?
After having a look at this StackOverflow answer, it seemed that the best location for i18n Markdown files would be their own action name directories under the config/locales directory, and that there was a good opportunity to refactor the render code on all views for the StaticPagesController. So, using about.html.haml as an example below, the call to render in the home, help, about, and contact views has been changed to the exact same code:
app/views/static_pages/about.html.haml
- provide(:title, t('.about_us'))
:markdown
#{render file: localized_page_for(action_name, params[:locale])}
The localized_page_for method is defined in the StaticPagesHelper:
app/helpers/static_pages_helper.rb
module StaticPagesHelper
def localized_page_for(action, locale)
"#{Rails.root}/config/locales/#{action}/#{action}.#{locale.to_s}.md"
end
end
So, now all the Markdown files have been taken out of the app/views/static_pages directory and are called from their respective logical directories (eg. config/locales/about/about.en.md etc) using ActionController's action_name attribute and the locale, making for less clutter.
As for question 2 above, string-interpolation seems to be common enough for this kind of problem, so I'll consider it "elegant" enough as well.
As for question 3 above, after exhaustive searching, I haven't found a way anyone has passed in variables in to a pure Markdown file, and the documentation doesn't seem to say anything about supporting them, so I'm going to conclude that it's not possible. If passing Ruby variables in to Markdown is absolutely necessary, the file will need to be run through another interpreter, kind of like is described in this StackOverflow answer.
Update:
After running security scanner Brakeman against the app, it came up with a potential Dynamic Render Path security warning (albeit a weak one) due to dynamically passing in params[:locale] to the render call instead of passing it a static string. So, I moved the call to the localized_page method out of the views, moved the method itself out of the StaticPagesHelper (so that file is now empty) and in to the StaticPagesController and then instantiated a #page instance variable in each method to pass to the view. In summary, the code now looks like this, which doesn't get the security warning:
app/controllers/static_pages_controller.rb
class StaticPagesController < ApplicationController
before_filter :localized_page, only: [:help, :about, :contact]
def home
if signed_in?
#micropost = current_user.microposts.build
#feed_items = current_user.feed.paginate(page: params[:page])
else
localized_page
end
end
def help
end
def about
end
def contact
end
private
def localized_page
#page = "#{Rails.root}/config/locales/"\
"#{action_name}/#{action_name}.#{params[:locale].to_s}.md"
end
end
app/views/static_pages/about.html.haml
- provide(:title, t('.about_us'))
:markdown
#{render file: #page}

render individual file in middleman

I am writing a helper and I need to get a rendered file as String.
I see that the method that I need exists in the middleman's library: http://rubydoc.info/github/middleman/middleman/Middleman/CoreExtensions/Rendering/InstanceMethods#render_individual_file-instance_method
How do I call this function from my helper class?
I tried:
require "middleman-core/lib/middleman-core/core_extensions/rendering.rb"
...
puts Middleman::CoreExtensions::Rendering::InstanceMethods.render_individual_file(filepath)
But it does not seem to find the file, any idea?
I'm not sure 3.0 beta is quite ready for primetime.
That said, it does sound like the partial method is what you're looking for.
Unless I'm missing something, the Middleman method seems like an overly-complex solution. For one of my sites I wanted to load entire text files into my templates, so I wrote this helper:
# Shortcut for loading raw text files. Obviously assumes that given file is in a valid format.
# #return [String] File contents
def load_textfile(filename)
File.read filename.to_s
end
Also, you should clarify if you are intending to use this within a template, or within Ruby code. It's not clear to me based on your question.
Here is an example of how one would use above helper:
Currently of note, Middleman is in the process of transitioning to version 4, and the conventions for loading helpers will change. The most straightforward way to define a helper is within a helper block in your config.rb file, as follows:
helpers do
# Define helper functions here to make them available in templates
end
I use Slim for templating. It really is the best. In slim you would appply helper as thus:
= load_textfile 'path'
p You can embed helper output in your page with interpolation, too: #{load_textfile 'path'}

Partial HAML templating in Ruby without Rails

I really don’t need the overhead of Rails for my very small project, so I’m trying to achieve this just using just plain Ruby and HAML.
I want to include another HAML file inside my HAML template. But I haven’t found a good—or really usable—way of doing this.
For example, I have these two HAML files:
documents.haml
%html
%body
= include(menu.haml) body
%article …
menu.haml
%ul
%li
%a whatever …
Include is obviously not the way to go here. But it does a nice job describing what I’m trying to achieve in this example.
I totally recommend the Tilt gem for these things. It provides a standard interface for rendering many different template langages with the same API, lets you set custom scope and locals, lets you use yield, and is robust and fast. Sinatra is using it for templates.
Example:
require 'haml'
require 'tilt'
template = Tilt.new('path/to/file.haml')
# => #<Tilt::HAMLTemplate #file="path/to/file.haml" ...>
layout = Tilt.new('path/to/layout.haml')
output = layout.render { template.render }
This lets you yield inside the layout to get the rendered template, just like Rails. As for partials, David already described a simple and nice way to go.
But actually, if what you're writing is going to be served over HTTP, i suggest you take a look at Sinatra, which already provides templating, and has the simplest request routing you could imagine.
I've done this before, just for a quick-and-dirty template producer. The easiest way is to just render the HAML inside the parent object:
%p some haml that's interesting
= Haml::Engine.new('%p this is a test').render
%p some more template
You'll more than likely want to build some methods to make this easier--a couple of helper methods. Maybe you write one called render_file that takes a filename as an argument. That method might look something like:
def render_file(filename)
contents = File.read(filename)
Haml::Engine.new(contents).render
end
Then, your template would look more like:
%p some template
= render_file('some_filename.haml')
%p more template
Note, you will probably need to pass self to the original Haml::Engine render so that it knows how to find your render_file method.
I've had great success just using the instructions posted by David Richards in a concatenated way, without variables, like this:
= Haml::Engine.new(File.read('/Volumes/Project/file_to_include.haml')).render
There's obviously a more elegant way. But for someone who just wants to include one or two files, this should work nicely. It's a drawback that all base files using these includes have to be recompiled after some changes to the latter. It might be worthwile to just use php include if the environment allows that.
def render_file(filename)
contents = File.read('views/'+filename)
Haml::Engine.new(contents).render
end
(Adding this semi-redundant answer to show how one might incorporate the techniques from other answers.)
Include something like this in your setup code to monkey-patch the Haml library.
module Haml::Helpers
def render_haml(filename)
contents = File.read(Rails.root.join("app", "assets", "templates", filename))
Haml::Engine.new(contents).render
end
end
Then in your Haml file
.foo
= render_haml('partial.haml')
.bar
Obviously this is a Rails-ish example (because I wanted to use HAML in my asset pipeline instead of as views)... You will want to replace Rails.root.join(...) with a way to find filename in your project's templates or partials directory.

Resources