How can I create specialized builders for semantic layout in rails?
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.
精彩评论