Files
test/source/blender/blenkernel/intern/path_templates.cc
Campbell Barton 6a1fa176ef Cleanup: spelling in comments & duplicate terms (check_spelling.py)
Also minor clarification in doc-string.
2025-06-04 01:51:29 +00:00

916 lines
29 KiB
C++

/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <fmt/format.h>
#include "BLT_translation.hh"
#include "BLI_span.hh"
#include "BKE_context.hh"
#include "BKE_main.hh"
#include "BKE_path_templates.hh"
#include "BKE_scene.hh"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "DNA_ID_enums.h"
#include "DNA_node_types.h"
namespace blender::bke::path_templates {
bool VariableMap::contains(blender::StringRef name) const
{
if (this->strings_.contains(name)) {
return true;
}
if (this->integers_.contains(name)) {
return true;
}
if (this->floats_.contains(name)) {
return true;
}
return false;
}
bool VariableMap::remove(blender::StringRef name)
{
if (this->strings_.remove(name)) {
return true;
}
if (this->integers_.remove(name)) {
return true;
}
if (this->floats_.remove(name)) {
return true;
}
return false;
}
bool VariableMap::add_string(blender::StringRef name, blender::StringRef value)
{
if (this->contains(name)) {
return false;
}
this->strings_.add_new(name, value);
return true;
}
bool VariableMap::add_integer(blender::StringRef name, const int64_t value)
{
if (this->contains(name)) {
return false;
}
this->integers_.add_new(name, value);
return true;
}
bool VariableMap::add_float(blender::StringRef name, const double value)
{
if (this->contains(name)) {
return false;
}
this->floats_.add_new(name, value);
return true;
}
std::optional<blender::StringRefNull> VariableMap::get_string(blender::StringRef name) const
{
const std::string *value = this->strings_.lookup_ptr(name);
if (value == nullptr) {
return std::nullopt;
}
return blender::StringRefNull(*value);
}
std::optional<int64_t> VariableMap::get_integer(blender::StringRef name) const
{
const int64_t *value = this->integers_.lookup_ptr(name);
if (value == nullptr) {
return std::nullopt;
}
return *value;
}
std::optional<double> VariableMap::get_float(blender::StringRef name) const
{
const double *value = this->floats_.lookup_ptr(name);
if (value == nullptr) {
return std::nullopt;
}
return *value;
}
bool operator==(const Error &left, const Error &right)
{
return left.type == right.type && left.byte_range == right.byte_range;
}
} // namespace blender::bke::path_templates
using namespace blender::bke::path_templates;
std::optional<VariableMap> BKE_build_template_variables_for_prop(const bContext *C,
PointerRNA *ptr,
PropertyRNA *prop)
{
/*
* This function should be maintained such that it always produces variables
* consistent with the variables produced elsewhere in the code base for the
* same property. For example, render paths are processed in the rendering
* code and variables are built for that purpose there, and this function
* should produce variables consistent with that for those render path
* properties here.
*
* The recommended strategy when adding support for additional path templating
* use cases (that don't already have an appropriate
* `PropertyPathTemplateType` item) is to:
*
* 1. Create a separate function to build variables for that use case (see
* e.g. `BKE_build_template_variables_for_render_path()`).
* 2. Call that function from here in the switch statement below.
* 3. Also call that function from the other parts of the code base that need
* it.
*/
/* No property passed, or it doesn't support path templates. */
if (ptr == nullptr || prop == nullptr ||
(RNA_property_flag(prop) & PROP_PATH_SUPPORTS_TEMPLATES) == 0)
{
return std::nullopt;
}
switch (RNA_property_path_template_type(prop)) {
case PROP_VARIABLES_NONE: {
BLI_assert_msg(
false,
"Should never have `PROP_VARIABLES_NONE` for a path that supports path templates.");
return VariableMap();
}
/* Scene render output path, the compositor's File Output node's paths, etc. */
case PROP_VARIABLES_RENDER_OUTPUT: {
const RenderData *render_data;
if (GS(ptr->owner_id->name) == ID_SCE) {
render_data = &reinterpret_cast<const Scene *>(ptr->owner_id)->r;
}
else {
const Scene *scene = CTX_data_scene(C);
render_data = scene ? &scene->r : nullptr;
}
return BKE_build_template_variables_for_render_path(BKE_main_blendfile_path_from_global(),
render_data);
}
}
/* All paths that support path templates should be handled above, and any that
* aren't should already be rejected by the test at the top of the function. */
BLI_assert_unreachable();
return std::nullopt;
}
VariableMap BKE_build_template_variables_for_render_path(const char *blend_file_path,
const RenderData *render_data)
{
VariableMap variables;
/* Blend file name. */
if (blend_file_path) {
const char *file_name = BLI_path_basename(blend_file_path);
const char *file_name_end = BLI_path_extension_or_end(file_name);
if (file_name[0] == '\0') {
/* If the file has never been saved (indicated by an empty file name),
* default to "Unsaved". */
variables.add_string("blend_name", blender::StringRef(DATA_("Unsaved")));
}
else if (file_name_end == file_name) {
/* When the filename has no extension, but starts with a period. */
variables.add_string("blend_name", blender::StringRef(file_name));
}
else {
/* Normal case. */
variables.add_string("blend_name", blender::StringRef(file_name, file_name_end));
}
}
/* Render resolution and fps. */
if (render_data) {
int res_x, res_y;
BKE_render_resolution(render_data, false, &res_x, &res_y);
variables.add_integer("resolution_x", res_x);
variables.add_integer("resolution_y", res_y);
/* FPS eval code copied from `BKE_cachefile_filepath_get()`.
*
* TODO: should probably use one function for this everywhere to ensure that
* fps is computed consistently, but at the time of writing no such function
* seems to exist. Every place in the code base just has its own bespoke
* code, using different precision, etc. */
const double fps = double(render_data->frs_sec) / double(render_data->frs_sec_base);
variables.add_float("fps", fps);
}
return variables;
}
/* -------------------------------------------------------------------- */
#define FORMAT_BUFFER_SIZE 128
namespace {
enum class FormatSpecifierType {
/* No format specifier given. Use default formatting. */
NONE = 0,
/* The format specifier was a string of just "#" characters. E.g. "####". */
INTEGER,
/* The format specifier was a string of "#" characters with a single ".". E.g.
* "###.##". */
FLOAT,
/* The format specifier was invalid due to incorrect syntax. */
SYNTAX_ERROR,
};
/**
* Specifies how a variable should be formatted into a string, or indicates a
* parse error.
*/
struct FormatSpecifier {
FormatSpecifierType type = FormatSpecifierType::NONE;
/* For INTEGER and FLOAT formatting types, the number of digits indicated on
* either side of the decimal point. */
std::optional<uint8_t> integer_digit_count;
std::optional<uint8_t> fractional_digit_count;
};
enum class TokenType {
/* Either "{variable_name}" or "{variable_name:format_spec}". */
VARIABLE_EXPRESSION,
/* "{{", which is an escaped "{". */
LEFT_CURLY_BRACE,
/* "}}", which is an escaped "}". */
RIGHT_CURLY_BRACE,
/* Encountered a syntax error while trying to parse a variable expression. */
VARIABLE_SYNTAX_ERROR,
/* Encountered an unescaped curly brace in an invalid position. */
UNESCAPED_CURLY_BRACE_ERROR,
};
/**
* A token that was parsed and should be substituted in the string, or an error.
*/
struct Token {
TokenType type = TokenType::VARIABLE_EXPRESSION;
/* Byte index range (exclusive on the right) of the token or syntax error in
* the path string. */
blender::IndexRange byte_range;
/* Reference to the variable name as written in the template string. Note
* that this points into the template string, and does not own the value.
*
* Only relevant when `type == VARIABLE_EXPRESSION`. */
blender::StringRef variable_name;
/* Indicates how the variable's value should be formatted into a string. This
* is derived from the format specification (e.g. the "###" in "{blah:###}").
*
* Only relevant when `type == VARIABLE_EXPRESSION`. */
FormatSpecifier format;
};
} // namespace
/**
* Format an integer into a string, according to `format`.
*
* Note: if `format` is not valid for integers, the resulting string will be
* empty.
*
* \return length of the produced string. Zero indicates an error.
*/
static int format_int_to_string(const FormatSpecifier &format,
const int64_t integer_value,
char r_output_string[FORMAT_BUFFER_SIZE])
{
BLI_assert(format.type != FormatSpecifierType::SYNTAX_ERROR);
r_output_string[0] = '\0';
int output_length = 0;
switch (format.type) {
case FormatSpecifierType::NONE: {
output_length =
fmt::format_to_n(r_output_string, FORMAT_BUFFER_SIZE - 1, "{}", integer_value).size;
r_output_string[output_length] = '\0';
break;
}
case FormatSpecifierType::INTEGER: {
BLI_assert(format.integer_digit_count.has_value());
BLI_assert(*format.integer_digit_count > 0);
output_length = fmt::format_to_n(r_output_string,
FORMAT_BUFFER_SIZE - 1,
"{:0{}}",
integer_value,
*format.integer_digit_count)
.size;
r_output_string[output_length] = '\0';
break;
}
case FormatSpecifierType::FLOAT: {
/* Formatting an integer as a float: we do *not* defer to the float
* formatter for this because we could lose precision with very large
* numbers. Instead we simply print the integer, and then append ".000..."
* to it. */
BLI_assert(format.fractional_digit_count.has_value());
BLI_assert(*format.fractional_digit_count > 0);
if (format.integer_digit_count.has_value()) {
BLI_assert(*format.integer_digit_count > 0);
output_length = fmt::format_to_n(r_output_string,
FORMAT_BUFFER_SIZE - 1,
"{:0{}}",
integer_value,
*format.integer_digit_count)
.size;
}
else {
output_length =
fmt::format_to_n(r_output_string, FORMAT_BUFFER_SIZE - 1, "{}", integer_value).size;
}
r_output_string[output_length] = '.';
output_length++;
for (int i = 0; i < *format.fractional_digit_count && i < (FORMAT_BUFFER_SIZE - 1); i++) {
r_output_string[output_length] = '0';
output_length++;
}
r_output_string[output_length] = '\0';
break;
}
case FormatSpecifierType::SYNTAX_ERROR: {
BLI_assert_msg(
false,
"Format specifiers with invalid syntax should have been rejected before getting here.");
break;
}
}
return output_length;
}
/**
* Format a floating point number into a string, according to `format`.
*
* Note: if `format` is not valid for floating point numbers, the resulting
* string will be empty.
*
* \return length of the produced string. Zero indicates an error
*/
static int format_float_to_string(const FormatSpecifier &format,
const double float_value,
char r_output_string[FORMAT_BUFFER_SIZE])
{
BLI_assert(format.type != FormatSpecifierType::SYNTAX_ERROR);
r_output_string[0] = '\0';
int output_length = 0;
switch (format.type) {
case FormatSpecifierType::NONE: {
/* When no format specification is given, we attempt to replicate Python's
* behavior in the same situation. The only major thing we can't replicate
* via `libfmt` is that in Python whole numbers are printed with a trailing
* ".0". So we handle that bit manually. */
output_length =
fmt::format_to_n(r_output_string, FORMAT_BUFFER_SIZE - 1, "{}", float_value).size;
r_output_string[output_length] = '\0';
/* If the string consists only of digits and a possible negative sign, then
* we append a ".0" to match Python. */
if (blender::StringRef(r_output_string).find_first_not_of("-0123456789") ==
std::string::npos)
{
r_output_string[output_length] = '.';
r_output_string[output_length + 1] = '0';
r_output_string[output_length + 2] = '\0';
output_length += 2;
}
break;
}
case FormatSpecifierType::INTEGER: {
/* Defer to the integer formatter with a rounded value. */
return format_int_to_string(format, std::round(float_value), r_output_string);
}
case FormatSpecifierType::FLOAT: {
BLI_assert(format.fractional_digit_count.has_value());
BLI_assert(*format.fractional_digit_count > 0);
if (format.integer_digit_count.has_value()) {
/* Both integer and fractional component lengths are specified. */
BLI_assert(*format.integer_digit_count > 0);
output_length = fmt::format_to_n(r_output_string,
FORMAT_BUFFER_SIZE - 1,
"{:0{}.{}f}",
float_value,
*format.integer_digit_count +
*format.fractional_digit_count + 1,
*format.fractional_digit_count)
.size;
r_output_string[output_length] = '\0';
}
else {
/* Only fractional component length is specified. */
output_length = fmt::format_to_n(r_output_string,
FORMAT_BUFFER_SIZE - 1,
"{:.{}f}",
float_value,
*format.fractional_digit_count)
.size;
r_output_string[output_length] = '\0';
}
break;
}
case FormatSpecifierType::SYNTAX_ERROR: {
BLI_assert_msg(
false,
"Format specifiers with invalid syntax should have been rejected before getting here.");
break;
}
}
return output_length;
}
/**
* Parse the "format specifier" part of a variable expression.
*
* The format specifier is e.g. the "##.###" in "{name:##.###}". The specifier
* string should be passed alone (just the "##.###"), without the rest of the
* variable expression.
*/
static FormatSpecifier parse_format_specifier(blender::StringRef format_specifier)
{
FormatSpecifier format = {};
/* A ":" was used, but no format specifier was given, which is invalid. */
if (format_specifier.is_empty()) {
format.type = FormatSpecifierType::SYNTAX_ERROR;
return format;
}
/* If it's all digit specifiers, then format as an integer. */
if (format_specifier.find_first_not_of("#") == std::string::npos) {
format.integer_digit_count = format_specifier.size();
format.type = FormatSpecifierType::INTEGER;
return format;
}
/* If it's digit specifiers and a dot, format as a float. */
const int64_t dot_index = format_specifier.find_first_of('.');
const int64_t dot_index_last = format_specifier.find_last_of('.');
const bool found_dot = dot_index != std::string::npos;
const bool only_one_dot = dot_index == dot_index_last;
if (format_specifier.find_first_not_of(".#") == std::string::npos && found_dot && only_one_dot) {
blender::StringRef left = format_specifier.substr(0, dot_index);
blender::StringRef right = format_specifier.substr(dot_index + 1);
/* We currently require that the fractional digits are specified, so bail if
* they aren't. */
if (right.is_empty()) {
format.type = FormatSpecifierType::SYNTAX_ERROR;
return format;
}
if (!left.is_empty()) {
format.integer_digit_count = left.size();
}
format.fractional_digit_count = right.size();
format.type = FormatSpecifierType::FLOAT;
return format;
}
format.type = FormatSpecifierType::SYNTAX_ERROR;
return format;
}
/**
* Find and parse the next valid token in `path` starting from index
* `from_char`.
*
* \param path The path string to parse.
*
* \param from_char The char index to start from.
*
* \return The parsed token information, or nullopt if no token is found in
* `path`.
*/
static std::optional<Token> next_token(blender::StringRef path, const int from_char)
{
Token token;
/* We use the magic number -1 here to indicate that a component hasn't been
* found yet. When a component is found, the respective token here is set
* to the byte offset it was found at. */
int start = -1; /* "{" */
int format_specifier_split = -1; /* ":" */
int end = -1; /* "}" */
for (int byte_index = from_char; byte_index < path.size(); byte_index++) {
/* Check for escaped "{". */
if (start == -1 && (byte_index + 1) < path.size() && path[byte_index] == '{' &&
path[byte_index + 1] == '{')
{
Token token;
token.type = TokenType::LEFT_CURLY_BRACE;
token.byte_range = blender::IndexRange::from_begin_end(byte_index, byte_index + 2);
return token;
}
/* Check for escaped "}".
*
* Note that we only do this check when not already inside a variable
* expression, since it could be a valid closing "}" followed by additional
* escaped closing braces. */
if (start == -1 && (byte_index + 1) < path.size() && path[byte_index] == '}' &&
path[byte_index + 1] == '}')
{
token.type = TokenType::RIGHT_CURLY_BRACE;
token.byte_range = blender::IndexRange::from_begin_end(byte_index, byte_index + 2);
return token;
}
/* Check for unescaped "}", which outside of a variable expression is
* illegal. */
if (start == -1 && path[byte_index] == '}') {
token.type = TokenType::UNESCAPED_CURLY_BRACE_ERROR;
token.byte_range = blender::IndexRange::from_begin_end(byte_index, byte_index + 1);
return token;
}
/* Check if we've found a starting "{". */
if (path[byte_index] == '{') {
if (start != -1) {
/* Already inside a variable expression. */
token.type = TokenType::VARIABLE_SYNTAX_ERROR;
token.byte_range = blender::IndexRange::from_begin_end(start, byte_index);
return token;
}
start = byte_index;
format_specifier_split = -1;
continue;
}
/* If we haven't found a start, we shouldn't try to parse the other bits
* yet. */
if (start == -1) {
continue;
}
/* Check if we've found a format splitter. */
if (path[byte_index] == ':') {
if (format_specifier_split == -1) {
/* Only set if it's the first ":" we've encountered in the variable
* expression. Subsequent ones will be handled in the format specifier
* parsing. */
format_specifier_split = byte_index;
}
continue;
}
/* Check if we've found the closing "}". */
if (path[byte_index] == '}') {
end = byte_index + 1; /* Exclusive end. */
break;
}
}
/* No variable expression found. */
if (start == -1) {
return std::nullopt;
}
/* Unclosed variable expression. Syntax error. */
if (end == -1) {
token.type = TokenType::VARIABLE_SYNTAX_ERROR;
token.byte_range = blender::IndexRange::from_begin_end(start, path.size());
return token;
}
/* Parse the variable expression we found. */
token.byte_range = blender::IndexRange::from_begin_end(start, end);
if (format_specifier_split == -1) {
/* No format specifier. */
token.variable_name = path.substr(start + 1, (end - 1) - (start + 1));
}
else {
/* Found format specifier. */
token.variable_name = path.substr(start + 1, format_specifier_split - (start + 1));
token.format = parse_format_specifier(
path.substr(format_specifier_split + 1, (end - 1) - (format_specifier_split + 1)));
if (token.format.type == FormatSpecifierType::SYNTAX_ERROR) {
token.type = TokenType::VARIABLE_SYNTAX_ERROR;
return token;
}
}
return token;
}
/* Parse the given template and return the list of tokens found, in the same
* order as they appear in the template. */
static blender::Vector<Token> parse_template(blender::StringRef path)
{
blender::Vector<Token> tokens;
for (int bytes_read = 0; bytes_read < path.size();) {
const std::optional<Token> token = next_token(path, bytes_read);
if (!token.has_value()) {
break;
}
bytes_read = token->byte_range.one_after_last();
tokens.append(*token);
}
return tokens;
}
/* Convert a token to its corresponding syntax error. If the token doesn't have
* an error, returns nullopt. */
static std::optional<Error> token_to_syntax_error(const Token &token)
{
switch (token.type) {
case TokenType::VARIABLE_SYNTAX_ERROR: {
if (token.format.type == FormatSpecifierType::SYNTAX_ERROR) {
return {{ErrorType::FORMAT_SPECIFIER, token.byte_range}};
}
else {
return {{ErrorType::VARIABLE_SYNTAX, token.byte_range}};
}
}
case TokenType::UNESCAPED_CURLY_BRACE_ERROR: {
return {{ErrorType::UNESCAPED_CURLY_BRACE, token.byte_range}};
}
/* Non-errors. */
case TokenType::VARIABLE_EXPRESSION:
case TokenType::LEFT_CURLY_BRACE:
case TokenType::RIGHT_CURLY_BRACE:
return std::nullopt;
}
BLI_assert_msg(false, "Unhandled token type.");
return std::nullopt;
}
bool BKE_path_contains_template_syntax(blender::StringRef path)
{
return path.find_first_of("{}") != std::string_view::npos;
}
/**
* Evaluates the path template in `in_path` and writes the result to `out_path`
* if provided.
*
* \param out_path: buffer to write the evaluated path to. May be null, in which
* case writing is skipped, and this function just acts to validate the
* templating in the path.
*
* \param out_path_max_length: The maximum length that template expansion is
* allowed to make the template-expanded path (in bytes), including the null
* terminator. In general, this should be the size of the underlying allocation
* of `out_path`.
*
* \param template_variables: map of variables and their values to use during
* template substitution.
*
* \return An empty vector on success, or a vector of templating errors on
* failure. Note that even if there are errors, `out_path` may get modified, and
* it should be treated as bogus data in that case.
*/
static blender::Vector<Error> eval_template(char *out_path,
const int out_path_max_length,
blender::StringRef in_path,
const VariableMap &template_variables)
{
if (out_path) {
in_path.copy_utf8_truncated(out_path, out_path_max_length);
}
const blender::Vector<Token> tokens = parse_template(in_path);
if (tokens.is_empty()) {
/* No tokens found, so nothing to do. */
return {};
}
/* Accumulates errors as we process the tokens. */
blender::Vector<Error> errors;
/* Tracks the change in string length due to the modifications as we go. We
* need this to properly map the token byte ranges to the being-modified
* string. */
int length_diff = 0;
for (const Token &token : tokens) {
/* Syntax errors. */
if (std::optional<Error> error = token_to_syntax_error(token)) {
errors.append(*error);
continue;
}
char replacement_string[FORMAT_BUFFER_SIZE];
switch (token.type) {
/* Syntax errors should have been handled above. */
case TokenType::VARIABLE_SYNTAX_ERROR:
case TokenType::UNESCAPED_CURLY_BRACE_ERROR: {
BLI_assert_msg(false, "Unhandled syntax error.");
continue;
}
/* Curly brace escapes. */
case TokenType::LEFT_CURLY_BRACE: {
strcpy(replacement_string, "{");
break;
}
case TokenType::RIGHT_CURLY_BRACE: {
strcpy(replacement_string, "}");
break;
}
/* Expand variable expression into the variable's value. */
case TokenType::VARIABLE_EXPRESSION: {
if (std::optional<blender::StringRefNull> string_value = template_variables.get_string(
token.variable_name))
{
/* String variable found, but we only process it if there's no format
* specifier: string variables do not support format specifiers. */
if (token.format.type != FormatSpecifierType::NONE) {
/* String variables don't take format specifiers: error. */
errors.append({ErrorType::FORMAT_SPECIFIER, token.byte_range});
continue;
}
BLI_strncpy(replacement_string, string_value->c_str(), sizeof(replacement_string));
break;
}
if (std::optional<int64_t> integer_value = template_variables.get_integer(
token.variable_name))
{
/* Integer variable found. */
format_int_to_string(token.format, *integer_value, replacement_string);
break;
}
if (std::optional<double> float_value = template_variables.get_float(token.variable_name))
{
/* Float variable found. */
format_float_to_string(token.format, *float_value, replacement_string);
break;
}
/* No matching variable found: error. */
errors.append({ErrorType::UNKNOWN_VARIABLE, token.byte_range});
continue;
}
}
/* Perform the actual substitution with the expanded value. */
if (out_path) {
/* We're off the end of the available space. */
if (token.byte_range.start() + length_diff >= out_path_max_length) {
break;
}
BLI_string_replace_range(out_path,
out_path_max_length,
token.byte_range.start() + length_diff,
token.byte_range.one_after_last() + length_diff,
replacement_string);
length_diff -= token.byte_range.size();
length_diff += strlen(replacement_string);
}
}
return errors;
}
blender::Vector<Error> BKE_path_validate_template(
blender::StringRef path, const blender::bke::path_templates::VariableMap &template_variables)
{
return eval_template(nullptr, 0, path, template_variables);
}
blender::Vector<Error> BKE_path_apply_template(char *path,
int path_max_length,
const VariableMap &template_variables)
{
BLI_assert(path != nullptr);
blender::Vector<char> path_buffer(path_max_length);
const blender::Vector<Error> errors = eval_template(
path_buffer.data(), path_buffer.size(), path, template_variables);
if (errors.is_empty()) {
/* No errors, so copy the modified path back to the original. */
BLI_strncpy(path, path_buffer.data(), path_max_length);
}
return errors;
}
std::string BKE_path_template_error_to_string(const Error &error, blender::StringRef path)
{
blender::StringRef subpath = path.substr(error.byte_range.start(), error.byte_range.size());
switch (error.type) {
case ErrorType::UNESCAPED_CURLY_BRACE: {
return std::string("Unescaped curly brace '") + subpath + "'.";
}
case ErrorType::VARIABLE_SYNTAX: {
return std::string("Invalid or incomplete template expression '") + subpath + "'.";
}
case ErrorType::FORMAT_SPECIFIER: {
return std::string("Invalid format specifier in template expression '") + subpath + "'.";
}
case ErrorType::UNKNOWN_VARIABLE: {
return std::string("Unknown variable referenced in template expression '") + subpath + "'.";
}
}
BLI_assert_msg(false, "Unhandled error type.");
return "Unknown error.";
}
void BKE_report_path_template_errors(ReportList *reports,
const eReportType report_type,
blender::StringRef path,
blender::Span<Error> errors)
{
BLI_assert(!errors.is_empty());
std::string error_message = "Parse errors in path '" + path + "':";
for (const Error &error : errors) {
error_message += "\n- " + BKE_path_template_error_to_string(error, path);
}
BKE_report(reports, report_type, error_message.c_str());
}
std::optional<std::string> BKE_path_template_format_float(
const blender::StringRef format_specifier, const double value)
{
const FormatSpecifier format = parse_format_specifier(format_specifier);
if (format.type == FormatSpecifierType::SYNTAX_ERROR) {
return std::nullopt;
}
char buffer[FORMAT_BUFFER_SIZE];
format_float_to_string(format, value, buffer);
return buffer;
}
std::optional<std::string> BKE_path_template_format_int(const blender::StringRef format_specifier,
const int64_t value)
{
const FormatSpecifier format = parse_format_specifier(format_specifier);
if (format.type == FormatSpecifierType::SYNTAX_ERROR) {
return std::nullopt;
}
char buffer[FORMAT_BUFFER_SIZE];
format_int_to_string(format, value, buffer);
return buffer;
}