# encoding: UTF-8
module Keynote
# HTML markup in Ruby.
#
# To invoke Rumble, call the `build_html` method in a presenter.
#
# ## 1. Syntax
#
# There are four basic forms:
#
# ```ruby
# tagname(content)
#
# tagname(content, attributes)
#
# tagname do
# content
# end
#
# tagname(attributes) do
# content
# end
# ```
#
# Example:
#
# ``` ruby
# build_html do
# div :id => :content do
# h1 'Hello World', :class => :main
# end
# end
# ```
#
# ``` html
#
#
Hello World
#
# ```
#
# ## 2. Element classes and IDs
#
# You can easily add classes and IDs by hooking methods onto the container:
#
# ``` ruby
# div.content! do
# h1.main 'Hello World'
# end
# ```
#
# You can mix and match as you'd like (`div.klass.klass1.id!`), but you can
# only provide content and attributes on the *last* call:
#
# ``` ruby
# # This is not valid:
# form(:action => :post).world do
# input
# end
#
# # But this is:
# form.world(:action => :post) do
# input
# end
# ```
#
# ## 3. Text
#
# Sometimes you need to insert plain text:
#
# ```ruby
# p.author do
# text 'Written by '
# a 'Bluebie', :href => 'http://creativepony.com/'
# br
# text link_to 'Home', '/'
# end
# ```
#
# ``` html
#
# Written by
# Bluebie
#
# Home
#
# ```
#
# You can also insert literal text by returning it from a block (or passing
# it as a parameter to the non-block form of a tag method):
#
# ``` ruby
# p.author do
# link_to 'Home', '/'
# end
# ```
#
# ``` html
#
# Home
#
# ```
#
# Be aware that Rumble ignores the string in a block if there's other tags
# there:
#
# ``` ruby
# div.comment do
# div.author "BitPuffin"
# "Silence!
"
# end
# ```
#
# ``` html
#
# ```
#
# ## 4. Escaping
#
# The version of Rumble that's embedded in Keynote follows normal Rails
# escaping rules. When text enters Rumble (by returning it from a block,
# passing it as a parameter to a tag method, or using the `text` method),
# it's escaped if and only if `html_safe?` returns false. That means that
# Rails helpers generally don't need special treatment, but strings need to
# have `html_safe` called on them to avoid escaping.
#
# ## 5. In practice
#
# ``` ruby
# class ArticlePresenter < Keynote::Presenter
# presents :article
#
# def published_at
# build_html do
# div.published_at do
# span.date publication_date
# span.time publication_time
# end
# end
# end
#
# def publication_date
# article.published_at.strftime("%A, %B %e").squeeze(" ")
# end
#
# def publication_time
# article.published_at.strftime("%l:%M%p").delete(" ")
# end
# end
# ```
#
# @author Rumble is (c) 2011 Magnus Holm (https://github.com/judofyr).
# @author Documentation mostly borrowed from Mab, (c) 2012 Magnus Holm.
# @see https://github.com/judofyr/rumble
# @see https://github.com/camping/mab
module Rumble
# A class for exceptions raised by Rumble.
class Error < StandardError
end
# A basic set of commonly-used HTML tags. These are included as methods
# on all presenters by default.
BASIC = %w[a b br button del div em form h1 h2 h3 h4 h5 h6 hr i img input
label li link ol optgroup option p pre script select span strong sub sup
table tbody td textarea tfoot th thead time tr ul]
# A more complete set of HTML5 tags. You can use these by calling
# `Keynote::Rumble.use_html5_tags(self)` in a presenter's class body.
COMPLETE = %w[a abbr acronym address applet area article aside audio b base
basefont bdo big blockquote body br button canvas caption center cite
code col colgroup command datalist dd del details dfn dir div dl dt em
embed fieldset figcaption figure font footer form frame frameset h1
h6 head header hgroup hr i iframe img input ins keygen kbd label legend
li link map mark menu meta meter nav noframes noscript object ol optgroup
option output p param pre progress q rp rt ruby s samp script section
select small source span strike strong style sub summary sup table tbody
td textarea tfoot th thead time title tr tt u ul var video wbr xmp]
# @private
SELFCLOSING = %w[base meta link hr br param img area input col frame]
# @private
def self.included(base)
define_tags(base, BASIC)
end
# @private
def self.define_tags(base, tags)
tags.each do |tag|
sc = SELFCLOSING.include?(tag).inspect
base.class_eval <<-RUBY
def #{tag}(*args, &blk) # def a(*args, &blk)
rumble_tag :#{tag}, #{sc}, *args, &blk # rumble_tag :a, false, *args, &blk
end # end
RUBY
end
end
# We need our own copy of this, the normal Rails html_escape helper, so
# that we can access it from inside Tag objects.
# @private
def self.html_escape(s)
s = s.to_s
if s.html_safe?
s
else
s.gsub(/[&"'><]/, ERB::Util::HTML_ESCAPE).html_safe
end
end
# @private
class Context < Array
def to_s
join.html_safe
end
end
# @private
class Tag
def initialize(context, instance, name, sc)
@context = context
@instance = instance
@name = name
@sc = sc
end
def attributes
@attributes ||= {}
end
def merge_attributes(attrs)
if defined?(@attributes)
@attributes.merge!(attrs)
else
@attributes = attrs
end
end
def method_missing(name, content = nil, attrs = nil, &blk)
name = name.to_s
if name[-1] == ?!
attributes[:id] = name[0..-2]
else
if attributes.has_key?(:class)
attributes[:class] += " #{name}"
else
attributes[:class] = name
end
end
insert(content, attrs, &blk)
end
def insert(content = nil, attrs = nil, &blk)
raise Error, "This tag is already closed" if @done
if content.is_a?(Hash)
attrs = content
content = nil
end
merge_attributes(attrs) if attrs
if block_given?
raise Error, "`#{@name}` is not allowed to have content" if @sc
@done = :block
before = @context.size
res = yield
@content = Rumble.html_escape(res) if @context.size == before
@context << "#{@name}>"
elsif content
raise Error, "`#{@name}` is not allowed to have content" if @sc
@done = true
@content = Rumble.html_escape(content)
elsif attrs
@done = true
end
self
rescue
@instance.rumble_cleanup
raise $!
end
def to_ary; nil end
def to_str; to_s end
def html_safe?
true
end
def to_s
if @instance.rumble_context.eql?(@context)
@instance.rumble_cleanup
@context.to_s
else
@result ||= begin
res = "<#{@name}#{attrs_to_s}>"
res << @content if @content
res << "#{@name}>" if !@sc && @done != :block
res.html_safe
end
end
end
def inspect; to_s.inspect end
def attrs_to_s
attributes.inject("") do |res, (name, value)|
if value
value = (value == true) ? name : Rumble.html_escape(value)
res << " #{name}=\"#{value}\""
end
res
end
end
end
# Generate HTML using Rumble tag methods. If tag methods are called
# outside a `build_html` block, they'll raise an exception.
def build_html
ctx = @rumble_context
@rumble_context = Context.new
yield
rumble_cleanup(ctx).to_s
end
# Generate a text node. This is helpful in situations where an element
# contains both text and markup.
def text(str = nil, &blk)
str = Rumble.html_escape(str || blk.call)
if @rumble_context
@rumble_context << str
else
str
end
end
# @private
def rumble_context
@rumble_context
end
# @private
def rumble_cleanup(value = nil)
@rumble_context
ensure
@rumble_context = value
end
private
def rumble_tag(name, sc, content = nil, attrs = nil, &blk)
if !@rumble_context
raise Rumble::Error, "Must enclose tags in `rumble { ... }` block"
end
context = @rumble_context
tag = Tag.new(context, self, name, sc)
context << tag
tag.insert(content, attrs, &blk)
end
end
end