diff --git a/src/language/ASTDotPrinter.cpp b/src/language/ASTDotPrinter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..270438e16b90d4cfd4c31ac35955878e9bcb81b1
--- /dev/null
+++ b/src/language/ASTDotPrinter.cpp
@@ -0,0 +1,43 @@
+#include <ASTDotPrinter.hpp>
+#include <EscapedString.hpp>
+
+#include <PEGGrammar.hpp>
+
+void
+ASTDotPrinter::_print(std::ostream& os, const ASTNode& node) const
+{
+  if (node.is_root()) {
+    os << "  x" << &node << " [ label=\"root \\n" << dataTypeName(node.m_data_type) << "\" ]\n";
+  } else {
+    if (node.has_content()) {
+      os << "  x" << &node << " [ label=\"" << node.name() << "\\n"
+         << node.string_view() << "\\n"
+         << dataTypeName(node.m_data_type) << "\" ]\n";
+    } else {
+      os << "  x" << &node << " [ label=\"" << node.name() << "\\n" << dataTypeName(node.m_data_type) << "\" ]\n";
+    }
+  }
+  if (!node.children.empty()) {
+    os << "  x" << &node << " -> { ";
+    for (auto& child : node.children) {
+      os << "x" << child.get() << ((child == node.children.back()) ? " }\n" : ", ");
+    }
+    for (auto& child : node.children) {
+      this->_print(os, *child);
+    }
+  }
+}
+
+std::ostream&
+operator<<(std::ostream& os, const ASTDotPrinter& ast_printer)
+{
+  os << "digraph parse_tree\n{\n";
+  ast_printer._print(os, ast_printer.m_node);
+  os << "}\n";
+  return os;
+}
+
+ASTDotPrinter::ASTDotPrinter(const ASTNode& node) : m_node{node}
+{
+  Assert(node.is_root());
+}
diff --git a/src/language/ASTDotPrinter.hpp b/src/language/ASTDotPrinter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..522f59b17a800664f21226da6f629c8aa83cebb3
--- /dev/null
+++ b/src/language/ASTDotPrinter.hpp
@@ -0,0 +1,25 @@
+#ifndef AST_DOT_PRINTER_HPP
+#define AST_DOT_PRINTER_HPP
+
+#include <ASTNode.hpp>
+
+class ASTDotPrinter
+{
+ private:
+  const ASTNode& m_node;
+
+  void _print(std::ostream& os, const ASTNode& node) const;
+
+ public:
+  friend std::ostream& operator<<(std::ostream& os, const ASTDotPrinter& ast_printer);
+
+  ASTDotPrinter(const ASTNode& node);
+
+  ASTDotPrinter(const ASTDotPrinter&) = delete;
+
+  ASTDotPrinter(ASTDotPrinter&&) = delete;
+
+  ~ASTDotPrinter() = default;
+};
+
+#endif   // AST_DOT_PRINTER_HPP
diff --git a/src/language/CMakeLists.txt b/src/language/CMakeLists.txt
index 6f9d049434b12ccbca3919d8dfcda247e853c23e..c99aa359270586271348d6e7b72a1392745baaf0 100644
--- a/src/language/CMakeLists.txt
+++ b/src/language/CMakeLists.txt
@@ -6,6 +6,7 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR})
 add_library(
   PugsLanguage
   ASTBuilder.cpp
+  ASTDotPrinter.cpp
   ASTNodeDataType.cpp
   ASTNodeAffectationExpressionBuilder.cpp
   ASTNodeBinaryOperatorExpressionBuilder.cpp
diff --git a/src/language/PugsParser.cpp b/src/language/PugsParser.cpp
index bdc4b87034e98e6cf1ebe5dca4621d4d7570812e..8f85044d192728a07edc34c2b8bded238ac38f2d 100644
--- a/src/language/PugsParser.cpp
+++ b/src/language/PugsParser.cpp
@@ -26,48 +26,11 @@
 #include <ASTSymbolInitializationChecker.hpp>
 #include <ASTSymbolTableBuilder.hpp>
 
+#include <ASTDotPrinter.hpp>
 #include <ASTPrinter.hpp>
 
 namespace language
 {
-namespace internal
-{
-void
-print_dot(std::ostream& os, const ASTNode& n)
-{
-  if (n.is_root()) {
-    os << "  x" << &n << " [ label=\"root \\n" << dataTypeName(n.m_data_type) << "\" ]\n";
-  } else {
-    if (n.has_content()) {
-      os << "  x" << &n << " [ label=\"" << n.name() << "\\n"
-         << n.string_view() << "\\n"
-         << dataTypeName(n.m_data_type) << "\" ]\n";
-    } else {
-      os << "  x" << &n << " [ label=\"" << n.name() << "\\n" << dataTypeName(n.m_data_type) << "\" ]\n";
-    }
-  }
-  if (!n.children.empty()) {
-    os << "  x" << &n << " -> { ";
-    for (auto& child : n.children) {
-      os << "x" << child.get() << ((child == n.children.back()) ? " }\n" : ", ");
-    }
-    for (auto& child : n.children) {
-      print_dot(os, *child);
-    }
-  }
-}
-
-}   // namespace internal
-
-void
-print_dot(std::ostream& os, const ASTNode& n)
-{
-  Assert(n.is_root());
-  os << "digraph parse_tree\n{\n";
-  internal::print_dot(os, n);
-  os << "}\n";
-}
-
 namespace internal
 {
 void
@@ -364,7 +327,8 @@ parser(const std::string& filename)
     {
       std::string dot_filename{"parse_tree.dot"};
       std::ofstream fout(dot_filename);
-      language::print_dot(fout, *root_node);
+      ASTDotPrinter dot_printer{*root_node};
+      fout << dot_printer;
       std::cout << "   AST dot file: " << dot_filename << '\n';
     }