trulia / hologram

A markdown based documentation system for style guides.
http://trulia.github.io/hologram
Other
2.16k stars 199 forks source link

Rendering Rails helpers in haml_example blocks #233

Open lovehasnologic opened 9 years ago

lovehasnologic commented 9 years ago

Out of the box, generating documentation that includes a rails helper in a haml_example code block results in the following error...

(haml):1:in `block in render': undefined method `link_to' for #<Object:0x007fcef187af10> (NoMethodError)
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/haml-4.0.6/lib/haml/engine.rb:129:in `eval'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/haml-4.0.6/lib/haml/engine.rb:129:in `render'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/hologram-1.4.0/lib/hologram/code_example_renderer/renderers/haml_renderer.rb:14:in `block (2 levels) in <top (required)>'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/hologram-1.4.0/lib/hologram/code_example_renderer/factory.rb:13:in `call'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/hologram-1.4.0/lib/hologram/code_example_renderer/factory.rb:13:in `block (2 levels) in define'
    from (erb):3:in `get_binding'
    from /Users/[username]/.rvm/rubies/ruby-2.2.0/lib/ruby/2.2.0/erb.rb:863:in `eval'
    from /Users/[username]/.rvm/rubies/ruby-2.2.0/lib/ruby/2.2.0/erb.rb:863:in `result'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/hologram-1.4.0/lib/hologram/block_code_renderer.rb:16:in `render'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/hologram-1.4.0/lib/hologram/markdown_renderer.rb:38:in `block_code'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/hologram-1.4.0/lib/hologram/doc_builder.rb:199:in `render'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/hologram-1.4.0/lib/hologram/doc_builder.rb:199:in `block in write_docs'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/hologram-1.4.0/lib/hologram/doc_builder.rb:185:in `each'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/hologram-1.4.0/lib/hologram/doc_builder.rb:185:in `write_docs'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/hologram-1.4.0/lib/hologram/doc_builder.rb:147:in `build_docs'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/hologram-1.4.0/lib/hologram/doc_builder.rb:87:in `build'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/hologram-1.4.0/lib/hologram/cli.rb:38:in `build'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/hologram-1.4.0/lib/hologram/cli.rb:30:in `run'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/gems/hologram-1.4.0/bin/hologram:6:in `<top (required)>'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/bin/hologram:23:in `load'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/bin/hologram:23:in `<main>'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/bin/ruby_executable_hooks:15:in `eval'
    from /Users/[username]/.rvm/gems/ruby-2.2.0@hologram-txi/bin/ruby_executable_hooks:15:in `<main>'

Does this need to be done with a custom renderer or is it not possible. Specifically, I'm looking at three types of helpers.

1. Common Rails Helpers

= link_to '#', '#'

2. Gem Helpers

= simple_form_for "example" do |f|
  = f.input :username
  = f.input :password
  = f.button :submit

3. Custom Helpers from application.rb

= custom_icon_helper 'iconName'

Thank you in advance. I tried to work through building a custom markdown renderer, but just ended up getting lost in my own head, as Ruby/Rails is not my expertise.

chris-canipe commented 9 years ago

I've handled this by passing the files through Rails (#172) and parsing the HAML like so:

  HELPERS_TO_FILTER = [:notification]
  HELPERS_TO_FILTER_REGEX = /
    (?<==)
    \s*
    (?=
      (?:#{HELPERS_TO_FILTER.join('|')})
      \b
    )
  /x
  HELPER_PREFIX = 'view_context.'

  protected

    def apply_styleguide_filters(html)
      apply_example_output_filter(html)
      html.to_html
    end

     def looks_like_haml?(string)
      !!(string =~ /\A\s*[=%.#]/m)
    end

  private

    def apply_example_output_filter(html)
      html.css('.exampleOutput').each do |example_output|
        example_output_content = example_output.content.strip
        next unless looks_like_haml?(example_output_content)
        example_output_with_prefixed_helpers =
          prefix_helpers(example_output_content)
        rendered_helper =
          render_helper_from_haml(example_output_with_prefixed_helpers)
        example_output.children.remove
        example_output.add_child rendered_helper
      end
    end

    # Helpers must be called within the controller's view context.
    def prefix_helpers(string)
      string.gsub(HELPERS_TO_FILTER_REGEX, HELPER_PREFIX)
    end

    def render_helper_from_haml(string)
      begin
        rendered_helper = Haml::Engine.new(string).render(binding)
      rescue => e
        raise "#{h(e)}<br><br>Perhaps this is a custom helper that needs filtering?".html_safe
      end
      Nokogiri::HTML::fragment(rendered_helper)
    end
lovehasnologic commented 9 years ago

Appreciate the feedback, but I'm still getting an error when I follow these instructions and try to load a URL (I read this approach as running through the regular render process and not needing to generate the static files.

No such file or directory @ rb_sysopen - /Users/lovehasnologic/Sites/hologram-txi/app/views/styleguide/javascripts_-_page_data.html

Extracted source (around line #22):
20
21    def set_page_html
22      file = File.open(@page_absolute_file_path)
23      @page_html = ::Nokogiri::HTML(file)
24      file.close
25    end

When I run hologram from the command line, I get:

(haml):1:in `block in render': undefined method `link_to' for #<Object:0x007fcecb023770> (NoMethodError)

Here are my files in full:

/hologram_config.yml

# Hologram will run from same directory where this config file resides
# All paths should be relative to there

# The directory containing the source files to parse recursively
source:
  - ./app/assets/javascripts
  - ./app/assets/stylesheets
  - ./vendor/assets/javascripts

# The directory that hologram will build to
destination: ./app/views/styleguide

# The assets needed to build the docs (includes header.html,
# footer.html, etc)
# You may put doc related assets here too: images, css, etc.
documentation_assets: ./app/views/styleguide

# The folder that contains templates for rendering code examples.
# If you want to change the way code examples appear in the styleguide,
# modify the files in this folder
code_example_templates: ./app/views/styleguide/code_example_templates

# The folder that contains custom code example renderers.
# If you want to create additional renderers that are not provided
# by Hologram (i.e. coffeescript renderer, jade renderer, etc)
# place them in this folder
code_example_renderers: ./app/views/styleguide/code_example_renderers

# To additionally output navigation for top level sections, set the value to
# 'section'. To output navigation for sub-sections,
# set the value to `all`
nav_level: all

# Hologram displays warnings when there are issues with your docs
# (e.g. if a component's parent is not found, if the _header.html and/or
#  _footer.html files aren't found)
# If you want Hologram to exit on these warnings, set the value to 'true'
# (Default value is 'false')
exit_on_warnings: false

/config/routes.rb

Rails.application.routes.draw do
  root to: 'styleguide#page', defaults: { page: :index, format: :html }
  get 'styleguide/*page', to: 'styleguide#page', format: :html
end

/app/controllers/styleguide_controller.rb

class StyleguideController < ApplicationController
  before_filter :set_page_uri
  before_filter :set_page_absolute_file_path
  before_filter :set_page_html
  before_filter :apply_styleguide_filters

  def page
    render text: @page_html and return
  end

  private

  def apply_styleguide_filters
    @page_html = super(@page_html)
  end

  def set_page_absolute_file_path
    @page_absolute_file_path = "#{Rails.root}/app/views/#{@page_uri}"
  end

  def set_page_html
    file = File.open(@page_absolute_file_path)
    @page_html = ::Nokogiri::HTML(file)
    file.close
  end

  def set_page_uri
    @page_uri = '%s/%s.%s' %
      params.values_at(:controller, :page, :format)
  end

end

/app/controllers/application.rb

class ApplicationController < ActionController::Base

  protect_from_forgery with: :exception
  before_action :set_default_meta_data
  helper_method :gon

  HELPERS_TO_FILTER = [:notification]
  HELPERS_TO_FILTER_REGEX = /
    (?<==)
    \s*
    (?=
      (?:#{HELPERS_TO_FILTER.join('|')})
      \b
    )
  /x
  HELPER_PREFIX = 'view_context.'

  protected

  def apply_styleguide_filters(html)
    apply_example_output_filter(html)
    html.to_html
  end

   def looks_like_haml?(string)
    !!(string =~ /\A\s*[=%.#]/m)
  end

  private

  def apply_example_output_filter(html)
    html.css('.exampleOutput').each do |example_output|
      example_output_content = example_output.content.strip
      next unless looks_like_haml?(example_output_content)
      example_output_with_prefixed_helpers =
        prefix_helpers(example_output_content)
      rendered_helper =
        render_helper_from_haml(example_output_with_prefixed_helpers)
      example_output.children.remove
      example_output.add_child rendered_helper
    end
  end

  # Helpers must be called within the controller's view context.
  def prefix_helpers(string)
    string.gsub(HELPERS_TO_FILTER_REGEX, HELPER_PREFIX)
  end

  def render_helper_from_haml(string)
    begin
      rendered_helper = Haml::Engine.new(string).render(binding)
    rescue => e
      raise "#{h(e)}<br><br>Perhaps this is a custom helper that needs filtering?".html_safe
    end
    Nokogiri::HTML::fragment(rendered_helper)
  end

  def set_default_meta_data
    @meta_data = {
      title: text("meta_data.title", scope: "controllers.application"),
      description: text("meta_data.description", scope: "controllers.application"),
      google_site_verification: Rails.application.secrets.google_site_verification_id,
      og_title: text("meta_data.og.title", scope: "controllers.application"),
      og_description: text("meta_data.og.description", scope: "controllers.application"),
      og_type: text("meta_data.og.type", scope: "controllers.application"),
      og_image: "cards/twit.png",
      og_sitename: text("meta_data.og.sitename", scope: "controllers.application"),
      fb_app_id: text("meta_data.app_id.fb", scope: "controllers.application"),
      twit_title: text("meta_data.twit.title", scope: "controllers.application"),
      twit_account: text("meta_data.twit.account", scope: "controllers.application"),
      twit_description: text("meta_data.twit.description", scope: "controllers.application"),
      twit_card: text("meta_data.twit.card", scope: "controllers.application"),
      twit_image: "cards/twit.png",
      apple_app_id: text("meta_data.app_id.apple", scope: "controllers.application") }
  end

end

Looks like I have everything in the right place, but I could have messed up something obvious.

Thanks in advance. Your help is (and has been) appreciated.

chris-canipe commented 9 years ago

Ah, yes. I forgot one key element that is unfortunate: put your HAML example inside of html_example not haml_example. It's a disconnect, but it prevents hologram from parsing and therefore bombing; instead, Rails handles the file when it's served up. Perhaps there's a better way, but this is what I've found so far.

Also, you'll need to adjust HELPERS_TO_FILTER to determine which haml helpers are parsed.

lovehasnologic commented 9 years ago

Still not working. I am now getting an error that says it can't copy an unknown file type.

I'm going to try some other approaches that some of the developers I work with suggested and if I hit on anything, I'll reply back in this thread.

Thanks again for your help.

lovehasnologic commented 9 years ago

I wanted to follow up since one of our developers had some time and was able to look at this. We ended up creating a custom haml renderer.

./styleguide_assets/code_example_renderers/haml.rb

load "config/environment.rb"

# Public: A context for rendering HAML that knows about helpers from Rails,
# gems and the current application.
#
# NOTE: This is totally hacked together.
class RailsRenderingContext

  # Public: Creates a new context into which we can render a chunk of HAML.
  #
  # Returns a properly-configured instance of ActionView::Base.
  def self.create
    # Create a new instance of ActionView::Base that has all of the helpers
    # that our ApplicationController does. This allows us to use normal Rails
    # helpers like `link_to`, most gem-provided helpers, and also custom
    # application helpers like `svg_icon`.
    view_context = ApplicationController.helpers

    # Add named route support to our view context, so we can reference things
    # like `root_path`.
    class << view_context; include Rails.application.routes.url_helpers; end

    # Create a new controller instance and give it a fake request; this vaguely
    # mirrors what happens when Rails receives a request and routes it. This
    # step allows us to use `simple_form_for`.
    controller = ApplicationController.new
    controller.request = ActionDispatch::TestRequest.new
    view_context.request = controller.request

    # Set up our view paths so that both `render` and gems that provide helpers
    # that use `render` (e.g. kaminari) can work.
    controller.append_view_path "app/views"
    view_context.view_paths = controller.view_paths
    view_context.controller = controller

    view_context
  end

end

# We overwrite the default "haml" handler from hologram to use our Rails-aware
# version.
Hologram::CodeExampleRenderer::Factory.define "haml" do
  example_template "markup_example_template"
  table_template "markup_table_template"
  lexer { Rouge::Lexer.find("haml") }

  rendered_example do |code|
    require "haml"
    haml_engine = Haml::Engine.new(code.strip)

    context = RailsRenderingContext.create
    haml_engine.render(context, {})
  end
end

As it says, this is hacked together and can probably be a bit cleaner (but I honestly have no idea). However, it works perfectly, and renders all rails helpers, be they default helpers, custom helpers or gem helpers. Quite a nice addition.