mirror of https://github.com/OpenMW/openmw.git
Load YAML files via Lua (feature 7590)
parent
e340b06411
commit
715efe892f
@ -0,0 +1,32 @@
|
|||||||
|
#include "markupbindings.hpp"
|
||||||
|
|
||||||
|
#include <components/lua/luastate.hpp>
|
||||||
|
#include <components/lua/yamlloader.hpp>
|
||||||
|
#include <components/resource/resourcesystem.hpp>
|
||||||
|
#include <components/vfs/manager.hpp>
|
||||||
|
#include <components/vfs/pathutil.hpp>
|
||||||
|
|
||||||
|
#include "../mwbase/environment.hpp"
|
||||||
|
|
||||||
|
#include "context.hpp"
|
||||||
|
|
||||||
|
namespace MWLua
|
||||||
|
{
|
||||||
|
sol::table initMarkupPackage(const Context& context)
|
||||||
|
{
|
||||||
|
sol::table api(context.mLua->sol(), sol::create);
|
||||||
|
|
||||||
|
auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
|
||||||
|
|
||||||
|
api["loadYaml"] = [lua = context.mLua, vfs](std::string_view fileName) {
|
||||||
|
auto normalizedName = VFS::Path::normalizeFilename(fileName);
|
||||||
|
auto file = vfs->getNormalized(normalizedName);
|
||||||
|
return LuaUtil::YamlLoader::load(*file, lua->sol());
|
||||||
|
};
|
||||||
|
api["decodeYaml"] = [lua = context.mLua](std::string_view inputData) {
|
||||||
|
return LuaUtil::YamlLoader::load(std::string(inputData), lua->sol());
|
||||||
|
};
|
||||||
|
|
||||||
|
return LuaUtil::makeReadOnly(api);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
#ifndef MWLUA_MARKUPBINDINGS_H
|
||||||
|
#define MWLUA_MARKUPBINDINGS_H
|
||||||
|
|
||||||
|
#include <sol/forward.hpp>
|
||||||
|
|
||||||
|
#include "context.hpp"
|
||||||
|
|
||||||
|
namespace MWLua
|
||||||
|
{
|
||||||
|
sol::table initMarkupPackage(const Context&);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // MWLUA_MARKUPBINDINGS_H
|
@ -0,0 +1,354 @@
|
|||||||
|
#include "gmock/gmock.h"
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <components/lua/yamlloader.hpp>
|
||||||
|
|
||||||
|
#include "../testing_util.hpp"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
template <typename T>
|
||||||
|
bool checkNumber(sol::state_view& lua, const std::string& inputData, T requiredValue)
|
||||||
|
{
|
||||||
|
sol::object result = LuaUtil::YamlLoader::load(inputData, lua);
|
||||||
|
if (result.get_type() != sol::type::number)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return result.as<T>() == requiredValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkBool(sol::state_view& lua, const std::string& inputData, bool requiredValue)
|
||||||
|
{
|
||||||
|
sol::object result = LuaUtil::YamlLoader::load(inputData, lua);
|
||||||
|
if (result.get_type() != sol::type::boolean)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return result.as<bool>() == requiredValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkNil(sol::state_view& lua, const std::string& inputData)
|
||||||
|
{
|
||||||
|
sol::object result = LuaUtil::YamlLoader::load(inputData, lua);
|
||||||
|
return result == sol::nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkNan(sol::state_view& lua, const std::string& inputData)
|
||||||
|
{
|
||||||
|
sol::object result = LuaUtil::YamlLoader::load(inputData, lua);
|
||||||
|
if (result.get_type() != sol::type::number)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return std::isnan(result.as<double>());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkString(sol::state_view& lua, const std::string& inputData, const std::string& requiredValue)
|
||||||
|
{
|
||||||
|
sol::object result = LuaUtil::YamlLoader::load(inputData, lua);
|
||||||
|
if (result.get_type() != sol::type::string)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return result.as<std::string>() == requiredValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkString(sol::state_view& lua, const std::string& inputData)
|
||||||
|
{
|
||||||
|
sol::object result = LuaUtil::YamlLoader::load(inputData, lua);
|
||||||
|
if (result.get_type() != sol::type::string)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return result.as<std::string>() == inputData;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LuaUtilYamlLoader, ScalarTypeDeduction)
|
||||||
|
{
|
||||||
|
sol::state lua;
|
||||||
|
|
||||||
|
ASSERT_TRUE(checkNil(lua, "null"));
|
||||||
|
ASSERT_TRUE(checkNil(lua, "Null"));
|
||||||
|
ASSERT_TRUE(checkNil(lua, "NULL"));
|
||||||
|
ASSERT_TRUE(checkNil(lua, "~"));
|
||||||
|
ASSERT_TRUE(checkNil(lua, ""));
|
||||||
|
ASSERT_FALSE(checkNil(lua, "NUll"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "NUll"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "'null'", "null"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "017", 17));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "-017", -17));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "+017", 17));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "17", 17));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "-17", -17));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "+17", 17));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "0o17", 15));
|
||||||
|
ASSERT_TRUE(checkString(lua, "-0o17"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "+0o17"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "0b1"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "1:00"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "'17'", "17"));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "0x17", 23));
|
||||||
|
ASSERT_TRUE(checkString(lua, "'-0x17'", "-0x17"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "'+0x17'", "+0x17"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "2.1e-05", 2.1e-5));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "-2.1e-05", -2.1e-5));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "+2.1e-05", 2.1e-5));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "2.1e+5", 210000));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "-2.1e+5", -210000));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "+2.1e+5", 210000));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "0.27", 0.27));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "-0.27", -0.27));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "+0.27", 0.27));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "2.7", 2.7));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "-2.7", -2.7));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "+2.7", 2.7));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, ".27", 0.27));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "-.27", -0.27));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "+.27", 0.27));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "27.", 27.0));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "-27.", -27.0));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "+27.", 27.0));
|
||||||
|
|
||||||
|
ASSERT_TRUE(checkNan(lua, ".nan"));
|
||||||
|
ASSERT_TRUE(checkNan(lua, ".NaN"));
|
||||||
|
ASSERT_TRUE(checkNan(lua, ".NAN"));
|
||||||
|
ASSERT_FALSE(checkNan(lua, "nan"));
|
||||||
|
ASSERT_FALSE(checkNan(lua, ".nAn"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "'.nan'", ".nan"));
|
||||||
|
ASSERT_TRUE(checkString(lua, ".nAn"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "1.7976931348623157E+308", std::numeric_limits<double>::max()));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "-1.7976931348623157E+308", std::numeric_limits<double>::lowest()));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "2.2250738585072014e-308", std::numeric_limits<double>::min()));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, ".inf", std::numeric_limits<double>::infinity()));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "+.inf", std::numeric_limits<double>::infinity()));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "-.inf", -std::numeric_limits<double>::infinity()));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, ".Inf", std::numeric_limits<double>::infinity()));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "+.Inf", std::numeric_limits<double>::infinity()));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "-.Inf", -std::numeric_limits<double>::infinity()));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, ".INF", std::numeric_limits<double>::infinity()));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "+.INF", std::numeric_limits<double>::infinity()));
|
||||||
|
ASSERT_TRUE(checkNumber(lua, "-.INF", -std::numeric_limits<double>::infinity()));
|
||||||
|
ASSERT_TRUE(checkString(lua, ".INf"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "-.INf"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "+.INf"));
|
||||||
|
|
||||||
|
ASSERT_TRUE(checkBool(lua, "true", true));
|
||||||
|
ASSERT_TRUE(checkBool(lua, "false", false));
|
||||||
|
ASSERT_TRUE(checkBool(lua, "True", true));
|
||||||
|
ASSERT_TRUE(checkBool(lua, "False", false));
|
||||||
|
ASSERT_TRUE(checkBool(lua, "TRUE", true));
|
||||||
|
ASSERT_TRUE(checkBool(lua, "FALSE", false));
|
||||||
|
ASSERT_TRUE(checkString(lua, "y"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "n"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "On"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "Off"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "YES"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "NO"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "TrUe"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "FaLsE"));
|
||||||
|
ASSERT_TRUE(checkString(lua, "'true'", "true"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LuaUtilYamlLoader, DepthLimit)
|
||||||
|
{
|
||||||
|
sol::state lua;
|
||||||
|
|
||||||
|
const std::string input = R"(
|
||||||
|
array1: &array1_alias
|
||||||
|
[
|
||||||
|
<: *array1_alias,
|
||||||
|
foo
|
||||||
|
]
|
||||||
|
)";
|
||||||
|
|
||||||
|
bool depthExceptionThrown = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
YAML::Node root = YAML::Load(input);
|
||||||
|
sol::object result = LuaUtil::YamlLoader::load(input, lua);
|
||||||
|
}
|
||||||
|
catch (const std::runtime_error& e)
|
||||||
|
{
|
||||||
|
ASSERT_EQ(std::string(e.what()), "Maximum layers depth exceeded, probably caused by a circular reference");
|
||||||
|
depthExceptionThrown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_TRUE(depthExceptionThrown);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LuaUtilYamlLoader, Collections)
|
||||||
|
{
|
||||||
|
sol::state lua;
|
||||||
|
|
||||||
|
sol::object map = LuaUtil::YamlLoader::load("{ x: , y: 2, 4: 5 }", lua);
|
||||||
|
ASSERT_EQ(map.as<sol::table>()["x"], sol::nil);
|
||||||
|
ASSERT_EQ(map.as<sol::table>()["y"], 2);
|
||||||
|
ASSERT_EQ(map.as<sol::table>()[4], 5);
|
||||||
|
|
||||||
|
sol::object array = LuaUtil::YamlLoader::load("[ 3, 4 ]", lua);
|
||||||
|
ASSERT_EQ(array.as<sol::table>()[1], 3);
|
||||||
|
|
||||||
|
sol::object emptyTable = LuaUtil::YamlLoader::load("{}", lua);
|
||||||
|
ASSERT_TRUE(emptyTable.as<sol::table>().empty());
|
||||||
|
|
||||||
|
sol::object emptyArray = LuaUtil::YamlLoader::load("[]", lua);
|
||||||
|
ASSERT_TRUE(emptyArray.as<sol::table>().empty());
|
||||||
|
|
||||||
|
ASSERT_THROW(LuaUtil::YamlLoader::load("{ null: 1 }", lua), std::runtime_error);
|
||||||
|
ASSERT_THROW(LuaUtil::YamlLoader::load("{ .nan: 1 }", lua), std::runtime_error);
|
||||||
|
|
||||||
|
const std::string scalarArrayInput = R"(
|
||||||
|
- First Scalar
|
||||||
|
- 1
|
||||||
|
- true)";
|
||||||
|
|
||||||
|
sol::object scalarArray = LuaUtil::YamlLoader::load(scalarArrayInput, lua);
|
||||||
|
ASSERT_EQ(scalarArray.as<sol::table>()[1], std::string("First Scalar"));
|
||||||
|
ASSERT_EQ(scalarArray.as<sol::table>()[2], 1);
|
||||||
|
ASSERT_EQ(scalarArray.as<sol::table>()[3], true);
|
||||||
|
|
||||||
|
const std::string scalarMapWithCommentsInput = R"(
|
||||||
|
string: 'str' # String value
|
||||||
|
integer: 65 # Integer value
|
||||||
|
float: 0.278 # Float value
|
||||||
|
bool: false # Boolean value)";
|
||||||
|
|
||||||
|
sol::object scalarMapWithComments = LuaUtil::YamlLoader::load(scalarMapWithCommentsInput, lua);
|
||||||
|
ASSERT_EQ(scalarMapWithComments.as<sol::table>()["string"], std::string("str"));
|
||||||
|
ASSERT_EQ(scalarMapWithComments.as<sol::table>()["integer"], 65);
|
||||||
|
ASSERT_EQ(scalarMapWithComments.as<sol::table>()["float"], 0.278);
|
||||||
|
ASSERT_EQ(scalarMapWithComments.as<sol::table>()["bool"], false);
|
||||||
|
|
||||||
|
const std::string mapOfArraysInput = R"(
|
||||||
|
x:
|
||||||
|
- 2
|
||||||
|
- 7
|
||||||
|
- true
|
||||||
|
y:
|
||||||
|
- aaa
|
||||||
|
- false
|
||||||
|
- 1)";
|
||||||
|
|
||||||
|
sol::object mapOfArrays = LuaUtil::YamlLoader::load(mapOfArraysInput, lua);
|
||||||
|
ASSERT_EQ(mapOfArrays.as<sol::table>()["x"][3], true);
|
||||||
|
ASSERT_EQ(mapOfArrays.as<sol::table>()["y"][1], std::string("aaa"));
|
||||||
|
|
||||||
|
const std::string arrayOfMapsInput = R"(
|
||||||
|
-
|
||||||
|
name: Name1
|
||||||
|
hr: 65
|
||||||
|
avg: 0.278
|
||||||
|
-
|
||||||
|
name: Name2
|
||||||
|
hr: 63
|
||||||
|
avg: 0.288)";
|
||||||
|
|
||||||
|
sol::object arrayOfMaps = LuaUtil::YamlLoader::load(arrayOfMapsInput, lua);
|
||||||
|
ASSERT_EQ(arrayOfMaps.as<sol::table>()[1]["avg"], 0.278);
|
||||||
|
ASSERT_EQ(arrayOfMaps.as<sol::table>()[2]["name"], std::string("Name2"));
|
||||||
|
|
||||||
|
const std::string arrayOfArraysInput = R"(
|
||||||
|
- [Name1, 65, 0.278]
|
||||||
|
- [Name2 , 63, 0.288])";
|
||||||
|
|
||||||
|
sol::object arrayOfArrays = LuaUtil::YamlLoader::load(arrayOfArraysInput, lua);
|
||||||
|
ASSERT_EQ(arrayOfArrays.as<sol::table>()[1][2], 65);
|
||||||
|
ASSERT_EQ(arrayOfArrays.as<sol::table>()[2][1], std::string("Name2"));
|
||||||
|
|
||||||
|
const std::string mapOfMapsInput = R"(
|
||||||
|
Name1: {hr: 65, avg: 0.278}
|
||||||
|
Name2 : {
|
||||||
|
hr: 63,
|
||||||
|
avg: 0.288,
|
||||||
|
})";
|
||||||
|
|
||||||
|
sol::object mapOfMaps = LuaUtil::YamlLoader::load(mapOfMapsInput, lua);
|
||||||
|
ASSERT_EQ(mapOfMaps.as<sol::table>()["Name1"]["hr"], 65);
|
||||||
|
ASSERT_EQ(mapOfMaps.as<sol::table>()["Name2"]["avg"], 0.288);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LuaUtilYamlLoader, Structures)
|
||||||
|
{
|
||||||
|
sol::state lua;
|
||||||
|
|
||||||
|
const std::string twoDocumentsInput
|
||||||
|
= "---\n"
|
||||||
|
" - First Scalar\n"
|
||||||
|
" - 2\n"
|
||||||
|
" - true\n"
|
||||||
|
"\n"
|
||||||
|
"---\n"
|
||||||
|
" - Second Scalar\n"
|
||||||
|
" - 3\n"
|
||||||
|
" - false";
|
||||||
|
|
||||||
|
sol::object twoDocuments = LuaUtil::YamlLoader::load(twoDocumentsInput, lua);
|
||||||
|
ASSERT_EQ(twoDocuments.as<sol::table>()[1][1], std::string("First Scalar"));
|
||||||
|
ASSERT_EQ(twoDocuments.as<sol::table>()[2][3], false);
|
||||||
|
|
||||||
|
const std::string anchorInput = R"(---
|
||||||
|
x:
|
||||||
|
- Name1
|
||||||
|
# Following node labeled as "a"
|
||||||
|
- &a Value1
|
||||||
|
y:
|
||||||
|
- *a # Subsequent occurrence
|
||||||
|
- Name2)";
|
||||||
|
|
||||||
|
sol::object anchor = LuaUtil::YamlLoader::load(anchorInput, lua);
|
||||||
|
ASSERT_EQ(anchor.as<sol::table>()["y"][1], std::string("Value1"));
|
||||||
|
|
||||||
|
const std::string compoundKeyInput = R"(
|
||||||
|
? - String1
|
||||||
|
- String2
|
||||||
|
: - 1
|
||||||
|
|
||||||
|
? [ String3,
|
||||||
|
String4 ]
|
||||||
|
: [ 2, 3, 4 ])";
|
||||||
|
|
||||||
|
ASSERT_THROW(LuaUtil::YamlLoader::load(compoundKeyInput, lua), std::runtime_error);
|
||||||
|
|
||||||
|
const std::string compactNestedMappingInput = R"(
|
||||||
|
- item : Item1
|
||||||
|
quantity: 2
|
||||||
|
- item : Item2
|
||||||
|
quantity: 4
|
||||||
|
- item : Item3
|
||||||
|
quantity: 11)";
|
||||||
|
|
||||||
|
sol::object compactNestedMapping = LuaUtil::YamlLoader::load(compactNestedMappingInput, lua);
|
||||||
|
ASSERT_EQ(compactNestedMapping.as<sol::table>()[2]["quantity"], 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(LuaUtilYamlLoader, Scalars)
|
||||||
|
{
|
||||||
|
sol::state lua;
|
||||||
|
|
||||||
|
const std::string literalScalarInput = R"(--- |
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c)";
|
||||||
|
|
||||||
|
ASSERT_TRUE(checkString(lua, literalScalarInput, "a\nb\nc"));
|
||||||
|
|
||||||
|
const std::string foldedScalarInput = R"(--- >
|
||||||
|
a
|
||||||
|
b
|
||||||
|
c)";
|
||||||
|
|
||||||
|
ASSERT_TRUE(checkString(lua, foldedScalarInput, "a b c"));
|
||||||
|
|
||||||
|
const std::string multiLinePlanarScalarsInput = R"(
|
||||||
|
plain:
|
||||||
|
This unquoted scalar
|
||||||
|
spans many lines.
|
||||||
|
|
||||||
|
quoted: "So does this
|
||||||
|
quoted scalar.\n")";
|
||||||
|
|
||||||
|
sol::object multiLinePlanarScalars = LuaUtil::YamlLoader::load(multiLinePlanarScalarsInput, lua);
|
||||||
|
ASSERT_TRUE(
|
||||||
|
multiLinePlanarScalars.as<sol::table>()["plain"] == std::string("This unquoted scalar spans many lines."));
|
||||||
|
ASSERT_TRUE(multiLinePlanarScalars.as<sol::table>()["quoted"] == std::string("So does this quoted scalar.\n"));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,241 @@
|
|||||||
|
#include "yamlloader.hpp"
|
||||||
|
|
||||||
|
#include <charconv>
|
||||||
|
#include <regex>
|
||||||
|
|
||||||
|
#include <components/misc/strings/format.hpp>
|
||||||
|
#include <components/misc/strings/lower.hpp>
|
||||||
|
|
||||||
|
namespace LuaUtil
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr uint64_t maxDepth = 250;
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::object YamlLoader::load(const std::string& input, const sol::state_view& lua)
|
||||||
|
{
|
||||||
|
std::vector<YAML::Node> rootNodes = YAML::LoadAll(input);
|
||||||
|
return LuaUtil::YamlLoader::load(rootNodes, lua);
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::object YamlLoader::load(std::istream& input, const sol::state_view& lua)
|
||||||
|
{
|
||||||
|
std::vector<YAML::Node> rootNodes = YAML::LoadAll(input);
|
||||||
|
return load(rootNodes, lua);
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::object YamlLoader::load(const std::vector<YAML::Node> rootNodes, const sol::state_view& lua)
|
||||||
|
{
|
||||||
|
if (rootNodes.empty())
|
||||||
|
return sol::nil;
|
||||||
|
|
||||||
|
if (rootNodes.size() == 1)
|
||||||
|
return getNode(rootNodes[0], lua, 0);
|
||||||
|
|
||||||
|
sol::table documentsTable(lua, sol::create);
|
||||||
|
for (const auto& root : rootNodes)
|
||||||
|
{
|
||||||
|
documentsTable.add(getNode(root, lua, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return documentsTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::object YamlLoader::getNode(const YAML::Node& node, const sol::state_view& lua, uint64_t depth)
|
||||||
|
{
|
||||||
|
if (depth >= maxDepth)
|
||||||
|
throw std::runtime_error("Maximum layers depth exceeded, probably caused by a circular reference");
|
||||||
|
|
||||||
|
++depth;
|
||||||
|
|
||||||
|
if (node.IsMap())
|
||||||
|
return getMap(node, lua, depth);
|
||||||
|
else if (node.IsSequence())
|
||||||
|
return getArray(node, lua, depth);
|
||||||
|
else if (node.IsScalar())
|
||||||
|
return getScalar(node, lua);
|
||||||
|
else if (node.IsNull())
|
||||||
|
return sol::nil;
|
||||||
|
|
||||||
|
nodeError(node, "An unknown YAML node encountered");
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::table YamlLoader::getMap(const YAML::Node& node, const sol::state_view& lua, uint64_t depth)
|
||||||
|
{
|
||||||
|
sol::table childTable(lua, sol::create);
|
||||||
|
|
||||||
|
for (const auto& pair : node)
|
||||||
|
{
|
||||||
|
if (pair.first.IsMap())
|
||||||
|
nodeError(pair.first, "Only scalar nodes can be used as keys, encountered map instead");
|
||||||
|
if (pair.first.IsSequence())
|
||||||
|
nodeError(pair.first, "Only scalar nodes can be used as keys, encountered array instead");
|
||||||
|
if (pair.first.IsNull())
|
||||||
|
nodeError(pair.first, "Only scalar nodes can be used as keys, encountered null instead");
|
||||||
|
|
||||||
|
auto key = getNode(pair.first, lua, depth);
|
||||||
|
if (key.get_type() == sol::type::number && std::isnan(key.as<double>()))
|
||||||
|
nodeError(pair.first, "Only scalar nodes can be used as keys, encountered nan instead");
|
||||||
|
|
||||||
|
childTable[key] = getNode(pair.second, lua, depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
return childTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::table YamlLoader::getArray(const YAML::Node& node, const sol::state_view& lua, uint64_t depth)
|
||||||
|
{
|
||||||
|
sol::table childTable(lua, sol::create);
|
||||||
|
|
||||||
|
for (const auto& child : node)
|
||||||
|
{
|
||||||
|
childTable.add(getNode(child, lua, depth));
|
||||||
|
}
|
||||||
|
|
||||||
|
return childTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
YamlLoader::ScalarType YamlLoader::getScalarType(const YAML::Node& node)
|
||||||
|
{
|
||||||
|
const auto& tag = node.Tag();
|
||||||
|
const auto& value = node.Scalar();
|
||||||
|
if (tag == "!")
|
||||||
|
return ScalarType::String;
|
||||||
|
|
||||||
|
// Note that YAML allows to explicitely specify a scalar type via tag (e.g. "!!bool"), but it makes no
|
||||||
|
// sense in Lua:
|
||||||
|
// 1. Both integers and floats use the "number" type prior to Lua 5.3
|
||||||
|
// 2. Strings can be quoted, which is more readable than "!!str"
|
||||||
|
// 3. Most of possible conversions are invalid or their result is unclear
|
||||||
|
// So ignore this feature for now.
|
||||||
|
if (tag != "?")
|
||||||
|
nodeError(node, "An invalid tag'" + tag + "' encountered");
|
||||||
|
|
||||||
|
if (value.empty())
|
||||||
|
return ScalarType::Null;
|
||||||
|
|
||||||
|
// Resolve type according to YAML 1.2 Core Schema (see https://yaml.org/spec/1.2.2/#103-core-schema)
|
||||||
|
static const std::regex boolRegex("true|True|TRUE|false|False|FALSE", std::regex_constants::extended);
|
||||||
|
if (std::regex_match(node.Scalar(), boolRegex))
|
||||||
|
return ScalarType::Boolean;
|
||||||
|
|
||||||
|
static const std::regex decimalRegex("[-+]?[0-9]+", std::regex_constants::extended);
|
||||||
|
if (std::regex_match(node.Scalar(), decimalRegex))
|
||||||
|
return ScalarType::Decimal;
|
||||||
|
|
||||||
|
static const std::regex floatRegex(
|
||||||
|
"[-+]?([.][0-9]+|[0-9]+([.][0-9]*)?)([eE][-+]?[0-9]+)?", std::regex_constants::extended);
|
||||||
|
if (std::regex_match(node.Scalar(), floatRegex))
|
||||||
|
return ScalarType::Float;
|
||||||
|
|
||||||
|
static const std::regex octalRegex("0o[0-7]+", std::regex_constants::extended);
|
||||||
|
if (std::regex_match(node.Scalar(), octalRegex))
|
||||||
|
return ScalarType::Octal;
|
||||||
|
|
||||||
|
static const std::regex hexdecimalRegex("0x[0-9a-fA-F]+", std::regex_constants::extended);
|
||||||
|
if (std::regex_match(node.Scalar(), hexdecimalRegex))
|
||||||
|
return ScalarType::Hexadecimal;
|
||||||
|
|
||||||
|
static const std::regex infinityRegex("[-+]?([.]inf|[.]Inf|[.]INF)", std::regex_constants::extended);
|
||||||
|
if (std::regex_match(node.Scalar(), infinityRegex))
|
||||||
|
return ScalarType::Infinity;
|
||||||
|
|
||||||
|
static const std::regex nanRegex("[.]nan|[.]NaN|[.]NAN", std::regex_constants::extended);
|
||||||
|
if (std::regex_match(node.Scalar(), nanRegex))
|
||||||
|
return ScalarType::NotNumber;
|
||||||
|
|
||||||
|
static const std::regex nullRegex("null|Null|NULL|~", std::regex_constants::extended);
|
||||||
|
if (std::regex_match(node.Scalar(), nullRegex))
|
||||||
|
return ScalarType::Null;
|
||||||
|
|
||||||
|
return ScalarType::String;
|
||||||
|
}
|
||||||
|
|
||||||
|
sol::object YamlLoader::getScalar(const YAML::Node& node, const sol::state_view& lua)
|
||||||
|
{
|
||||||
|
auto type = getScalarType(node);
|
||||||
|
const auto& value = node.Scalar();
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case ScalarType::Null:
|
||||||
|
return sol::nil;
|
||||||
|
case ScalarType::String:
|
||||||
|
return sol::make_object<std::string>(lua, value);
|
||||||
|
case ScalarType::NotNumber:
|
||||||
|
return sol::make_object<double>(lua, std::nan(""));
|
||||||
|
case ScalarType::Infinity:
|
||||||
|
{
|
||||||
|
if (!value.empty() && value[0] == '-')
|
||||||
|
return sol::make_object<double>(lua, -std::numeric_limits<double>::infinity());
|
||||||
|
|
||||||
|
return sol::make_object<double>(lua, std::numeric_limits<double>::infinity());
|
||||||
|
}
|
||||||
|
case ScalarType::Boolean:
|
||||||
|
{
|
||||||
|
if (Misc::StringUtils::lowerCase(value) == "true")
|
||||||
|
return sol::make_object<bool>(lua, true);
|
||||||
|
|
||||||
|
if (Misc::StringUtils::lowerCase(value) == "false")
|
||||||
|
return sol::make_object<bool>(lua, false);
|
||||||
|
|
||||||
|
nodeError(node, "Can not read a boolean value '" + value + "'");
|
||||||
|
}
|
||||||
|
case ScalarType::Decimal:
|
||||||
|
{
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
// std::from_chars does not support "+" sign
|
||||||
|
if (!value.empty() && value[0] == '+')
|
||||||
|
++offset;
|
||||||
|
|
||||||
|
int result = 0;
|
||||||
|
const auto status = std::from_chars(value.data() + offset, value.data() + value.size(), result);
|
||||||
|
if (status.ec == std::errc())
|
||||||
|
return sol::make_object<int>(lua, result);
|
||||||
|
|
||||||
|
nodeError(node, "Can not read a decimal value '" + value + "'");
|
||||||
|
}
|
||||||
|
case ScalarType::Float:
|
||||||
|
{
|
||||||
|
// Not all compilers support std::from_chars for floats
|
||||||
|
double result = 0.0;
|
||||||
|
bool success = YAML::convert<double>::decode(node, result);
|
||||||
|
if (success)
|
||||||
|
return sol::make_object<double>(lua, result);
|
||||||
|
|
||||||
|
nodeError(node, "Can not read a float value '" + value + "'");
|
||||||
|
}
|
||||||
|
case ScalarType::Hexadecimal:
|
||||||
|
{
|
||||||
|
int result = 0;
|
||||||
|
const auto status = std::from_chars(value.data() + 2, value.data() + value.size(), result, 16);
|
||||||
|
if (status.ec == std::errc())
|
||||||
|
return sol::make_object<int>(lua, result);
|
||||||
|
|
||||||
|
nodeError(node, "Can not read a hexadecimal value '" + value + "'");
|
||||||
|
}
|
||||||
|
case ScalarType::Octal:
|
||||||
|
{
|
||||||
|
int result = 0;
|
||||||
|
const auto status = std::from_chars(value.data() + 2, value.data() + value.size(), result, 8);
|
||||||
|
if (status.ec == std::errc())
|
||||||
|
return sol::make_object<int>(lua, result);
|
||||||
|
|
||||||
|
nodeError(node, "Can not read an octal value '" + value + "'");
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
nodeError(node, "An unknown scalar '" + value + "' encountered");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[[noreturn]] void YamlLoader::nodeError(const YAML::Node& node, const std::string& message)
|
||||||
|
{
|
||||||
|
const auto& mark = node.Mark();
|
||||||
|
std::string error = Misc::StringUtils::format(
|
||||||
|
" at line=%d column=%d position=%d", mark.line + 1, mark.column + 1, mark.pos + 1);
|
||||||
|
throw std::runtime_error(message + error);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
#ifndef COMPONENTS_LUA_YAMLLOADER_H
|
||||||
|
#define COMPONENTS_LUA_YAMLLOADER_H
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <sol/sol.hpp>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <yaml-cpp/yaml.h>
|
||||||
|
|
||||||
|
namespace LuaUtil
|
||||||
|
{
|
||||||
|
|
||||||
|
class YamlLoader
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static sol::object load(const std::string& input, const sol::state_view& lua);
|
||||||
|
|
||||||
|
static sol::object load(std::istream& input, const sol::state_view& lua);
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class ScalarType
|
||||||
|
{
|
||||||
|
Boolean,
|
||||||
|
Decimal,
|
||||||
|
Float,
|
||||||
|
Hexadecimal,
|
||||||
|
Infinity,
|
||||||
|
NotNumber,
|
||||||
|
Null,
|
||||||
|
Octal,
|
||||||
|
String
|
||||||
|
};
|
||||||
|
|
||||||
|
static sol::object load(const std::vector<YAML::Node> rootNodes, const sol::state_view& lua);
|
||||||
|
|
||||||
|
static sol::object getNode(const YAML::Node& node, const sol::state_view& lua, uint64_t depth);
|
||||||
|
|
||||||
|
static sol::table getMap(const YAML::Node& node, const sol::state_view& lua, uint64_t depth);
|
||||||
|
|
||||||
|
static sol::table getArray(const YAML::Node& node, const sol::state_view& lua, uint64_t depth);
|
||||||
|
|
||||||
|
static ScalarType getScalarType(const YAML::Node& node);
|
||||||
|
|
||||||
|
static sol::object getScalar(const YAML::Node& node, const sol::state_view& lua);
|
||||||
|
|
||||||
|
[[noreturn]] static void nodeError(const YAML::Node& node, const std::string& message);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // COMPONENTS_LUA_YAMLLOADER_H
|
@ -0,0 +1,7 @@
|
|||||||
|
Package openmw.markup
|
||||||
|
=====================
|
||||||
|
|
||||||
|
.. include:: version.rst
|
||||||
|
|
||||||
|
.. raw:: html
|
||||||
|
:file: generated_html/openmw_markup.html
|
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
-- `openmw.markup` allows to work with markup languages.
|
||||||
|
-- @module markup
|
||||||
|
-- @usage local markup = require('openmw.markup')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
-- Convert YAML data to Lua object
|
||||||
|
-- @function [parent=#markup] decodeYaml
|
||||||
|
-- @param #string inputData Data to decode. It has such limitations:
|
||||||
|
--
|
||||||
|
-- 1. YAML format of [version 1.2](https://yaml.org/spec/1.2.2) is used.
|
||||||
|
-- 2. Map keys should be scalar values (strings, booleans, numbers).
|
||||||
|
-- 3. YAML tag system is not supported.
|
||||||
|
-- 4. If scalar is quoted, it is treated like a string.
|
||||||
|
-- Othewise type deduction works according to YAML 1.2 [Core Schema](https://yaml.org/spec/1.2.2/#103-core-schema).
|
||||||
|
-- 5. Circular dependencies between YAML nodes are not allowed.
|
||||||
|
-- 6. Lua 5.1 does not have integer numbers - all numeric scalars use a #number type (which use a floating point).
|
||||||
|
-- 7. Integer scalars numbers values are limited by the "int" range. Use floating point notation for larger number in YAML files.
|
||||||
|
-- @return #any Lua object (can be table or scalar value).
|
||||||
|
-- @usage local result = markup.decodeYaml('{ "x": 1 }');
|
||||||
|
-- -- prints 1
|
||||||
|
-- print(result["x"])
|
||||||
|
|
||||||
|
---
|
||||||
|
-- Load YAML file from VFS to Lua object. Conventions are the same as in @{#markup.decodeYaml}.
|
||||||
|
-- @function [parent=#markup] loadYaml
|
||||||
|
-- @param #string fileName YAML file path in VFS.
|
||||||
|
-- @return #any Lua object (can be table or scalar value).
|
||||||
|
-- @usage -- file contains '{ "x": 1 }' data
|
||||||
|
-- local result = markup.loadYaml('test.yaml');
|
||||||
|
-- -- prints 1
|
||||||
|
-- print(result["x"])
|
||||||
|
|
||||||
|
|
||||||
|
return nil
|
Loading…
Reference in New Issue