|
|
@@ -1,10 +1,176 @@ |
|
|
|
#include <dds/build/plan/template.hpp> |
|
|
|
|
|
|
|
#include <dds/error/errors.hpp> |
|
|
|
#include <dds/library/root.hpp> |
|
|
|
#include <dds/util/fs.hpp> |
|
|
|
#include <dds/util/string.hpp> |
|
|
|
|
|
|
|
#include <ctre.hpp> |
|
|
|
#include <semester/json.hpp> |
|
|
|
|
|
|
|
#include <string> |
|
|
|
#include <string_view> |
|
|
|
|
|
|
|
using namespace dds; |
|
|
|
|
|
|
|
void render_template_plan::render(build_env_ref env) const { |
|
|
|
using json_data = semester::basic_data<semester::json_traits<std::allocator<void>>>; |
|
|
|
|
|
|
|
namespace { |
|
|
|
|
|
|
|
static constexpr ctll::fixed_string IDENT_RE{"([_a-zA-Z]\\w*)(.*)"}; |
|
|
|
|
|
|
|
std::string_view skip(std::string_view in) { |
|
|
|
auto nspace_pos = in.find_first_not_of(" \t\n\r\f"); |
|
|
|
in = in.substr(nspace_pos); |
|
|
|
if (starts_with(in, "/*")) { |
|
|
|
// It's a block comment. Find the block-end marker. |
|
|
|
auto block_end = in.find("*/"); |
|
|
|
if (block_end == in.npos) { |
|
|
|
throw_user_error<errc::template_error>("Unterminated block comment"); |
|
|
|
} |
|
|
|
in = in.substr(block_end + 2); |
|
|
|
// Recursively skip some more |
|
|
|
return skip(in); |
|
|
|
} |
|
|
|
if (starts_with(in, "//")) { |
|
|
|
more: |
|
|
|
// It's a line comment. Find the next not-continued newline |
|
|
|
auto cn_nl = in.find("\\\n"); |
|
|
|
auto nl = in.find("\n"); |
|
|
|
if (cn_nl < nl) { |
|
|
|
// The next newline is a continuation of the comment. Keep looking |
|
|
|
in = in.substr(nl + 1); |
|
|
|
goto more; |
|
|
|
} |
|
|
|
if (nl == in.npos) { |
|
|
|
// We've reached the end. Okay. |
|
|
|
return in.substr(nl); |
|
|
|
} |
|
|
|
} |
|
|
|
// Not a comment, and not whitespace. Okay. |
|
|
|
return in; |
|
|
|
} |
|
|
|
|
|
|
|
std::string stringify(const json_data& dat) { |
|
|
|
if (dat.is_bool()) { |
|
|
|
return dat.as_bool() ? "true" : "false"; |
|
|
|
} else if (dat.is_double()) { |
|
|
|
return std::to_string(dat.as_double()); |
|
|
|
} else if (dat.is_null()) { |
|
|
|
return "nullptr"; |
|
|
|
} else if (dat.is_string()) { |
|
|
|
/// XXX: This probably isn't quite enough sanitization for edge cases. |
|
|
|
auto str = dat.as_string(); |
|
|
|
str = replace(str, "\n", "\\n"); |
|
|
|
str = replace(str, "\"", "\\\""); |
|
|
|
return "\"" + str + "\""; |
|
|
|
} else { |
|
|
|
throw_user_error<errc::template_error>("Cannot render un-stringable data type"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
std::pair<std::string, std::string_view> eval_expr_tail(std::string_view in, const json_data& dat) { |
|
|
|
in = skip(in); |
|
|
|
if (starts_with(in, ".")) { |
|
|
|
// Accessing a subproperty of the data |
|
|
|
in.remove_prefix(1); |
|
|
|
in = skip(in); |
|
|
|
// We _must_ see an identifier |
|
|
|
auto [is_ident, ident, tail] = ctre::match<IDENT_RE>(in); |
|
|
|
if (!is_ident) { |
|
|
|
throw_user_error<errc::template_error>("Expected identifier following dot `.`"); |
|
|
|
} |
|
|
|
if (!dat.is_mapping()) { |
|
|
|
throw_user_error<errc::template_error>("Cannot use dot `.` on non-mapping object"); |
|
|
|
} |
|
|
|
auto& map = dat.as_mapping(); |
|
|
|
auto found = map.find(ident.to_view()); |
|
|
|
if (found == map.end()) { |
|
|
|
throw_user_error<errc::template_error>("No subproperty '{}'", ident.to_view()); |
|
|
|
} |
|
|
|
return eval_expr_tail(tail, found->second); |
|
|
|
} |
|
|
|
return {stringify(dat), in}; |
|
|
|
} |
|
|
|
|
|
|
|
std::pair<std::string, std::string_view> eval_primary_expr(std::string_view in, |
|
|
|
const json_data& dat) { |
|
|
|
in = skip(in); |
|
|
|
|
|
|
|
if (in.empty()) { |
|
|
|
throw_user_error<errc::template_error>("Expected primary expression"); |
|
|
|
} |
|
|
|
|
|
|
|
if (in.front() == '(') { |
|
|
|
in = in.substr(1); |
|
|
|
auto [ret, tail] = eval_primary_expr(in, dat); |
|
|
|
if (!starts_with(tail, ")")) { |
|
|
|
throw_user_error<errc::template_error>( |
|
|
|
"Expected closing parenthesis `)` following expression"); |
|
|
|
} |
|
|
|
return {ret, tail.substr(1)}; |
|
|
|
} |
|
|
|
|
|
|
|
auto [is_ident, ident, tail_1] = ctre::match<IDENT_RE>(in); |
|
|
|
|
|
|
|
if (is_ident) { |
|
|
|
auto& map = dat.as_mapping(); |
|
|
|
auto found = map.find(ident.to_view()); |
|
|
|
if (found == map.end()) { |
|
|
|
throw_user_error<errc::template_error>("Unknown top-level identifier '{}'", |
|
|
|
ident.to_view()); |
|
|
|
} |
|
|
|
|
|
|
|
return eval_expr_tail(tail_1, found->second); |
|
|
|
} |
|
|
|
|
|
|
|
return {"nope", in}; |
|
|
|
} |
|
|
|
|
|
|
|
std::string render_template(std::string_view tmpl, const library_root& lib) { |
|
|
|
std::string acc; |
|
|
|
std::string_view MARKER_STRING = "__dds"; |
|
|
|
|
|
|
|
// Fill out a data structure that will be exposed to the template |
|
|
|
json_data dat = json_data::mapping_type({ |
|
|
|
{ |
|
|
|
"lib", |
|
|
|
json_data::mapping_type{ |
|
|
|
{"name", lib.manifest().name}, |
|
|
|
{"root", lib.path().string()}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
}); |
|
|
|
|
|
|
|
while (!tmpl.empty()) { |
|
|
|
// Find the next marker in the template string |
|
|
|
auto next_marker = tmpl.find(MARKER_STRING); |
|
|
|
if (next_marker == tmpl.npos) { |
|
|
|
// We've reached the end of the template. Stop |
|
|
|
acc.append(tmpl); |
|
|
|
break; |
|
|
|
} |
|
|
|
// Append the string up to the next marker |
|
|
|
acc.append(tmpl.substr(0, next_marker)); |
|
|
|
// Consume up to the next marker |
|
|
|
tmpl = tmpl.substr(next_marker + MARKER_STRING.size()); |
|
|
|
auto next_not_space = tmpl.find_first_not_of(" \t"); |
|
|
|
if (next_not_space == tmpl.npos || tmpl[next_not_space] != '(') { |
|
|
|
throw_user_error<errc::template_error>( |
|
|
|
"Expected `(` following `__dds` identifier in template file"); |
|
|
|
} |
|
|
|
|
|
|
|
auto [inner, tail] = eval_primary_expr(tmpl, dat); |
|
|
|
acc.append(inner); |
|
|
|
tmpl = tail; |
|
|
|
} |
|
|
|
|
|
|
|
return acc; |
|
|
|
} |
|
|
|
|
|
|
|
} // namespace |
|
|
|
|
|
|
|
void render_template_plan::render(build_env_ref env, const library_root& lib) const { |
|
|
|
auto content = slurp_file(_source.path); |
|
|
|
|
|
|
|
// Calculate the destination of the template rendering |
|
|
@@ -12,5 +178,16 @@ void render_template_plan::render(build_env_ref env) const { |
|
|
|
dest.replace_filename(dest.stem().stem().filename().string() + dest.extension().string()); |
|
|
|
fs::create_directories(dest.parent_path()); |
|
|
|
|
|
|
|
fs::copy_file(_source.path, dest, fs::copy_options::overwrite_existing); |
|
|
|
auto result = render_template(content, lib); |
|
|
|
if (fs::is_regular_file(dest)) { |
|
|
|
auto existing_content = slurp_file(dest); |
|
|
|
if (result == existing_content) { |
|
|
|
/// The content of the file has not changed. Do not write a file. |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
auto ofile = open(dest, std::ios::binary | std::ios::out); |
|
|
|
ofile << result; |
|
|
|
ofile.close(); // Throw any exceptions while closing the file |
|
|
|
} |