File Coverage

blib/lib/Regexp/Debugger.pm
Criterion Covered Total %
statement 103 1256 8.2
branch 19 672 2.8
condition 2 322 0.6
subroutine 26 74 35.1
pod 0 1 0.0
total 150 2325 6.4


line stmt bran cond sub pod time code
1             package Regexp::Debugger;
2              
3 1     1   68168 use warnings;
  1         3  
  1         34  
4 1     1   6 use strict;
  1         2  
  1         47  
5 1     1   8 eval "use feature 'evalbytes'"; # Experimental fix for Perl 5.16
  1         1  
  1         52  
6              
7             our $VERSION = '0.002004';
8              
9             # Handle Perl 5.18's new-found caution...
10 1     1   661 no if $] >= 5.018, warnings => "experimental::smartmatch";
  1         14  
  1         5  
11              
12             # Give an accurate warning if used with an antique Perl...
13             BEGIN {
14 1 50   1   131 if ($] < 5.010001) {
15 0         0 die sprintf "Regexp::Debugger requires Perl v5.10.1 or later (at %s line %s)\n",
16             (caller 2)[1..2];
17             }
18             }
19              
20 1     1   23 use 5.010001; # ...activate all the tasty 5.10 goodies
  1         4  
21              
22 1     1   6 use List::Util qw< min max first sum >;
  1         2  
  1         249  
23              
24             # Track configurable options lexically...
25             my @config;
26              
27             # Track debugging history in various formats...
28             my %history_of;
29              
30             # Persistent information within debugger...
31             my $prev_regex_pos; # ...track where we were previously in the regex
32             my $start_str_pos; # ...track where we started matching in the string
33             my $prev_str_pos; # ...track where we were previously in the string
34             my $prev_match_was_null; # ...under /g was previous match a null match?
35             my %capture; # ...track capture groups within regex
36             my @pre_is_pending; # ...did we try something last event?
37             my $interaction_quit; # ...did we get a quit request?
38             my $interaction_mode; # ...step-by-step, jump to match, or continue?
39             my $interaction_depth; # ...depth at which this interaction was initiated
40             my $display_mode; # ...how is the match being visualized at present?
41              
42             # Bounds on speed of displaying states...
43             my $MIN_SKIP_DURATION = 0.001; # ...1/1000 second
44             my $MAX_SKIP_DURATION = 0.2; # ...2/10 second
45             my $SKIP_ACCELERATION = 0.98; # ...increase by 2% each step
46              
47             # Colours for heatmaps...
48             my @DEF_HEAT_COLOUR = (
49             'white on_black', # 0-20 percentile
50             'cyan on_blue', # 20-40 percentile
51             'blue on_cyan', # 40-60 percentile
52             'red on_yellow', # 60-80 percentile
53             'yellow on_red', # 80-100 percentile
54             );
55              
56             # Colours for detailed regex descriptions...
57             my %DESCRIPTION_COLOUR = (
58             desc_sep_col => 'blue on_black underline',
59             desc_regex_col => 'white on_black',
60             desc_text_col => 'cyan on_black',
61             );
62              
63             # Colour for error messages...
64             my $ERR_COL = 'red';
65              
66              
67             # Default config which any explicit config modifies...
68             my @SHOW_WS_OPTIONS = qw< compact visible original >;
69             my %DEFAULT_CONFIG = (
70             # How debugging info is displayed initially...
71             display_mode => 'visual',
72              
73             # Colour scheme for debugging info...
74             info_col => ' white on_black',
75             try_col => 'bold magenta on_black',
76             match_col => ' bold cyan on_black',
77             fail_col => ' yellow on_red',
78             ws_col => ' bold blue underline',
79              
80             # Colour scheme for regex descriptions...
81             %DESCRIPTION_COLOUR,
82              
83             # Where debugging info is written to (undef --> STDOUT)...
84             save_to_fh => undef,
85              
86             # How whitespace is managed...
87             show_ws => $SHOW_WS_OPTIONS[0],
88             );
89              
90             # The current config...
91             my $lexical_config = \%DEFAULT_CONFIG;
92             # Simulate print() and say() on appropriate filehandle...
93             sub _print {
94 0 0   0   0 if (!$lexical_config->{save_to_fh}) {
95 1     1   15 no warnings 'utf8';
  1         3  
  1         100  
96 0 0       0 print map { defined($_) ? $_ : '' } @_;
  0         0  
97             }
98             }
99              
100             sub _say {
101 0 0   0   0 if (!$lexical_config->{save_to_fh}) {
102 1     1   7 no warnings 'utf8';
  1         2  
  1         195  
103 0 0       0 say map { defined($_) ? $_ : '' } @_;
  0         0  
104             }
105             }
106              
107             # How should matches be indicated???
108             my $MATCH_DRAG = ' ';
109              
110             # Will heatmaps be visible???
111             my $heatmaps_invisible;
112              
113             # Indent unit for hierarchical display...
114             my $INDENT = q{ };
115              
116             # Simulate Term::ANSIColor badly (if necessary)...
117             CHECK {
118             my $can_color
119             = ( $^O ne 'MSWin32' or eval { require Win32::Console::ANSI } )
120 1   33 1   1244 && eval { require Term::ANSIColor };
121              
122 1 50       9399 if ( !$can_color ) {
123 0         0 *Term::ANSIColor::colored = sub { return shift };
  0         0  
124 0         0 $MATCH_DRAG = '_';
125 0         0 $heatmaps_invisible = 1;
126             }
127             }
128              
129             # Load the module...
130             sub import {
131 1     1   7 use Carp;
  1         9  
  1         147  
132              
133             # Don't need the module name...
134 1     1   13 shift;
135              
136             # Export re 'eval' semantics...
137 1         3 $^H |= 0x00200000;
138              
139             # Unpack the arguments...
140 1 50       6 if (@_ % 2) {
141 0         0 croak 'Odd number of configuration args after "use Regexp::Debugger"';
142             }
143 1         3 my %arg = @_;
144              
145             # Creat a new lexically scoped config and remember its index...
146 1         8 push @config, { %DEFAULT_CONFIG };
147 1         5 $^H{'Regexp::Debugger::lexical_scope'} = $#config;
148              
149 1         6 _load_config(\%arg);
150              
151             # Signal lexical scoping (active, unless something was exported)...
152 1         5 $^H{'Regexp::Debugger::active'} = 1;
153              
154             # Process any regexes in module's active lexical scope...
155 1     1   1205 use overload;
  1         973  
  1         17  
156             overload::constant(
157             qr => sub {
158 0     0   0 my ($raw, $cooked, $type) = @_;
159              
160 0   0     0 my $hints = (caller 1)[10] // {};
161 0         0 my $lexical_scope = $hints->{'Regexp::Debugger::lexical_scope'};
162              
163             # In active scope and really a regex and interactivity possible...
164 0   0     0 my $is_interactive = defined $arg{save_to} || -t *STDIN && -t *STDOUT;
165 0 0 0     0 if (_module_is_active() && $type =~ /qq?/ && $is_interactive) {
      0        
166 0         0 return bless {cooked=>$cooked, lexical_scope=>$lexical_scope}, 'Regexp::Debugger::Precursor';
167             }
168             # Ignore everything else...
169             else {
170 0         0 return $cooked;
171             }
172             }
173 1         9 );
174             }
175              
176             # Deactivate module's regex effect when it is "anti-imported" with 'no'...
177             sub unimport {
178             # Signal lexical (non-)scoping...
179 0     0   0 $^H{'Regexp::Debugger::active'} = 0;
180             }
181              
182             # Encapsulate the hoopy user-defined pragma interface...
183             sub _module_is_active {
184 0   0 0   0 my $hints = (caller 1)[10] // return 0;
185 0         0 return $hints->{'Regexp::Debugger::active'};
186             }
187              
188             # Load ~/.rxrx config...
189             sub _load_config {
190 1     1   3 my $explicit_config_ref = shift();
191 1         2 my %config;
192              
193             # Work out where to look...
194 1         3 my $home_dir = $ENV{HOME};
195 1 50 33     4 if (!$home_dir && eval { require File::HomeDir } ) {
  0         0  
196 0         0 $home_dir = File::HomeDir->my_home;
197             }
198              
199             # Find config file...
200             CONFIG_FILE:
201 1 50       6 for my $config_file ( '.rxrx', ( $home_dir ? "$home_dir/.rxrx" : () ) ) {
202              
203             # Is this a readable config file???
204 2 50       63 open my $fh, '<', $config_file
205             or next CONFIG_FILE;
206              
207             # Read and parse config file...
208             CONFIG_LINE:
209 0         0 for my $config_line (<$fh>) {
210 0 0       0 if ($config_line =~ /^\s*(.*?)\s*[:=]\s*(.*?)\s*$/) {
211 0         0 $config{$1} = $2;
212             }
213             }
214              
215 0         0 last CONFIG_FILE;
216             }
217              
218             # Make any explicit args override .rxrxrc config...
219 1         5 %config = (display => 'visual', %config, %{$explicit_config_ref});
  1         4  
220              
221             # Configure colour scheme for displays...
222 1         14 for my $colour (grep /_col$/, keys %DEFAULT_CONFIG) {
223 8 50       17 if (exists $config{$colour}) {
224 0         0 $config[-1]{$colour} = $config{$colour}
225             }
226             }
227              
228             # Configure how whitespace is displayed...
229 1         2 my $show_ws = $config{show_ws};
230 1 50       3 if (defined $show_ws) {
231 0 0       0 if ($show_ws ~~ @SHOW_WS_OPTIONS) {
232 0         0 $config[-1]{show_ws} = $show_ws;
233             }
234             else {
235 0         0 croak "Unknown 'show_ws' option: '$show_ws'";
236             }
237             }
238              
239             # Configure heatmap colour scheme...
240             my @heatmap_cols =
241 0         0 map { $config{$_} }
242             sort {
243             # Sort numerically (if feasible), else alphabetically...
244 0 0       0 my $a_key = $a =~ /(\d+)/ ? $1 : undef;
245 0 0       0 my $b_key = $b =~ /(\d+)/ ? $1 : undef;
246 0 0 0     0 defined $a_key && defined $b_key
247             ? $a_key <=> $b_key
248             : $a cmp $b;
249             }
250 1         4 grep { /^heatmap/ }
  1         5  
251             keys %config;
252              
253 1 50       6 if (!@heatmap_cols) {
254 1         8 @heatmap_cols = @DEF_HEAT_COLOUR;
255             }
256              
257 1         3 $config[-1]{heatmap_col} = \@heatmap_cols;
258              
259              
260             # Configure initial display mode...
261 1         2 my $display = $config{display};
262 1 50       4 if (defined $display) {
263             $config[-1]{display_mode}
264 1 0       8 = $display =~ m{^events}i ? 'events'
    50          
    50          
    50          
265             : $display =~ m{^heatmap}i ? 'heatmap'
266             : $display =~ m{^visual}i ? 'visual'
267             : $display =~ m{^JSON}i ? 'JSON'
268             : croak "Unknown 'display' option: '$display'";
269             }
270              
271             # Configure destination of debugging info...
272 1         3 my $save_to = $config{save_to};
273 1 50       6 if (defined $save_to) {
274 1     1   805 use Scalar::Util qw< openhandle >;
  1         15  
  1         680  
275 0 0         if (openhandle($save_to)) {
    0          
276 0           $config[-1]{save_to_fh} = $save_to;
277             }
278             elsif (!ref $save_to) {
279 0           my ($mode, $filename) = $save_to =~ m{ (>{0,2}) (.*) }x;
280 0 0 0       open my $fh, $mode||'>', $filename
281             or croak "Invalid 'save_to' option: '$save_to'\n($!)";
282 0           $config[-1]{save_to_fh} = $fh;
283             }
284             else {
285 0           croak "Invalid 'save_to' option: ", ref($save_to);
286             }
287             }
288             }
289              
290              
291             # General memory for each state of each regex...
292             # (structure is: $state{REGEX_NUM}{STATE_NUMBER}{ATTRIBUTE})
293             my %state;
294             my $next_regex_ID = 0;
295              
296              
297             #=====[ COMPILE-TIME INTERIM REPRESENTATION OF REGEXES ]===================
298             {
299             package Regexp::Debugger::Precursor;
300              
301             # Only translate precursors once...
302             state %regex_cache;
303              
304             use overload (
305             # Concatenation/interpolation just concatenates to the precursor...
306             q{.} => sub {
307 0     0   0 my ($x, $y, $reversed) = @_;
308              
309             # Where are we from???
310 0   0     0 my $lexical_scope = $x->{lexical_scope} // 0;
311              
312             # Reorder if necessary...
313 0 0       0 if ($reversed) { ($y,$x) = ($x,$y); }
  0         0  
314              
315             # Unpack if objects...
316 0 0 0     0 if (ref $x) { $x = eval{ $x->{cooked} } // $x }
  0         0  
  0         0  
317 0 0 0     0 if (ref $y) { $y = eval{ $y->{cooked} } // $y }
  0         0  
  0         0  
318              
319             # Undo overeager \Q if necessary...
320 0 0       0 if ($x =~ m{^\\\(\\\?\\\#R_d\\:(\d+)\\\)}) { $x = '\\Q' . $state{$1}{raw_regex} . '\\E' }
  0 0       0  
    0          
321 0         0 elsif ($x =~ m{^\(\?\#R_D:(\d+)\)}) { $x = '\\U' . uc($state{$1}{raw_regex}) . '\\E' }
322 0         0 elsif ($x =~ m{^\(\?\#r_d:(\d+)\)}) { $x = '\\L' . lc($state{$1}{raw_regex}) . '\\E' }
323 0 0       0 if ($y =~ m{^\\\(\\\?\\\#R_d\\:(\d+)\\\)}) { $y = '\\Q' . $state{$1}{raw_regex} . '\\E' }
  0 0       0  
    0          
324 0         0 elsif ($y =~ m{^\(\?\#R_D:(\d+)\)}) { $y = '\\U' . uc($state{$1}{raw_regex}) . '\\E' }
325 0         0 elsif ($y =~ m{^\(\?\#r_d:(\d+)\)}) { $y = '\\L' . lc($state{$1}{raw_regex}) . '\\E' }
326              
327             # Do the concatenation...
328 0   0     0 $x .= $y//q{};
329              
330             # Rebless as a precursor object...
331 0         0 return bless {cooked=>$x, lexical_scope=>$lexical_scope}, 'Regexp::Debugger::Precursor';
332             },
333              
334             # Using as a string (i.e. matching) preprocesses the precursor...
335             q{""} => sub {
336 0     0   0 my ($obj) = @_;
337              
338 1     1   8 use Scalar::Util qw< refaddr >;
  1         2  
  1         105  
339             return $regex_cache{ refaddr($obj) }
340 0   0     0 //= Regexp::Debugger::_build_debugging_regex( @{$obj}{'cooked', 'lexical_scope'} );
  0         0  
341             },
342              
343             # Everything else, as usual...
344 1         15 fallback => 1,
345 1     1   7 );
  1         2  
346             }
347              
348              
349             #=====[ Augment a regex with debugging statements ]================
350              
351              
352             # Build code insertions for before and after elements in a regex...
353             # (the final $^R ensure these extra code blocks are "transparent")
354              
355              
356             sub _build_event {
357 0     0     my ($regex_ID, $event_ID, $event_desc_ref) = @_;
358 0   0       $event_desc_ref->{quantifier} //= q{};
359 0           $state{$regex_ID}{$event_ID} = $event_desc_ref;
360              
361             # Work around for bug in infinite-recursion checking in Perl 5.24 to 5.30...
362 0 0 0       state $lookahead = $] <= 5.022 || $] >= 5.032 ? q{(?=)} : q{(?=[\d\D]?(?{1}))};
363              
364 0           return qq{(?{Regexp::Debugger::_report_event($regex_ID, $event_ID, pos()); \$^R})$lookahead};
365             }
366              
367             sub _build_whitespace_event {
368 0     0     my ($construct,$regex_ID, $event_ID, $event_desc_ref) = @_;
369 0   0       $event_desc_ref->{quantifier} //= q{};
370 0           my %event_desc_copy = %{$event_desc_ref};
  0            
371 0           $state{$regex_ID}{$event_ID} = { %event_desc_copy, event_type => 'pre' };
372 0           $state{$regex_ID}{$event_ID+1} = { %event_desc_copy, event_type => 'post', msg => 'Matched' };
373              
374 0           return qq{(?>(?{local \$Regexp::Debugger::prevpos=pos})$construct(?{
375             if (defined \$Regexp::Debugger::prevpos && \$Regexp::Debugger::prevpos < pos){
376             Regexp::Debugger::_report_event($regex_ID, $event_ID, \$Regexp::Debugger::prevpos);
377             Regexp::Debugger::_report_event($regex_ID, $event_ID+1, pos());
378             }\$^R })|(?{
379             Regexp::Debugger::_report_event($regex_ID, $event_ID, pos());
380             \$^R
381             })(?!))};
382             }
383              
384              
385             # Translate lookaround markers...
386             my %LOOKTYPE = (
387             '(?=' => 'positive lookahead',
388             '(?!' => 'negative lookahead',
389             '(?<=' => 'positive lookbehind',
390             '(? 'negative lookbehind',
391             );
392              
393             sub _build_debugging_regex {
394 0     0     my ( $raw_regex, $lexical_scope ) = @_;
395 0   0       $lexical_scope //= 0;
396              
397             # How does this regexp show whitespace???
398 0           our $show_ws = $config[$lexical_scope]{show_ws};
399              
400             # Build a clean, compacted version of the regex in this var...
401 0           my $clean_regex = q{};
402              
403             # Track nested parentheticals so we can correctly mark each ')'...
404 0           my @paren_stack = ( {} );
405              
406             # Give this regex a unique ID...
407 0           my $regex_ID = $next_regex_ID++;
408              
409             # Remember raw data in case of over-eager quotemeta'ing...
410 0           $state{$regex_ID}{raw_regex} = $raw_regex;
411              
412             # Remember location of regex...
413 0           my ($filename, $end_line) = (caller 1)[1,2];
414 0           my $regex_lines = $raw_regex =~ tr/\n//;
415 0           my $start_line = $end_line - $regex_lines;
416             $state{$regex_ID}{location}
417 0 0         = $start_line == $end_line ? qq{'$filename' line $start_line}
418             : qq{'$filename' lines $start_line-$end_line};
419              
420             # Track each inserted debugging statement...
421 0           my $next_event_ID = 0;
422              
423             # Track capture groups...
424 0           my $next_capture_group = 0;
425 0           my $max_capture_group = 0;
426              
427             # Track named capture aliases...
428 0           my @capture_names_for;
429              
430             # Track \Q...\E
431 0           my $in_quote = 0;
432 0           my $shared_quote_pos;
433              
434             # Describe construct...
435 0           our $construct_desc;
436 0           our $quantifier_desc;
437              
438             # Check for likely problems in the regex...
439 0           our @problems = ();
440 0           ()= $raw_regex =~ m{
441             ( \( & [^\W\d]\w*+ \) )
442 0           (?{ push @problems, { line => 1 + substr($_,0,pos()-length($^N)) =~ tr/\n/\n/,
443             desc => $^N,
444             type => 'subpattern call',
445             dym => "(?" . substr($^N,1)
446             }
447             })
448             |
449             ( \( [<'] [^\W\d]\w*+ [>'] (?= \s*+ [^\s)]++ ) )
450 0           (?{ push @problems, { line => 1 + substr($_,0,pos()-length($^N)) =~ tr/\n/\n/,
451             desc => "$^N ... )",
452             type => 'named capture or subpattern definition',
453             dym => "(?" . substr($^N,1) . ' ... )'
454             }
455             })
456             }xmsgc;
457 0           $state{$regex_ID}{regex_problems} = [@problems];
458              
459             # Translate each component...
460 1     1   829 use re 'eval';
  1         2  
  1         761  
461 0           $raw_regex =~ s{
462             # Set-up...
463 0           (?{ $quantifier_desc = q{}; $construct_desc = q{}; })
  0            
464              
465             # Match the next construct...
466             (?
467             (? \A )
468             |
469             (? \z )
470             |
471             (?
472 0 0         (??{!$Regexp::Debugger::in_quote ? q{} : q{(?!)} })
473             \\Q
474 0           (?{$Regexp::Debugger::in_quote = 1})
475             )
476 0 0         | (??{$Regexp::Debugger::in_quote ? q{} : q{(?!)} })
477             (
478             (? \s++ )
479             |
480             (? \\E )
481 0           (?{$Regexp::Debugger::in_quote = 0})
482             |
483             (? \S )
484             )
485             |
486             (?
487 0           \\U (?{$construct_desc = 'an auto-uppercased sequence'})
488 0           | \\L (?{$construct_desc = 'an auto-lowercased sequence'})
489             )
490             |
491             (?
492             \\E
493             )
494             |
495 0           (?{$quantifier_desc = '';})
496             (? [)] ) (? (?&QUANTIFIER) )?
497             |
498             (?
499 0           (?(?{ $show_ws eq 'compact' })
500             (?
501             ( (?: \s | (?&COMMENT) )+ )
502 0           (?! (?&UNSPACED_QUANTIFIER) ) (?{ $quantifier_desc = q{} })
503 0           (?{$construct_desc = $^N})
504             |
505             ( \s )
506 0           (?{$construct_desc = $^N})
507             (? (?&UNSPACED_QUANTIFIER) )
508             )
509             |
510             (?!)
511             )
512             |
513 0           (?(?{ $show_ws eq 'visible' })
514             (?
515             ( [^\S\n\t]+ )
516 0           (?! (?&UNSPACED_QUANTIFIER) ) (?{ $quantifier_desc = q{} })
517 0           (?{$construct_desc = $^N})
518             |
519             ( [^\S\n\t] )
520 0           (?{$construct_desc = $^N})
521             (? (?&UNSPACED_QUANTIFIER) )
522             )
523             |
524             (?!)
525             )
526             |
527 0           (?(?{ $show_ws eq 'original'})
528             (?
529             ( [^\S\n\t] )
530 0           (?{$construct_desc = $^N})
531             (? (?&UNSPACED_QUANTIFIER) )?
532             )
533             |
534             (?!)
535             )
536             |
537             (? \n )
538             (? (?&UNSPACED_QUANTIFIER) )?
539 0           (?{$construct_desc = 'a literal newline character'})
540             |
541             (? \t )
542             (? (?&UNSPACED_QUANTIFIER) )?
543 0           (?{$construct_desc = 'a literal tab character'})
544             )
545             |
546             (?
547             [(][?][#] \s* (?i: BREAK ) \s* [)]
548             )
549             |
550             (?
551             (?&COMMENT)
552             )
553             |
554             (?
555             [(] [?] (?&MODIFIERS) [)]
556             )
557             |
558             (?
559             (?<_anchor>
560             \^
561 0           (?{$construct_desc = 'at start of string (or line)'})
562             |
563             \$
564 0           (?{$construct_desc = 'at end of string (or final newline)'})
565             |
566             \\ (?:
567 0           A (?{$construct_desc = 'at start of string'})
568             |
569 0           B (?{$construct_desc = 'not at an identifier boundary'})
570             |
571 0           b (?{$construct_desc = 'at an identifier boundary'})
572             |
573 0           G (?{$construct_desc = 'at previous match position'})
574             |
575 0           Z (?{$construct_desc = 'at end of string (or final newline)'})
576             |
577 0           z (?{$construct_desc = 'at end of string'})
578             )
579             )
580             )
581             |
582             (?
583             [(] [?][?] (?&CODEBLOCK) [)]
584             )
585             |
586             (?
587             [(] [?] (?&CODEBLOCK) [)]
588             )
589             |
590             # Control verbs like (*PRUNE) and (*MARK:name)...
591             (?
592             \(\* [[:upper:]]*+ (?: : [^)]++ )? \)
593             )
594             |
595             (? [(] [?] (?&MODIFIERS)? : )
596             |
597             (? [(] [?] [<]?[=!] )
598             |
599             (? [(] [?] [>] )
600 0           (?{$construct_desc = 'a non-backtracking group'})
601             |
602             (? [(] [?] [|] )
603             |
604             (? [(] (?! [?]) )
605             |
606             (? [(] [?] [(] DEFINE [)] )
607             |
608             (?
609             [(] [?] [(]
610             (?
611             \d+
612             | R \d*
613             | R& (?&IDENTIFIER)
614             | < (?&IDENTIFIER) >
615             | ' (?&IDENTIFIER) '
616             | [?] (?&CODEBLOCK)
617             )
618             [)]
619             )
620             |
621             (? (?
622             [(] [?] (?= [(] [?]
623             ))
624             |
625             (?
626             [(] [?] P? < (? (?&IDENTIFIER) ) >
627             | [(] [?] ' (? (?&IDENTIFIER) ) '
628             )
629             |
630             (?<_alternation> [|] )
631             |
632             (? \\K )
633             |
634             (?
635             (?<_self_matching> \w{2,} ) (?! (?&QUANTIFIER) )
636 1     1   471 (?{$quantifier_desc = ''; $construct_desc = qq{a literal sequence ("$+{_self_matching}")}})
  1         414  
  1         6661  
  0            
  0            
637             |
638 0           (?{$quantifier_desc = '';})
639             (?<_self_matching> (?&NONMETA) ) (? (?&QUANTIFIER) )?
640 0           (?{$construct_desc = qq{a literal '$+{_self_matching}' character}})
641             |
642 0           (?{$quantifier_desc = '';})
643             (?<_metacharacter>
644 0           [.] (?{$construct_desc = 'any character (except newline)'})
645             |
646             \\
647 0           (?: (0[0-7]++) (?{$construct_desc = "a literal '".chr(oct($^N))."' character"})
648 0           | (\d++) (?{$construct_desc = "what was captured in \$$^N"})
649 0           | a (?{$construct_desc = 'an alarm/bell character'})
650 0           | c ([A-Z]) (?{$construct_desc = "a CTRL-$^N character"})
651 0           | C (?{$construct_desc = 'a C-language octet'})
652 0           | d (?{$construct_desc = 'a digit'})
653 0           | D (?{$construct_desc = 'a non-digit'})
654 0           | e (?{$construct_desc = 'an escape character'})
655 0           | f (?{$construct_desc = 'a form-feed character'})
656 0           | g (\d+) (?{$construct_desc = "what was captured in \$$^N"})
657 0 0         | g - (\d+) (?{$construct_desc = $^N == 1 ? "what was captured by the nearest preceding capture group"
658             : "what was captured $^N capture groups back" })
659 0           | g \{ (\d+) \} (?{$construct_desc = "what was captured in \$$^N"})
660 0 0         | g \{ - (\d+) \} (?{$construct_desc = $^N == 1 ? "what was captured by the nearest preceding capture group"
661             : "what was captured $^N capture groups back" })
662 0           | g \{ (\w++) \} (?{$construct_desc = "what the named capture <$^N> matched"})
663 0           | h (?{$construct_desc = 'a horizontal whitespace character'})
664 0           | H (?{$construct_desc = 'a non-horizontal-whitespace character'})
665 0           | k \< (\w++) \> (?{$construct_desc = "what the named capture <$^N> matched"})
666 0           | n (?{$construct_desc = 'a newline character'})
667 0           | N \{ ([^\}]++) \} (?{$construct_desc = "a single \L$^N\E character"})
668 0           | N (?{$construct_desc = 'a non-newline character'})
669 0           | p (\w++) (?{$construct_desc = "a character matching the Unicode property: $^N"})
670 0           | P (\w++) (?{$construct_desc = "a character not matching the Unicode property: $^N"})
671 0           | P \{ ([^\}]++) \} (?{$construct_desc = "a character not matching the Unicode property: $^N"})
672 0           | p \{ ([^\}]++) \} (?{$construct_desc = "a character matching the Unicode property: $^N"})
673 0           | r (?{$construct_desc = 'a return character'})
674 0           | R (?{$construct_desc = 'an end-of-line sequence'})
675 0           | S (?{$construct_desc = 'a non-whitespace character'})
676 0           | s (?{$construct_desc = 'a whitespace character'})
677 0           | t (?{$construct_desc = 'a tab character'})
678 0           | V (?{$construct_desc = 'a non-vertical-whitespace character'})
679 0           | v (?{$construct_desc = 'a vertical whitespace character'})
680 0           | w (?{$construct_desc = 'an identifier character'})
681 0           | W (?{$construct_desc = 'an non-identifier character'})
682 0           | x ([0-9A-Za-z]++) (?{$construct_desc = "a literal '".chr(oct('0x'.$^N))."' character"})
683 0           | x \{ ([0-9A-Za-z ]++) \} (?{$construct_desc = "a literal '".chr(oct('0x'.$^N))."' character"})
684 0           | X (?{$construct_desc = 'a Unicode grapheme cluster'})
685 0           | (.) (?{$construct_desc = "a literal '$^N' character"})
686             )
687             |
688             [(][?] P = (\w++) [)] # PCRE version of \k
689 0           (?{$construct_desc = "what the named capture <$^N> matched"})
690              
691             ) (? (?&QUANTIFIER) )?
692             |
693 0           (?{$quantifier_desc = '';})
694             (?<_charset> (?&CHARSET) ) (? (?&QUANTIFIER) )?
695 0 0         (?{$construct_desc = substr($+{_charset},0,2) eq '[^'
696             ? 'any character not listed'
697             : 'any of the listed characters'
698             })
699             |
700 0           (?{$quantifier_desc = '';})
701             (?<_named_subpattern_call>
702             [(][?]
703             (?:
704 0           [&] ((?&IDENTIFIER)) (?{$construct_desc = "a call to the subpattern named <$^N>"})
705 0           | P> ((?&IDENTIFIER)) (?{$construct_desc = "a call to the subpattern named <$^N>"})
706 0           | [+]? (\d++) (?{$construct_desc = 'a call to subpattern number $^N'})
707 0 0         | [-] (\d++) (?{$construct_desc = $^N == 1 ? "a call to the nearest preceding subpattern"
708             : "a call to the subpattern $^N back" })
709 0           | R (?{$construct_desc = 'a recursive call to the current regex'})
710             )
711             [)]
712             )
713             (? (?&QUANTIFIER) )?
714             )
715             |
716             (? \\. | . )
717             )
718              
719             (?(DEFINE)
720             # Miscellaneous useful pattern fragments...
721             (? [(][?][#] (?! \s* BREAK \s* ) .*? [)] | \# [^\n]* (?= \n | \z ) )
722             (? \[ \^?+ \]?+ (?: \[:\w+:\] | \\. | [^]\\] )*+ \] )
723             (? [^\W\d]\w* )
724             (? \{ (?: (?&CODEBLOCK) | . )*? \} )
725             (? [adlupimsx]+ (?: - [imsx]+ )?
726             | - [imsx]+
727             | \^ [alupimsx]+
728             )
729             (? \s* (?&UNSPACED_QUANTIFIER) )
730             (?
731 0           [*][+] (?{ $quantifier_desc = 'zero-or-more times (without backtracking)' })
732 0           | [*][?] (?{ $quantifier_desc = 'zero-or-more times (as few as possible)' })
733 0           | [*] (?{ $quantifier_desc = 'zero-or-more times (as many as possible)' })
734 0           | [+][+] (?{ $quantifier_desc = 'one-or-more times (without backtracking)' })
735 0           | [+][?] (?{ $quantifier_desc = 'one-or-more times (as few as possible)' })
736 0           | [+] (?{ $quantifier_desc = 'one-or-more times (as many as possible)' })
737 0           | [?][+] (?{ $quantifier_desc = 'one-or-zero times (without backtracking)' })
738 0           | [?][?] (?{ $quantifier_desc = 'zero-or-one times (as few as possible)' })
739 0           | [?] (?{ $quantifier_desc = 'one-or-zero times (as many as possible)' })
740 0           | {\d+,?\d*}[+] (?{ $quantifier_desc = 'the specified number of times (without backtracking)' })
741 0           | {\d+,?\d*}[?] (?{ $quantifier_desc = 'the specified number of times (as few as possible)' })
742 0           | {\d+,?\d*} (?{ $quantifier_desc = 'the specified number of times (as many as possible)' })
743             )
744             (? [\w~`!%&=:;"'<>,/-] )
745             )
746             }{
747             # Which event is this???
748 0           my $event_ID = $next_event_ID++;
749              
750             # What are we debugging???
751 0           my $construct = $+{construct};
752              
753             # How deep in parens???
754 0           my $depth = scalar(@paren_stack);
755 0           my $indent = $INDENT x $depth;
756              
757             # All events get this standard information...
758             my %std_info = (
759 0     0     construct_type => (first { /^_/ } keys %+),
760             construct => $construct,
761             regex_pos => length($clean_regex),
762 0   0       quantifier => $+{quantifier} // q{},
763             depth => $depth,
764             indent => $indent,
765             );
766              
767             # use Data::Dumper 'Dumper';
768             # warn Dumper { std_info => \%std_info, '%+' => \%+ };
769              
770             # Record the construct for display...
771             $clean_regex .=
772             exists $+{newline_char} ? ($std_info{construct} = q{\n} . $std_info{quantifier})
773             : exists $+{tab_char} ? ($std_info{construct} = q{\t} . $std_info{quantifier})
774             : exists $+{whitespace_chars} ? ($std_info{construct} = q{ } . $std_info{quantifier})
775 0 0         : $construct
    0          
    0          
776             ;
777              
778             # Determine and remember the necessary translation...
779 0           my $translation = do {
780              
781             # Beginning and end of regex...
782 0 0         if (exists $+{start}) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
783             # Prime paren-tracking stack...
784 0           push @paren_stack, {};
785              
786             # Insert an event to report (re-)starting...
787 0           _build_event($regex_ID, $event_ID => {
788             %std_info,
789             construct_type => '_START',
790             event_type => 'pre',
791             depth => 1,
792             lexical_scope => $lexical_scope,
793             })
794             . '(?:';
795             }
796              
797             # At end of regex (if we get here, we matched)...
798             elsif (exists $+{end}) {
799             # Insert a final event to report successful match...
800             ')'
801             . _build_event($regex_ID, $event_ID => {
802             %std_info,
803             construct_type => '_END',
804             event_type => 'post',
805             depth => 1,
806 0     0     msg => sub { my $steps = @{$history_of{visual}};
  0            
807 0 0         $steps .= ' step' . ($steps != 1 ? 's' : '');
808              
809             # Was this a second null match???
810 0           my $match_was_null = (pos == $start_str_pos);
811 0 0 0       if ($match_was_null && $prev_match_was_null) {
812 0           return "Regex matched in $steps but failed to advance within string";
813             }
814             else {
815 0           $prev_match_was_null = $match_was_null;
816 0           return "Regex matched in $steps";
817             }
818             },
819             })
820             . '|'
821             . _build_event($regex_ID, $event_ID+1 => {
822             %std_info,
823             construct_type => '_END',
824             event_type => 'post',
825             regex_failed => 1,
826             depth => 1,
827 0   0 0     msg => sub { my $steps = @{$history_of{visual}//[]};
  0            
828 0 0         "Regex failed to match"
    0          
829             . ($steps ? " after $steps step" . ($steps != 1 ? 's' : '')
830             : ' (unable to advance within string)');
831             },
832             })
833 0           . '(?!)';
834             }
835              
836             # Alternatives marked by a |...
837             elsif (exists $+{_alternation}) {
838             # Reset capture numbers if in reset group...
839 0 0         if (my $reset = $paren_stack[-1]{is_branch_reset}) {
840 0           $next_capture_group = $reset-1;
841             }
842              
843             # We need two events, so add an extra one...
844 0           $event_ID = $next_event_ID++;
845              
846             # Insert events to indicate which side of the | we're trying now...
847 0           _build_event($regex_ID, $event_ID-1 => {
848             %std_info,
849             event_type => 'end',
850             msg => 'End of successful alternative',
851             desc => 'Or...',
852             indent => $INDENT x ($depth-1),
853             })
854             . $construct
855             . _build_event($regex_ID, $event_ID => {
856             %std_info,
857             event_type => 'start',
858             msg => 'Trying next alternative',
859             })
860             }
861              
862             # Whitespace has to be treated specially (because it may or may not be significant...
863             elsif (exists $+{whitespace}) {
864             # The two events communicate privately via this variable...
865 0           my $shared_str_pos;
866              
867             # Two events required, so add an extra ID...
868 0           $next_event_ID++;
869              
870 0 0         $construct_desc = join q{}, map { $_ eq "\n" ? '\n'
  0 0          
    0          
871             : $_ eq "\t" ? '\t'
872             : $_ eq " " ? '\N{SPACE}'
873             : $_
874             } split '', $construct_desc;
875              
876             # Insert the appropriate events...
877 0           _build_whitespace_event($construct, $regex_ID, $event_ID => {
878             %std_info,
879             matchable => 1,
880             msg => "Trying literal whitespace ('$construct_desc') $quantifier_desc",
881             shared_str_pos => \$shared_str_pos,
882             })
883             }
884              
885             # \L and \U start case-shifted sequences...
886             elsif (exists $+{case_start}) {
887 0           _build_event($regex_ID, $event_ID => {
888             %std_info,
889             event_type => 'pre',
890             msg => "Starting $construct_desc",
891             desc => 'The start of ' . $construct_desc,
892             })
893             }
894              
895             elsif (exists $+{case_end}) {
896 0           _build_event($regex_ID, $event_ID => {
897             %std_info,
898             event_type => 'pre',
899             msg => 'End of autocasing',
900             desc => 'The end of autocasing',
901             })
902             }
903              
904             # \Q starts a quoted sequence...
905             elsif (exists $+{quote_start}) {
906             # Set up communication channel between \Q and \E...
907 0           my $shared_pos;
908 0           $shared_quote_pos = \$shared_pos;
909              
910 0           _build_event($regex_ID, $event_ID => {
911             %std_info,
912             event_type => 'pre',
913             msg => 'Starting quoted sequence',
914             desc => 'The start of a quoted sequence',
915             shared_str_pos => $shared_quote_pos,
916             })
917             }
918              
919             # \E ends a quoted sequence...
920             elsif (exists $+{quote_end}) {
921             # Retrieve communication channel between \Q and \E...
922 0           my $shared_pos = $shared_quote_pos;
923 0           $shared_quote_pos = undef;
924              
925 0           _build_event($regex_ID, $event_ID => {
926             %std_info,
927             event_type => 'post',
928             msg => 'End of quoted sequence',
929             desc => 'The end of a quoted sequence',
930             shared_str_pos => $shared_pos,
931             })
932             }
933              
934              
935             # Quoted subsequences...
936             elsif (exists $+{quote_space}) {
937             # The two events communicate privately via this variable...
938 0           my $shared_str_pos;
939              
940             # Two events, so add an extra ID...
941 0           $event_ID = $next_event_ID++;
942              
943 0           _build_event($regex_ID, $event_ID-1 => {
944             %std_info,
945             matchable => 1,
946             event_type => 'pre',
947             msg => 'Trying autoquoted literal whitespace',
948             shared_str_pos => \$shared_str_pos,
949             })
950             . quotemeta($construct)
951             . _build_event($regex_ID, $event_ID => {
952             %std_info,
953             matchable => 1,
954             event_type => 'post',
955             msg => 'Matched autoquoted literal whitespace',
956             shared_str_pos => \$shared_str_pos,
957             })
958             }
959              
960             elsif (exists $+{quote_nonspace}) {
961             # The two events communicate privately via this variable...
962 0           my $shared_str_pos;
963              
964             # Two events, so add an extra ID...
965 0           $event_ID = $next_event_ID++;
966              
967 0           _build_event($regex_ID, $event_ID-1 => {
968             %std_info,
969             matchable => 1,
970             event_type => 'pre',
971             msg => 'Trying an autoquoted literal character',
972             desc => 'Match an autoquoted literal character',
973             shared_str_pos => \$shared_str_pos,
974             })
975             . quotemeta($construct)
976             . _build_event($regex_ID, $event_ID => {
977             %std_info,
978             matchable => 1,
979             event_type => 'post',
980             msg => 'Matched a literal character',
981             shared_str_pos => \$shared_str_pos,
982             })
983             }
984              
985             # Atoms are any elements that match and emit debugging info before and after matching...
986             elsif (exists $+{atom}) {
987             # The two events communicate privately via this variable...
988 0           my $shared_str_pos;
989              
990             # Track depth of subpattern calls...
991 0           my $is_subpattern_call = exists $+{_named_subpattern_call};
992 0 0         my $subpattern_call_prefix
993             = $is_subpattern_call
994             ? q{(?{local $Regexp::Debugger::subpattern_depth = $Regexp::Debugger::subpattern_depth + 1})}
995             : q{};
996 0 0         my $subpattern_call_suffix
997             = $is_subpattern_call
998             ? q{(?{local $Regexp::Debugger::subpattern_depth = $Regexp::Debugger::subpattern_depth - 1})}
999             : q{};
1000              
1001             # Two events, so add an extra ID...
1002 0           $event_ID = $next_event_ID++;
1003 0 0         _build_event($regex_ID, $event_ID-1 => {
    0          
    0          
1004             %std_info,
1005             matchable => 1,
1006             event_type => 'pre',
1007             msg => "Trying $construct_desc" . (length($quantifier_desc) ? ", $quantifier_desc" : q{}),
1008             desc => "Match $construct_desc" . (length($quantifier_desc) ? ", $quantifier_desc" : q{}),
1009             shared_str_pos => \$shared_str_pos,
1010             })
1011             . $subpattern_call_prefix
1012             . $construct
1013             . $subpattern_call_suffix
1014             . _build_event($regex_ID, $event_ID => {
1015             %std_info,
1016             matchable => 1,
1017             event_type => 'post',
1018             msg => 'Matched'
1019             . ($is_subpattern_call ? " (discarding subpattern's captures)": q{}),
1020             shared_str_pos => \$shared_str_pos,
1021             })
1022             }
1023              
1024             # Code blocks (?{...})...
1025             elsif (exists $+{code_block}) {
1026             # Add an event beforehand to indicate execution of the block...
1027 0           _build_event($regex_ID, $event_ID => {
1028             %std_info,
1029             matchable => 0,
1030             event_type => 'action',
1031             msg => 'Executing code block',
1032             desc => 'Execute a block of code',
1033             })
1034             . $construct
1035             }
1036              
1037             # Code blocks that generate dynamic patterns (??{...})...
1038             elsif (exists $+{matchable_code_block}) {
1039             # These events communicate privately via this variable...
1040 0           my $shared_str_pos;
1041              
1042             # Modify construct to generate but not match...
1043 0           substr($construct, 1, 1) = q{};
1044              
1045             # Inserting three events, so add an extra two IDs...
1046 0           $event_ID = ($next_event_ID+=3);
1047             # First event pair reports executing the block...
1048             _build_event($regex_ID, $event_ID-4 => {
1049             %std_info,
1050             matchable => 0,
1051             event_type => 'action',
1052             msg => 'Executing code block of postponed subpattern',
1053             desc => "Execute a code block, then match the block's final value",
1054             })
1055             . $construct
1056             . _build_event($regex_ID, $event_ID-3 => {
1057             %std_info,
1058             matchable => 0,
1059             event_type => 'action',
1060 0     0     msg => sub { "Code block returned: '$^R'" },
1061             })
1062             # Second event pair reports match of subpattern the block returned...
1063             . _build_event($regex_ID, $event_ID-2 => {
1064             %std_info,
1065             matchable => 1,
1066             event_type => 'pre',
1067 0     0     msg => sub{ "Trying: qr{$^R}" },
1068 0           shared_str_pos => \$shared_str_pos,
1069             })
1070             . '(??{ $^R })'
1071             . _build_event($regex_ID, $event_ID-1 => {
1072             %std_info,
1073             matchable => 1,
1074             event_type => 'post',
1075             msg => 'Matched',
1076             shared_str_pos => \$shared_str_pos,
1077             })
1078             }
1079              
1080             # Keep marker...
1081             elsif (exists $+{keep_marker}) {
1082             # Insert events reporting testing the assertion, and if the test succeeds...
1083 0           _build_event($regex_ID, $event_ID => {
1084             %std_info,
1085             matchable => 0,
1086             event_type => 'action',
1087             msg => "Forgetting everything matched to this point",
1088             desc => 'Pretend the final match starts here',
1089             })
1090             . $construct
1091             . '(?{ local $Regexp::Grammars::match_start_pos = pos() })'
1092             }
1093              
1094             # Zero-width assertions...
1095             elsif (exists $+{zero_width}) {
1096             # Two events, so add an extra ID...
1097 0           $event_ID = $next_event_ID++;
1098              
1099             # Insert events reporting testing the assertion, and if the test succeeds...
1100 0           _build_event($regex_ID, $event_ID-1 => {
1101             %std_info,
1102             matchable => 1,
1103             event_type => 'pre',
1104             msg => "Testing if $construct_desc",
1105             desc => "Match only if $construct_desc",
1106             })
1107             . $construct
1108             . _build_event($regex_ID, $event_ID => {
1109             %std_info,
1110             matchable => 1,
1111             event_type => 'post',
1112             msg => 'Assertion satisfied',
1113             })
1114             }
1115              
1116             # Control verbs: (*PRUNE) (*SKIP) (*FAIL) etc...
1117             elsif (exists $+{control}) {
1118             # Two events, so add an extra ID...
1119 0           $event_ID = $next_event_ID++;
1120              
1121             # Insert events to report both the attempt and its success...
1122 0           _build_event($regex_ID, $event_ID-1 => {
1123             %std_info,
1124             matchable => 1,
1125             event_type => 'pre',
1126             msg => 'Executing a control',
1127             desc => 'Execute a backtracking control',
1128             })
1129             . $construct
1130             . _build_event($regex_ID, $event_ID => {
1131             %std_info,
1132             matchable => 1,
1133             event_type => 'post',
1134             msg => 'Control succeeded',
1135             })
1136             }
1137              
1138             # Start of DEFINE block...
1139             elsif (exists $+{define_block}) {
1140             # It's an unbalanced opening paren, so remember it on the stack...
1141 0           push @paren_stack, {
1142             is_capture => 0,
1143             construct_type => '_DEFINE_block',
1144             is_definition => 1,
1145             };
1146              
1147             # Insert and event to report skipping the entire block...
1148             _build_event($regex_ID, $event_ID => {
1149             %std_info,
1150 0           %{$paren_stack[-1]},
  0            
1151             matchable => 0,
1152             event_type => 'pre',
1153             msg => 'Skipping definitions',
1154             desc => 'The start of a definition block (skipped during matching)',
1155             })
1156             . $construct . '(?:'
1157             }
1158              
1159             # Modifier set: (?is-mx) etc...
1160             elsif (exists $+{modifier_set}) {
1161             # Insert an event to report the change of active modifiers...
1162             _build_event($regex_ID, $event_ID => {
1163             %std_info,
1164 0           %{$paren_stack[-1]},
  0            
1165             matchable => 0,
1166             event_type => 'compile',
1167             msg => 'Changing modifiers',
1168             desc => 'Change current modifiers',
1169             })
1170             . $construct
1171             }
1172              
1173             # Conditional parens: (?(COND) X | Y )...
1174             elsif (exists $+{conditional_paren}) {
1175             # It's an unbalanced opening paren, so remember it on the stack...
1176             push @paren_stack, {
1177             is_capture => 0,
1178             is_conditional => 1,
1179             is_pending => exists $+{pending_condition}, # ...expecting a lookahead?
1180 0           construct_type => '_conditional_group',
1181             };
1182              
1183             # Insert an event to report the test...
1184             '(?:'
1185             . _build_event($regex_ID, $event_ID => {
1186             %std_info,
1187 0           %{$paren_stack[-1]},
  0            
1188             event_type => 'pre',
1189             msg => 'Testing condition',
1190             desc => 'The start of a conditional block',
1191             })
1192             . $construct;
1193             }
1194              
1195             # Branch-reset parens...
1196             elsif (exists $+{branch_reset_paren}) {
1197             # It's an unbalanced opening paren, so remember it on the stack...
1198 0           push @paren_stack, {
1199             is_capture => 0,
1200             is_branch_reset => $next_capture_group+1,
1201             construct_type => '_branch_reset_group',
1202             };
1203              
1204             # Insert an event to report the start of branch-reseting...
1205             '(?:'
1206             . _build_event($regex_ID, $event_ID => {
1207             %std_info,
1208 0           %{$paren_stack[-1]},
  0            
1209             event_type => 'pre',
1210             msg => 'Starting branch-resetting group',
1211             desc => 'The start of a branch-resetting group',
1212             })
1213             . $construct;
1214             }
1215              
1216             # Non-capturing parens...
1217             elsif (exists $+{noncapturing_paren}) {
1218             # Do the non-capturing parens have embedded modifiers???
1219 0 0         my $addendum = length($construct) > 3 ? ', changing modifiers' : q{};
1220              
1221             # It's an unbalanced opening paren, so remember it on the stack...
1222 0           push @paren_stack, {
1223             is_capture => 0,
1224             construct_type => '_noncapture_group',
1225             };
1226              
1227             # Insert an event to report the start of a non-capturing group...
1228             '(?:'
1229             . _build_event($regex_ID, $event_ID => {
1230             %std_info,
1231 0           %{$paren_stack[-1]},
  0            
1232             event_type => 'pre',
1233             msg => 'Starting non-capturing group' . $addendum,
1234             desc => 'The start of a non-capturing group',
1235             })
1236             . $construct;
1237             }
1238              
1239             # Non-backtracking parens...
1240             elsif (exists $+{non_backtracking_paren}) {
1241             # It's an unbalanced opening paren, so remember it on the stack...
1242 0           push @paren_stack, {
1243             is_capture => 0,
1244             is_nonbacktrack => 1,
1245             construct_type => '_nonbacktracking_group',
1246             };
1247              
1248             # Insert an event to report the start of a non-backtracking group...
1249             '(?:'
1250             . _build_event($regex_ID, $event_ID => {
1251             %std_info,
1252 0           %{$paren_stack[-1]},
  0            
1253             event_type => 'pre',
1254             msg => 'Starting non-backtracking group',
1255             desc => 'The start of a non-backtracking group',
1256             })
1257             . '(?>';
1258             }
1259              
1260             # Positive lookahead/lookbehind parens...
1261             elsif (exists $+{lookaround_paren}) {
1262             # It's an unbalanced opening paren, so remember it on the stack...
1263             push @paren_stack, {
1264             is_capture => 0,
1265 0           is_lookaround => $LOOKTYPE{$construct},
1266             construct_type => '_lookaround',
1267             };
1268              
1269             # Is this lookaround the test of a (?(COND) X | Y) conditional???
1270 0 0 0       if ($paren_stack[-2]{is_conditional} && $paren_stack[-2]{is_pending}) {
1271             # If so, the test is no longer pending...
1272 0           delete $paren_stack[-2]{is_pending};
1273              
1274             # Insert an event to report the test...
1275             $construct
1276             . '(?:'
1277             . _build_event($regex_ID, $event_ID => {
1278             %std_info,
1279 0           %{$paren_stack[-1]},
1280             event_type => 'pre',
1281             msg => 'Testing for ' . $LOOKTYPE{$construct},
1282 0           desc => 'Match ' . lc $LOOKTYPE{$construct},
1283             });
1284             }
1285             else {
1286             # Otherwise, insert an event to report the start of the lookaround...
1287             '(?:'
1288             . _build_event($regex_ID, $event_ID => {
1289             %std_info,
1290 0           %{$paren_stack[-1]},
1291             event_type => 'pre',
1292             msg => 'Starting ' . $LOOKTYPE{$construct},
1293 0           desc => 'Match ' . $LOOKTYPE{$construct},
1294             })
1295             . $construct;
1296             }
1297             }
1298              
1299             # Capturing parens...
1300             elsif (exists $+{capturing_paren}) {
1301             # The events communicate privately via this variable...
1302 0           my $shared_str_pos;
1303              
1304             # Get the corresponding capture group number...
1305 0           $next_capture_group++;
1306              
1307             # Track the maximum group number (for after branch resets)...
1308 0           $max_capture_group = max($max_capture_group, $next_capture_group);
1309              
1310             # It's an unbalanced opening paren, so remember it on the stack...
1311 0           push @paren_stack, {
1312             is_capture => 1,
1313             construct_type => '_capture_group',
1314             capture_name => '$'.$next_capture_group,
1315             shared_str_pos => \$shared_str_pos,
1316             };
1317              
1318             # Insert an event to report the start of capturing...
1319             '('
1320             . _build_event($regex_ID, $event_ID => {
1321             %std_info,
1322 0           %{$paren_stack[-1]},
  0            
1323             event_type => 'pre',
1324             msg => 'Capture to $'.$next_capture_group,
1325             desc => "The start of a capturing block (\$$next_capture_group)",
1326             })
1327             . '(?:';
1328             }
1329              
1330             # Named capturing parens...
1331             elsif (exists $+{named_capturing_paren}) {
1332             # The events communicate privately via this variable...
1333 0           my $shared_str_pos;
1334              
1335             # Named capture groups are also numbered, so get the number...
1336 0           $next_capture_group++;
1337              
1338             # Track the maximum group number (for after branch resets)...
1339 0           $max_capture_group = max($max_capture_group, $next_capture_group);
1340              
1341             # If this creates a new numbered capture, remember the number...
1342 0 0 0       if (!@{$capture_names_for[$next_capture_group]//[]}) {
  0            
1343 0           push @{$capture_names_for[$next_capture_group]}, '$'.$next_capture_group;
  0            
1344             }
1345              
1346             # Add this name to the list of aliases for the same numbered capture...
1347             # (Needed because named captures in two reset branches may alias
1348             # to the same underlying numbered capture variable. See perlre)
1349 0           push @{$capture_names_for[$next_capture_group]}, '$+{'.$+{capture_name}.'}';
  0            
1350              
1351             # It's an unbalanced opening paren, so remember it on the stack...
1352 0           push @paren_stack, {
1353             is_capture => 1,
1354             construct_type => '_capture_group',
1355             capture_name => $capture_names_for[$next_capture_group],
1356             shared_str_pos => \$shared_str_pos,
1357             };
1358              
1359             # Insert an event to report the start of the named capture...
1360             $construct
1361             . _build_event($regex_ID, $event_ID => {
1362             %std_info,
1363 0           %{$paren_stack[-1]},
  0            
1364             event_type => 'pre',
1365             msg => $capture_names_for[$next_capture_group],
1366             desc => "The start of a named capturing block (also \$$next_capture_group)",
1367             })
1368             . '(?:';
1369             }
1370              
1371             # Closing parens have to be deciphered...
1372             elsif (exists $+{closing_paren}) {
1373             # The top of the paren stack tells us what kind of group we're closing...
1374 0   0       my $paren_data = pop(@paren_stack) // { type=>'unmatched closing )' };
1375              
1376             # Update the next capture group number, if after a branch reset group...
1377 0 0         if ($paren_data->{is_branch_reset}) {
1378 0           $next_capture_group = $max_capture_group;
1379             }
1380              
1381             # Generate an appropriate message for the type of group being closed...
1382             my $msg = $paren_data->{is_capture} && ref $paren_data->{capture_name}
1383             ? $paren_data->{capture_name}
1384             : $paren_data->{is_capture} ? 'End of ' . $paren_data->{capture_name}
1385             : $paren_data->{is_definition} ? 'End of definition block'
1386             : $paren_data->{is_branch_reset} ? 'End of branch-resetting group'
1387             : $paren_data->{is_lookaround} ? 'End of ' . $paren_data->{is_lookaround}
1388             : $paren_data->{is_conditional} ? 'End of conditional group'
1389 0 0 0       : $paren_data->{is_nonbacktrack} ? 'End of non-backtracking group'
    0          
    0          
    0          
    0          
    0          
    0          
1390             : 'End of non-capturing group'
1391             ;
1392              
1393 0 0         if (length($std_info{quantifier})) {
1394 0           $msg .= " (matching $quantifier_desc)";
1395             }
1396              
1397             # Two events, so add an extra ID...
1398 0           $event_ID = $next_event_ID++;
1399              
1400             # Append an event reporting the completion of the group...
1401             ')'
1402             . _build_event($regex_ID, $event_ID-1 => {
1403             %std_info,
1404 0           %{$paren_data},
1405             event_type => 'post',
1406             msg => $msg,
1407             desc => ( ref $msg ? 'The end of the named capturing block' : 'The e' . substr($msg,1) ),
1408             depth => $depth - 1,
1409             indent => $INDENT x ($depth - 1),
1410             })
1411             . ($paren_data->{is_nonbacktrack}
1412             ? '|'
1413             . _build_event($regex_ID, $event_ID => {
1414             %std_info,
1415 0           %{$paren_data},
1416             event_type => 'failed_nonbacktracking',
1417             msg => 'non-backtracking group',
1418             depth => $depth - 1,
1419             indent => $INDENT x ($depth - 1),
1420             })
1421             . q{(?!)}
1422             : q{}
1423             )
1424             . ')'
1425 0 0         . $std_info{quantifier};
    0          
1426             }
1427              
1428             # Skip comments...
1429             elsif (exists $+{break_comment}) {
1430             # Insert an event reporting that the break comment is being skipped...
1431             _build_event($regex_ID, $event_ID => {
1432             %std_info,
1433 0           %{$paren_stack[-1]},
  0            
1434             matchable => 0,
1435             event_type => 'break',
1436             msg => 'Breaking at (and skipping) comment',
1437             desc => 'Ignore this comment (but Regexp::Debugger will break here)',
1438             })
1439             }
1440              
1441             # Skip comments...
1442             elsif (exists $+{comment}) {
1443             # Insert an event reporting that the comment is being skipped...
1444             _build_event($regex_ID, $event_ID => {
1445             %std_info,
1446 0           %{$paren_stack[-1]},
  0            
1447             matchable => 0,
1448             event_type => 'skip',
1449             msg => 'Skipping comment',
1450             desc => 'Ignore this comment',
1451             })
1452             }
1453              
1454             # Ignore (but preserve) anything else...
1455             else {
1456 0           $construct;
1457             }
1458             };
1459             }exmsg;
1460              
1461             # Remember the regex...
1462 0           $state{$regex_ID}{regex_src} = $clean_regex;
1463              
1464             # Add a preface to reset state variables in the event handler...
1465 0           $raw_regex = '(?>\A(?{Regexp::Debugger::_reset_debugger_state()})(?!)'
1466             . '|\G(?{Regexp::Debugger::_reset_debugger_state_rematch()})(?!))'
1467             . "|(?:$raw_regex)";
1468              
1469             # say "(?#R_d:$regex_ID)".$raw_regex;
1470 0           return "(?#R_d:$regex_ID)".$raw_regex;
1471             }
1472              
1473             #====[ Dispatch in-regex events ]================================
1474              
1475             # How big the display window is...
1476             my $MAX_WIDTH = 80;
1477             my $MAX_HEIGHT = 60;
1478              
1479             # What to print so as to "clear" the screen...
1480             my $CLEAR_SCREEN = "\n" x $MAX_HEIGHT;
1481              
1482             # How wide is each column in event mode...
1483             my $EVENT_COL_WIDTH = 15;
1484              
1485              
1486             sub _record_event {
1487 0     0     my ($data_mode, $event_desc) = @_;
1488              
1489             # Accumulate history...
1490             my $history_to_date
1491 0 0 0       = @{$history_of{$data_mode}//[]} ? $history_of{$data_mode}[-1]{display} : q{};
  0            
1492              
1493             # Remember, always....
1494 0           push @{$history_of{$data_mode}}, {
  0            
1495             display => $history_to_date . $event_desc . "\n"
1496             };
1497             }
1498              
1499             sub _show_if_active {
1500 0     0     my ($data_mode, $display_mode, $event_desc) = @_;
1501              
1502             # Show, if appropriate...
1503 0 0         if ($display_mode eq $data_mode) {
1504 0 0 0       if (!$lexical_config->{save_to_fh} || $data_mode ne 'JSON') {
1505 0           _print $CLEAR_SCREEN;
1506 0           _say $history_of{$data_mode}[-1]{display};
1507             }
1508             }
1509             }
1510              
1511 0     0     sub _show_JSON { _show_if_active('JSON', @_) }
1512 0     0     sub _show_event { _show_if_active('events', @_) }
1513              
1514              
1515             # Add a new animation "frame"...
1516             sub _new_visualize {
1517 0     0     our $subpattern_depth;
1518 0           my ($data_mode) = @_;
1519 0           push @{$history_of{$data_mode}}, { display=>q{}, is_match => 0, depth => $subpattern_depth };
  0            
1520             }
1521              
1522             # Output the args and also add them to the current animation "frame"
1523             sub _visualize {
1524 0     0     my ($data_mode, @output) = @_;
1525 0           state $NO_MATCH = 0;
1526 0           state $NO_FAIL = 0;
1527 0           _visualize_matchfail($data_mode, $NO_MATCH, $NO_FAIL, @output);
1528             }
1529              
1530             sub _visualize_matchfail {
1531 0     0     my ($data_mode, $is_match, $is_fail, @output) = @_;
1532 0           my $output = join q{}, grep {defined} @output;
  0            
1533              
1534 0 0         $history_of{$data_mode}[-1]{is_fail} = 1 if $is_fail;
1535 0 0         $history_of{$data_mode}[-1]{is_match} = 1 if $is_match;
1536 0           $history_of{$data_mode}[-1]{display} .= $output . "\n";
1537             }
1538              
1539             # Show previous animation frames...
1540             sub _revisualize {
1541 0     0     my ($regex_ID, $input, $step) = @_;
1542              
1543             # Start at the previous step unless otherwise specified...
1544 0   0       $step //= max(0, @{$history_of{$display_mode}}-2);
  0            
1545              
1546             STEP:
1547 0           while (1) {
1548             # Did we fall out of available history???
1549 0 0         last STEP if $step >= @{$history_of{$display_mode}};
  0            
1550              
1551             # A terminates the process...
1552 0 0 0       if ($input eq "\cC") {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
1553 0           kill 9, $$;
1554             }
1555              
1556             # An 'x' exits the process...
1557             elsif ($input eq 'x') {
1558 0           exit(0);
1559             }
1560              
1561             # A redraws the screen at the current step...
1562             elsif ($input eq "\cL") {
1563             # Do nothing else
1564             }
1565              
1566             # Step back (if possible)...
1567             elsif ($input eq '-') {
1568 0           $step = max(0, $step-1);
1569             }
1570              
1571             # Display explanation of regex...
1572             elsif ($input eq 'd') {
1573 0           _show_regex_description($regex_ID);
1574             }
1575              
1576             # Help!
1577             elsif ($input eq '?') {
1578 0           _show_help();
1579             }
1580              
1581             # Swap to requested mode...
1582             elsif ($input eq 'v') {
1583 0           $display_mode = 'visual';
1584             }
1585              
1586             elsif ($input eq 'h') {
1587             # Can we use heatmap mode?
1588 0 0         if ($heatmaps_invisible) {
1589 0           say 'Cannot show heatmaps (Term::ANSIColor unavailable)';
1590 0           say "Try 'H' instead";
1591 0           $input = '?';
1592             }
1593             # If heatmaps available, check for misuse of 'h' instead of '?'...
1594             else {
1595 0           my $prompt_help = $display_mode eq 'heatmap';
1596 0           $display_mode = 'heatmap';
1597 0 0         if ($prompt_help) {
1598 0           say "(Type '?' for help)";
1599             }
1600             }
1601             }
1602              
1603             elsif ($input eq 'e') {
1604 0           $display_mode = 'events';
1605             # say _info_colourer(
1606             # qq{\n\n[Events of regex at $state{$regex_ID}{location}]}
1607             # . qq{ [step: $step]}
1608             # );
1609             }
1610              
1611             elsif ($input eq 'j') {
1612 0           $display_mode = 'JSON';
1613             # say _info_colourer(
1614             # qq{\n\n[JSON data of regex at $state{$regex_ID}{location}]}
1615             # . qq{ [step: $step]}
1616             # );
1617             }
1618              
1619             # Quit entirely...
1620             elsif ($input eq 'q' || $input eq "\cD") {
1621 0           last STEP;
1622             }
1623              
1624             # Take a snapshot...
1625 0           elsif ($input eq 'V') { _save_snapshot('full_visual', $step); }
1626 0           elsif ($input eq 'H') { _save_snapshot('full_heatmap', $step); }
1627 0           elsif ($input eq 'E') { _save_snapshot('events', $step); }
1628 0           elsif ($input eq 'J') { _save_snapshot('JSON', $step); }
1629 0           elsif ($input eq 'D') { _show_regex_description($regex_ID,'save'); }
1630              
1631             # Step forward until end...
1632             elsif ($input eq 'c') {
1633 0           my $skip_duration = $MAX_SKIP_DURATION;
1634              
1635 0           while (1) {
1636 0           $step++;
1637 0 0         last STEP if $step >= @{$history_of{$display_mode}}-1;
  0            
1638              
1639 0           _print $CLEAR_SCREEN;
1640 0           _print $history_of{$display_mode}[$step]{display};
1641 0           _pause($skip_duration);
1642 0           $skip_duration = max($MIN_SKIP_DURATION, $skip_duration * $SKIP_ACCELERATION);
1643             }
1644             }
1645              
1646             elsif ($input eq 'C') {
1647 0           $interaction_depth = $history_of{$display_mode}[$step]{depth};
1648 0           my $skip_duration = $MAX_SKIP_DURATION;
1649              
1650 0           while (1) {
1651 0           $step++;
1652 0 0         last STEP if $step >= @{$history_of{$display_mode}}-1;
  0            
1653              
1654 0           my $event = $history_of{$display_mode}[$step];
1655 0   0       my $depth = $event->{depth} // 0;
1656              
1657 0 0         if ($depth <= $interaction_depth) {
1658 0           _print $CLEAR_SCREEN;
1659 0           _print $event->{display};
1660 0           _pause($skip_duration);
1661 0           $skip_duration = max($MIN_SKIP_DURATION, $skip_duration * $SKIP_ACCELERATION);
1662             }
1663             }
1664             }
1665              
1666              
1667             # Step forward to match...
1668             elsif ($input eq 'm') {
1669 0           my $skip_duration = $MAX_SKIP_DURATION;
1670              
1671             SEARCH:
1672 0           while (1) {
1673 0           $step++;
1674 0 0         last STEP if $step >= @{$history_of{$display_mode}}-1;
  0            
1675 0 0         last SEARCH if $history_of{$display_mode}[$step]{is_match};
1676              
1677 0           _print $CLEAR_SCREEN;
1678 0           _print $history_of{$display_mode}[$step]{display};
1679 0           _pause($skip_duration);
1680 0           $skip_duration = max($MIN_SKIP_DURATION, $skip_duration * $SKIP_ACCELERATION);
1681             }
1682             }
1683              
1684             elsif ($input eq 'M') {
1685 0           $interaction_depth = $history_of{$display_mode}[$step]{depth};
1686 0           my $skip_duration = $MAX_SKIP_DURATION;
1687              
1688             SEARCH:
1689 0           while (1) {
1690 0           $step++;
1691 0 0         last STEP if $step >= @{$history_of{$display_mode}}-1;
  0            
1692              
1693 0           my $event = $history_of{$display_mode}[$step];
1694 0   0       my $depth = $event->{depth} // 0;
1695 0 0 0       last SEARCH if $event->{is_match} && $depth <= $interaction_depth;
1696              
1697 0 0         if ($depth <= $interaction_depth) {
1698 0           _print $CLEAR_SCREEN;
1699 0           _print $event->{display};
1700 0           _pause($skip_duration);
1701 0           $skip_duration = max($MIN_SKIP_DURATION, $skip_duration * $SKIP_ACCELERATION);
1702             }
1703             }
1704             }
1705              
1706             # Step forward to fail...
1707             elsif ($input eq 'f') {
1708 0           $interaction_depth = $history_of{$display_mode}[$step]{depth};
1709 0           my $skip_duration = $MAX_SKIP_DURATION;
1710              
1711             SEARCH:
1712 0           while (1) {
1713 0           $step++;
1714 0 0         last STEP if $step >= @{$history_of{$display_mode}}-1;
  0            
1715 0 0         last SEARCH if $history_of{$display_mode}[$step]{is_fail};
1716              
1717 0           _print $CLEAR_SCREEN;
1718 0           _print $history_of{$display_mode}[$step]{display};
1719 0           _pause($skip_duration);
1720 0           $skip_duration = max($MIN_SKIP_DURATION, $skip_duration * $SKIP_ACCELERATION);
1721             }
1722             }
1723              
1724             elsif ($input eq 'F') {
1725 0           $interaction_depth = $history_of{$display_mode}[$step]{depth};
1726 0           my $skip_duration = $MAX_SKIP_DURATION;
1727              
1728             SEARCH:
1729 0           while (1) {
1730 0           $step++;
1731 0 0         last STEP if $step >= @{$history_of{$display_mode}}-1;
  0            
1732              
1733 0           my $event = $history_of{$display_mode}[$step];
1734 0   0       my $depth = $event->{depth} // 0;
1735 0 0 0       last SEARCH if $event->{is_fail} && $depth <= $interaction_depth;
1736              
1737 0 0         if ($depth <= $interaction_depth) {
1738 0           _print $CLEAR_SCREEN;
1739 0           _print $event->{display};
1740 0           _pause($skip_duration);
1741 0           $skip_duration = max($MIN_SKIP_DURATION, $skip_duration * $SKIP_ACCELERATION);
1742             }
1743             }
1744             }
1745              
1746             # Return from current subpattern...
1747             elsif ($input eq 'r') {
1748 0           $interaction_depth = $history_of{$display_mode}[$step]{depth};
1749 0           my $skip_duration = $MAX_SKIP_DURATION;
1750              
1751             SEARCH:
1752 0           while (1) {
1753 0           $step++;
1754 0 0         last STEP if $step >= @{$history_of{$display_mode}}-1;
  0            
1755 0 0         last SEARCH if $history_of{$display_mode}[$step]{depth} < $interaction_depth;
1756             }
1757             }
1758              
1759             # Step forward, skipping subpatterns...
1760             elsif ($input eq 'n') {
1761 0           $interaction_depth = $history_of{$display_mode}[$step]{depth};
1762 0           $step++;
1763 0 0         last STEP if $step >= @{$history_of{$display_mode}}-1;
  0            
1764 0           while ($history_of{$display_mode}[$step]{depth} > $interaction_depth) {
1765 0 0         last STEP if $step >= @{$history_of{$display_mode}}-1;
  0            
1766 0           $step++;
1767             }
1768             }
1769              
1770             # Step back, skipping subpatterns...
1771             elsif ($input eq 'p') {
1772 0           $interaction_depth = $history_of{$display_mode}[$step+1]{depth};
1773 0           $step = max(0, $step-1);
1774 0           until ($history_of{$display_mode}[$step]{depth} <= $interaction_depth) {
1775 0           $step = max(0, $step-1);
1776             }
1777             }
1778              
1779             # Step all the way back, skipping subpatterns...
1780             elsif ($input eq 'R') {
1781 0           $interaction_depth = $history_of{$display_mode}[0]{depth};
1782 0           $step = 0;
1783             }
1784              
1785              
1786             # Otherwise just step forward...
1787             else {
1788 0           $step++;
1789             }
1790              
1791             # Clear display and show the requested step...
1792 0 0         if ($input ne '?') {
1793 0           _print $CLEAR_SCREEN;
1794 0           _print $history_of{$display_mode}[$step]{display};
1795 0 0 0       if ($display_mode eq 'events' || $display_mode eq 'JSON') {
1796 0 0         if (!$lexical_config->{save_to_fh}) {
1797 0           say _info_colourer(
1798             qq{\n\n[\u$display_mode of regex at $state{$regex_ID}{location}]}
1799             . qq{ [step: $step]}
1800             );
1801             }
1802             }
1803             }
1804              
1805             # Next input (but use starting cmd if one given)...
1806 0           $input = _interact();
1807              
1808             }
1809              
1810             # Update the screen...
1811 0 0         if (defined $history_of{$display_mode}[$step]{display}) {
1812 0           _print $CLEAR_SCREEN;
1813 0           _print $history_of{$display_mode}[$step]{display};
1814             }
1815              
1816             # Return final command...
1817 0           return ($input, $step);
1818             }
1819              
1820             sub _build_visualization {
1821             # Unpack all the info needed...
1822 0     0     my ($data_mode, $named_args_ref) = @_;
1823              
1824             my ($regex_ID, $regex_src, $regex_pos, $construct_len,
1825             $str_src, $str_pos,
1826             $is_match, $is_fail, $is_trying, $is_capture,
1827             $backtrack, $forward_step, $nested_because,
1828             $msg, $colourer, $no_window, $step)
1829 0           = @{$named_args_ref}{qw(
  0            
1830             regex_ID regex_src regex_pos construct_len
1831             str_src str_pos
1832             is_match is_fail is_trying is_capture
1833             backtrack forward_step nested_because
1834             msg colourer no_window step
1835             )};
1836              
1837             # Clear screen...
1838 0           _new_visualize($data_mode);
1839 0 0         if (!$no_window) {
1840 0           _visualize $data_mode, q{} for 1..$MAX_HEIGHT;
1841             }
1842              
1843             # Remember originals...
1844 0           my $raw_str_src = $str_src;
1845 0           my $raw_regex_src = $regex_src;
1846              
1847             # Unwindowed displays show the title first...
1848 0 0         if ($no_window) {
1849 0           _visualize $data_mode,
1850             _info_colourer(
1851             qq{\n[\u$data_mode of regex at $state{$regex_ID}{location}]\n\n}
1852             . qq{ [step: $step]}
1853             );
1854             }
1855              
1856             # Visualize capture vars, if available...
1857 0           my $max_name_width = 1 + max map {length} 0, keys %capture;
  0            
1858             CAPVAR:
1859 1     1   9 for my $name (do{ no warnings 'numeric'; sort { substr($a,1) <=> substr($b,1) } keys %capture}) {
  1         2  
  1         767  
  0            
  0            
  0            
1860             # Remove any captures that are invalidated by backtracking...
1861 0 0         if ($capture{$name}{start_pos} > $regex_pos) {
1862 0           delete @{$capture{$name}}{'from','to'};
  0            
1863             }
1864              
1865             # Clean up and visualize each remaining variable...
1866 0   0       my $start = $capture{$name}{from} // next CAPVAR;
1867 0   0       my $end = $capture{$name}{to} // next CAPVAR;
1868 0           my $cap_str = _quote_ws(substr($_,$start,$end-$start));
1869              
1870             # Truncate captured value to maximum width by removing middle...
1871 0           my $cap_len = length($cap_str);
1872 0 0         if ($cap_len > $MAX_WIDTH) {
1873 0           my $middle = $MAX_WIDTH/2 - 2;
1874 0           substr($cap_str, $middle, -$middle, '....');
1875             }
1876              
1877             # Display capture var and value...
1878 0           _visualize $data_mode,
1879             _info_colourer(sprintf qq{%*s = '%s'}, $max_name_width, $name, $cap_str);
1880             }
1881              
1882             # Visualize special var, if used in regex...
1883 0           _visualize $data_mode, q{};
1884 0 0 0       if (index($raw_regex_src, '$^N') >= 0 && defined $^N) {
1885 0           my $special_val = $^N;
1886              
1887             # Truncate captured value to maximum width by removing middle...
1888 0           my $cap_len = length($special_val);
1889 0 0         if ($cap_len > $MAX_WIDTH) {
1890 0           my $middle = $MAX_WIDTH/2 - 2;
1891 0           substr($special_val, $middle, -$middle, '....');
1892             }
1893              
1894             # Display capture var and value...
1895 0           _visualize $data_mode,
1896             _info_colourer(sprintf qq{%*s = '%s'}, $max_name_width, '$^N', $special_val);
1897             }
1898              
1899             # Leave a gap...
1900 0           _visualize $data_mode, q{} for 1..2;
1901              
1902             # Show matching...
1903 0           _visualize_matchfail $data_mode, $is_match, $is_fail;
1904              
1905             # Reconfigure regex within visible window...
1906             ($regex_src, $regex_pos)
1907             = _make_window(
1908             text => $regex_src,
1909             pos => $regex_pos,
1910 0 0         heat => substr($data_mode, -7) eq 'heatmap' ? $history_of{match_heatmap} : [],
1911             ws_colour => substr($data_mode, -7) eq 'heatmap',
1912             no_window => $no_window,
1913             );
1914              
1915             # How wide is the display???
1916 0 0         my $display_width
1917             = $no_window ? $regex_pos
1918             : max(0,min($regex_pos, $MAX_WIDTH - length($msg)));
1919              
1920             # Draw the regex with a message and a positional marker...
1921 0 0         if ($data_mode ne 'full_heatmap') {
1922 0           _visualize $data_mode, q{ }, q{ } x $display_width, $colourer->($msg);
1923 0           _visualize $data_mode, q{ }, q{ } x $regex_pos , $colourer->('|');
1924 0   0       _visualize $data_mode, q{ }, q{ } x $regex_pos , $colourer->('V') x ($construct_len || 1);
1925             }
1926             else {
1927 0           _visualize $data_mode, q{ }, q{ } x $regex_pos , _info_colourer('|');
1928 0   0       _visualize $data_mode, q{ }, q{ } x $regex_pos , _info_colourer('V' x ($construct_len || 1) );
1929             }
1930              
1931             # Draw regex itself...
1932 0           _visualize $data_mode, q{/}, $regex_src, q{/};
1933              
1934             # Leave a gap...
1935 0           _visualize $data_mode, q{ } for 1..2;
1936              
1937             # Create marker for any match or capture within string...
1938 0           $forward_step = min($forward_step, $MAX_WIDTH);
1939 0 0 0       my $last_match_marker
    0 0        
    0          
1940             = (q{ } x ($str_pos - max(0,$forward_step)))
1941             . ( $nested_because eq 'failed' ? q{}
1942             : $is_capture && $forward_step == 1 ? 'V'
1943             : $is_capture && $forward_step > 1 ? '\\' . ('_' x ($forward_step-2)) . '/'
1944             : '^' x $forward_step
1945             )
1946             ;
1947              
1948              
1949             # Reconfigure string within visible window...
1950 0           my $match_start;
1951             ($str_src, $str_pos, $match_start, $last_match_marker)
1952             = _make_window(
1953             text => $str_src,
1954             pos => $str_pos,
1955             start => $Regexp::Grammars::match_start_pos,
1956 0 0         heat => substr($data_mode, -7) eq 'heatmap' ? $history_of{string_heatmap} : [],
1957             ws_colour => substr($data_mode, -7) eq 'heatmap',
1958             marker => $last_match_marker,
1959             no_window => $no_window,
1960             );
1961              
1962             # Trim match start position...
1963 0 0         if ($match_start > $str_pos) {
1964 0           $match_start = $str_pos;
1965             }
1966              
1967             # Colour match marker...
1968             $last_match_marker
1969 0 0         = substr($last_match_marker,0,1) eq '^' ? _match_colourer($last_match_marker, 'reverse')
1970             : _info_colourer($last_match_marker);
1971              
1972             # Draw the string with a positional marker...
1973 0           _visualize $data_mode,
1974             q{ }, _info_colourer( substr(q{ } x $str_pos . '|' . $backtrack, 0, $MAX_WIDTH-2) );
1975 0           _visualize $data_mode,
1976             q{ }, q{ } x $match_start, _match_colourer($MATCH_DRAG x ($str_pos-$match_start)), _info_colourer('V');
1977 1     1   616 use Data::Dumper 'Dumper';
  1         6701  
  1         1374  
1978 0 0         $str_src = # Heatmap is already coloured...
    0          
    0          
1979             substr($data_mode, -7) eq 'heatmap' ?
1980             $str_src
1981              
1982             # On failure, fail-colour to current position...
1983             : $nested_because eq 'failed' ?
1984             _fail_colourer( substr($str_src, 0, $str_pos), 'ws' )
1985             . _ws_colourer( substr($str_src, $str_pos) )
1986              
1987             # When trying, try-colour current position
1988             : $is_trying ?
1989             _fail_colourer( substr($str_src, 0, $match_start), 'ws' )
1990             . _match_colourer( substr($str_src, $match_start, $str_pos-$match_start), 'underline', 'ws' )
1991             . _try_colourer( substr($str_src, $str_pos, 1), 'underline bold', 'ws' )
1992             . _ws_colourer( substr($str_src, min(length($str_src),$str_pos+1)) )
1993              
1994             : # Otherwise, report pre-failure and current match...
1995             _fail_colourer( substr($str_src, 0, $match_start), 'ws' )
1996             . _match_colourer( substr($str_src, $match_start, $str_pos-$match_start), 'underline', 'ws' )
1997             . _ws_colourer( substr($str_src, $str_pos) );
1998              
1999 0           _visualize $data_mode, q{'}, $str_src, q{'}; # String itself
2000              
2001             # Draw a marker for any match or capture within the string...
2002 0           _visualize $data_mode, q{ }, $last_match_marker;
2003              
2004             # Windowed displays show the title last...
2005 0 0         if (!$no_window) {
2006 0           _visualize $data_mode,
2007             _info_colourer(
2008             qq{\n[\u$data_mode of regex at $state{$regex_ID}{location}]}
2009             . qq{ [step: $step]}
2010             );
2011             }
2012              
2013             # Special case: full heatmaps are reported as a table too...
2014 0 0         if ( $data_mode eq 'full_heatmap' ) {
2015             # Tabulate regex...
2016 0           _visualize $data_mode, _info_colourer("\n\nHeatmap for regex:\n");
2017 0           _visualize $data_mode, _build_tabulated_heatmap($raw_regex_src, $history_of{match_heatmap});
2018              
2019             # Tabulate string...
2020 0           _visualize $data_mode, _info_colourer("\n\nHeatmap for string:\n");
2021 0           _visualize $data_mode, _build_tabulated_heatmap($raw_str_src, $history_of{string_heatmap});
2022             }
2023             }
2024              
2025             # Convert a heatmapped string to a table...
2026             my $TABLE_STR_WIDTH = 15;
2027             sub _build_tabulated_heatmap {
2028 0     0     my ($str, $heatmap_ref) = @_;
2029              
2030             # Normalized data...
2031 0   0       my $max_heat = max(1, map { $_ // 0 } @{$heatmap_ref});
  0            
  0            
2032 0   0       my @heat = map { ($_//0) / $max_heat } @{$heatmap_ref};
  0            
  0            
2033 0           my $count_size = length($max_heat);
2034              
2035             # Determine colours to be used...
2036 0           my @HEAT_COLOUR = @{$lexical_config->{heatmap_col}};
  0            
2037              
2038             # Accumulate graph
2039 0           my @graph;
2040 0           for my $index (0..length($str)-1) {
2041              
2042             # Locate next char and its heat value...
2043 0           my $char = substr($str, $index, 1);
2044 0   0       my $abs_heat = $heatmap_ref->[$index] // 0;
2045 0 0         my $display_char = $char eq "\n" ? '\n'
    0          
2046             : $char eq "\t" ? '\t'
2047             : $char;
2048              
2049             # Graph it...
2050 0 0 0       if (@graph && length($graph[-1]{text} . $display_char) < $TABLE_STR_WIDTH && $graph[-1]{heat} == $abs_heat) {
    0 0        
      0        
2051 0           $graph[-1]{text} .= $display_char;
2052             }
2053             elsif ($char ne q{ } || $abs_heat != 0) {
2054 0   0       my $rel_heat = $heat[$index] // 0;
2055 0           push @graph, {
2056             text => $display_char,
2057             heat => $abs_heat,
2058             rel_heat => $rel_heat,
2059             bar => q{*} x (($MAX_WIDTH-$TABLE_STR_WIDTH) * $rel_heat),
2060             };
2061             }
2062             }
2063              
2064             # Draw table...
2065 0           my $table;
2066 0           for my $entry (@graph) {
2067 0           my $colour_index = int( 0.5 + $#HEAT_COLOUR * $entry->{rel_heat} );
2068             $table .=
2069             q{ } .
2070             Term::ANSIColor::colored(
2071             substr($entry->{text} . q{ } x $TABLE_STR_WIDTH, 0, $TABLE_STR_WIDTH) .
2072 0   0       sprintf("| %-*s |%s\n", $count_size, $entry->{heat} || q{ }, $entry->{bar}),
2073             $HEAT_COLOUR[$colour_index]
2074             );
2075             }
2076              
2077 0           return $table;
2078             }
2079              
2080             # These need to be localized within regexes, so have to be package vars...
2081             our $subpattern_depth; # ...how many levels down in named subpatterns?
2082              
2083             # Reset debugger variables at start of match...
2084             sub _reset_debugger_state {
2085 0     0     $prev_regex_pos = 0; # ...start of regex
2086 0           $start_str_pos = 0; # ...starting point of match of string
2087 0           $prev_str_pos = 0; # ...start of string
2088 0           $prev_match_was_null = 0; # ...no previous match (to have been null)
2089 0           @pre_is_pending = (); # ...no try is pending
2090 0           $interaction_mode = 's'; # ...always start in step-by-step mode
2091 0           $interaction_quit = 0; # ...reset quit command for each regex
2092 0           $subpattern_depth = 0; # ...start at top level of named subcalls
2093              
2094 0           $Regexp::Grammars::match_start_pos = 0; # ...start matching at start of string
2095              
2096             # Also leave a gap in the event history and JSON representations...
2097 0           _record_event 'events', q{};
2098 0           _record_event 'JSON', q{};
2099 0           _show_event $lexical_config->{display_mode};
2100 0           _show_JSON $lexical_config->{display_mode}, q{};
2101             }
2102              
2103              
2104             # Reset some debugger variables at restart of match...
2105             sub _reset_debugger_state_rematch {
2106 0     0     $prev_regex_pos = 0; # ...start of regex
2107 0           $start_str_pos = pos; # ...starting point of match of string
2108 0           $prev_str_pos = pos; # ...point of rematch
2109 0           @pre_is_pending = (); # ...no try is pending
2110 0           $interaction_mode = 's'; # ...always start in step-by-step mode
2111 0           $subpattern_depth = 0; # ...start at top level of named subcalls
2112              
2113 0           $Regexp::Grammars::match_start_pos = pos; # ...start matching at rematch point
2114              
2115             # Also leave a gap in the event history and JSON representations...
2116 0           _record_event 'events', q{};
2117 0           _show_event $lexical_config->{display_mode};
2118 0           _record_event 'JSON', q{};
2119 0           _show_JSON $lexical_config->{display_mode};
2120             }
2121              
2122              
2123             # Set up a JSON encoder...
2124             my ($JSON_encoder, $JSON_decoder);
2125             BEGIN {
2126             ($JSON_encoder, $JSON_decoder) =
2127 1         195 eval{ require JSON::XS; } ? do {
2128 0         0 my $json = JSON::XS->new->utf8(1)->pretty(1);
2129             (
2130 0         0 sub { return $json->encode(shift) },
2131 0         0 sub { return $json->decode(shift) },
2132             )
2133 0         0 }
2134 1         238 : eval{ require JSON; } ? do {
2135 0         0 my $json = JSON->new->pretty(1);
2136             (
2137 0         0 sub { return $json->encode(shift) },
2138 0         0 sub { return $json->decode(shift) },
2139             )
2140 0         0 }
2141 1         16 : eval{ require 5.014;
2142 1         159 require JSON::DWIW; } ? (
2143 0         0 sub { JSON::DWIW->to_json(shift, {pretty=>1}) },
2144 0         0 sub { JSON::DWIW->from_json(shift, {pretty=>1}) },
2145             )
2146 1         568 : eval{ require JSON::Syck; } ? (
2147             \&JSON::Syck::Dump,
2148             \&JSON::Syck::Load,
2149             )
2150             : (
2151 0         0 sub { '{}' },
2152 0         0 sub { {} },
2153 1 50   1   5 );
    50          
    50          
    50          
2154             }
2155              
2156             # Report some activity within the regex match...
2157             sub _report_event {
2158             # Did the user quit the interactive debugger???
2159 0 0   0     return if $interaction_quit;
2160              
2161             # What are we matching (convert it to string if necessary)....
2162 0           my $str_src = "$_";
2163              
2164             # Which regex? Which event? Where in the string? Is this a recursive call?
2165 0           my ($regex_ID, $event_ID, $str_pos, %opt) = @_;
2166 0   0       my $nested_because = $opt{nested_because} // q{};
2167 0           my $non_interactive = $opt{non_iteractive};
2168              
2169             # Locate state info for this event...
2170 0           my $state_ref = $state{$regex_ID};
2171 0           my $event_ref = $state_ref->{$event_ID};
2172              
2173             # Report any problems before reporting the event....
2174 0 0         if (@{ $state_ref->{regex_problems} }) {
  0            
2175 0           for my $problem (@{$state_ref->{regex_problems}}) {
  0            
2176 0           print { *STDERR}
  0            
2177             "Possible typo in $problem->{type} at line $problem->{line} of regex:\n",
2178             " Found: $problem->{desc}\n",
2179             " Maybe: $problem->{dym}\n\n";
2180             }
2181 0           print {*STDERR} "[Press any key to continue]";
  0            
2182 0           _interact();
2183 0           delete $state_ref->{regex_problems};
2184             }
2185              
2186             # Unpack the necessary info...
2187             my ($matchable, $is_capture, $event_type, $construct, $depth)
2188 0           = @{$event_ref}{qw< matchable is_capture event_type construct depth>};
  0            
2189             my ($construct_type, $quantifier, $regex_pos, $capture_name, $msg)
2190 0           = @{$event_ref}{qw< construct_type quantifier regex_pos capture_name msg>};
  0            
2191 0   0       $construct_type //= q{};
2192              
2193             # Reset display_mode, capture variables, and starting position on every restart...
2194 0 0         if ($construct_type eq '_START') {
2195 0           %capture = ();
2196 0           $Regexp::Grammars::match_start_pos = pos();
2197 0           $lexical_config = $config[$event_ref->{lexical_scope}];
2198              
2199             # Reset display mode only on start (i.e. not on restart)...
2200 0 0         if ($str_pos == 0) {
2201 0           $display_mode = $lexical_config->{display_mode};
2202             }
2203             }
2204              
2205             # Ignore final failure messages, except at the very end...
2206 0 0         if ($event_ref->{regex_failed}) {
2207 0 0 0       return if ($str_pos//0) < length($str_src);
2208             }
2209              
2210             # This variable allows us to query the start position of a submatch when at the end of the submatch...
2211 0           my $shared_str_pos_ref = $event_ref->{shared_str_pos};
2212              
2213             # Use the shared string pos on failure...
2214 0 0         if ($nested_because eq 'failed') {
2215 0   0       $str_pos = ${$shared_str_pos_ref // \$prev_str_pos} // $str_pos;
  0   0        
2216             }
2217              
2218             # Flatten aliased capture name(s)...
2219 0 0         if (ref $capture_name) {
2220 0           $capture_name = join ' and ', @{$capture_name}
  0            
2221             }
2222              
2223             # If we've matched, what did we match???
2224 0           my $forward_step = 0; # ... will eventually contain how far forward we stepped
2225 0 0 0       if (($matchable || $is_capture) && $event_type eq 'post' && $construct ne '|') {
      0        
      0        
2226 0 0         $forward_step = $str_pos - ($shared_str_pos_ref ? ${$shared_str_pos_ref} : $str_pos);
  0            
2227             }
2228              
2229 0           my $backtrack = q{}; # ...will store the arrow demonstrating the backtracking
2230              
2231             # Are we backtracking?
2232 0           my $str_backtrack_len = min($EVENT_COL_WIDTH-1, $prev_str_pos-$str_pos);
2233 0           my $regex_backtrack_len = min($EVENT_COL_WIDTH-1, $prev_regex_pos-$regex_pos);
2234 1     1   8 my $event_str = '<' . do{ no warnings; '~' x $str_backtrack_len };
  1         2  
  1         61  
  0            
  0            
2235 1     1   7 my $event_regex = '<' . do{ no warnings; '~' x $regex_backtrack_len };
  1         2  
  1         5332  
  0            
  0            
2236 0 0         if ($nested_because ne 'failed') {
2237             # Generate backtracking arrow...
2238 0 0 0       if ($str_pos < ($prev_str_pos//0)) {
    0 0        
2239 0           $backtrack = '<' . '~' x ($prev_str_pos-$str_pos-1);
2240             }
2241             elsif ($regex_pos < ($prev_regex_pos//0)) {
2242 0           $backtrack = ' ';
2243             }
2244              
2245             # Remember where we were...
2246 0           $prev_str_pos = $str_pos;
2247 0           $prev_regex_pos = $regex_pos;
2248             }
2249              
2250             # Were there failed attempts pending???
2251 0   0       while (!$nested_because && @pre_is_pending && $pre_is_pending[-1][1] >= $subpattern_depth) {
      0        
2252 0   0       my ($pending_event_ID, $pending_event_depth) = @{ pop(@pre_is_pending) // []};
  0            
2253 0 0 0       next if $event_type eq 'post' && $backtrack
      0        
      0        
2254             || !defined $pending_event_ID
2255             || $pending_event_ID == $event_ID;
2256              
2257 0           local $subpattern_depth = $pending_event_depth;
2258 0           _report_event($regex_ID, $pending_event_ID, undef, nested_because=>'failed');
2259             }
2260              
2261             # Get the source code of the regex...
2262 0           my $regex_src = $state_ref->{regex_src};
2263              
2264             # How long is this piece of the regex???
2265 0           my $construct_len = length $construct;
2266              
2267             # Build msg if it's dynamic...
2268 0 0         if (ref($msg) eq 'CODE') {
2269 0           $msg = $msg->();
2270             }
2271              
2272             # Construct status message (if necessary)...
2273             $msg = $nested_because eq 'failed' ? q{Failed}
2274 0           : $event_type eq 'pre' && ref $msg ? 'Capture to ' . join ' and ', @{$msg}
2275 0 0 0       : $event_type eq 'post' && ref $msg ? 'End of ' . join ' and ', @{$msg}
  0 0 0        
    0 0        
    0          
    0          
    0          
2276             : defined $msg ? $msg
2277             : pos && pos == $prev_str_pos && $construct_type eq '_START' ? q{Restarting regex match}
2278             : $construct_type eq '_START' ? q{Starting regex match}
2279             : q{}
2280             ;
2281              
2282             # Report back-tracking occurred (but not when returning from named subpatterns)...
2283 0 0         if ($regex_backtrack_len > 0) {
2284 0 0 0       $msg = $event_type eq 'failed_nonbacktracking'
    0          
2285             ? q{Back-tracking past } . lc($msg) . q{ without rematching}
2286             : $construct_type ne '_named_subpattern_call' && index(lc($msg), 'failed') < 0
2287             ? q{Back-tracked within regex and re} . lc($msg)
2288             : $msg;
2289              
2290 0           my $re_idx = index($msg, 'and rere');
2291 0 0         if ($re_idx >= 0) {
2292 0           substr($msg, $re_idx, 8, 'and re');
2293             }
2294 0           $re_idx = index($msg, 'and reend');
2295 0 0         if ($re_idx >= 0) {
2296 0           substr($msg, $re_idx, 9, 'and end');
2297             }
2298             }
2299              
2300             # Track trying and matching...
2301 0   0       my $is_match = index($msg, 'matched') >= 0 || index($msg, 'Matched') >= 0;
2302 0   0       my $is_rematch = index($msg, 'rematched') >= 0 || index($msg, 'Rematched') >= 0;
2303 0   0       my $is_trying = index($msg, 'trying') >= 0 || index($msg, 'Trying') >= 0;
2304 0   0       my $is_skip = index($msg, 'skipping') >= 0 || index($msg, 'Skipping') >= 0;
2305 0   0       my $is_fail = index($msg, 'failed') >= 0 || index($msg, 'Failed') >= 0;
2306              
2307             # Track string heatmap...
2308 0 0         if ($forward_step) {
    0          
2309 0           my @str_range = $str_pos-$forward_step+1 .. $str_pos-1;
2310 0           $_++ for @{$history_of{string_heatmap}}[@str_range];
  0            
2311             }
2312             elsif ($is_trying) {
2313 0           $history_of{string_heatmap}[$str_pos]++;
2314             }
2315              
2316             # Trace regex heatmap...
2317 0 0 0       if ($is_rematch || !$is_match && !$is_fail && !$is_skip) {
      0        
2318 0           my @regex_range = $regex_pos..$regex_pos+length($construct)-1;
2319 0           $_++ for @{$history_of{match_heatmap}}[@regex_range];
  0            
2320             }
2321              
2322             # Track start and end positions for each capture...
2323 0 0         if ($construct_type eq '_capture_group') {
2324 0 0         if ($event_type eq 'pre') {
    0          
2325 0           $capture{$capture_name}{from} = $str_pos;
2326 0           $capture{$capture_name}{start_pos} = $regex_pos;
2327             }
2328             elsif ($event_type eq 'post') {
2329 0           $capture{$capture_name}{to} = $str_pos;
2330             }
2331             }
2332              
2333             # Remember when a match/fail is pending...
2334 0   0       my $is_pending = $matchable
2335             && $event_type eq 'pre'
2336             # && $construct_type ne '_named_subpattern_call';
2337             ;
2338 0 0         if ($is_pending) {
2339             # Pre- and post- events have adjacent IDs so add 1 to get post ID...
2340 0   0       push @pre_is_pending, [$event_ID + 1, $subpattern_depth // 0];
2341             }
2342              
2343             # Send starting position to corresponding post- event...
2344 0 0 0       if ($shared_str_pos_ref && $event_type eq 'pre' && $construct ne '|') {
      0        
2345 0           ${$shared_str_pos_ref} = $str_pos;
  0            
2346             }
2347              
2348             # Compute indent for message (from paren depth + subcall depth)...
2349 0           my $indent = $INDENT x ($event_ref->{depth} + $subpattern_depth);
2350              
2351             # Indicate any backtracking...
2352 0 0 0       if (length($event_str) > 1 || length($event_regex) > 1) {
2353 0 0         $event_str = q{} if length($event_str) == 1;
2354 0 0         $event_regex = q{} if length($event_regex) == 1;
2355 0 0 0       my $backtrack_msg
    0          
    0          
2356             = $event_str && $event_regex ? 'Back-tracking in both regex and string'
2357             : $event_str ? 'Back-tracking ' . $str_backtrack_len
2358             . ' character'
2359             . ($str_backtrack_len == 1 ? q{} : 's')
2360             . ' in string'
2361             : "Back-tracking in regex"
2362             ;
2363 0           $backtrack_msg = _info_colourer($backtrack_msg);
2364 0           $event_regex .= q{ } x ($EVENT_COL_WIDTH - length $event_regex);
2365 0           $event_str .= q{ } x ($EVENT_COL_WIDTH - length $event_str);
2366 0           _record_event 'events',
2367             sprintf("%s | %s | %s",
2368             _info_colourer($event_str),
2369             _info_colourer($event_regex),
2370             $indent . $backtrack_msg);
2371 0 0 0       _show_event $display_mode
2372             if index('nrFMC', $interaction_mode) < 0 || $subpattern_depth <= $interaction_depth;
2373             }
2374              
2375             # Colour the message...
2376 0           my $colourer = _colourer_for($msg);
2377              
2378             # Log (and perhaps display) event...
2379 0           _record_event 'events',
2380             sprintf("%-s | %-${EVENT_COL_WIDTH}s | %s",
2381             _ws_colourer(substr($str_src . (q{ } x $EVENT_COL_WIDTH), $str_pos, $EVENT_COL_WIDTH)),
2382             substr($regex_src, $regex_pos, $EVENT_COL_WIDTH),
2383             $indent . $colourer->($msg));
2384 0 0 0       _show_event $display_mode
2385             if index('nrFMC', $interaction_mode) < 0 || $subpattern_depth <= $interaction_depth;
2386              
2387             # Display event mode line, if appropriate...
2388 0 0 0       if ($display_mode eq 'events' && !$lexical_config->{save_to_fh}) {
2389 0 0 0       say _info_colourer( qq{\n[Events of regex at $state{$regex_ID}{location}]} )
2390             if index('nrFMC', $interaction_mode) < 0 || $subpattern_depth <= $interaction_depth;
2391             }
2392              
2393             # Generate (and perhaps display) the JSON...
2394             {
2395             # The data we're encoding...
2396 0           my $data = {
2397             regex_pos => $regex_pos,
2398             str_pos => $str_pos,
2399 0           event => { %{$event_ref}, msg => $msg },
  0            
2400             };
2401              
2402             # But sanitize any procedural msg...
2403 0 0         if (ref $data->{event}{msg} eq 'CODE') {
2404 0           delete $data->{event}{msg};
2405             }
2406              
2407             # And sanitize any reference to internal communications channel...
2408 0           my $starting_str_pos = delete $data->{event}{shared_str_pos};
2409 0 0 0       if (ref $starting_str_pos eq 'SCALAR' && ${$starting_str_pos} && ${$starting_str_pos} ne $str_pos) {
  0   0        
  0            
2410 0           $data->{starting_str_pos} = ${$starting_str_pos};
  0            
2411             }
2412              
2413 0           my $json_rep = $JSON_encoder->($data);
2414              
2415             # Display opening delimiter at start...
2416 0 0 0       if ($construct_type eq '_START' && $str_pos == 0) {
2417 0           _record_event 'JSON', '[';
2418 0           _show_JSON $display_mode;
2419             }
2420              
2421             # Display event data (with comma, if needed)...
2422 0 0         my $comma = $construct_type eq '_END' ? q{} : q{,};
2423 0           _record_event 'JSON', qq{ $json_rep$comma};
2424 0           _show_JSON $display_mode;
2425              
2426             # Display closing delimiter at end...
2427 0 0         if ($construct_type eq '_END') {
2428 0           _record_event 'JSON', ']';
2429 0           _show_JSON $display_mode;
2430             }
2431              
2432             # Display mode line...
2433 0 0 0       if ($display_mode eq 'JSON' && !$lexical_config->{save_to_fh}) {
2434 0 0 0       say _info_colourer( qq{\n[JSON data of regex at $state{$regex_ID}{location}]} )
2435             if index('nrFMC', $interaction_mode) < 0 || $subpattern_depth <= $interaction_depth;
2436             }
2437             }
2438              
2439             # Build and display (if appropriate) the "2D" visualizations...
2440             my %data = (
2441             regex_ID => $regex_ID,
2442             regex_src => $regex_src,
2443             regex_pos => $regex_pos,
2444             construct_len => $construct_len,
2445             str_src => $str_src,
2446             str_pos => $str_pos,
2447             is_match => $is_match,
2448             is_fail => $is_fail,
2449             is_trying => $is_trying,
2450             is_capture => $is_capture,
2451             backtrack => $backtrack,
2452             forward_step => $forward_step,
2453             nested_because => $nested_because,
2454             msg => $msg,
2455             colourer => $colourer,
2456 0 0         step => scalar @{$history_of{visual}||[]},
  0            
2457             );
2458 0           _build_visualization('visual', \%data);
2459 0           _build_visualization('heatmap', \%data);
2460              
2461 0           $data{no_window} = 1;
2462 0           _build_visualization('full_visual', \%data);
2463 0           _build_visualization('full_heatmap', \%data);
2464              
2465 0 0 0       if ($display_mode eq 'visual' && (index('nrFMC', $interaction_mode) < 0 || $subpattern_depth <= $interaction_depth)) {
      0        
2466 0           _print $CLEAR_SCREEN;
2467 0           _print $history_of{$display_mode}[-1]{display};
2468             }
2469              
2470             # Do any interaction...
2471 0           my $input;
2472             INPUT:
2473 0           while (!$non_interactive) {
2474             # Adaptive rate of display when skipping interactions...
2475 0           state $skip_duration = $MAX_SKIP_DURATION;
2476 0           $skip_duration = max($MIN_SKIP_DURATION, $skip_duration * $SKIP_ACCELERATION);
2477 0 0 0       _pause($skip_duration)
2478             if index('nrFMC', $interaction_mode) < 0 || $subpattern_depth <= $interaction_depth;
2479              
2480             # Skip interactions if current mode does not require them...
2481 0 0 0       last INPUT if $event_type ne 'break' && (
      0        
2482             # Skip-to-match mode...
2483             lc($interaction_mode) eq 'm'
2484             && (!$is_match || $interaction_mode eq 'M' && $subpattern_depth > $interaction_depth)
2485             && index($msg,'restarting regex match') < 0
2486             && $construct_type ne '_END'
2487             ||
2488             # Skip-to-fail mode...
2489             lc($interaction_mode) eq 'f'
2490             && (!$is_fail || $interaction_mode eq 'F' && $subpattern_depth > $interaction_depth)
2491             && index($msg,'restarting regex match') < 0
2492             && $construct_type ne '_END'
2493             ||
2494             # Skip-to-return mode...
2495             $interaction_mode eq 'r'
2496             && $subpattern_depth > 0
2497             && $subpattern_depth > $interaction_depth
2498             && index($msg,'restarting regex match') < 0
2499             && $construct_type ne '_END'
2500             ||
2501             # Skip-to-next mode...
2502             $interaction_mode eq 'n'
2503             && $subpattern_depth > $interaction_depth
2504             && index($msg,'restarting regex match') < 0
2505             ||
2506             # Skip-to-end mode...
2507             lc($interaction_mode) eq 'c'
2508             && $construct_type ne '_END'
2509             );
2510              
2511             # Reset adaptive skip rate on any interaction...
2512 0           $skip_duration = $MAX_SKIP_DURATION;
2513              
2514             # Reset to step mode on a break...
2515 0 0         if ($event_type eq 'break') {
2516 0           $interaction_mode = 's';
2517             }
2518              
2519             # Do what, John???
2520 0           $input = _interact();
2521              
2522             # A terminates the process...
2523 0 0 0       if ($input eq "\cC") {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
2524 0           kill 9, $$;
2525             }
2526              
2527             # An 'x' exits the process...
2528             elsif ($input eq 'x') {
2529 0           exit(0);
2530             }
2531              
2532             # A redraws the screen...
2533             elsif ($input eq "\cL") {
2534 0           _print $history_of{$display_mode}[-1]{display};
2535 0 0 0       if ($display_mode eq 'events' || $display_mode eq 'JSON') {
2536 0           say _info_colourer( qq{\n\n[\u$display_mode of regex at $state{$regex_ID}{location}]} );
2537             }
2538 0           next INPUT;
2539             }
2540              
2541             # Display explanation of regex...
2542             elsif ($input eq 'd') {
2543 0           _show_regex_description($regex_ID);
2544 0           next INPUT;
2545             }
2546              
2547             # Help!
2548             elsif ($input eq '?') {
2549 0           _show_help();
2550 0           next INPUT;
2551             }
2552              
2553             # Quit all debugging???
2554             elsif ($input eq 'q' || $input eq "\cD") {
2555 0           $interaction_quit = 1;
2556 0           last INPUT;
2557             }
2558              
2559             # Step backwards...
2560             elsif (index('-p', $input) >= 0) {
2561 0           my $step;
2562 0           ($input, $step) = _revisualize($regex_ID, $input);
2563 0 0 0       if ($input eq 'q' || $input eq "\cD") {
    0          
2564 0           $interaction_quit = 1;
2565 0           last INPUT;
2566             }
2567             elsif (index('smnrMfFcC', $input) >= 0) {
2568 0           $interaction_mode = $input;
2569 0           $subpattern_depth = $history_of{$display_mode}[$step-1]{depth};
2570 0           $is_match = $history_of{$display_mode}[$step-1]{is_match};
2571 0           $is_fail = $history_of{$display_mode}[$step-1]{is_fail};
2572             }
2573 0           next INPUT;
2574             }
2575              
2576             # Step all the way back to start...
2577             elsif ($input eq 'R') {
2578 0           my $step;
2579 0           ($input, $step) = _revisualize($regex_ID, $input, 0);
2580 0 0 0       if ($input eq 'q' || $input eq "\cD") {
    0          
2581 0           $interaction_quit = 1;
2582 0           last INPUT;
2583             }
2584             elsif (index('smnrMfFcC', $input) >= 0) {
2585 0           $interaction_mode = $input;
2586 0           $subpattern_depth = $history_of{$display_mode}[$step-1]{depth};
2587 0           $is_match = $history_of{$display_mode}[$step-1]{is_match};
2588 0           $is_fail = $history_of{$display_mode}[$step-1]{is_fail};
2589             }
2590 0           next INPUT;
2591             }
2592              
2593             # Switch between visualizer/event/heatmap/JSON modes...
2594             elsif ($input eq 'v') {
2595 0           $display_mode = 'visual';
2596 0           _print $CLEAR_SCREEN;
2597 0           _print $history_of{'visual'}[-1]{display};
2598 0           next INPUT;
2599             }
2600             elsif ($input eq 'h') {
2601             # Can we use heatmap mode?
2602 0 0         if ($heatmaps_invisible) {
2603 0           say 'Cannot show heatmaps (Term::ANSIColor unavailable)';
2604 0           say "Try 'H' instead";
2605 0           $input = '?';
2606             }
2607             # If heatmaps available, check for misuse of 'h' instead of '?'...
2608             else {
2609 0           my $prompt_help = $display_mode eq 'heatmap';
2610 0           $display_mode = 'heatmap';
2611 0           _print $CLEAR_SCREEN;
2612 0           _print $history_of{'heatmap'}[-1]{display};
2613 0 0         if ($prompt_help) {
2614 0           say "(Type '?' for help)";
2615             }
2616             }
2617 0           next INPUT;
2618             }
2619             elsif ($input eq 'e') {
2620 0           $display_mode = 'events';
2621 0           _print $CLEAR_SCREEN;
2622 0           _print $history_of{'events'}[-1]{display};
2623 0           say _info_colourer( qq{\n\n[Events of regex at $state{$regex_ID}{location}]} );
2624 0           next INPUT;
2625             }
2626             elsif ($input eq 'j') {
2627 0           $display_mode = 'JSON';
2628 0           _print $CLEAR_SCREEN;
2629 0           _print $history_of{'JSON'}[-1]{display};
2630 0           say _info_colourer( qq{\n\n[JSON data of regex at $state{$regex_ID}{location}]} );
2631 0           next INPUT;
2632             }
2633              
2634             # Take a snapshot...
2635 0           elsif ($input eq 'V') { _save_snapshot('full_visual') ; next INPUT; }
  0            
2636 0           elsif ($input eq 'H') { _save_snapshot('full_heatmap') ; next INPUT; }
  0            
2637 0           elsif ($input eq 'E') { _save_snapshot('events') ; next INPUT; }
  0            
2638 0           elsif ($input eq 'J') { _save_snapshot('JSON') ; next INPUT; }
  0            
2639 0           elsif ($input eq 'D') { _show_regex_description($regex_ID,'save') ; next INPUT; }
  0            
2640              
2641             # Change of interaction mode???
2642             elsif (index('fFmMnscC', $input) >= 0) {
2643 0           $interaction_mode = $input;
2644 0           $interaction_depth = $subpattern_depth;
2645 0           last INPUT;
2646             }
2647             elsif ($input eq 'r') {
2648 0           $interaction_mode = $input;
2649 0           $interaction_depth = $subpattern_depth - 1;
2650 0           last INPUT;
2651             }
2652              
2653             # Otherwise, move on...
2654             else {
2655 0           last INPUT;
2656             }
2657             }
2658              
2659             # At end of debugging, save data to file (if requested), and clean up...
2660 0 0         if ($construct_type eq '_END') {
2661 0           _save_to_fh($regex_ID, $str_src);
2662              
2663 0           %history_of = ();
2664 0           $history_of{match_heatmap} = [];
2665 0           $history_of{string_heatmap} = [];
2666             }
2667              
2668 0           return $input;
2669             }
2670              
2671             # Dump all history and config data to a stream...
2672             sub _save_to_fh {
2673 0     0     my ($regex_ID, $str_src) = @_;
2674              
2675             # No-op if not saving to file...
2676             my $fh = delete $lexical_config->{save_to_fh}
2677 0 0         or return;
2678              
2679             # Extract data to correct level...
2680 0           my $match_heatmap = delete $history_of{match_heatmap};
2681 0           my $string_heatmap = delete $history_of{string_heatmap};
2682 0           my $location = $state{$regex_ID}{location};
2683 0           my $regex_display = $state{$regex_ID}{regex_src};
2684 0           my $regex_original = $state{$regex_ID}{raw_regex};
2685              
2686             # Ensure print prints everything...
2687 0           my $prev_select = select $fh;
2688 0           local $|=1;
2689              
2690             # Encode and print...
2691 0           print {$fh} $JSON_encoder->({
2692             regex_ID => $regex_ID,
2693             regex_location => $location,
2694             regex_original => $regex_original,
2695             regex_display => $regex_display,
2696             string_display => $str_src,
2697             config => $lexical_config,
2698 0           match_data => $JSON_decoder->($history_of{JSON}[-1]{display}),
2699             match_heatmap => $match_heatmap,
2700             string_heatmap => $string_heatmap,
2701             visualization => \%history_of,
2702             }), "\n";
2703              
2704             # Restore filehandles...
2705 0           select $prev_select;
2706 0           $lexical_config->{save_to_fh} = $fh;
2707             }
2708              
2709             sub _show_regex_description {
2710 0     0     my ($regex_ID, $save) = @_;
2711              
2712             # How wide to display regex components...
2713 0           my $MAX_DISPLAY = 20;
2714              
2715             # The info we're displaying...
2716 0           my $info = $state{$regex_ID};
2717              
2718             # Coloured separator...
2719             my $separator = $save ? q{}
2720             : Term::ANSIColor::colored(
2721             q{ } x $MAX_WIDTH . "\n",
2722             $lexical_config->{desc_sep_col}
2723 0 0         );
2724              
2725             # Direct the output...
2726 0           my $STDOUT;
2727 0 0         if ($save) {
2728 0           $STDOUT = _prompt_for_file('description');
2729             }
2730             else {
2731 0   0       my $pager = $ENV{PAGER} // 'more';
2732 0 0         if ($pager eq 'less') {
2733 0           $pager .= ' -R';
2734             }
2735 0 0         open $STDOUT, '|-', $pager or return;
2736             }
2737              
2738             # Build the display...
2739 0           say {$STDOUT}
2740             $separator
2741             . join q{},
2742             map {
2743 0           my $indent = $info->{$_}{indent};
2744 0           my $construct = sprintf('%-*s', $MAX_DISPLAY, $indent . $info->{$_}{construct});
2745 0           my $desc = $indent . $info->{$_}{desc};
2746              
2747             # Decorate according to destination...
2748 0 0         if ($save) {
2749 0           $desc = '#' . $desc
2750             }
2751             else {
2752 0           $construct = Term::ANSIColor::colored($construct, $lexical_config->{desc_regex_col});
2753 0           $desc = Term::ANSIColor::colored($desc, $lexical_config->{desc_text_col});
2754             }
2755              
2756             # Format and return...
2757 0 0         if (length($indent . $info->{$_}{construct}) > 20) {
2758 0           $construct . "\n"
2759             . q{ } x ($MAX_DISPLAY+2) . "$desc\n"
2760             . $separator
2761             }
2762             else {
2763 0           "$construct $desc\n"
2764             . $separator
2765             }
2766             }
2767 0           sort { $a <=> $b }
2768 0 0         grep { /^\d+$/ && exists $info->{$_}{desc} }
  0            
2769             keys %$info;
2770             }
2771              
2772             sub _show_help {
2773 0     0     say <<'END_HELP';
2774             ________________________________________________/ Help \______
2775              
2776             Motion: s : step forwards (and into named subpattern calls)
2777             n : step forwards (but over named subpattern calls)
2778             - : step backwards (and into named subpattern calls)
2779             p : step backwards (but over named subpattern calls)
2780             m : continue to next partial match
2781             M : continue to next partial match in this named subpattern
2782             f : continue to next partial failure
2783             F : continue to next partial failure in this named subpattern
2784             r : continue until this named subpattern returns
2785             c : continue to end of full match
2786             C : continue to end of full match (stepping over named subpatterns)
2787             R : rewind to the start of the entire match
2788             : repeat last motion
2789              
2790             Display: v : change to visualization
2791             e : change to event log
2792             h : change to heatmaps
2793             j : change to JSON representation
2794             d : describe the regex in detail
2795              
2796             Snapshot: V : take snapshot of current visualization
2797             E : take snapshot of current event log
2798             H : take snapshot of current heatmaps
2799             J : take snapshot of current JSON representation
2800             D : take snapshot of regex description
2801              
2802             Control: q : quit debugger and continue program
2803             x : exit debugger and terminate program
2804              
2805             ______________________________________________________________
2806             END_HELP
2807             }
2808              
2809             # Take a snapshot of the current debugger state...
2810             my @ERR_MODE = ( -timeout => 10, -style => $ERR_COL, -single);
2811              
2812             sub _prompt_for_file {
2813 0     0     my ($data_mode) = @_;
2814              
2815 0 0         if (!eval { require Time::HiRes; }) {
  0            
2816 0     0     *Time::HiRes::time = sub { time };
  0            
2817             }
2818              
2819             # Default target for save...
2820 0           my $open_mode = '>';
2821 0           my $filename = 'rxrx_' . $data_mode . '_' . Time::HiRes::time();
2822              
2823             # Request a filename...
2824 0           print "Save $data_mode snapshot as: ";
2825 0           my $input = _interact();
2826              
2827             # Default to paged-to-screen...
2828 0 0         if ($input eq "\n") {
    0          
2829 0           say '';
2830 0           $open_mode = '|-';
2831 0   0       $filename = $ENV{PAGER} // 'more';
2832 0 0         if ($filename eq 'less') {
2833 0           $filename .= ' -R';
2834             }
2835             }
2836              
2837             # selects precomputed filename...
2838             elsif ($input eq "\t") {
2839 0           say $filename;
2840 0           _pause(2);
2841             }
2842              
2843             # Otherwise, use whatever they type...
2844             else {
2845 0           $filename = $input;
2846 0           print $input;
2847 0           $filename .= readline *STDIN;
2848 0           chomp $filename;
2849             }
2850              
2851             # Set up the output stream...
2852 0 0         open my $fh, $open_mode, $filename or do {
2853 0           say Term::ANSIColor::colored("Can't open $filename: $!", $ERR_COL);
2854 0           say Term::ANSIColor::colored("(Hit any key to continue)", $ERR_COL);
2855 0           _interact();
2856 0           return;
2857             };
2858              
2859 0           return $fh;
2860             }
2861              
2862             sub _save_snapshot {
2863 0     0     my ($data_mode, $step) = @_;
2864 0   0       $step //= -1;
2865              
2866             # Open the save target...
2867 0           my $fh = _prompt_for_file($data_mode);
2868              
2869             # Output current state (appropriately trimmed)...
2870 0           my $state = $history_of{$data_mode}[$step]{display};
2871 0           while (substr($state, 0, 1) eq "\n") {
2872 0           substr($state, 0, 1, q{});
2873             }
2874 0           print {$fh} $state;
  0            
2875              
2876             # JSON output may be partial...
2877 0 0 0       if ($data_mode eq 'JSON' && substr($state, -2) eq ",\n") {
2878 0           print {$fh} " { MATCH_INCOMPLETE => 1 }\n]\n";
  0            
2879             }
2880              
2881             # Clean up...
2882 0           close $fh;
2883              
2884             # Restore previous visuals...
2885 0           _print $history_of{$display_mode}[-1]{display};
2886              
2887 0           return;
2888             }
2889              
2890             sub _build_heatmap {
2891 0     0     my ($str, $count_ref) = @_;
2892              
2893             # Determine colours to be used...
2894 0           my @HEAT_COLOUR = @{$lexical_config->{heatmap_col}};
  0            
2895              
2896             # Normalize counts to match @HEAT_COLOUR entries...
2897 0   0       my $max = max 1, map { $_ // 0 } @{$count_ref};
  0            
  0            
2898 0   0       my @count = map { int( 0.5 + $#HEAT_COLOUR * ($_//0) / $max ) } @{$count_ref};
  0            
  0            
2899              
2900             # Colour each character...
2901 0           my $heatmap = q{};
2902 0           for my $n (0..length($str)-1) {
2903 0   0       my $heat = $HEAT_COLOUR[$count[$n] // 0];
2904 0           $heatmap .= _ws_colourer(substr($str,$n,1), $heat);
2905             }
2906              
2907 0           return $heatmap;
2908             }
2909              
2910             # Extract a window-into-string to fit it on screen...
2911             sub _make_window {
2912 0     0     my %arg = @_;
2913              
2914 0   0       my $src = $arg{text} // q{};
2915 0   0       my $pos = $arg{pos} // 0;
2916 0   0       my $start_pos = $arg{start} // 0;
2917 0   0       my @heatmap = @{ $arg{heat} // [] };
  0            
2918 0           my $ws_colour = $arg{ws_colour};
2919 0           my $window = !$arg{no_window};
2920 0           my $marker = $arg{marker};
2921              
2922             # Extend heatmap and marker to length of text...
2923 0 0         if (@heatmap) {
2924 0           push @heatmap, (0) x (length($src) - @heatmap);
2925             }
2926 0 0         if ($marker) {
2927 0           $marker .= q{ } x (length($src) - length($marker));
2928             }
2929              
2930             # Crop to window, if necessary...
2931 0 0         if ($window) {
2932              
2933             # How big is the space we have to fill???
2934 0           my $window_width = $MAX_WIDTH - 2; # ...allow 2 chars for delimiters
2935 0           my $mid_window = $MAX_WIDTH/2;
2936              
2937             # Only modify values if content longer than window...
2938 0 0         if (length($src) > $window_width) {
2939             # At the start of the string, chop off the end...
2940 0 0         if ($pos <= $mid_window) {
    0          
2941 0 0         if ($marker) {
2942 0           $marker = substr($marker, 0, $window_width);
2943             }
2944 0           $src = substr($src, 0, $window_width);
2945 0           substr($src,-3,3,q{...});
2946             }
2947             # At the end of the string, chop off the start...
2948             elsif (length($src) - $pos < $mid_window) {
2949 0           $pos = $window_width - length($src) + $pos;
2950 0           $start_pos = $window_width - length($src) + $start_pos;
2951 0 0         if (@heatmap) {
2952 0           @heatmap = @heatmap[length($src)-$window_width..$#heatmap];
2953             }
2954 0 0         if ($marker) {
2955 0           $marker = substr($marker, length($src)-$window_width, $window_width);
2956             }
2957 0           $src = substr($src, -$window_width);
2958 0           substr($src,0,3,q{...});
2959             }
2960             # In the middle of the string, centre the window on the position...
2961             else {
2962 0           $src = substr($src, $pos-$mid_window+1, $window_width);
2963 0 0         if (@heatmap) {
2964 0           @heatmap= splice(@heatmap, $pos-$mid_window+1, $window_width);
2965             }
2966 0 0         if ($marker) {
2967 0           $marker = substr($marker, $pos-$mid_window+1, $window_width);
2968             }
2969 0           $start_pos -= $pos;
2970 0           $pos = $window_width/2;
2971 0           $start_pos += $pos;
2972 0           substr($src,0,3,q{...});
2973 0           substr($src,-3,3,q{...});
2974             }
2975             }
2976             }
2977              
2978             # Convert to heatmap, if requested...
2979 0 0         if (@heatmap) {
    0          
2980 0           $src = _build_heatmap($src, \@heatmap);
2981             }
2982             elsif ($ws_colour) {
2983 0           $src = _ws_colourer($src);
2984             }
2985              
2986             # Trim trailing whitespace from marker...
2987 0   0       while ($marker && substr($marker,-1) eq q{ }) {
2988 0           substr($marker, -1) = q{};
2989             }
2990              
2991 0           return ($src, $pos, max($start_pos,0), $marker);
2992             }
2993              
2994             # Colour message appropriately...
2995             sub _fail_colourer {
2996 0     0     my ($str, $ws_colouring) = @_;
2997 0 0         my $colourer = $ws_colouring ? \&_ws_colourer : \&Term::ANSIColor::colored;
2998 0           return $colourer->($str, $lexical_config->{fail_col});
2999             }
3000              
3001             sub _info_colourer {
3002 0     0     my ($str, $ws_colouring) = @_;
3003 0 0         my $colourer = $ws_colouring ? \&_ws_colourer : \&Term::ANSIColor::colored;
3004 0           return $colourer->($str, $lexical_config->{info_col});
3005             }
3006              
3007             sub _try_colourer {
3008 0     0     my ($str, $extras, $ws_colouring) = @_;
3009 0   0       $extras //= q{};
3010 0 0         my $colourer = $ws_colouring ? \&_ws_colourer : \&Term::ANSIColor::colored;
3011 0           return $colourer->($str, "$lexical_config->{try_col} $extras");
3012             }
3013              
3014             sub _match_colourer {
3015 0     0     my ($str, $extras, $ws_colouring) = @_;
3016 0   0       $extras //= q{};
3017 0 0         my $colourer = $ws_colouring ? \&_ws_colourer : \&Term::ANSIColor::colored;
3018 0           return $colourer->($str, "$lexical_config->{match_col} $extras");
3019             }
3020              
3021             my %DISPLAY_FOR = (
3022             "\n" => 'n',
3023             "\t" => 't',
3024             "\r" => 'r',
3025             "\f" => 'f',
3026             "\b" => 'b',
3027             "\a" => 'a',
3028             "\e" => 'e',
3029             "\0" => '0',
3030             );
3031              
3032             sub _ws_colourer {
3033 0     0     my ($str, $colour_scheme) = @_;
3034              
3035             # How to colour the text...
3036 0   0       $colour_scheme //= 'clear';
3037 0           my $ws_colour_scheme = "$colour_scheme $lexical_config->{ws_col}";
3038              
3039             # Accumulate the text...
3040 0           my $coloured_str = q{};
3041 0           my $prefix = q{};
3042              
3043             # Step through char-by-char...
3044             CHAR:
3045 0           for my $n (0..length($str)-1) {
3046 0           my $char = substr($str, $n, 1);
3047              
3048             # If it's special, handle it...
3049 0           for my $special_char (keys %DISPLAY_FOR) {
3050 0 0         if ($char eq $special_char) {
3051 0 0         if (length($prefix)) {
3052 0           $coloured_str .= Term::ANSIColor::colored($prefix, $colour_scheme);
3053 0           $prefix = q{};
3054             }
3055 0           $coloured_str .= Term::ANSIColor::colored($DISPLAY_FOR{$special_char}, $ws_colour_scheme);
3056 0           next CHAR;
3057             }
3058             }
3059              
3060             # Otherwise, accumulate it...
3061 0           $prefix .= $char;
3062             }
3063              
3064             # Clean up any remaining text...
3065 0 0         if (length($prefix)) {
3066 0           $coloured_str .= Term::ANSIColor::colored($prefix, $colour_scheme);
3067             }
3068              
3069 0           return $coloured_str;
3070             }
3071              
3072             sub _colourer_for {
3073 0     0     my $msg = shift;
3074              
3075 0 0 0       if (index($msg,'forgetting') >= 0 || index($msg,'Forgetting') >= 0) {
3076 0           return \&_info_colourer;
3077             }
3078 0 0 0       if (index($msg,'try') >= 0 || index($msg,'Try') >= 0) {
3079 0           return \&_try_colourer;
3080             }
3081 0 0 0       if (index($msg,'failed') >= 0 || index($msg,'Failed') >= 0) {
3082 0           return \&_fail_colourer;
3083             }
3084 0 0 0       if (index($msg,'matched') >= 0 || index($msg,'Matched') >= 0) {
3085 0           return \&_match_colourer;
3086             }
3087 0           return \&_info_colourer;
3088             }
3089              
3090             # Set up interaction as spiffily as possible...
3091              
3092             if (eval{ require Term::ReadKey }) {
3093             *_interact = sub {
3094             # No interactions when piping output to a filehandle...
3095             return 'c' if $lexical_config->{save_to_fh};
3096              
3097             # Otherwise grab a single key and return it...
3098             Term::ReadKey::ReadMode('raw');
3099             my $input = Term::ReadKey::ReadKey(0);
3100             Term::ReadKey::ReadMode('restore');
3101             return $input;
3102             }
3103             }
3104             else {
3105             *_interact = sub {
3106             # No interactions when piping output to a filehandle...
3107 0 0   0     return 'c' if $lexical_config->{save_to_fh};
3108              
3109             # Otherwise return the first letter typed...
3110 0           my $input = readline;
3111 0           return substr($input, 0, 1);
3112             }
3113             }
3114              
3115              
3116             #====[ REPL (a.k.a. rxrx) ]=======================
3117              
3118             # Deal with v5.16 weirdness...
3119             BEGIN {
3120 1 50   1   8 if ($] >= 5.016) {
3121 1         6 require feature;
3122 1         105 feature->import('evalbytes');
3123 1         819 *evaluate = \&CORE::evalbytes;
3124             }
3125             else {
3126 0         0 *evaluate = sub{ eval shift };
  0         0  
3127             }
3128             }
3129              
3130             my $FROM_START = 0;
3131              
3132             sub rxrx {
3133             # Handle: rxrx
3134 0 0   0 0   if (@_) {
3135 0           local @ARGV = @_;
3136              
3137             # If file is a debugger dump, decode and step through it...
3138 0           my $filetext = do { local $/; <> };
  0            
  0            
3139 0           my $dumped_data = eval { $JSON_decoder->($filetext) };
  0            
3140 0 0 0       if (ref($dumped_data) eq 'HASH' && defined $dumped_data->{regex_ID} ) {
3141             # Reconstruct internal state...
3142 0           my $regex_ID = $dumped_data->{regex_ID};
3143 0           %history_of = %{ $dumped_data->{visualization} };
  0            
3144 0           $history_of{match_heatmap} = $dumped_data->{match_heatmap};
3145 0           $history_of{string_heatmap} = $dumped_data->{string_heatmap};
3146 0           $display_mode = $dumped_data->{config}{display_mode};
3147 0           $state{$regex_ID}{location} = $dumped_data->{regex_location};
3148              
3149             # Display...
3150 0           my $step = $FROM_START;
3151 0           my $cmd;
3152 0           while (1) {
3153 0           ($cmd, $step) = _revisualize($regex_ID, '-', $step);
3154 0 0         last if lc($cmd) eq 'q';
3155 0           $step = min($step, @{$history_of{visual}}-1);
  0            
3156             }
3157 0           exit;
3158             }
3159              
3160             # Otherwise, assume it's a perl source file and debug it...
3161             else {
3162 0 0         exec $^X, '-MRegexp::Debugger', @_
3163             or die "Couldn't invoke perl: $!";
3164             }
3165             }
3166              
3167             # Otherwise, be interactive...
3168              
3169             # Track input history...
3170 0           my $str_history = [];
3171 0           my $regex_history = [];
3172              
3173             # Start with empty data...
3174 0           my $input_regex = '';
3175 0           my $regex = '';
3176 0           my $regex_flags = '';
3177 0           my $string = '';
3178              
3179             # And display it...
3180 0           _display($string, $input_regex,q{});
3181              
3182             INPUT:
3183 0           while (1) {
3184 0           my $input = _prompt('>');
3185              
3186             # String history mode?
3187 0 0         if ($input =~ /^['"]$/) {
    0          
3188 0           $input = _rxrx_history($str_history);
3189             }
3190              
3191             # Regex history mode?
3192             elsif ($input eq '/') {
3193 0           $input = _rxrx_history($regex_history);
3194             }
3195              
3196              
3197             # Are we updating the regex or string???
3198 0 0         if ($input =~ m{^ (? [+]\s*[/]|[/"']) (? .*?) (? \k (? [imsxlaud]*) )? \s* \z }x) {
    0          
    0          
    0          
    0          
    0          
3199 0           my ($cmd, $data, $endcmd, $flags) = @+{qw< cmd data endcmd flags >};
3200              
3201             # Load the rest of the regex (if any)...
3202 0 0         if ($cmd =~ m{[+]\s*[/]}xms) {
3203 0           $cmd = '/';
3204 0           while (my $input = _prompt(' +')) {
3205 0 0         last if $input eq q{};
3206 0 0         if ($input =~ m{\A (?.*) [/][imsxlaud]*\Z}xms) {
3207 0           $data .= "\n$+{data}";
3208 0           last;
3209             }
3210             else {
3211 0           $data .= "\n$input";
3212             }
3213             }
3214             }
3215              
3216             # Compile and save the new regex...
3217 0 0         if ($cmd eq q{/}) {
    0          
    0          
3218 0 0         if ($data eq q{}) {
3219 0           state $NULL_REGEX = eval q{use Regexp::Debugger; qr{(?#NULL)}; };
3220 0           $regex = $NULL_REGEX;
3221             }
3222             else {
3223 0           $input_regex = $data;
3224 0   0       $regex_flags = $flags // 'x';
3225 1     1   9 use re 'eval';
  1         2  
  1         1152  
3226 0           $regex = evaluate qq{\n# line 0 rxrx\nuse re 'eval'; use Regexp::Debugger; qr/$data/$regex_flags;};
3227             }
3228              
3229             # Report any errors...
3230 0 0         if (!defined $regex) {
3231 0           $input_regex = "Invalid regex:\n$@";
3232 0           say '>', eval qq{\n# line 0 rxrx\n qr/$data/$regex_flags;};
3233             }
3234             else { # Remember it...
3235 0           push @{$regex_history}, $input;
  0            
3236             }
3237             }
3238              
3239             # Otherwise compile the string (interpolated or not)...
3240             elsif ($+{cmd} eq q{"}) {
3241 0           $string = evaluate qq{"$+{data}"};
3242              
3243             # Report any errors...
3244 0 0         print "$@\n" if $@;
3245 0 0         print "Invalid input\n" if !defined $string;
3246              
3247             # Remember it...
3248 0           push @{$str_history}, $input;
  0            
3249             }
3250             elsif ($+{cmd} eq q{'}) {
3251 0           $string = evaluate qq{'$+{data}'};
3252              
3253             # Report any errors...
3254 0 0         print "$@\n" if $@;
3255 0 0         print "Invalid input\n" if !defined $string;
3256              
3257             # Remember it...
3258 0           push @{$str_history}, $input;
  0            
3259             }
3260             }
3261              
3262             # Quit if quitting requested...
3263             elsif ($input =~ /^ \s* [xXqQ]/x) {
3264 0           say q{};
3265 0           last INPUT;
3266             }
3267              
3268             # Help...
3269             elsif ($input =~ /^ \s* [?hH]/x) {
3270 0           print "\n" x 2;
3271 0           say '____________________________________________/ Help \____';
3272 0           say ' ';
3273 0           say ' / : Enter a pattern in a single line';
3274 0           say ' +/ : Enter first line of a multi-line pattern';
3275 0           say " ' : Enter a new literal string";
3276 0           say ' " : Enter a new double-quoted string';
3277 0 0         if (eval { require IO::Prompter }) {
  0            
3278 0           say '';
3279 0           say 'CTRL-R : History completion - move backwards one input';
3280 0           say 'CTRL-N : History completion - move forwards one input';
3281 0           say '';
3282 0           say 'CTRL-B : Cursor motion - move back one character';
3283 0           say 'CTRL-F : Cursor motion - move forwards one character';
3284 0           say 'CTRL-A : Cursor motion - move to start of input';
3285 0           say 'CTRL-E : Cursor motion - move to end of input';
3286             }
3287 0           say '';
3288 0           say ' m : Match current string against current pattern';
3289 0           say '';
3290 0           say ' g : Exhaustively match against current pattern';
3291 0           say '';
3292 0           say ' d : Deconstruct and explain the current regex';
3293 0           say '';
3294 0           say 'q or x : quit debugger and exit';
3295 0           next INPUT;
3296             }
3297              
3298             # Visualize the match...
3299             elsif ($input =~ /m/i) {
3300 0           $string =~ $regex;
3301             }
3302              
3303             # Visualize the matches...
3304             elsif ($input =~ /g/i) {
3305 0           () = $string =~ /$regex/g;
3306             }
3307              
3308             # Explain the regex...
3309             elsif ($input =~ /d/i) {
3310 0           _show_regex_description($next_regex_ID-1);
3311 0           next INPUT;
3312             }
3313              
3314             # Redisplay the new regex and/or string...
3315 0 0 0       if (defined $string && defined $input_regex) {
3316 0           _display($string, $input_regex, $regex_flags);
3317             }
3318             }
3319             }
3320              
3321             # Lay out the regex and string as does Regexp::Debugger...
3322             sub _display {
3323 0     0     my ($string, $regex, $flags) = @_;
3324              
3325 0           say "\n" x 100;
3326 0           say Term::ANSIColor::colored('regex:', 'white');
3327 0           say qq{/$regex/$flags\n\n\n};
3328 0           say Term::ANSIColor::colored('string:', 'white');
3329 0           say q{'} . _ws_colourer($string) . qq{'\n\n\n};
3330             }
3331              
3332              
3333             # Make whitespace characters visible (without using a regex)...
3334             sub _quote_ws {
3335 0     0     my $str = shift;
3336              
3337 0           my $index;
3338 0           for my $ws_char ( ["\n"=>'\n'], ["\t"=>'\n'] ) {
3339             SEARCH:
3340 0           while (1) {
3341 0           $index = index($str, $ws_char->[0]);
3342 0 0         last SEARCH if $index < 0;
3343 0           substr($str, $index, 1, $ws_char->[1]);
3344             }
3345             }
3346              
3347 0           return $str;
3348             }
3349              
3350             # Hi-res sleep...
3351             sub _pause {
3352 0     0     select undef, undef, undef, shift;
3353             }
3354              
3355             # Simple prompter...
3356             *_prompt = eval { require IO::Prompter }
3357             ? sub {
3358             return IO::Prompter::prompt(@_)
3359             }
3360             : sub {
3361 0     0     my ($prompt) = @_;
3362              
3363 0           print "$prompt ";
3364 0           my $input = readline *STDIN;
3365 0           chomp $input;
3366 0           return $input;
3367             };
3368              
3369              
3370             1; # Magic true value required at end of module
3371             __END__