File Coverage

blib/lib/Regexp/Debugger.pm
Criterion Covered Total %
statement 109 1286 8.4
branch 19 688 2.7
condition 2 319 0.6
subroutine 28 76 36.8
pod 0 1 0.0
total 158 2370 6.6


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