Skip to content

Commit

Permalink
Tag helper (#859)
Browse files Browse the repository at this point in the history
  • Loading branch information
joeldrapper authored Feb 14, 2025
1 parent 7ceae6b commit 41a58c8
Show file tree
Hide file tree
Showing 4 changed files with 272 additions and 9 deletions.
80 changes: 71 additions & 9 deletions lib/phlex/html.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,88 @@ def doctype
nil
end

# Outputs an `<svg>` tag
# @return [nil]
# @see https://developer.mozilla.org/docs/Web/SVG/Element/svg
def svg(...)
# Outputs an `<svg>` tag.
#
# [MDN Docs](https://developer.mozilla.org/docs/Web/SVG/Element/svg)
# [Spec](https://html.spec.whatwg.org/#the-svg-element)
def svg(*, **, &)
if block_given?
super do
render Phlex::SVG.new do |svg|
yield(svg)
end
end
super { render Phlex::SVG.new(&) }
else
super
end
end

# Override to provide a filename for the HTML file
def filename
nil
end

# Returns the string "text/html"
def content_type
"text/html"
end

# Output an HTML tag dynamically, e.g:
#
# ```ruby
# @tag_name = :h1
# tag(@tag_name, class: "title")
# ```
def tag(name, **attributes, &)
state = @_state
block_given = block_given?
buffer = state.buffer

unless state.should_render?
yield(self) if block_given
return nil
end

unless Symbol === name
raise Phlex::ArgumentError.new("Expected the tag name to be a Symbol.")
end

if (tag = StandardElements.__registered_elements__[name]) || (tag = name.name.tr("_", "-")).include?("-")
if attributes.length > 0 # with attributes
if block_given # with content block
buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << ">"
if tag == "svg"
render Phlex::SVG.new(&)
else
__yield_content__(&)
end
buffer << "</#{tag}>"
else # without content
buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << "></#{tag}>"
end
else # without attributes
if block_given # with content block
buffer << ("<#{tag}>")
if tag == "svg"
render Phlex::SVG.new(&)
else
__yield_content__(&)
end
buffer << "</#{tag}>"
else # without content
buffer << "<#{tag}></#{tag}>"
end
end
elsif (tag = VoidElements.__registered_elements__[name])
if block_given
raise Phlex::ArgumentError.new("Void elements cannot have content blocks.")
end

if attributes.length > 0 # with attributes
buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << ">"
else # without attributes
buffer << "<#{tag}>"
end

nil
else
raise Phlex::ArgumentError.new("Invalid HTML tag: #{name}")
end
end
end
1 change: 1 addition & 0 deletions lib/phlex/sgml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ def safe(value)

alias_method :🦺, :safe

# Flush the current state to the output buffer.
def flush
@_state.flush
end
Expand Down
39 changes: 39 additions & 0 deletions lib/phlex/svg.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,50 @@ class Phlex::SVG < Phlex::SGML

include StandardElements

# Returns the string "image/svg+xml"
def content_type
"image/svg+xml"
end

# Override to provide a filename for the SVG file
def filename
nil
end

def tag(name, **attributes, &)
state = @_state
block_given = block_given?
buffer = state.buffer

unless state.should_render?
yield(self) if block_given
return nil
end

unless Symbol === name
raise Phlex::ArgumentError.new("Expected the tag name to be a Symbol.")
end

if (tag = StandardElements.__registered_elements__[name]) || (tag = name.name.tr("_", "-")).include?("-")
if attributes.length > 0 # with attributes
if block_given # with content block
buffer << "<#{tag}" << (Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << ">"
__yield_content__(&)
buffer << "</#{tag}>"
else # without content
buffer << "<#{tag}" << (::Phlex::ATTRIBUTE_CACHE[attributes] ||= __attributes__(attributes)) << "></#{tag}>"
end
else # without attributes
if block_given # with content block
buffer << ("<#{tag}>")
__yield_content__(&)
buffer << "</#{tag}>"
else # without content
buffer << "<#{tag}></#{tag}>"
end
end
else
raise Phlex::ArgumentError.new("Invalid SVG tag: #{name}")
end
end
end
161 changes: 161 additions & 0 deletions quickdraw/tag.test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# frozen_string_literal: true

class HTMLComponent < Phlex::HTML
def initialize(tag, **attributes)
@tag = tag
@attributes = attributes
end

def view_template(&)
tag(@tag, **@attributes, &)
end
end

class SVGComponent < Phlex::SVG
def initialize(tag, **attributes)
@tag = tag
@attributes = attributes
end

def view_template(&)
tag(@tag, **@attributes, &)
end
end

Phlex::HTML::VoidElements.__registered_elements__.each do |method_name, tag|
test "<#{tag}> HTML tag without attributes" do
output = HTMLComponent.call(tag.to_sym)

assert_equal_html output, <<~HTML.strip
<#{tag}>
HTML
end

test "<#{tag}> HTML tag with attributes" do
output = HTMLComponent.call(tag.to_sym, class: "class", id: "id", disabled: true)

assert_equal_html output, <<~HTML.strip
<#{tag} class="class" id="id" disabled>
HTML
end

test "<#{tag}> HTML tag with content" do
error = assert_raises Phlex::ArgumentError do
HTMLComponent.call(tag.to_sym) do
"Hello, world!"
end
end

assert_equal error.message, "Void elements cannot have content blocks."
end
end

Phlex::HTML::StandardElements.__registered_elements__.each do |method_name, tag|
test "<#{tag}> HTML tag without attributes" do
output = HTMLComponent.call(tag.to_sym)

assert_equal_html output, <<~HTML.strip
<#{tag}></#{tag}>
HTML
end

test "<#{tag}> HTML tag with attributes" do
output = HTMLComponent.call(tag.to_sym, class: "class", id: "id", disabled: true)

assert_equal_html output, <<~HTML.strip
<#{tag} class="class" id="id" disabled></#{tag}>
HTML
end

test "<#{tag}> HTML tag with content" do
output = HTMLComponent.call(tag.to_sym) do
"Hello, world!"
end

assert_equal_html output, <<~HTML.strip
<#{tag}>Hello, world!</#{tag}>
HTML
end

test "<#{tag}> HTML tag with content and attributes" do
output = HTMLComponent.call(tag.to_sym, class: "class", id: "id", disabled: true) do
"Hello, world!"
end

assert_equal_html output, <<~HTML.strip
<#{tag} class="class" id="id" disabled>Hello, world!</#{tag}>
HTML
end
end

Phlex::SVG::StandardElements.__registered_elements__.each do |method_name, tag|
test "<#{tag}> SVG tag without attributes" do
output = SVGComponent.call(tag.to_sym)

assert_equal_html output, <<~HTML.strip
<#{tag}></#{tag}>
HTML
end

test "<#{tag}> SVG tag with attributes" do
output = SVGComponent.call(tag.to_sym, class: "class", id: "id", disabled: true)

assert_equal_html output, <<~HTML.strip
<#{tag} class="class" id="id" disabled></#{tag}>
HTML
end

test "<#{tag}> SVG tag with content" do
output = SVGComponent.call(tag.to_sym) do
"Hello, world!"
end

assert_equal_html output, <<~HTML.strip
<#{tag}>Hello, world!</#{tag}>
HTML
end

test "<#{tag}> SVG tag with content and attributes" do
output = SVGComponent.call(tag.to_sym, class: "class", id: "id", disabled: true) do
"Hello, world!"
end

assert_equal_html output, <<~HTML.strip
<#{tag} class="class" id="id" disabled>Hello, world!</#{tag}>
HTML
end
end

test "svg tag in HTML" do
output = HTMLComponent.call(:svg) do |svg|
svg.circle(cx: 50, cy: 50, r: 40, fill: "red")
end

assert_equal_html output, <<~HTML.strip
<svg><circle cx="50" cy="50" r="40" fill="red"></circle></svg>
HTML
end

test "with invalid tag name" do
error = assert_raises Phlex::ArgumentError do
HTMLComponent.call(:invalidtag)
end

assert_equal error.message, "Invalid HTML tag: invalidtag"
end

test "with invalid tag name input type" do
error = assert_raises Phlex::ArgumentError do
HTMLComponent.call("div")
end

assert_equal error.message, "Expected the tag name to be a Symbol."
end

test "with custom tag name" do
output = HTMLComponent.call(:custom_tag)

assert_equal_html output, <<~HTML.strip
<custom-tag></custom-tag>
HTML
end

0 comments on commit 41a58c8

Please sign in to comment.