开发者

How can I create specialized builders for semantic layout in rails?

开发者 https://www.devze.com 2023-02-05 23:58 出处:网络
This is how I\'d like to write markup in say index.html.erb <%= page_for \"Super Cool Page\" do |p| %>

This is how I'd like to write markup in say index.html.erb

<%= page_for "Super Cool Page" do |p| %>
    <%= p.header do %>
        Ruby is Cool
    <% end %>
    <%= p.body do %>
        Witty discourse on Ruby.
    <% end %>
    <% if page.has_sidebar? %>
        <%= p.sidebar do %>
            <ul><li>Option 1</li></ul>开发者_JAVA技巧;
        <% end %>
    <% end %>
<% end %>

Which would output

<div class="page">
    <header><h1>Super Cool Page</h1></header>
    <section>
    Witty discourse on Ruby.
    </section>
</div>

and when page.has_sidebar? is true

<div class="page">
    <header><h1>Super Cool Page</h1></header>
    <asside><ul><li>Option 1</li></ul></asside>
    <section>
    Witty discourse on Ruby.
    </section>
</div>

I've taken a look at the FormHelper class in rails for guidance, but it seems like I'd have to duplicate a lot of work which I'm trying to avoid. I'm really just trying to figure out where to hang the classes/modules/methods in the framework and whit kind of object |p| should be.

My first inclination was to create a PageBuilder class that implements header, body and sidebar methods. But I got stuck on the rendering pipeline to get everything output just right.

Is there a gem that already provides this type of semantic generation? If not I'd love any insight on how to set this up.


I use something similar in my templates. Here's a modification. This should work in Rails 3.

application_helper.rb:

module ApplicationHelper
  class PageBuilder
    def initialize(title, template)
      @title, @template = title, template
      @header, @body, @sidebar = nil, nil, nil
      @options = { :page => {} , :header => {}, :sidebar => {}, :body => {}, :title => {} } 
      @logger = Rails.logger
    end
    def parse(&block)
      if block_given?
        if @template.respond_to?(:is_haml?) && @template.is_haml?
          contents = @template.capture_haml(&block) 
        else
          #erb
          contents = @template.capture(&block)
        end
      else
        contents = ""
      end
      contents
    end

    def page (options,&block)
      options[:class] ||= "page"
      @options[:page] = options
      parse(&block)
      content = ""
      content += @template.content_tag(:title, @options[:title]) { @title } unless @title.nil?
      content += @template.content_tag(:header,@options[:header]) do
        @template.content_tag( :h1) { @header } 
      end unless @header.nil?
      content += @template.content_tag(:asside, @options[:sidebar]) { @sidebar } unless @sidebar.nil?
      content += @template.content_tag(:section, @options[:section]) { @body } unless @body.nil?
      return @template.content_tag(:div, @options[:page]) { content.html_safe }
    end
    def header(options={},&block)
      @options[:header] = options
      @header = parse(&block)
      nil
    end
    def sidebar(options={},&block)
      @options[:sidebar] = options
      @sidebar = parse(&block)
      nil
    end
    def body(options={},&block)
      @options[:body] = options
      @body = parse(&block)
      nil
    end
  end

  def page_for(title, options = {}, &block )
    raise ArgumentError, "Missing block" unless block_given?
    builder = PageBuilder.new(title, view_context )
    return builder.page(options) do 
      block.call(builder)
    end
  end
end

Now, in your sample code, when page.has_sidebar? == false, you will get

<div class="page"><title>Super Cool Page</title><header><h1>
    Ruby is Cool
</h1></header><section>
    Witty discourse on Ruby.
</section></div>

and when page.has_sidebar? == true, you will get

<div class="page"><title>Super Cool Page</title><header><h1>
    Ruby is Cool
</h1></header><asside>
      <ul><li>Option 1</li></ul>
</asside><section>
    Witty discourse on Ruby.
</section></div>

You can rearrange stuff in the page method to get any desired layout as the output.


Not exactly what you are asking for, but have you tried looking at Haml?

It has a much more succinct syntax so the example you suggested might be written as:

.page
  %header
    %h1 Super Cool Page
  %asside
    %ul
      %li Option 1
  %section
    Witty Discourse on Ruby

As you can see structure in Haml is provided using indentation which helps with reading Haml source too.

If on the other hand you are attempting this as a learning exercise on how to build a template parser yourself then maybe look into the source of Haml or one of the other templating engines.


What you are doing can be achieved with content_for.

Haml is supercool, Slim could even be better?


I use a helper to process the args, which then calls the various partials to render.


Take a look at a gem called builder. It may be able to provide the framework that you can build your example above upon.

Here's a rough attempt to implement the example you gave above:

require 'builder'
page = Page.new             # you'll have to implement this class
builder = Builder::XmlMarkup.new(:target=>STDOUT, :indent=>2)
builder.div(:class => 'page') do |div|
  if page.has_header?
    div.header do |header|
      header.h1("Super Cool Page")
    end
  end
  div.body("whitty discourse on Ruby")
  if page.has_sidebar?
    div.sidebar do |side|
      side.ul do |ul|
        ul.li("Option 1")
      end
    end
  end
end

which outputs:

<div class="page">
  <header>
    <h1>Super Cool Page</h1>
  </header>
  <body>whitty discourse on Ruby</body>
  <sidebar>
    <ul>
      <li>Option 1</li>
    </ul>
  </sidebar>
</div>


You could write a tiny DSL (Domain Specific Language) to do this. In fact, it is a good fit IMO. The code I wrote is not used exactly how you've shown in your question, though.

Usage

#Example 1:
def some_boolean_method?
  true
end

output = String.new
PageDsl.generate(output) do  #or, use STDOUT to output to console
  page_for do
    "Super Cool Page"
    header do
      "Ruby is Cool"
    end
    body do
      "Witty discourse on Ruby."
    end
    if some_boolean_method?
      sidebar do
        "<ul><li>Option 1</li></ul>"
      end
    end
  end
end

p output
# => "<div class='page'><header>Ruby is Cool</header><body>Witty discourse on Ruby.</body><section><ul><li>Option 1</li></ul></section></div>"


#Example 2:
PageDsl.generate(STDOUT) do
  some_cool_tag do
    "Super Cool Page"
    gak! do { :class=> "ff" }
      "Ruby is Cool"
    end
  end
end
# => <some_cool_tag><gak!>Ruby is Cool</gak!></some_cool_tag>

Implementation

class PageDsl
  def initialize(output)
    @output = output
  end

  def content(text)
    @output << text.to_s
    nil
  end

  def translate_semantic_tag(tagname,attributes={})
    newline = "" # "\r\n" uncomment to add newlines
    case tagname.to_sym
    when :page_for
      tagname = "div"
      attributes[:class] =  "page"
    when :header
      tagname = "header"
    when :body
      tagname = "section"
    when :sidebar
      tagname = "asside"
    end

    @output << "<#{tagname}"
    attributes.each { |attr,value| @output << " #{attr}='#{value}'" }
    if block_given?
      @output << ">" << newline
      content = yield
      if content
        @output << content.to_s << newline
      end
      @output << "</#{tagname}>" << newline
    else
      @output << "/>" << newline
    end
    nil
  end
  alias method_missing translate_semantic_tag

  def self.generate(output, &block)
    PageDsl.new(output).instance_eval(&block)
  end
end

Note, the implementation does not do nesting, for example <header><h1>..</h1></header>, but that should be simple to implement.

0

精彩评论

暂无评论...
验证码 换一张
取 消