Repository: mhanberg/temple (elixir)

Issue:
SVG void elements don't have a slash
Temple is amazing so far. Thanks for this library. 

**Describe the bug**
SVG void elements nest siblings without a self-closing slash

**To Reproduce**
Steps to reproduce the behavior:
```elixir
def test do
  temple do
    rect width: "256"
    circle cx: "128"
  end
end
# "<rect width=\"256\">\n<circle cx=\"128\">\n"
```

In browser, unintended closing tag is added

```html
<!-- -->
<rect width="256">
  <circle cx="128">
</rect>
```

**Expected behavior**
A clear and concise description of what you expected to happen.

```elixir
rect width: "256"
circle cx: "128"
```

Should result in

```html
<!-- In browser, unintended closing tag is added -->
<rect width="256" />
<circle cx="128" />
```

Should equal

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS: macOS Monterey 12.7.6
 - Temple Version: 13
 - Elixir Version: 1.17
 - Erlang Version: 27

**Additional context**
Add any other context about the problem here.

Current workaround is `/: true` attribute. 

I think Temple could add a slash to all self-closing tags because HTML has no problem with them.

Pull request:
fix: void foreign elements get self closing tags
Closes #242

fix: add mathml tags to formatter


Patch:
diff --git a/.formatter.exs b/.formatter.exs
index 6d31ace..143ae4c 100644
--- a/.formatter.exs
+++ b/.formatter.exs
@@ -20,103 +20,29 @@ html = ~w[
 ]a
 
 svg = ~w[
-  circle
-  ellipse
-  line
-  path
-  polygon
-  polyline
-  rect
-  stop
-  use
-  a
-  altGlyph
-  altGlyphDef
-  altGlyphItem
-  animate
-  animateColor
-  animateMotion
-  animateTransform
-  animation
-  audio
-  canvas
-  clipPath
-  cursor
-  defs
-  desc
-  discard
-  feBlend
-  feColorMatrix
-  feComponentTransfer
-  feComposite
-  feConvolveMatrix
-  feDiffuseLighting
-  feDisplacementMap
-  feDistantLight
-  feDropShadow
-  feFlood
-  feFuncA
-  feFuncB
-  feFuncG
-  feFuncR
-  feGaussianBlur
-  feImage
-  feMerge
-  feMergeNode
-  feMorphology
-  feOffset
-  fePointLight
-  feSpecularLighting
-  feSpotLight
-  feTile
-  feTurbulence
-  filter
-  font
-  foreignObject
-  g
-  glyph
-  glyphRef
-  handler
-  hatch
-  hatchpath
-  hkern
-  iframe
-  image
-  linearGradient
-  listener
-  marker
-  mask
-  mesh
-  meshgradient
-  meshpatch
-  meshrow
-  metadata
-  mpath
-  pattern
-  prefetch
-  radialGradient
-  script
-  set
-  solidColor
-  solidcolor
-  style
-  svg
-  switch
-  symbol
-  tbreak
-  text
-  textArea
-  textPath
-  title
-  tref
-  tspan
-  unknown
-  video
-  view
-  vkern
+  circle ellipse line path polygon polyline rect stop use a
+  altGlyph altGlyphDef altGlyphItem animate animateColor animateMotion
+  animateTransform animation audio canvas clipPath cursor defs desc
+  discard feBlend feColorMatrix feComponentTransfer feComposite
+  feConvolveMatrix feDiffuseLighting feDisplacementMap feDistantLight
+  feDropShadow feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur
+  feImage feMerge feMergeNode feMorphology feOffset fePointLight
+  feSpecularLighting feSpotLight feTile feTurbulence filter font
+  foreignObject g glyph glyphRef handler hatch hatchpath hkern iframe
+  image linearGradient listener marker mask mesh meshgradient meshpatch
+  meshrow metadata mpath pattern prefetch radialGradient script set
+  solidColor solidcolor style svg switch symbol tbreak text textArea
+  textPath title tref tspan unknown video view vkern
 ]a
 
-locals_without_parens = Enum.map(temple ++ html ++ svg, &{&1, :*})
+mathml = ~w[
+  math mi mn mo ms mspace mtext
+  merror mfrac mpadded mphantom mroot mrow msqrt mstyle
+  mmultiscripts mover msub msubsup msup munder munderover
+  mtable mtd mtr annotation semantics mprescripts
+]a
+
+locals_without_parens = Enum.map(temple ++ html ++ svg ++ mathml, &{&1, :*})
 
 [
   import_deps: [:typed_struct],
diff --git a/lib/temple/parser.ex b/lib/temple/parser.ex
index 9f72dc9..4646f6e 100644
--- a/lib/temple/parser.ex
+++ b/lib/temple/parser.ex
@@ -205,6 +205,9 @@ defmodule Temple.Parser do
   def void_elements,
     do: @void_elements ++ Keyword.values(@void_svg_lookup) ++ Keyword.values(@void_mathml_lookup)
 
+  def foreign_void_elements,
+    do: Keyword.values(@void_svg_lookup) ++ Keyword.values(@void_mathml_lookup)
+
   def void_elements_aliases,
     do: @void_elements_aliases ++ @void_svg_aliases ++ @void_mathml_aliases
 
diff --git a/lib/temple/renderer.ex b/lib/temple/renderer.ex
index 270315f..3f44967 100644
--- a/lib/temple/renderer.ex
+++ b/lib/temple/renderer.ex
@@ -233,7 +233,14 @@ defmodule Temple.Renderer do
           end
       end
 
-    state.engine.handle_text(buffer, [], ">\n")
+    ending =
+      if ast.name in Temple.Parser.foreign_void_elements() do
+        "/>"
+      else
+        ">"
+      end
+
+    state.engine.handle_text(buffer, [], "#{ending}\n")
   end
 
   def render(buffer, state, %AnonymousFunctions{} = ast) do


Test patch:
diff --git a/test/temple/renderer_test.exs b/test/temple/renderer_test.exs
index 4c0670d..79607c2 100644
--- a/test/temple/renderer_test.exs
+++ b/test/temple/renderer_test.exs
@@ -67,6 +67,26 @@ defmodule Temple.RendererTest do
       assert_html expected, result
     end
 
+    test "foreign void elements get self closing tags" do
+      result =
+        Renderer.compile do
+          input type: "text"
+          mprescripts foo: "bar"
+          rect width: "256"
+          circle cx: "128"
+        end
+
+      # html
+      expected = """
+      <input type="text">
+      <mprescripts foo="bar"/>
+      <rect width="256"/>
+      <circle cx="128"/>
+      """
+
+      assert_html expected, result
+    end
+
     test "a match does not emit" do
       result =
         Renderer.compile do