Skip to content

Instantly share code, notes, and snippets.

@baweaver
Last active August 3, 2021 19:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save baweaver/53aef0b33a9b4cc12fca631371c0aa06 to your computer and use it in GitHub Desktop.
Save baweaver/53aef0b33a9b4cc12fca631371c0aa06 to your computer and use it in GitHub Desktop.
Quick HTML builder idea
module HTML
VALID_TAGS = %i(html p div span a ol ul li strong em table thead tr td th tbody article aside)
SELF_CLOSING_TAGS = %i(img br hr)
# Evaluates a block passed to it for HTML tags. This is done to isolate
# defined tag methods from the outside world, hence `instance_eval` to
# evaluate that block in the context of an HTML structure.
class Evaluator
attr_reader :contents
# Creates an Evaluator
#
# @param &evaluator [Proc]
#
# @return [Evaluator]
def initialize(&evaluator)
@contents = []
instance_eval(&evaluator)
end
# Each tag has a method which ammends to the current contents captured by the
# evaluator. If it has the following:
#
# ul do
# li 'a'
# li 'b'
# end
#
# It will have two `Tag` instances in contents, and they'll both be those `li` tags.
#
# @param content [String]
# Literal content for the tag
#
# @param attributes [Hash[Symbol, Any]]
# HTML attributes, can include style, but does nothing special with that attribute currently
#
# @param evaluator [Proc]
# Block to run to get contents of the child tag
#
# @return [Array]
# Current contents of the evaluator
VALID_TAGS.each do |tag|
define_method(tag) do |content = nil, **attributes, &evaluator|
@contents << Tag.new(content, name: tag, **attributes, &evaluator)
end
end
SELF_CLOSING_TAGS.each do |tag|
define_method(tag) do |content = nil, **attributes, &evaluator|
@contents << Tag.new(content, name: tag, self_closing: true, **attributes, &evaluator)
end
end
end
# An HTML tag
class Tag
# What indentation should be based at
INDENT_LEVEL = 2
attr_reader :name, :attributes, :content
# Creates an HTML tag
#
# @param content = nil [String]
# Literal content for the tag, optional in the case of an evaluator being passed.
#
# @param name: [String]
# Name of the tag
#
# @param self_closing: false [Boolean]
# Whether or not the tag is self-closing, like `<br />`
#
# @param **attributes [Hash[Symbol, Any]]
# HTML attributes and their values
#
# @param &evaluator [Proc]
# Evaluator to evaluate potentially nested tags and their contents
#
# @return [Tag]
def initialize(content = nil, name:, self_closing: false, **attributes, &evaluator)
@name = name
@attributes = attributes
@self_closing = self_closing
@content = if content
content
elsif block_given?
Evaluator.new(&evaluator).contents
else
''
end
end
# HTML attributes as a String
#
# @return [String]
def serialized_attributes
return '' if attributes.empty?
' ' + attributes.map { |k, v| "#{k}='#{v}'" }.join(' ')
end
# Contents of the tag.
#
# @param indent: 0 [Integer]
# How much the current tag is indented by. This increases for deeper nesting
# for cleaner output.
#
# @return [String]
def serialized_contents(indent: 0)
# Prefix string for indentation, multiply one space by current count.
indentation = ' ' * indent
case content
when Array
content
.map { |tag| tag.to_html(indent: indent) }
.tap { |tags| tags.last.chomp! } # Take newline off of last tag
.join("\n")
when Tag
content.to_html(indent: indent)
else
"#{indentation}#{content}"
end
end
# Converts a tag to HTML
#
# @param indent: 0 [Integer]
# How much the current tag is indented by. This increases for deeper nesting
# for cleaner output.
#
# @return [String]
def to_html(indent: 0)
# Prefix string for indentation, multiply one space by current count.
indentation = ' ' * indent
# If the tag is self-closing we have a different output format
return "#{indentation}<#{name}#{serialized_attributes} />\n" if @self_closing
# Otherwise use HEREDOCs to create the HTML. Notice this amends one level of indentation
# for the serialized contents.
<<~HTML
#{indentation}<#{name}#{serialized_attributes}>
#{serialized_contents(indent: indent + INDENT_LEVEL)}
#{indentation}</#{name}>
HTML
end
end
# Utility method to evaluate an entire HTML document from one entry point.
#
# @param &evaluator [Proc]
# Evaluator to evaluate potentially nested tags and their contents
#
# @return [Tag]
def self.parse(&evaluator)
Tag.new(Evaluator.new(&evaluator).contents, name: :html)
end
# Generates an HTML document into a String directly without intermediary construction steps
# needed for the user. Most will prefer this method for general use.
#
# @param &evaluator [Proc]
# Evaluator to evaluate potentially nested tags and their contents
#
# @return [String]
def self.generate(&evaluator)
parse(&evaluator).to_html
end
end
results = HTML.generate do
strong 'test'
br
ul do
li 'a'
li 'b'
end
table do
thead do
th 'Name'
th 'Age'
end
tbody do
tr color: '#FF0' do
td 'Brandon'
td 30
end
tr color: 'yellow' do
td 'Alice'
td 42
end
end
end
end
puts results
@baweaver
Copy link
Author

Generates:

<html>
  <strong>
    test
  </strong>

  <br />

  <ul>
    <li>
      a
    </li>

    <li>
      b
    </li>
  </ul>

  <table>
    <thead>
      <th>
        Name
      </th>

      <th>
        Age
      </th>
    </thead>

    <tbody>
      <tr color='#FF0'>
        <td>
          Brandon
        </td>

        <td>
          30
        </td>
      </tr>

      <tr color='yellow'>
        <td>
          Alice
        </td>

        <td>
          42
        </td>
      </tr>
    </tbody>
  </table>
</html>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment