diff --git a/src/language/PugsParser.cpp b/src/language/PugsParser.cpp
index 5696d752824beac0590f5fa5845a7b3db98645f8..08f9c48613731f25795401c85dcca2a942eb89d5 100644
--- a/src/language/PugsParser.cpp
+++ b/src/language/PugsParser.cpp
@@ -19,6 +19,8 @@
 #include <PEGGrammar.hpp>
 #include <SymbolTable.hpp>
 
+#include <EscapedString.hpp>
+
 #include <ASTNodeExpressionBuilder.hpp>
 
 namespace language
@@ -358,65 +360,7 @@ build_node_values(Node& n, std::shared_ptr<SymbolTable>& symbol_table)
         ss >> v;
         n.m_value = v;
       } else if (n.is<language::literal>()) {
-        const std::string& node_string = n.string();
-        std::stringstream ss;
-        for (size_t i = 1; i < node_string.size() - 1; ++i) {
-          char c = node_string[i];
-          if (c == '\\') {
-            ++i;
-            char next = node_string[i];
-            switch (next) {
-            case '\'': {
-              ss << '\'';
-              break;
-            }
-            case '"': {
-              ss << '\"';
-              break;
-            }
-            case '?': {
-              ss << '\?';
-              break;
-            }
-            case '\\': {
-              ss << '\\';
-              break;
-            }
-            case 'a': {
-              ss << '\a';
-              break;
-            }
-            case 'b': {
-              ss << '\b';
-              break;
-            }
-            case 'f': {
-              ss << '\f';
-              break;
-            }
-            case 'n': {
-              ss << '\n';
-              break;
-            }
-            case 'r': {
-              ss << '\r';
-              break;
-            }
-            case 't': {
-              ss << '\t';
-              break;
-            }
-            case 'v': {
-              ss << '\v';
-              break;
-            }
-            }
-          } else {
-            ss << node_string[i];
-          }
-        }
-
-        n.m_value = ss.str();
+        n.m_value = unescapeString(n.string());
       } else if (n.is<language::for_test>()) {
         // if AST contains a for_test statement, it means that no test were
         // given to the for-loop, so its value is always true
@@ -557,57 +501,7 @@ print(const Node& n)
       if constexpr (std::is_same_v<T, std::monostate>) {
         std::cout << "--";
       } else if constexpr (std::is_same_v<T, std::string>) {
-        const std::string& node_string = value;
-        std::stringstream ss;
-        for (size_t i = 0; i < node_string.size(); ++i) {
-          char c = node_string[i];
-          switch (c) {
-          case '\\': {
-            ss << R"(\\)";
-            break;
-          }
-          case '\"': {
-            ss << R"(\")";
-            break;
-          }
-          case '?': {
-            ss << R"(\?)";
-            break;
-          }
-          case '\a': {
-            ss << R"(\a)";
-            break;
-          }
-          case '\b': {
-            ss << R"(\b)";
-            break;
-          }
-          case '\f': {
-            ss << R"(\f)";
-            break;
-          }
-          case '\n': {
-            ss << R"(\n)";
-            break;
-          }
-          case '\r': {
-            ss << R"(\r)";
-            break;
-          }
-          case '\t': {
-            ss << R"(\t)";
-            break;
-          }
-          case '\v': {
-            ss << R"(\v)";
-            break;
-          }
-          default: {
-            ss << c;
-          }
-          }
-        }
-        std::cout << '\"' << ss.str() << '\"';
+        std::cout << '\"' << escapeString(value) << '\"';
       } else {
         std::cout << value;
       }
diff --git a/src/utils/EscapedString.hpp b/src/utils/EscapedString.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a89fea0277cef63923d57f4f6d18edf545ee5657
--- /dev/null
+++ b/src/utils/EscapedString.hpp
@@ -0,0 +1,127 @@
+#ifndef ESCAPED_STRING_HPP
+#define ESCAPED_STRING_HPP
+
+#include <sstream>
+#include <string>
+
+#include <PugsMacros.hpp>
+
+PUGS_INLINE std::string
+unescapeString(const std::string& input_string)
+{
+  std::stringstream ss;
+  for (size_t i = 1; i < input_string.size() - 1; ++i) {
+    char c = input_string[i];
+    if (c == '\\') {
+      ++i;
+      char next = input_string[i];
+      switch (next) {
+      case '\'': {
+        ss << '\'';
+        break;
+      }
+      case '"': {
+        ss << '\"';
+        break;
+      }
+      case '?': {
+        ss << '\?';
+        break;
+      }
+      case '\\': {
+        ss << '\\';
+        break;
+      }
+      case 'a': {
+        ss << '\a';
+        break;
+      }
+      case 'b': {
+        ss << '\b';
+        break;
+      }
+      case 'f': {
+        ss << '\f';
+        break;
+      }
+      case 'n': {
+        ss << '\n';
+        break;
+      }
+      case 'r': {
+        ss << '\r';
+        break;
+      }
+      case 't': {
+        ss << '\t';
+        break;
+      }
+      case 'v': {
+        ss << '\v';
+        break;
+      }
+      }
+    } else {
+      ss << input_string[i];
+    }
+  }
+
+  return ss.str();
+}
+
+PUGS_INLINE std::string
+escapeString(const std::string& input_string)
+{
+  std::stringstream ss;
+  for (size_t i = 0; i < input_string.size(); ++i) {
+    char c = input_string[i];
+    switch (c) {
+    case '\\': {
+      ss << R"(\\)";
+      break;
+    }
+    case '\"': {
+      ss << R"(\")";
+      break;
+    }
+    case '?': {
+      ss << R"(\?)";
+      break;
+    }
+    case '\a': {
+      ss << R"(\a)";
+      break;
+    }
+    case '\b': {
+      ss << R"(\b)";
+      break;
+    }
+    case '\f': {
+      ss << R"(\f)";
+      break;
+    }
+    case '\n': {
+      ss << R"(\n)";
+      break;
+    }
+    case '\r': {
+      ss << R"(\r)";
+      break;
+    }
+    case '\t': {
+      ss << R"(\t)";
+      break;
+    }
+    case '\v': {
+      ss << R"(\v)";
+      break;
+    }
+    default: {
+      ss << c;
+    }
+    }
+  }
+  return ss.str();
+}
+
+#endif   // ESCAPED_STRING_HPP