diff --git a/maud/src/escape.rs b/maud/src/escape.rs
index f5c24d2..94cdeec 100644
--- a/maud/src/escape.rs
+++ b/maud/src/escape.rs
@@ -1,3 +1,7 @@
+// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+// !!!!! PLEASE KEEP THIS IN SYNC WITH `maud_macros/src/escape.rs` !!!!!
+// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
 extern crate alloc;
 
 use alloc::string::String;
diff --git a/maud_macros/src/escape.rs b/maud_macros/src/escape.rs
deleted file mode 120000
index fc68ead..0000000
--- a/maud_macros/src/escape.rs
+++ /dev/null
@@ -1 +0,0 @@
-../../maud/src/escape.rs
\ No newline at end of file
diff --git a/maud_macros/src/escape.rs b/maud_macros/src/escape.rs
new file mode 100644
index 0000000..49ece77
--- /dev/null
+++ b/maud_macros/src/escape.rs
@@ -0,0 +1,34 @@
+// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+// !!!!!!!! PLEASE KEEP THIS IN SYNC WITH `maud/src/escape.rs` !!!!!!!!!
+// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+extern crate alloc;
+
+use alloc::string::String;
+
+pub fn escape_to_string(input: &str, output: &mut String) {
+    for b in input.bytes() {
+        match b {
+            b'&' => output.push_str("&"),
+            b'<' => output.push_str("&lt;"),
+            b'>' => output.push_str("&gt;"),
+            b'"' => output.push_str("&quot;"),
+            _ => unsafe { output.as_mut_vec().push(b) },
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    extern crate alloc;
+
+    use super::escape_to_string;
+    use alloc::string::String;
+
+    #[test]
+    fn it_works() {
+        let mut s = String::new();
+        escape_to_string("<script>launchMissiles()</script>", &mut s);
+        assert_eq!(s, "&lt;script&gt;launchMissiles()&lt;/script&gt;");
+    }
+}