mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-12 03:27:55 +00:00
feat: add regex_replace() string transform to placeholder templates (#14650)
feat: add regex_replace() string transform to filename templates
The filename template language could test strings (=~, !~, one_of) but
never rewrite one, so there was no supported way to reshape a placeholder
value, such as dropping a file extension from {first_object_name}.
Add regex_replace(subject, /pattern/, replacement), reusing the existing
regex-literal syntax and boost::regex engine. Every placeholder keeps
returning its exact value and the template does the transform explicitly:
{regex_replace(first_object_name, /\.[^.]*$/, "")} strip any extension
The replacement may reference capture groups ($1, $2, ...). It is one
grammar function mirroring digits(), with the name registered as a keyword
so it is not parsed as a variable.
This commit is contained in:
@@ -712,6 +712,26 @@ namespace client
|
|||||||
static void regex_matches (expr &lhs, IteratorRange &rhs) { return regex_op(lhs, rhs, '=', lhs); }
|
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); }
|
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<Iterator>(
|
||||||
|
pattern.begin(), pattern.end(), spirit::info(std::string("*Regular expression compilation failed: ") + ex.what())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void one_of_test_init(expr &out) {
|
static void one_of_test_init(expr &out) {
|
||||||
out.set_b(false);
|
out.set_b(false);
|
||||||
}
|
}
|
||||||
@@ -2323,6 +2343,8 @@ namespace client
|
|||||||
[ px::bind(&expr::digits<false>, _val, _2, _3) ]
|
[ px::bind(&expr::digits<false>, _val, _2, _3) ]
|
||||||
| (kw["zdigits"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > optional_parameter(_r1))
|
| (kw["zdigits"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > optional_parameter(_r1))
|
||||||
[ px::bind(&expr::digits<true>, _val, _2, _3) ]
|
[ px::bind(&expr::digits<true>, _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["int"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::to_int, _1, _val) ]
|
||||||
| (kw["round"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::round, _1, _val) ]
|
| (kw["round"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::round, _1, _val) ]
|
||||||
| (kw["ceil"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::ceil, _1, _val) ]
|
| (kw["ceil"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::ceil, _1, _val) ]
|
||||||
@@ -2404,6 +2426,7 @@ namespace client
|
|||||||
("min")
|
("min")
|
||||||
("max")
|
("max")
|
||||||
("random")
|
("random")
|
||||||
|
("regex_replace")
|
||||||
("filament_change")
|
("filament_change")
|
||||||
("repeat")
|
("repeat")
|
||||||
("round")
|
("round")
|
||||||
|
|||||||
@@ -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(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.)); }
|
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.
|
// Test the "coFloatOrPercent" and "xxx_line_width" substitutions.
|
||||||
// min_width_top_surface ratio_over inner_wall_line_width.
|
// min_width_top_surface ratio_over inner_wall_line_width.
|
||||||
SECTION("line_width") { REQUIRE(std::stod(parser.process("{line_width}")) == Catch::Approx(0.67500001192092896)); }
|
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 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("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 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 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"); }
|
SECTION("create a string global variable") { REQUIRE(parser.process("{global mystr = \"mine\" + \"only\" + \"mine\"}{mystr}", 0, nullptr, nullptr, &context_with_global_dict) == "mineonlymine"); }
|
||||||
|
|||||||
Reference in New Issue
Block a user