diff --git a/core/html/escape.odin b/core/html/escape.odin
new file mode 100644
index 000000000..dfdcc6f92
--- /dev/null
+++ b/core/html/escape.odin
@@ -0,0 +1,59 @@
+package html
+
+// escape_string escapes special characters like '&' to become '&'.
+// It escapes only 5 different characters: & ' < > and ".
+@(require_results)
+escape_string :: proc(s: string, allocator := context.allocator, loc := #caller_location) -> (output: string, was_allocation: bool) {
+ /*
+ & -> &
+ ' -> ' // ' is shorter than ' (NOTE: ' was not available until HTML 5)
+ < -> <
+ > -> >
+ " -> " // " is shorter than "
+ */
+
+ b := transmute([]byte)s
+
+ extra_bytes_needed := 0
+
+ for c in b {
+ switch c {
+ case '&': extra_bytes_needed += 4
+ case '\'': extra_bytes_needed += 4
+ case '<': extra_bytes_needed += 3
+ case '>': extra_bytes_needed += 3
+ case '"': extra_bytes_needed += 4
+ }
+ }
+
+ if extra_bytes_needed == 0 {
+ return s, false
+ }
+
+ t, err := make([]byte, len(s) + extra_bytes_needed, allocator, loc)
+ if err != nil {
+ return
+ }
+ was_allocation = true
+
+ w := 0
+ for c in b {
+ s := ""
+ switch c {
+ case '&': s = "&"
+ case '\'': s = "'"
+ case '<': s = "<"
+ case '>': s = ">"
+ case '"': s = """
+ }
+ if s != "" {
+ copy(t[w:], s)
+ w += len(s)
+ } else {
+ t[w] = c
+ w += 1
+ }
+ }
+ output = string(t[0:w])
+ return
+}
\ No newline at end of file