From eceac8031aecb2044417756c1c4f91b76c7013c7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Asko=20N=C3=B5mm?= <asko@nth.ee>
Date: Thu, 7 Nov 2024 17:15:32 +0200
Subject: [PATCH] Auto fill boolean html attributes

---
 Htmt/Parser.cs          | 48 +++++++++++++++++++++++++++++++++++++++++
 HtmtTests/ParserTest.cs | 18 ++++++++++++++++
 2 files changed, 66 insertions(+)

diff --git a/Htmt/Parser.cs b/Htmt/Parser.cs
index 798685c..fb4fd16 100644
--- a/Htmt/Parser.cs
+++ b/Htmt/Parser.cs
@@ -72,6 +72,7 @@ private void Parse()
         _docType = GetDoctype(Template);
 
         RemoveDoctype();
+        FillBooleanAttributes();
         CloseVoidElements();
         TransformHtmlEntities();
 
@@ -190,6 +191,53 @@ private void CloseVoidElements()
             Template = Template.Replace(element, newElement);
         }
     }
+    
+    [GeneratedRegex(@"(<[^>]*?)(?<attr>\s[\w-]+)(?=\s|>|/>)", RegexOptions.IgnoreCase)]
+    private static partial Regex BooleanAttributeRegex(); 
+
+    /// <summary>
+    /// Fills boolean HTML attributes such as "checked" or "defer".
+    /// </summary>
+    private void FillBooleanAttributes()
+    {
+        var voidAttributes = new Dictionary<string, string>
+        {
+            { "allowfullscreen", "" },
+            { "async", "" },
+            { "autofocus", "" },
+            { "autoplay", "" },
+            { "checked", "" },
+            { "controls", "" },
+            { "default", "" },
+            { "defer", "" },
+            { "disabled", "" },
+            { "formnovalidate", "" },
+            { "hidden", "" },
+            { "ismap", "" },
+            { "itemscope", "" },
+            { "loop", "" },
+            { "multiple", "" },
+            { "muted", "" },
+            { "nomodule", "" },
+            { "novalidate", "" },
+            { "open", "" },
+            { "playsinline", "" },
+            { "readonly", "" },
+            { "required", "" },
+            { "reversed", "" },
+            { "selected", "" }
+        };
+        
+        foreach (Match match in BooleanAttributeRegex().Matches(Template))
+        {
+            var attribute = match.Groups["attr"].Value.Trim();
+
+            if (!voidAttributes.TryGetValue(attribute, out var val)) continue;
+
+            // Replace the attribute with the filled one
+            Template = Template.Replace(attribute, $"{attribute}=\"{val}\"");
+        }
+    }
 
     /// <summary>
     /// The regex for HTML entities.
diff --git a/HtmtTests/ParserTest.cs b/HtmtTests/ParserTest.cs
index 45e84e2..46002e9 100644
--- a/HtmtTests/ParserTest.cs
+++ b/HtmtTests/ParserTest.cs
@@ -115,4 +115,22 @@ public void TestRemoveXmlnsFromChildren()
 
         Assert.AreEqual("<html xmlns=\"http://www.w3.org/1999/xhtml\"><head></head><body><div><span>One</span></div><div><span>Two</span></div><div><span>Three</span></div></body></html>", parser.ToHtml());
     }
+
+    [TestMethod]
+    public void TestFillVoidAttributes()
+    {
+        const string template = "<html><head></head><body><input type=\"checkbox\" checked /></body></html>";
+        var parser = new Htmt.Parser { Template = template };
+        
+        Assert.AreEqual("<html><head></head><body><input type=\"checkbox\" checked=\"\" /></body></html>", parser.ToHtml());
+    }
+    
+    [TestMethod]
+    public void TestFillVoidAttributes2()
+    {
+        const string template = "<html><head><script defer src=\"\"></script></head><body></body></html>";
+        var parser = new Htmt.Parser { Template = template };
+        
+        Assert.AreEqual("<html><head><script defer=\"\" src=\"\"></script></head><body></body></html>", parser.ToHtml());
+    }
 }
\ No newline at end of file