commit 4d81da87af8ff2d3224f2a99f1cb0df8bbd7272d
Author: tri <tri@thac.loan>
Date:   Wed Oct 1 21:15:13 2025 +0700

    add percentEncode util

diff --git a/src/html.zig b/src/html.zig
index 3e0bd52..d0c18f2 100644
--- a/src/html.zig
+++ b/src/html.zig
@@ -39,3 +39,37 @@ test "escape" {
         try t.expectEqualStrings(expected, w.buffered());
     }
 }
+
+pub fn percentEncode(out: *std.Io.Writer, text: []const u8) !void {
+    for (text) |char| {
+        switch (char) {
+            ':' => try out.writeAll("%3A"),
+            '/' => try out.writeAll("%2F"),
+            '?' => try out.writeAll("%3F"),
+            '#' => try out.writeAll("%23"),
+            '[' => try out.writeAll("%5B"),
+            ']' => try out.writeAll("%5D"),
+            '@' => try out.writeAll("%40"),
+            '!' => try out.writeAll("%21"),
+            '$' => try out.writeAll("%24"),
+            '&' => try out.writeAll("%26"),
+            '\'' => try out.writeAll("%27"),
+            '(' => try out.writeAll("%28"),
+            ')' => try out.writeAll("%29"),
+            '*' => try out.writeAll("%2A"),
+            '+' => try out.writeAll("%2B"),
+            ',' => try out.writeAll("%2C"),
+            ';' => try out.writeAll("%3B"),
+            '=' => try out.writeAll("%3D"),
+            '%' => try out.writeAll("%25"),
+            ' ' => try out.writeAll("%20"),
+            else => try out.writeByte(char),
+        }
+    }
+}
+
+pub fn percentEncodeAlloc(arena: std.mem.Allocator, text: []const u8) ![]const u8 {
+    var w = std.Io.Writer.Allocating.init(arena);
+    try percentEncode(&w.writer, text);
+    return w.written();
+}