File Coverage

blib/lib/Regexp/Debugger.pm
Criterion Covered Total %
statement 103 1279 8.0
branch 19 688 2.7
condition 2 319 0.6
subroutine 26 74 35.1
pod 0 1 0.0
total 150 2361 6.3


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