Last active
August 3, 2021 19:58
-
-
Save baweaver/53aef0b33a9b4cc12fca631371c0aa06 to your computer and use it in GitHub Desktop.
Quick HTML builder idea
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Generates: