diff --git a/src/libslic3r/PlaceholderParser.cpp b/src/libslic3r/PlaceholderParser.cpp index 865b70434b..e3a4037590 100644 --- a/src/libslic3r/PlaceholderParser.cpp +++ b/src/libslic3r/PlaceholderParser.cpp @@ -712,6 +712,26 @@ namespace client static void regex_matches (expr &lhs, IteratorRange &rhs) { return regex_op(lhs, rhs, '=', lhs); } static void regex_doesnt_match(expr &lhs, IteratorRange &rhs) { return regex_op(lhs, rhs, '!', lhs); } + // Replace every match of the regular expression 'pattern' in the string 'subject' with 'replacement'. + // The replacement may reference capture groups ($1, $2, ...). Store the result into subject. + static void regex_replace(expr &subject, IteratorRange &pattern, expr &replacement) + { + if (subject.type() == TYPE_EMPTY) + // Inside an if / else block to be skipped + return; + if (subject.type() != TYPE_STRING) + subject.throw_exception("regex_replace() first parameter must be a string."); + try { + std::string re(++ pattern.begin(), -- pattern.end()); + std::string result = SLIC3R_REGEX_NAMESPACE::regex_replace(subject.s(), SLIC3R_REGEX_NAMESPACE::regex(re), replacement.to_string()); + subject.set_s(std::move(result)); + } catch (SLIC3R_REGEX_NAMESPACE::regex_error &ex) { + // Syntax error in the regular expression + boost::throw_exception(qi::expectation_failure( + pattern.begin(), pattern.end(), spirit::info(std::string("*Regular expression compilation failed: ") + ex.what()))); + } + } + static void one_of_test_init(expr &out) { out.set_b(false); } @@ -2323,6 +2343,8 @@ namespace client [ px::bind(&expr::digits, _val, _2, _3) ] | (kw["zdigits"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > optional_parameter(_r1)) [ px::bind(&expr::digits, _val, _2, _3) ] + | (kw["regex_replace"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > regular_expression > ',' > conditional_expression(_r1) > ')') + [ px::bind(&expr::regex_replace, _val, _2, _3) ] | (kw["int"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::to_int, _1, _val) ] | (kw["round"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::round, _1, _val) ] | (kw["ceil"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::ceil, _1, _val) ] @@ -2404,6 +2426,7 @@ namespace client ("min") ("max") ("random") + ("regex_replace") ("filament_change") ("repeat") ("round") diff --git a/tests/libslic3r/test_placeholder_parser.cpp b/tests/libslic3r/test_placeholder_parser.cpp index 8b96305b95..ec72b72769 100644 --- a/tests/libslic3r/test_placeholder_parser.cpp +++ b/tests/libslic3r/test_placeholder_parser.cpp @@ -66,6 +66,19 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") { SECTION("math: interpolate_table(13, (0, 0), (20, 20), (30, 20))") { REQUIRE(std::stod(parser.process("{interpolate_table(13, (0, 0), (20, 20), (30, 20))}")) == Catch::Approx(13.)); } SECTION("math: interpolate_table(25, (0, 0), (20, 20), (30, 20))") { REQUIRE(std::stod(parser.process("{interpolate_table(25, (0, 0), (20, 20), (30, 20))}")) == Catch::Approx(20.)); } + // regex_replace(subject, /pattern/, replacement): the string-transform primitive. + SECTION("regex_replace: strips a file extension") { REQUIRE(parser.process("{regex_replace(\"part.stl\", /\\.[^.]*$/, \"\")}") == "part"); } + SECTION("regex_replace: leaves a non-matching dot untouched") { REQUIRE(parser.process("{regex_replace(\"Bracket v2.1\", /\\.stl$/, \"\")}") == "Bracket v2.1"); } + SECTION("regex_replace: replaces every match") { REQUIRE(parser.process("{regex_replace(\"a-b-c\", /-/, \"_\")}") == "a_b_c"); } + SECTION("regex_replace: replacement may reference a capture group") { REQUIRE(parser.process("{regex_replace(\"v12\", /v(\\d+)/, \"$1\")}") == "12"); } + // The result is an ordinary string, usable in further expressions (the real filename-template shape). + SECTION("regex_replace: result composes with concatenation") { REQUIRE(parser.process("{regex_replace(\"part.stl\", /\\.[^.]*$/, \"\") + \".gcode\"}") == "part.gcode"); } + // A malformed pattern and a non-string subject are both hard errors. + SECTION("regex_replace: an invalid pattern throws") { REQUIRE_THROWS(parser.process("{regex_replace(\"x\", /[/, \"\")}")); } + SECTION("regex_replace: a non-string subject throws") { REQUIRE_THROWS(parser.process("{regex_replace(123, /2/, \"\")}")); } + // Inside a skipped branch the subject is TYPE_EMPTY and the call must no-op (exercises the guard). + SECTION("regex_replace: is skipped inside a false if-branch") { REQUIRE(parser.process("{if false}{regex_replace(\"x\", /x/, \"y\")}{endif}done") == "done"); } + // Test the "coFloatOrPercent" and "xxx_line_width" substitutions. // min_width_top_surface ratio_over inner_wall_line_width. SECTION("line_width") { REQUIRE(std::stod(parser.process("{line_width}")) == Catch::Approx(0.67500001192092896)); } @@ -130,6 +143,7 @@ SCENARIO("Placeholder parser variables", "[PlaceholderParser]") { SECTION("create an int local variable") { REQUIRE(parser.process("{local myint = 33+2}{myint}", 0, nullptr, nullptr, nullptr) == "35"); } SECTION("create a string local variable") { REQUIRE(parser.process("{local mystr = \"mine\" + \"only\" + \"mine\"}{mystr}", 0, nullptr, nullptr, nullptr) == "mineonlymine"); } + SECTION("regex_replace transforms a string variable") { REQUIRE(parser.process("{local n = \"part.stl\"}{regex_replace(n, /\\.[^.]*$/, \"\")}", 0, nullptr, nullptr, nullptr) == "part"); } SECTION("create a bool local variable") { REQUIRE(parser.process("{local mybool = 1 + 1 == 2}{mybool}", 0, nullptr, nullptr, nullptr) == "true"); } SECTION("create an int global variable") { REQUIRE(parser.process("{global myint = 33+2}{myint}", 0, nullptr, nullptr, &context_with_global_dict) == "35"); } SECTION("create a string global variable") { REQUIRE(parser.process("{global mystr = \"mine\" + \"only\" + \"mine\"}{mystr}", 0, nullptr, nullptr, &context_with_global_dict) == "mineonlymine"); }