Loading...
Searching...
No Matches
Regex.hpp
1#pragma once
2#include <ossia/dataflow/exec_state_facade.hpp>
3#include <ossia/dataflow/token_request.hpp>
4#include <ossia/dataflow/value_port.hpp>
5#include <ossia/detail/thread.hpp>
6
7#include <halp/controls.hpp>
8#include <halp/meta.hpp>
9#include <halp/string_list.hpp>
10#include <re2/re2.h>
11
12#include <AvndProcesses/ValueSerialization.hpp>
13#include <fmt/format.h>
14
15#include <algorithm>
16#include <charconv>
17#include <cmath>
18#include <cstdint>
19#include <functional>
20#include <iterator>
21#include <limits>
22#include <memory>
23#include <optional>
24#include <string>
25#include <string_view>
26#include <vector>
27
28namespace ao
29{
43struct Regex
44{
45 halp_meta(name, "Regex")
46 halp_meta(c_name, "avnd_regex")
47 halp_meta(category, "Control/Mappings")
48 halp_meta(author, "ossia score")
49 halp_meta(
50 description,
51 "Match, search, replace or split text with a regular expression (RE2 syntax). "
52 "Each capture group gets an outlet, named after the group for (?P<name>...). "
53 "Numbers and lists are matched as their text.")
54 halp_meta(uuid, "ef63f9b2-cc68-4b25-825e-e3b126875086")
55
69
72 static constexpr int max_capture_outlets = 64;
73
74 struct
75 {
76 struct
77 {
78 halp_meta(name, "Input")
79 halp_meta(description, "The text to process. Numbers and lists are their text; "
80 "an impulse processes the last input again.")
81 ossia::value_port* value{};
82 static constexpr bool is_event() { return true; }
83 } input;
84
85 struct : halp::lineedit<"Pattern", "">
86 {
87 halp_meta(description, "RE2 syntax; (?P<name>...) names a group and its outlet")
88 void update(Regex& self) { self.compile(); }
89 static std::function<void(Regex&, const std::string&)> on_controller_interaction()
90 {
91 return [](Regex& self, const std::string& pattern) {
92 self.outputs.captures.set_rows(Regex::captureRows(pattern));
93 };
94 }
95 } pattern;
96
97 struct : halp::combobox_t<"Mode", Mode>
98 {
99 void update(Regex& self) { self.compile(); }
100 } mode;
101
102 struct : halp::lineedit<"Replacement", "">
103 {
104 halp_meta(description, "Replace mode: \\0 is the match, \\1 to \\9 the groups "
105 "($1 and %1 work too)")
106 void update(Regex& self) { self.compile(); }
107 } replacement;
108
109 struct : halp::toggle<"Global", halp::default_on_toggle>
110 {
111 halp_meta(description, "Replace mode: every match, or only the first one")
112 } global;
113
114 struct : halp::toggle<"Ignore case">
115 {
116 void update(Regex& self) { self.compile(); }
117 } ignore_case;
118
119 struct : halp::toggle<"Multiline">
120 {
121 halp_meta(description, "^ and $ match at every line")
122 void update(Regex& self) { self.compile(); }
123 } multiline;
124
125 struct : halp::toggle<"Numbers">
126 {
127 halp_meta(description, "Groups that are numbers are sent as numbers")
128 } numbers;
129 } inputs;
130
132 {
133 halp_meta(name, "Group {}")
134 ossia::value_port* value{};
135 static constexpr bool is_event() { return true; }
136 };
137
138 struct
139 {
140 struct
141 {
142 halp_meta(name, "Match")
143 halp_meta(description, "The match; Search all: the list of matches; Replace: "
144 "the text replaced; Split: the list of pieces")
145 ossia::value_port* value{};
146 static constexpr bool is_event() { return true; }
147 } match;
148 struct
149 {
150 halp_meta(name, "Groups")
151 halp_meta(description, "The groups of the match, as a list; Search all: a list "
152 "per match")
153 ossia::value_port* value{};
154 static constexpr bool is_event() { return true; }
155 } groups;
156 struct
157 {
158 halp_meta(name, "Matched")
159 halp_meta(description, "true or false, for every input")
160 ossia::value_port* value{};
161 static constexpr bool is_event() { return true; }
162 } matched;
163 struct
164 {
165 halp_meta(name, "Unmatched")
166 halp_meta(description, "The input, when it does not match")
167 ossia::value_port* value{};
168 static constexpr bool is_event() { return true; }
169 } unmatched;
170 struct
171 {
172 halp_meta(name, "Error")
173 halp_meta(description, "Why the pattern or the replacement is not valid; an "
174 "empty string once they are")
175 ossia::value_port* value{};
176 static constexpr bool is_event() { return true; }
177 } error;
178 halp::keyed_dynamic_port<capture_port> captures;
179 } outputs;
180
181 using tick = ossia::token_request;
182 ossia::exec_state_facade ossia_state;
183
189 static std::vector<std::string> captureGroups(const std::string& pattern)
190 {
191 std::vector<std::string> groups;
192 RE2::Options opts;
193 opts.set_log_errors(false);
194 RE2 re{pattern, opts};
195 if(re.ok())
196 {
197 const auto& names = re.CapturingGroupNames();
198 for(int i = 1; i <= re.NumberOfCapturingGroups(); i++)
199 {
200 auto it = names.find(i);
201 groups.push_back(it != names.end() ? it->second : std::string{});
202 }
203 return groups;
204 }
205
206 bool in_class = false;
207 for(std::size_t i = 0; i < pattern.size(); i++)
208 {
209 const char c = pattern[i];
210 if(c == '\\')
211 {
212 i++;
213 continue;
214 }
215 if(in_class)
216 {
217 in_class = c != ']';
218 continue;
219 }
220 if(c == '[')
221 {
222 in_class = true;
223 continue;
224 }
225 if(c != '(')
226 continue;
227 if(i + 1 < pattern.size() && pattern[i + 1] == '?')
228 {
229 // (?P<name>...) and (?<name>...) capture; (?:...), (?i)... do not.
230 std::size_t n = i + 2;
231 if(n < pattern.size() && pattern[n] == 'P')
232 n++;
233 if(n < pattern.size() && pattern[n] == '<')
234 {
235 const auto close = pattern.find('>', n + 1);
236 groups.push_back(
237 pattern.substr(n + 1, close == std::string::npos ? std::string::npos
238 : close - n - 1));
239 }
240 continue;
241 }
242 groups.emplace_back();
243 }
244 return groups;
245 }
246
249 static halp::string_list_value captureRows(const std::string& pattern)
250 {
251 const auto groups = captureGroups(pattern);
252 const int count = std::min<int>(groups.size(), max_capture_outlets);
253 halp::string_list_value rows;
254 rows.reserve(count);
255 std::vector<int> used;
256 for(int i = 1; i <= count; i++)
257 {
258 int key{};
259 const std::string& name = groups[i - 1];
260 if(!name.empty())
261 {
262 // FNV-1a of the name: the group keeps its outlet wherever it moves.
263 uint32_t h = 2166136261u;
264 for(unsigned char c : name)
265 h = (h ^ c) * 16777619u;
266 key = 20000 + int(h % 1'000'000u);
267 while(std::find(used.begin(), used.end(), key) != used.end())
268 key++;
269 }
270 else
271 {
272 key = 10000 + i;
273 }
274 used.push_back(key);
275 rows.emplace_back(key, name);
276 }
277 return rows;
278 }
279
281 static std::string toRE2Rewrite(std::string_view s)
282 {
283 std::string out;
284 out.reserve(s.size());
285 for(std::size_t i = 0; i < s.size(); i++)
286 {
287 const char c = s[i];
288 if((c == '$' || c == '%') && i + 1 < s.size())
289 {
290 const char n = s[i + 1];
291 if(n >= '0' && n <= '9')
292 {
293 out += '\\';
294 out += n;
295 i++;
296 continue;
297 }
298 if(n == c)
299 {
300 out += c;
301 i++;
302 continue;
303 }
304 }
305 out += c;
306 }
307 return out;
308 }
309
311 struct Spec
312 {
313 std::string pattern;
314 std::string replacement;
315 Mode mode{};
316 bool ignore_case{};
317 bool multiline{};
318 };
319
321 struct Compiled
322 {
323 std::unique_ptr<RE2> re;
324 std::string rewrite;
325 std::string error;
326 std::vector<re2::StringPiece> submatches;
327 };
328
329 static Compiled build(const Spec& spec)
330 {
331 Compiled c;
332 RE2::Options opts;
333 opts.set_log_errors(false);
334 opts.set_case_sensitive(!spec.ignore_case);
335 auto re = std::make_unique<RE2>(
336 spec.multiline ? "(?m)" + spec.pattern : spec.pattern, opts);
337 if(!re->ok())
338 {
339 c.error = re->error();
340 return c;
341 }
342
343 std::string rw = toRE2Rewrite(spec.replacement);
344 if(spec.mode == Replace)
345 {
346 if(std::string why; !re->CheckRewriteString(rw, &why))
347 {
348 c.error = std::move(why);
349 return c;
350 }
351 }
352 c.submatches.assign(1 + re->NumberOfCapturingGroups(), {});
353 c.re = std::move(re);
354 c.rewrite = std::move(rw);
355 return c;
356 }
357
358 struct worker
359 {
360 std::function<void(Spec, uint64_t)> request;
361 static std::function<void(Regex&)> work(Spec spec, uint64_t generation)
362 {
363 auto c = std::make_shared<Compiled>(build(spec));
364 return [c = std::move(c), generation](Regex& self) mutable {
365 // A later edit may have been compiled first: only the latest counts
366 if(generation == self.generation)
367 self.apply(std::move(*c));
368 };
369 }
370 } worker;
371
372 std::unique_ptr<RE2> re;
373 std::string rewrite;
374 std::string error_text;
377 bool had_error{};
378 std::vector<re2::StringPiece> submatches;
379 std::string last_text;
380 std::string text;
381 uint64_t generation{};
382
383 void setError(std::string message)
384 {
385 const bool failing = !message.empty();
386 if(failing || had_error)
387 {
388 error_text = std::move(message);
389 error_pending = true;
390 }
391 had_error = failing;
392 }
393
394 void apply(Compiled&& c)
395 {
396 re = std::move(c.re);
397 rewrite = std::move(c.rewrite);
398 submatches = std::move(c.submatches);
399 setError(std::move(c.error));
400 }
401
404 void compile()
405 {
406 Spec spec{
407 inputs.pattern.value, inputs.replacement.value, inputs.mode.value,
408 inputs.ignore_case.value, inputs.multiline.value};
409 ++generation;
410 const auto thread = ossia::get_current_thread_type();
411 if(worker.request
412 && (thread == ossia::thread_type::Audio || thread == ossia::thread_type::AudioTask))
413 worker.request(std::move(spec), generation);
414 else
415 apply(build(spec));
416 }
417
420 static void appendText(std::string& out, const ossia::value& v)
421 {
422 switch(v.get_type())
423 {
424 case ossia::val_type::STRING:
425 out += *v.target<std::string>();
426 break;
427 case ossia::val_type::INT:
428 out += std::to_string(*v.target<int>());
429 break;
430 case ossia::val_type::FLOAT: {
431 fmt::format_to(std::back_inserter(out), "{}", *v.target<float>());
432 break;
433 }
434 case ossia::val_type::BOOL:
435 out += *v.target<bool>() ? "true" : "false";
436 break;
437 case ossia::val_type::LIST: {
438 bool first = true;
439 for(auto& e : *v.target<std::vector<ossia::value>>())
440 {
441 if(!first)
442 out += ' ';
443 first = false;
444 appendText(out, e);
445 }
446 break;
447 }
448 case ossia::val_type::VEC2F:
449 case ossia::val_type::VEC3F:
450 case ossia::val_type::VEC4F:
451 appendText(out, ossia::convert<std::vector<ossia::value>>(v));
452 break;
453 default:
454 break;
455 }
456 }
457
459 ossia::value groupValue(re2::StringPiece s) const
460 {
461 std::string_view text{s.data(), s.size()};
462 if(inputs.numbers.value && !text.empty())
463 {
464 int i{};
465 if(auto [p, ec] = std::from_chars(text.data(), text.data() + text.size(), i);
466 ec == std::errc{} && p == text.data() + text.size())
467 return i;
468 double d{};
469 if(avnd_tools::value_serialization::parse_float64(text, d) && std::isfinite(d)
470 && std::abs(d) <= std::numeric_limits<float>::max())
471 return float(d);
472 }
473 return std::string{text};
474 }
475
476 static void write(ossia::value_port* port, ossia::value v, int64_t ts)
477 {
478 if(port)
479 port->write_value(std::move(v), ts);
480 }
481
485 std::vector<ossia::value> sendGroups(int64_t ts)
486 {
487 std::vector<ossia::value> groups;
488 groups.reserve(submatches.size() - 1);
489 for(std::size_t g = 1; g < submatches.size(); g++)
490 {
491 const auto& s = submatches[g];
492 if(!s.data())
493 {
494 groups.emplace_back(std::string{});
495 continue;
496 }
497 auto v = groupValue(s);
498 if(g - 1 < outputs.captures.ports.size())
499 write(outputs.captures.ports[g - 1].value, v, ts);
500 groups.push_back(std::move(v));
501 }
502 return groups;
503 }
504
505 void process(const ossia::value& input, int64_t ts)
506 {
507 // The text buffers are reused from one input to the next
508 if(input.target<ossia::impulse>())
509 {
510 text = last_text;
511 }
512 else
513 {
514 text.clear();
515 appendText(text, input);
516 last_text = text;
517 }
518
519 const re2::StringPiece piece{text};
520 const int n = submatches.size();
521 bool found = false;
522
523 switch(inputs.mode.value)
524 {
525 case Search:
526 case Match: {
527 const auto anchor = inputs.mode.value == Match ? RE2::ANCHOR_BOTH : RE2::UNANCHORED;
528 found = re->Match(piece, 0, piece.size(), anchor, submatches.data(), n);
529 if(found)
530 {
531 write(outputs.match.value, std::string{submatches[0]}, ts);
532 write(outputs.groups.value, sendGroups(ts), ts);
533 }
534 break;
535 }
536 case SearchAll:
537 case Split: {
538 std::vector<ossia::value> matches, groups, pieces;
539 std::size_t pos = 0, last = 0;
540 while(pos <= piece.size()
541 && re->Match(piece, pos, piece.size(), RE2::UNANCHORED, submatches.data(), n))
542 {
543 found = true;
544 const auto& m = submatches[0];
545 const std::size_t begin = m.data() - piece.data();
546 const std::size_t end = begin + m.size();
547 if(inputs.mode.value == Split)
548 {
549 pieces.emplace_back(std::string{text.substr(last, begin - last)});
550 }
551 else
552 {
553 matches.emplace_back(std::string{m});
554 groups.emplace_back(sendGroups(ts));
555 }
556 last = end;
557 // An empty match moves on by one character, or it would match again
558 pos = end > begin ? end : end + 1;
559 }
560 if(!found)
561 break;
562 if(inputs.mode.value == Split)
563 {
564 pieces.emplace_back(std::string{text.substr(std::min(last, text.size()))});
565 write(outputs.match.value, std::move(pieces), ts);
566 }
567 else
568 {
569 write(outputs.match.value, std::move(matches), ts);
570 write(outputs.groups.value, std::move(groups), ts);
571 }
572 break;
573 }
574 case Replace: {
575 std::string result = text;
576 found = inputs.global.value ? RE2::GlobalReplace(&result, *re, rewrite) > 0
577 : RE2::Replace(&result, *re, rewrite);
578 if(found)
579 write(outputs.match.value, std::move(result), ts);
580 break;
581 }
582 }
583
584 write(outputs.matched.value, found, ts);
585 if(!found)
586 write(outputs.unmatched.value, input, ts);
587 }
588
589 void operator()(const tick& t)
590 {
591 const auto [start, frames] = ossia_state.timings(t);
592 if(error_pending)
593 {
594 write(outputs.error.value, error_text, start);
595 error_pending = false;
596 }
597 if(!inputs.input.value || !re)
598 return;
599 for(const auto& event : inputs.input.value->get_data())
600 {
601 if(event.timestamp < start || event.timestamp >= start + frames)
602 continue;
603 process(event.value, event.timestamp);
604 }
605 }
606};
607}
bool anchor(::State::Address &a, const score::DocumentContext &ctx)
Anchors a local-device address to the object published there; returns whether it has an anchor.
Definition ScriptableReference.cpp:70
A compiled pattern, or why there is none.
Definition Regex.hpp:322
What the pattern is compiled from.
Definition Regex.hpp:312
Definition Regex.hpp:132
Definition Regex.hpp:359
Regular expressions on the values that arrive, with RE2.
Definition Regex.hpp:44
bool error_pending
Error text not sent yet; a failure after success and the reverse.
Definition Regex.hpp:376
void compile()
Definition Regex.hpp:404
ossia::value groupValue(re2::StringPiece s) const
A group as sent: its text, or the number it is when Numbers is on.
Definition Regex.hpp:459
static void appendText(std::string &out, const ossia::value &v)
Definition Regex.hpp:420
static std::string toRE2Rewrite(std::string_view s)
$1 and %1 as \1; $$ and %% as $ and %.
Definition Regex.hpp:281
static constexpr int max_capture_outlets
Definition Regex.hpp:72
static halp::string_list_value captureRows(const std::string &pattern)
Definition Regex.hpp:249
std::vector< ossia::value > sendGroups(int64_t ts)
Definition Regex.hpp:485
static std::vector< std::string > captureGroups(const std::string &pattern)
Definition Regex.hpp:189
Mode
Definition Regex.hpp:57
@ Search
The first match anywhere in the text.
Definition Regex.hpp:59
@ Split
The pieces of text between the matches.
Definition Regex.hpp:67
@ Replace
The text with the matches replaced.
Definition Regex.hpp:65
@ SearchAll
Every match, in order.
Definition Regex.hpp:63
@ Match
The whole text must match.
Definition Regex.hpp:61
Definition ObjectMatches.hpp:6