# frozen_string_literal: false
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
# `use_html_5_tags` in a presenter's class body.
COMPLETE = %w[abbr acronym address applet area article aside audio base
basefont bdo big blockquote body canvas caption center cite code col
colgroup command datalist dd details dfn dir dl dt embed fieldset
figcaption figure font footer frame frameset head header hgroup iframe
ins keygen kbd legend map mark menu meta meter nav noframes noscript
object output param progress q rp rt ruby s samp section small source
strike style summary title tt u 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, __FILE__, __LINE__ + 1
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
# @private
def self.use_html_5_tags(base)
define_tags(base, COMPLETE)
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
@done, @content = nil
end
def attributes
@attributes ||= {}
end
def merge_attributes(attrs)
if defined?(@attributes)
@attributes.merge!(attrs)
else
@attributes = attrs
end
end
def respond_to_missing?(name, include_private = false)
true
end
def method_missing(name, content = nil, attrs = nil, &blk)
name = name.to_s
if name[-1] == "!"
attributes[:id] = name[0..-2]
elsif attributes.has_key?(:class)
attributes[:class] += " #{name}"
else
attributes[:class] = name
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
attrs = flatten_attr_hash(attrs, :aria)
attrs = flatten_attr_hash(attrs, :data)
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
private
# If the given attrs contain an `aria` or `data` hash, flatten its
# contents into hyphenated attribute names.
def flatten_attr_hash(attrs, name)
if attrs && attrs[name].is_a?(Hash)
attrs = attrs.dup
attrs.delete(name).each do |key, value|
next if value.nil?
attrs[:"#{name}-#{key}"] = value.to_s
end
end
attrs
end
def attrs_to_s
attributes.inject("") do |res, (name, value)|
next res unless value
value =
if value.is_a?(Array)
value.map { |val| Rumble.html_escape(val) }.join(" ")
elsif value == true
name.to_s
else
Rumble.html_escape(value)
end
res << " #{name}=\"#{value.gsub('"', """)}\""
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
if defined?(@rumble_context)
ctx = @rumble_context
end
@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 defined?(@rumble_context) && @rumble_context
@rumble_context << str
else
str
end
end
# @private
def rumble_context
defined?(@rumble_context) ? @rumble_context : nil
end
# @private
def rumble_cleanup(value = nil)
defined?(@rumble_context) ? @rumble_context : nil
ensure
@rumble_context = value
end
private
def rumble_tag(name, sc, content = nil, attrs = nil, &blk)
if !defined?(@rumble_context) || !@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