Throw exception if a file cannot be opened, fix travis-ci builds (#117)

* throw exception if a file cannot be opened

* Add a new function in utils.hpp: open_file_or_throw: This function
  returns an opened std::ifstream or throws by calling `inja_throw`.
* Use this function in Parser::load_file which previously returned an
  empty string if the file couldn't be opened.
* Use this function in Environment::load_json which previously threw
  a `nlohmann::detail::parse_error` if the file couldn't be opened.
* In Parser::parse_statement: When including files through `include`,
  do not attempt to (re-)parse templates from files that were already
  included. Additionally, this prevents inja from attempting to load
  in-memory templates by their name from disk.
* Add tests that check if an exception is thrown when attempting to
  open files that do not exist.

* cmake: enable C++11

* cmake: require C++11 when depending on single_inja

* code style
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c7b0e99..4c4f719 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -36,7 +36,7 @@
   $<INSTALL_INTERFACE:${INJA_INSTALL_INCLUDE_DIR}>
 )
 
-# target_compile_features(inja INTERFACE cxx_std_11)
+target_compile_features(inja INTERFACE cxx_std_11)
 
 set(INJA_PACKAGE_USE_EMBEDDED_JSON OFF)
 
@@ -89,6 +89,7 @@
 
 
   add_library(single_inja INTERFACE)
+  target_compile_features(single_inja INTERFACE cxx_std_11)
   target_include_directories(single_inja INTERFACE single_include include third_party/include)
 
   add_executable(single_inja_test
diff --git a/include/inja/environment.hpp b/include/inja/environment.hpp
index 1c7901a..a590c5f 100644
--- a/include/inja/environment.hpp
+++ b/include/inja/environment.hpp
@@ -15,6 +15,7 @@
 #include "renderer.hpp"
 #include "string_view.hpp"
 #include "template.hpp"
+#include "utils.hpp"
 
 
 namespace inja {
@@ -144,7 +145,7 @@
 	}
 
   json load_json(const std::string& filename) {
-		std::ifstream file(m_impl->input_path + filename);
+		std::ifstream file = open_file_or_throw(m_impl->input_path + filename);
 		json j;
 		file >> j;
 		return j;
diff --git a/include/inja/parser.hpp b/include/inja/parser.hpp
index c2364ba..9992ea4 100644
--- a/include/inja/parser.hpp
+++ b/include/inja/parser.hpp
@@ -384,8 +384,10 @@
       }
       // sys::path::remove_dots(pathname, true, sys::path::Style::posix);
 
-      Template include_template = parse_template(pathname);
-      m_included_templates.emplace(pathname, include_template);
+      if (m_included_templates.find(pathname) == m_included_templates.end()) {
+        Template include_template = parse_template(pathname);
+        m_included_templates.emplace(pathname, include_template);
+      }
 
       // generate a reference bytecode
       tmpl.bytecodes.emplace_back(Bytecode::Op::Include, json(pathname), Bytecode::Flag::ValueImmediate);
@@ -505,7 +507,7 @@
   }
 
   std::string load_file(nonstd::string_view filename) {
-		std::ifstream file(static_cast<std::string>(filename));
+		std::ifstream file = open_file_or_throw(static_cast<std::string>(filename));
 		std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
 		return text;
 	}
diff --git a/include/inja/utils.hpp b/include/inja/utils.hpp
index e791af5..77c65c8 100644
--- a/include/inja/utils.hpp
+++ b/include/inja/utils.hpp
@@ -1,6 +1,7 @@
 #ifndef PANTOR_INJA_UTILS_HPP
 #define PANTOR_INJA_UTILS_HPP
 
+#include <fstream>
 #include <stdexcept>
 
 #include "string_view.hpp"
@@ -12,6 +13,17 @@
   throw std::runtime_error("[inja.exception." + type + "] " + message);
 }
 
+inline std::ifstream open_file_or_throw(const std::string& path) {
+  std::ifstream file;
+  file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
+  try {
+    file.open(path);
+  } catch(const std::ios_base::failure& e) {
+    inja_throw("file_error", "failed accessing file at '" + path + "'");
+  }
+  return file;
+}
+
 namespace string_view {
   inline nonstd::string_view slice(nonstd::string_view view, size_t start, size_t end) {
     start = std::min(start, view.size());
diff --git a/single_include/inja/inja.hpp b/single_include/inja/inja.hpp
index 679b420..0cb64f2 100644
--- a/single_include/inja/inja.hpp
+++ b/single_include/inja/inja.hpp
@@ -1693,6 +1693,7 @@
 #ifndef PANTOR_INJA_UTILS_HPP
 #define PANTOR_INJA_UTILS_HPP
 
+#include <fstream>
 #include <stdexcept>
 
 // #include "string_view.hpp"
@@ -1705,6 +1706,17 @@
   throw std::runtime_error("[inja.exception." + type + "] " + message);
 }
 
+inline std::ifstream open_file_or_throw(const std::string& path) {
+  std::ifstream file;
+  file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
+  try {
+    file.open(path);
+  } catch(const std::ios_base::failure& e) {
+    inja_throw("file_error", "failed accessing file at '" + path + "'");
+  }
+  return file;
+}
+
 namespace string_view {
   inline nonstd::string_view slice(nonstd::string_view view, size_t start, size_t end) {
     start = std::min(start, view.size());
@@ -2384,8 +2396,10 @@
       }
       // sys::path::remove_dots(pathname, true, sys::path::Style::posix);
 
-      Template include_template = parse_template(pathname);
-      m_included_templates.emplace(pathname, include_template);
+      if (m_included_templates.find(pathname) == m_included_templates.end()) {
+        Template include_template = parse_template(pathname);
+        m_included_templates.emplace(pathname, include_template);
+      }
 
       // generate a reference bytecode
       tmpl.bytecodes.emplace_back(Bytecode::Op::Include, json(pathname), Bytecode::Flag::ValueImmediate);
@@ -2505,7 +2519,7 @@
   }
 
   std::string load_file(nonstd::string_view filename) {
-		std::ifstream file(static_cast<std::string>(filename));
+		std::ifstream file = open_file_or_throw(static_cast<std::string>(filename));
 		std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
 		return text;
 	}
@@ -3184,6 +3198,8 @@
 
 // #include "template.hpp"
 
+// #include "utils.hpp"
+
 
 
 namespace inja {
@@ -3313,7 +3329,7 @@
 	}
 
   json load_json(const std::string& filename) {
-		std::ifstream file(m_impl->input_path + filename);
+		std::ifstream file = open_file_or_throw(m_impl->input_path + filename);
 		json j;
 		file >> j;
 		return j;
diff --git a/test/unit-files.cpp b/test/unit-files.cpp
index 25d3ca4..afd7614 100644
--- a/test/unit-files.cpp
+++ b/test/unit-files.cpp
@@ -23,6 +23,12 @@
 	SECTION("File includes should be rendered") {
 		CHECK( env.render_file(test_file_directory + "include.txt", data) == "Answer: Hello Jeff." );
 	}
+
+	SECTION("File error should throw") {
+		std::string path(test_file_directory + "does-not-exist");
+		CHECK_THROWS_WITH( env.load_file(path), "[inja.exception.file_error] failed accessing file at '" + path + "'" );
+		CHECK_THROWS_WITH( env.load_json(path), "[inja.exception.file_error] failed accessing file at '" + path + "'" );
+	}
 }
 
 TEST_CASE("complete-files") {
diff --git a/test/unit-renderer.cpp b/test/unit-renderer.cpp
index 6380948..310eb41 100644
--- a/test/unit-renderer.cpp
+++ b/test/unit-renderer.cpp
@@ -378,6 +378,7 @@
 
 		inja::Template t2 = env.parse("{% include \"greeting\" %}!");
 		CHECK( env.render(t2, data) == "Hello Peter!" );
+		CHECK_THROWS_WITH( env.parse("{% include \"does-not-exist\" %}!"), "[inja.exception.file_error] failed accessing file at 'does-not-exist'" );
 	}
 }