Blender V5.0
path_templates.cc
Go to the documentation of this file.
1/* SPDX-FileCopyrightText: 2025 Blender Authors
2 *
3 * SPDX-License-Identifier: GPL-2.0-or-later */
4
5#include <fmt/format.h>
6
7#include "BLT_translation.hh"
8
9#include "BLI_math_base.hh"
10#include "BLI_path_utils.hh"
11#include "BLI_span.hh"
12
13#include "BKE_context.hh"
14#include "BKE_library.hh"
15#include "BKE_main.hh"
16#include "BKE_path_templates.hh"
17#include "BKE_scene.hh"
18
19#include "RNA_access.hh"
20#include "RNA_prototypes.hh"
21
22#include "DNA_ID_enums.h"
23#include "DNA_node_types.h"
24
26
28{
29 if (this->strings_.contains(name)) {
30 return true;
31 }
32 if (this->filepaths_.contains(name)) {
33 return true;
34 }
35 if (this->integers_.contains(name)) {
36 return true;
37 }
38 if (this->floats_.contains(name)) {
39 return true;
40 }
41 return false;
42}
43
45{
46 if (this->strings_.remove(name)) {
47 return true;
48 }
49 if (this->filepaths_.remove(name)) {
50 return true;
51 }
52 if (this->integers_.remove(name)) {
53 return true;
54 }
55 if (this->floats_.remove(name)) {
56 return true;
57 }
58 return false;
59}
60
62{
63 if (this->contains(name)) {
64 return false;
65 }
66 this->strings_.add_new(name, value);
67 return true;
68}
69
71{
72 if (this->contains(name)) {
73 return false;
74 }
75 this->filepaths_.add_new(name, value);
76 return true;
77}
78
80{
81 if (this->contains(name)) {
82 return false;
83 }
84 this->integers_.add_new(name, value);
85 return true;
86}
87
89{
90 if (this->contains(name)) {
91 return false;
92 }
93 this->floats_.add_new(name, value);
94 return true;
95}
96
97std::optional<blender::StringRefNull> VariableMap::get_string(blender::StringRef name) const
98{
99 const std::string *value = this->strings_.lookup_ptr(name);
100 if (value == nullptr) {
101 return std::nullopt;
102 }
103 return blender::StringRefNull(*value);
104}
105
106std::optional<blender::StringRefNull> VariableMap::get_filepath(blender::StringRef name) const
107{
108 const std::string *value = this->filepaths_.lookup_ptr(name);
109 if (value == nullptr) {
110 return std::nullopt;
111 }
112 return blender::StringRefNull(*value);
113}
114
116{
117 const int64_t *value = this->integers_.lookup_ptr(name);
118 if (value == nullptr) {
119 return std::nullopt;
120 }
121 return *value;
122}
123
125{
126 const double *value = this->floats_.lookup_ptr(name);
127 if (value == nullptr) {
128 return std::nullopt;
129 }
130 return *value;
131}
132
134 StringRefNull full_path,
135 StringRef fallback)
136{
137 const char *file_name = BLI_path_basename(full_path.c_str());
138 const char *file_name_end = BLI_path_extension_or_end(file_name);
139
140 if (file_name[0] == '\0') {
141 /* If there is no file name, default to the fallback. */
142 return this->add_filepath(var_name, fallback);
143 }
144 else if (file_name_end == file_name) {
145 /* When the filename has no extension, but starts with a period. */
146 return this->add_filepath(var_name, StringRef(file_name));
147 }
148 else {
149 /* Normal case. */
150 return this->add_filepath(var_name, StringRef(file_name, file_name_end));
151 }
152}
153
155 StringRefNull full_path,
156 StringRef fallback)
157{
158 /* Empty path. */
159 if (full_path.is_empty()) {
160 return this->add_filepath(var_name, fallback);
161 }
162
163 /* No filename at the end. */
164 if (BLI_path_basename(full_path.c_str()) == full_path.end()) {
165 return this->add_filepath(var_name, full_path);
166 }
167
168 Vector<char> dir_path(full_path.size() + 1);
169 full_path.copy_unsafe(dir_path.data());
170
171 const bool success = BLI_path_parent_dir(dir_path.data());
172
173 if (!success || dir_path[0] == '\0') {
174 /* If no path before the filename, default to the fallback. */
175 return this->add_filepath(var_name, fallback);
176 }
177
178 return this->add_filepath(var_name, dir_path.data());
179}
180
181bool operator==(const Error &left, const Error &right)
182{
183 return left.type == right.type && left.byte_range == right.byte_range;
184}
185
186} // namespace blender::bke::path_templates
187
188using namespace blender::bke::path_templates;
189
190std::optional<VariableMap> BKE_build_template_variables_for_prop(const bContext *C,
192 PropertyRNA *prop)
193{
194 /*
195 * This function should be maintained such that it always produces variables
196 * consistent with the variables produced elsewhere in the code base for the
197 * same property. For example, render paths are processed in the rendering
198 * code and the variables for that purpose are built there; this function
199 * should produce variables consistent with that for the same render path
200 * properties here.
201 *
202 * This function is organized into three sections: one for "general"
203 * variables, one for "purpose-specific" variables, and one for
204 * "type-specific" variables. (See the top-level documentation in
205 * BKE_path_templates.hh for details on what that means).
206 *
207 * To add support for additional variables here:
208 *
209 * - For "general" variables, simply add them to
210 * #BKE_add_template_variables_general(). Nothing else special needs to be
211 * done.
212 * - For "purpose-specific" variables, add them to the appropriate
213 * purpose-specific function (e.g.
214 * #BKE_add_template_variables_for_render_path()). If no function exists
215 * for your purpose yet, add a new enum item to #PropertyPathTemplateType
216 * and a corresponding new function, add your variable to the new function,
217 * and then call it from the `switch` on #RNA_property_path_template_type()
218 * below.
219 * - For "type-specific" variables, add them to the appropriate type-specific
220 * function (e.g. #BKE_add_template_variables_for_node()). If no function
221 * exists for that type yet, create a new function for it, add the variable
222 * there, and then call it from the bottom section of this function, with an
223 * appropriate guard on the struct type.
224 */
225
226 /* No property passed, or it doesn't support path templates. */
227 if (ptr == nullptr || prop == nullptr ||
229 {
230 return std::nullopt;
231 }
232
233 VariableMap variables;
234
235 /* General variables. */
236 BKE_add_template_variables_general(variables, ptr->owner_id);
237
238 /* Purpose-specific variables. */
239 switch (RNA_property_path_template_type(prop)) {
240 case PROP_VARIABLES_NONE: {
241 /* Do nothing: no purpose-specific variables. */
242 break;
243 }
244
245 /* Scene render output path, the compositor's File Output node's paths, etc. */
247 const Scene *scene;
248 if (GS(ptr->owner_id->name) == ID_SCE) {
249 scene = reinterpret_cast<const Scene *>(ptr->owner_id);
250 }
251 else {
252 scene = CTX_data_scene(C);
253 }
254
256 break;
257 }
258 }
259
260 /* Type-specific variables. */
261
262 /* Nodes. */
263 if (std::optional<AncestorPointerRNA> node_rna_ptr = RNA_struct_search_closest_ancestor_by_type(
264 ptr, &RNA_Node))
265 {
266 const bNode *bnode = reinterpret_cast<const bNode *>(node_rna_ptr->data);
267 BKE_add_template_variables_for_node(variables, *bnode);
268 }
269
270 return variables;
271}
272
273void BKE_add_template_variables_general(VariableMap &variables, const ID *path_owner_id)
274{
275 /* Global blend filepath (a.k.a. path to the blend file that's currently
276 * open). */
277 {
278 const char *g_blend_file_path = BKE_main_blendfile_path_from_global();
279
280 variables.add_filename_only(
281 "blend_name", g_blend_file_path, blender::StringRef(DATA_("Unsaved")));
282
283 /* Note: fallback to `./` for unsaved files, which if used at the start of a
284 * path is equivalent to the current working directory. This is consistent
285 * with how `//` works. */
286 variables.add_path_up_to_file("blend_dir", g_blend_file_path, blender::StringRef("./"));
287 }
288
289 /* Library blend filepath (a.k.a. path to the blend file that actually owns the ID). */
290 if (path_owner_id) {
291 const char *lib_blend_file_path = ID_BLEND_PATH_FROM_GLOBAL(path_owner_id);
292 variables.add_filename_only(
293 "blend_name_lib", lib_blend_file_path, blender::StringRef(DATA_("Unsaved")));
294
295 /* Note: fallback to `./` for unsaved files, which if used at the start of a
296 * path is equivalent to the current working directory. This is consistent
297 * with how `//` works. */
298 variables.add_path_up_to_file("blend_dir_lib", lib_blend_file_path, blender::StringRef("./"));
299 }
300}
301
303{
304 /* Resolution variables. */
305 int res_x, res_y;
306 BKE_render_resolution(&scene.r, false, &res_x, &res_y);
307 variables.add_integer("resolution_x", res_x);
308 variables.add_integer("resolution_y", res_y);
309
310 /* FPS variable.
311 *
312 * FPS eval code copied from #BKE_cachefile_filepath_get().
313 *
314 * TODO: should probably use one function for this everywhere to ensure that
315 * fps is computed consistently, but at the time of writing no such function
316 * seems to exist. Every place in the code base just has its own bespoke
317 * code, using different precision, etc. */
318 const double fps = double(scene.r.frs_sec) / double(scene.r.frs_sec_base);
319 variables.add_float("fps", fps);
320
321 /* Scene name variable. */
322 variables.add_string("scene_name", scene.id.name + 2);
323
324 /* Camera name variable. */
325 if (scene.camera) {
326 variables.add_string("camera_name", scene.camera->id.name + 2);
327 }
328}
329
331 const bNode &owning_node)
332{
333 variables.add_string("node_name", owning_node.name);
334}
335
336/* -------------------------------------------------------------------- */
337
338#define FORMAT_BUFFER_SIZE 512
339
340namespace {
341
342enum class FormatSpecifierType {
343 /* No format specifier given. Use default formatting. */
344 NONE = 0,
345
346 /* The format specifier was a string of just "#" characters. E.g. "####". */
347 INTEGER,
348
349 /* The format specifier was a string of "#" characters with a single ".". E.g.
350 * "###.##". */
351 FLOAT,
352
353 /* The format specifier was invalid due to incorrect syntax. */
354 SYNTAX_ERROR,
355};
356
361struct FormatSpecifier {
362 FormatSpecifierType type = FormatSpecifierType::NONE;
363
364 /* For INTEGER and FLOAT formatting types, the number of digits indicated on
365 * either side of the decimal point. */
366 std::optional<uint8_t> integer_digit_count;
367 std::optional<uint8_t> fractional_digit_count;
368};
369
370enum class TokenType {
371 /* Either "{variable_name}" or "{variable_name:format_spec}". */
372 VARIABLE_EXPRESSION,
373
374 /* "{{", which is an escaped "{". */
375 LEFT_CURLY_BRACE,
376
377 /* "}}", which is an escaped "}". */
378 RIGHT_CURLY_BRACE,
379
380 /* Encountered a syntax error while trying to parse a variable expression. */
381 VARIABLE_SYNTAX_ERROR,
382
383 /* Encountered an unescaped curly brace in an invalid position. */
384 UNESCAPED_CURLY_BRACE_ERROR,
385};
386
390struct Token {
391 TokenType type = TokenType::VARIABLE_EXPRESSION;
392
393 /* Byte index range (exclusive on the right) of the token or syntax error in
394 * the path string. */
395 blender::IndexRange byte_range;
396
397 /* Reference to the variable name as written in the template string. Note
398 * that this points into the template string, and does not own the value.
399 *
400 * Only relevant when `type == VARIABLE_EXPRESSION`. */
401 blender::StringRef variable_name;
402
403 /* Indicates how the variable's value should be formatted into a string. This
404 * is derived from the format specification (e.g. the "###" in "{blah:###}").
405 *
406 * Only relevant when `type == VARIABLE_EXPRESSION`. */
407 FormatSpecifier format;
408};
409
410} // namespace
411
420static int format_int_to_string(const FormatSpecifier &format,
421 const int64_t integer_value,
422 char r_output_string[FORMAT_BUFFER_SIZE])
423{
424 BLI_assert(format.type != FormatSpecifierType::SYNTAX_ERROR);
425
426 r_output_string[0] = '\0';
427 int output_length = 0;
428
429 switch (format.type) {
430 case FormatSpecifierType::NONE: {
431 output_length =
432 fmt::format_to_n(r_output_string, FORMAT_BUFFER_SIZE - 1, "{}", integer_value).size;
433 r_output_string[output_length] = '\0';
434 break;
435 }
436
437 case FormatSpecifierType::INTEGER: {
438 BLI_assert(format.integer_digit_count.has_value());
439 BLI_assert(*format.integer_digit_count > 0);
440 output_length = fmt::format_to_n(r_output_string,
442 "{:0{}}",
443 integer_value,
444 *format.integer_digit_count)
445 .size;
446 r_output_string[output_length] = '\0';
447 break;
448 }
449
450 case FormatSpecifierType::FLOAT: {
451 /* Formatting an integer as a float: we do *not* defer to the float
452 * formatter for this because we could lose precision with very large
453 * numbers. Instead we simply print the integer, and then append ".000..."
454 * to it. */
455 BLI_assert(format.fractional_digit_count.has_value());
456 BLI_assert(*format.fractional_digit_count > 0);
457
458 if (format.integer_digit_count.has_value()) {
459 BLI_assert(*format.integer_digit_count > 0);
460 output_length = fmt::format_to_n(r_output_string,
462 "{:0{}}",
463 integer_value,
464 *format.integer_digit_count)
465 .size;
466 }
467 else {
468 output_length =
469 fmt::format_to_n(r_output_string, FORMAT_BUFFER_SIZE - 1, "{}", integer_value).size;
470 }
471
472 r_output_string[output_length] = '.';
473 output_length++;
474
475 for (int i = 0; i < *format.fractional_digit_count && i < (FORMAT_BUFFER_SIZE - 1); i++) {
476 r_output_string[output_length] = '0';
477 output_length++;
478 }
479
480 r_output_string[output_length] = '\0';
481
482 break;
483 }
484
485 case FormatSpecifierType::SYNTAX_ERROR: {
487 false,
488 "Format specifiers with invalid syntax should have been rejected before getting here.");
489 break;
490 }
491 }
492
493 return output_length;
494}
495
504static int format_float_to_string(const FormatSpecifier &format,
505 const double float_value,
506 char r_output_string[FORMAT_BUFFER_SIZE])
507{
508 BLI_assert(format.type != FormatSpecifierType::SYNTAX_ERROR);
509
510 r_output_string[0] = '\0';
511 int output_length = 0;
512
513 switch (format.type) {
514 case FormatSpecifierType::NONE: {
515 /* When no format specification is given, we attempt to replicate Python's
516 * behavior in the same situation. The only major thing we can't replicate
517 * via `libfmt` is that in Python whole numbers are printed with a trailing
518 * ".0". So we handle that bit manually. */
519 output_length =
520 fmt::format_to_n(r_output_string, FORMAT_BUFFER_SIZE - 1, "{}", float_value).size;
521 r_output_string[output_length] = '\0';
522
523 /* If the string consists only of digits and a possible negative sign, then
524 * we append a ".0" to match Python. */
525 if (blender::StringRef(r_output_string).find_first_not_of("-0123456789") ==
526 std::string::npos)
527 {
528 r_output_string[output_length] = '.';
529 r_output_string[output_length + 1] = '0';
530 r_output_string[output_length + 2] = '\0';
531 output_length += 2;
532 }
533 break;
534 }
535
536 case FormatSpecifierType::INTEGER: {
537 /* Defer to the integer formatter with a rounded value. */
538 return format_int_to_string(format, std::round(float_value), r_output_string);
539 }
540
541 case FormatSpecifierType::FLOAT: {
542 BLI_assert(format.fractional_digit_count.has_value());
543 BLI_assert(*format.fractional_digit_count > 0);
544
545 if (format.integer_digit_count.has_value()) {
546 /* Both integer and fractional component lengths are specified. */
547 BLI_assert(*format.integer_digit_count > 0);
548 output_length = fmt::format_to_n(r_output_string,
550 "{:0{}.{}f}",
551 float_value,
552 *format.integer_digit_count +
553 *format.fractional_digit_count + 1,
554 *format.fractional_digit_count)
555 .size;
556 r_output_string[output_length] = '\0';
557 }
558 else {
559 /* Only fractional component length is specified. */
560 output_length = fmt::format_to_n(r_output_string,
562 "{:.{}f}",
563 float_value,
564 *format.fractional_digit_count)
565 .size;
566 r_output_string[output_length] = '\0';
567 }
568
569 break;
570 }
571
572 case FormatSpecifierType::SYNTAX_ERROR: {
574 false,
575 "Format specifiers with invalid syntax should have been rejected before getting here.");
576 break;
577 }
578 }
579
580 return output_length;
581}
582
590static FormatSpecifier parse_format_specifier(blender::StringRef format_specifier)
591{
592 FormatSpecifier format = {};
593
594 /* A ":" was used, but no format specifier was given, which is invalid. */
595 if (format_specifier.is_empty()) {
596 format.type = FormatSpecifierType::SYNTAX_ERROR;
597 return format;
598 }
599
600 /* If it's all digit specifiers, then format as an integer. */
601 if (format_specifier.find_first_not_of("#") == std::string::npos) {
602 format.integer_digit_count = format_specifier.size();
603
604 format.type = FormatSpecifierType::INTEGER;
605 return format;
606 }
607
608 /* If it's digit specifiers and a dot, format as a float. */
609 const int64_t dot_index = format_specifier.find_first_of('.');
610 const int64_t dot_index_last = format_specifier.find_last_of('.');
611 const bool found_dot = dot_index != std::string::npos;
612 const bool only_one_dot = dot_index == dot_index_last;
613 if (format_specifier.find_first_not_of(".#") == std::string::npos && found_dot && only_one_dot) {
614 blender::StringRef left = format_specifier.substr(0, dot_index);
615 blender::StringRef right = format_specifier.substr(dot_index + 1);
616
617 /* We currently require that the fractional digits are specified, so bail if
618 * they aren't. */
619 if (right.is_empty()) {
620 format.type = FormatSpecifierType::SYNTAX_ERROR;
621 return format;
622 }
623
624 if (!left.is_empty()) {
625 format.integer_digit_count = left.size();
626 }
627
628 format.fractional_digit_count = right.size();
629
630 format.type = FormatSpecifierType::FLOAT;
631 return format;
632 }
633
634 format.type = FormatSpecifierType::SYNTAX_ERROR;
635 return format;
636}
637
648static std::optional<Token> next_token(blender::StringRef path, const int from_char)
649{
650 Token token;
651
652 /* We use the magic number -1 here to indicate that a component hasn't been
653 * found yet. When a component is found, the respective token here is set
654 * to the byte offset it was found at. */
655 int start = -1; /* "{" */
656 int format_specifier_split = -1; /* ":" */
657 int end = -1; /* "}" */
658
659 for (int byte_index = from_char; byte_index < path.size(); byte_index++) {
660 /* Check for escaped "{". */
661 if (start == -1 && (byte_index + 1) < path.size() && path[byte_index] == '{' &&
662 path[byte_index + 1] == '{')
663 {
664 Token token;
665 token.type = TokenType::LEFT_CURLY_BRACE;
666 token.byte_range = blender::IndexRange::from_begin_end(byte_index, byte_index + 2);
667 return token;
668 }
669
670 /* Check for escaped "}".
671 *
672 * Note that we only do this check when not already inside a variable
673 * expression, since it could be a valid closing "}" followed by additional
674 * escaped closing braces. */
675 if (start == -1 && (byte_index + 1) < path.size() && path[byte_index] == '}' &&
676 path[byte_index + 1] == '}')
677 {
678 token.type = TokenType::RIGHT_CURLY_BRACE;
679 token.byte_range = blender::IndexRange::from_begin_end(byte_index, byte_index + 2);
680 return token;
681 }
682
683 /* Check for unescaped "}", which outside of a variable expression is
684 * illegal. */
685 if (start == -1 && path[byte_index] == '}') {
686 token.type = TokenType::UNESCAPED_CURLY_BRACE_ERROR;
687 token.byte_range = blender::IndexRange::from_begin_end(byte_index, byte_index + 1);
688 return token;
689 }
690
691 /* Check if we've found a starting "{". */
692 if (path[byte_index] == '{') {
693 if (start != -1) {
694 /* Already inside a variable expression. */
695 token.type = TokenType::VARIABLE_SYNTAX_ERROR;
696 token.byte_range = blender::IndexRange::from_begin_end(start, byte_index);
697 return token;
698 }
699 start = byte_index;
700 format_specifier_split = -1;
701 continue;
702 }
703
704 /* If we haven't found a start, we shouldn't try to parse the other bits
705 * yet. */
706 if (start == -1) {
707 continue;
708 }
709
710 /* Check if we've found a format splitter. */
711 if (path[byte_index] == ':') {
712 if (format_specifier_split == -1) {
713 /* Only set if it's the first ":" we've encountered in the variable
714 * expression. Subsequent ones will be handled in the format specifier
715 * parsing. */
716 format_specifier_split = byte_index;
717 }
718 continue;
719 }
720
721 /* Check if we've found the closing "}". */
722 if (path[byte_index] == '}') {
723 end = byte_index + 1; /* Exclusive end. */
724 break;
725 }
726 }
727
728 /* No variable expression found. */
729 if (start == -1) {
730 return std::nullopt;
731 }
732
733 /* Unclosed variable expression. Syntax error. */
734 if (end == -1) {
735 token.type = TokenType::VARIABLE_SYNTAX_ERROR;
736 token.byte_range = blender::IndexRange::from_begin_end(start, path.size());
737 return token;
738 }
739
740 /* Parse the variable expression we found. */
741 token.byte_range = blender::IndexRange::from_begin_end(start, end);
742 if (format_specifier_split == -1) {
743 /* No format specifier. */
744 token.variable_name = path.substr(start + 1, (end - 1) - (start + 1));
745 }
746 else {
747 /* Found format specifier. */
748 token.variable_name = path.substr(start + 1, format_specifier_split - (start + 1));
749 token.format = parse_format_specifier(
750 path.substr(format_specifier_split + 1, (end - 1) - (format_specifier_split + 1)));
751 if (token.format.type == FormatSpecifierType::SYNTAX_ERROR) {
752 token.type = TokenType::VARIABLE_SYNTAX_ERROR;
753 return token;
754 }
755 }
756
757 return token;
758}
759
760/* Parse the given template and return the list of tokens found, in the same
761 * order as they appear in the template. */
763{
765
766 for (int bytes_read = 0; bytes_read < path.size();) {
767 const std::optional<Token> token = next_token(path, bytes_read);
768
769 if (!token.has_value()) {
770 break;
771 }
772
773 bytes_read = token->byte_range.one_after_last();
774 tokens.append(*token);
775 }
776
777 return tokens;
778}
779
780/* Convert a token to its corresponding syntax error. If the token doesn't have
781 * an error, returns nullopt. */
782static std::optional<Error> token_to_syntax_error(const Token &token)
783{
784 switch (token.type) {
785 case TokenType::VARIABLE_SYNTAX_ERROR: {
786 if (token.format.type == FormatSpecifierType::SYNTAX_ERROR) {
787 return {{ErrorType::FORMAT_SPECIFIER, token.byte_range}};
788 }
789 else {
790 return {{ErrorType::VARIABLE_SYNTAX, token.byte_range}};
791 }
792 }
793
794 case TokenType::UNESCAPED_CURLY_BRACE_ERROR: {
795 return {{ErrorType::UNESCAPED_CURLY_BRACE, token.byte_range}};
796 }
797
798 /* Non-errors. */
799 case TokenType::VARIABLE_EXPRESSION:
800 case TokenType::LEFT_CURLY_BRACE:
801 case TokenType::RIGHT_CURLY_BRACE:
802 return std::nullopt;
803 }
804
805 BLI_assert_msg(false, "Unhandled token type.");
806 return std::nullopt;
807}
808
810{
811 return path.find_first_of("{}") != std::string_view::npos;
812}
813
835 const int out_path_maxncpy,
836 blender::StringRef in_path,
837 const VariableMap &template_variables)
838{
839 if (out_path) {
840 in_path.copy_bytes_truncated(out_path, out_path_maxncpy);
841 }
842
843 const blender::Vector<Token> tokens = parse_template(in_path);
844
845 if (tokens.is_empty()) {
846 /* No tokens found, so nothing to do. */
847 return {};
848 }
849
850 /* Accumulates errors as we process the tokens. */
852
853 /* Tracks the change in string length due to the modifications as we go. We
854 * need this to properly map the token byte ranges to the being-modified
855 * string. */
856 int length_diff = 0;
857
858 for (const Token &token : tokens) {
859 /* Syntax errors. */
860 if (std::optional<Error> error = token_to_syntax_error(token)) {
861 errors.append(*error);
862 continue;
863 }
864
865 char replacement_string[FORMAT_BUFFER_SIZE];
866
867 switch (token.type) {
868 /* Syntax errors should have been handled above. */
869 case TokenType::VARIABLE_SYNTAX_ERROR:
870 case TokenType::UNESCAPED_CURLY_BRACE_ERROR: {
871 BLI_assert_msg(false, "Unhandled syntax error.");
872 continue;
873 }
874
875 /* Curly brace escapes. */
876 case TokenType::LEFT_CURLY_BRACE: {
877 strcpy(replacement_string, "{");
878 break;
879 }
880 case TokenType::RIGHT_CURLY_BRACE: {
881 strcpy(replacement_string, "}");
882 break;
883 }
884
885 /* Expand variable expression into the variable's value. */
886 case TokenType::VARIABLE_EXPRESSION: {
887 if (std::optional<blender::StringRefNull> string_value = template_variables.get_string(
888 token.variable_name))
889 {
890 if (token.format.type != FormatSpecifierType::NONE) {
891 /* String variables don't take format specifiers: error. */
892 errors.append({ErrorType::FORMAT_SPECIFIER, token.byte_range});
893 continue;
894 }
895 STRNCPY(replacement_string, string_value->c_str());
896 BLI_path_make_safe_filename(replacement_string);
897 break;
898 }
899
900 if (std::optional<blender::StringRefNull> path_value = template_variables.get_filepath(
901 token.variable_name))
902 {
903 if (token.format.type != FormatSpecifierType::NONE) {
904 /* Path variables don't take format specifiers: error. */
905 errors.append({ErrorType::FORMAT_SPECIFIER, token.byte_range});
906 continue;
907 }
908 STRNCPY(replacement_string, path_value->c_str());
909 break;
910 }
911
912 if (std::optional<int64_t> integer_value = template_variables.get_integer(
913 token.variable_name))
914 {
915 /* Integer variable found. */
916 format_int_to_string(token.format, *integer_value, replacement_string);
917 break;
918 }
919
920 if (std::optional<double> float_value = template_variables.get_float(token.variable_name))
921 {
922 /* Float variable found. */
923 format_float_to_string(token.format, *float_value, replacement_string);
924 break;
925 }
926
927 /* No matching variable found: error. */
928 errors.append({ErrorType::UNKNOWN_VARIABLE, token.byte_range});
929 continue;
930 }
931 }
932
933 /* Perform the actual substitution with the expanded value. */
934 if (out_path) {
935 /* We're off the end of the available space. */
936 if (token.byte_range.start() + length_diff >= out_path_maxncpy) {
937 break;
938 }
939
941 out_path_maxncpy,
942 token.byte_range.start() + length_diff,
943 token.byte_range.one_after_last() + length_diff,
944 replacement_string);
945
946 length_diff -= token.byte_range.size();
947 length_diff += strlen(replacement_string);
948 }
949 }
950
951 return errors;
952}
953
956{
957 return eval_template(nullptr, 0, path, template_variables);
958}
959
961 int path_maxncpy,
962 const VariableMap &template_variables)
963{
964 BLI_assert(path != nullptr);
965
966 blender::Vector<char> path_buffer(path_maxncpy);
967
969 path_buffer.data(), path_buffer.size(), path, template_variables);
970
971 if (errors.is_empty()) {
972 /* No errors, so copy the modified path back to the original. */
973 BLI_strncpy(path, path_buffer.data(), path_maxncpy);
974 }
975 return errors;
976}
977
979{
980 blender::StringRef subpath = path.substr(error.byte_range.start(), error.byte_range.size());
981
982 switch (error.type) {
984 return std::string("Unescaped curly brace '") + subpath + "'.";
985 }
986
988 return std::string("Invalid or incomplete template expression '") + subpath + "'.";
989 }
990
992 return std::string("Invalid format specifier in template expression '") + subpath + "'.";
993 }
994
996 return std::string("Unknown variable referenced in template expression '") + subpath + "'.";
997 }
998 }
999
1000 BLI_assert_msg(false, "Unhandled error type.");
1001 return "Unknown error.";
1002}
1003
1005 const eReportType report_type,
1006 blender::StringRef path,
1007 blender::Span<Error> errors)
1008{
1009 BLI_assert(!errors.is_empty());
1010
1011 std::string error_message = "Parse errors in path '" + path + "':";
1012 for (const Error &error : errors) {
1013 error_message += "\n- " + BKE_path_template_error_to_string(error, path);
1014 }
1015
1016 BKE_report(reports, report_type, error_message.c_str());
1017}
1018
1019std::optional<std::string> BKE_path_template_format_float(
1020 const blender::StringRef format_specifier, const double value)
1021{
1022 const FormatSpecifier format = parse_format_specifier(format_specifier);
1023 if (format.type == FormatSpecifierType::SYNTAX_ERROR) {
1024 return std::nullopt;
1025 }
1026 char buffer[FORMAT_BUFFER_SIZE];
1027 format_float_to_string(format, value, buffer);
1028 return buffer;
1029}
1030
1031std::optional<std::string> BKE_path_template_format_int(const blender::StringRef format_specifier,
1032 const int64_t value)
1033{
1034 const FormatSpecifier format = parse_format_specifier(format_specifier);
1035 if (format.type == FormatSpecifierType::SYNTAX_ERROR) {
1036 return std::nullopt;
1037 }
1038 char buffer[FORMAT_BUFFER_SIZE];
1039 format_int_to_string(format, value, buffer);
1040 return buffer;
1041}
Scene * CTX_data_scene(const bContext *C)
const char * BKE_main_blendfile_path_from_global()
Definition main.cc:892
Functions and classes for evaluating template expressions in filepaths.
void BKE_report_path_template_errors(ReportList *reports, eReportType report_type, blender::StringRef path, blender::Span< blender::bke::path_templates::Error > errors)
std::optional< blender::bke::path_templates::VariableMap > BKE_build_template_variables_for_prop(const bContext *C, PointerRNA *ptr, PropertyRNA *prop)
std::optional< std::string > BKE_path_template_format_int(blender::StringRef format_specifier, int64_t value)
std::optional< std::string > BKE_path_template_format_float(blender::StringRef format_specifier, double value)
std::string BKE_path_template_error_to_string(const blender::bke::path_templates::Error &error, blender::StringRef path)
void BKE_add_template_variables_for_render_path(blender::bke::path_templates::VariableMap &variables, const Scene &scene)
void BKE_add_template_variables_general(blender::bke::path_templates::VariableMap &variables, const ID *path_owner_id)
blender::Vector< blender::bke::path_templates::Error > BKE_path_validate_template(blender::StringRef path, const blender::bke::path_templates::VariableMap &template_variables)
void BKE_add_template_variables_for_node(blender::bke::path_templates::VariableMap &variables, const bNode &owning_node)
bool BKE_path_contains_template_syntax(blender::StringRef path)
blender::Vector< blender::bke::path_templates::Error > BKE_path_apply_template(char *path, int path_maxncpy, const blender::bke::path_templates::VariableMap &template_variables)
eReportType
Definition BKE_report.hh:33
void BKE_report(ReportList *reports, eReportType type, const char *message)
Definition report.cc:153
void BKE_render_resolution(const RenderData *r, const bool use_crop, int *r_width, int *r_height)
Definition scene.cc:2915
#define BLI_assert(a)
Definition BLI_assert.h:46
#define BLI_assert_msg(a, msg)
Definition BLI_assert.h:53
bool BLI_path_parent_dir(char *path) ATTR_NONNULL(1)
void void void const char * BLI_path_basename(const char *path) ATTR_NONNULL(1) ATTR_WARN_UNUSED_RESULT
const char * BLI_path_extension_or_end(const char *filepath) ATTR_NONNULL(1) ATTR_WARN_UNUSED_RESULT ATTR_RETURNS_NONNULL
bool BLI_path_make_safe_filename(char *filename) ATTR_NONNULL(1)
char * STRNCPY(char(&dst)[N], const char *src)
Definition BLI_string.h:693
char * BLI_strncpy(char *__restrict dst, const char *__restrict src, size_t dst_maxncpy) ATTR_NONNULL(1
size_t BLI_string_replace_range(char *string, size_t string_maxncpy, int src_beg, int src_end, const char *dst)
#define DATA_(msgid)
#define ID_BLEND_PATH_FROM_GLOBAL(_id)
Definition DNA_ID.h:688
Enumerations for DNA_ID.h.
@ ID_SCE
@ PROP_VARIABLES_RENDER_OUTPUT
Definition RNA_types.hh:492
@ PROP_VARIABLES_NONE
Definition RNA_types.hh:487
@ PROP_PATH_SUPPORTS_TEMPLATES
Definition RNA_types.hh:470
#define C
Definition RandGen.cpp:29
@ NONE
long long int int64_t
static constexpr IndexRange from_begin_end(const int64_t begin, const int64_t end)
constexpr bool is_empty() const
Definition BLI_span.hh:260
void copy_unsafe(char *dst) const
constexpr int64_t find_last_of(StringRef chars, int64_t pos=INT64_MAX) const
constexpr const char * end() const
constexpr int64_t find_first_not_of(StringRef chars, int64_t pos=0) const
constexpr bool is_empty() const
constexpr StringRef substr(int64_t start, int64_t size) const
void copy_bytes_truncated(char *dst, int64_t dst_size) const
Definition string_ref.cc:47
constexpr int64_t find_first_of(StringRef chars, int64_t pos=0) const
constexpr int64_t size() const
constexpr const char * c_str() const
int64_t size() const
void append(const T &value)
bool is_empty() const
bool add_path_up_to_file(blender::StringRef var_name, blender::StringRefNull full_path, blender::StringRef fallback)
std::optional< blender::StringRefNull > get_string(blender::StringRef name) const
bool add_filepath(blender::StringRef name, blender::StringRef value)
bool add_filename_only(blender::StringRef var_name, blender::StringRefNull full_path, blender::StringRef fallback)
std::optional< int64_t > get_integer(blender::StringRef name) const
bool add_string(blender::StringRef name, blender::StringRef value)
std::optional< blender::StringRefNull > get_filepath(blender::StringRef name) const
bool contains(blender::StringRef name) const
bool add_float(blender::StringRef name, double value)
bool remove(blender::StringRef name)
bool add_integer(blender::StringRef name, int64_t value)
std::optional< double > get_float(blender::StringRef name) const
nullptr FLOAT
#define GS(x)
format
static int left
static void error(const char *str)
bool operator==(const Error &left, const Error &right)
static int format_float_to_string(const FormatSpecifier &format, const double float_value, char r_output_string[FORMAT_BUFFER_SIZE])
#define FORMAT_BUFFER_SIZE
static blender::Vector< Error > eval_template(char *out_path, const int out_path_maxncpy, blender::StringRef in_path, const VariableMap &template_variables)
static int format_int_to_string(const FormatSpecifier &format, const int64_t integer_value, char r_output_string[FORMAT_BUFFER_SIZE])
static blender::Vector< Token > parse_template(blender::StringRef path)
static std::optional< Token > next_token(blender::StringRef path, const int from_char)
static FormatSpecifier parse_format_specifier(blender::StringRef format_specifier)
static std::optional< Error > token_to_syntax_error(const Token &token)
const char * name
std::optional< AncestorPointerRNA > RNA_struct_search_closest_ancestor_by_type(PointerRNA *ptr, const StructRNA *srna)
PropertyPathTemplateType RNA_property_path_template_type(PropertyRNA *prop)
int RNA_property_flag(PropertyRNA *prop)
Definition DNA_ID.h:414
char name[258]
Definition DNA_ID.h:432
struct RenderData r
struct Object * camera
char name[64]
i
Definition text_draw.cc:230
PointerRNA * ptr
Definition wm_files.cc:4238