package Padre::Wx::Dialog::RegexEditor; # The Regex Editor for Padre use 5.008; use strict; use warnings; use Padre::Wx 'RichText'; use Padre::Wx::Icon (); use Padre::Wx::Role::Main (); our $VERSION = '0.94'; our @ISA = qw{ Padre::Wx::Role::Main Wx::Dialog }; ###################################################################### # Constructor sub new { my $class = shift; my $parent = shift; # Create the basic object my $self = $class->SUPER::new( $parent, -1, Wx::gettext('Regex Editor'), Wx::DefaultPosition, Wx::DefaultSize, Wx::DEFAULT_FRAME_STYLE, ); # Set basic dialog properties $self->SetIcon(Padre::Wx::Icon::PADRE); $self->SetMinSize( [ 380, 500 ] ); # create sizer that will host all controls my $sizer = Wx::BoxSizer->new(Wx::HORIZONTAL); $self->{sizer} = $sizer; # Create the controls $self->_create_controls($sizer); # Bind the control events $self->_bind_events; # Tune the size and position it appears $self->SetSizer($sizer); $self->Fit; $self->CentreOnParent; return $self; } # # A private method that returns a hash of regex groups along with their meaning # sub _regex_groups { my $self = shift; return ( '00' => { label => Wx::gettext('Character classes'), value => { '00.' => Wx::gettext('Any character except a newline'), '01\d' => Wx::gettext('Any decimal digit'), '02\D' => Wx::gettext('Any non-digit'), '03\s' => Wx::gettext('Any whitespace character'), '04\S' => Wx::gettext('Any non-whitespace character'), '05\w' => Wx::gettext('Any word character'), '06\W' => Wx::gettext('Any non-word character'), } }, '01' => { label => Wx::gettext('&POSIX Character classes'), value => { '00[:alpha:]' => Wx::gettext('Alphabetic characters'), '01[:alnum:]' => Wx::gettext('Alphanumeric characters'), '02[:ascii:]' => Wx::gettext('7-bit US-ASCII character'), '03[:blank:]' => Wx::gettext('Space and tab'), '04[:cntrl:]' => Wx::gettext('Control characters'), '05[:digit:]' => Wx::gettext('Digits'), '06[:graph:]' => Wx::gettext('Visible characters'), '07[:lower:]' => Wx::gettext('Lowercase characters'), '08[:print:]' => Wx::gettext('Visible characters and spaces'), '09[:punct:]' => Wx::gettext('Punctuation characters'), '10[:space:]' => Wx::gettext('Whitespace characters'), '11[:upper:]' => Wx::gettext('Uppercase characters'), '12[:word:]' => Wx::gettext('Alphanumeric characters plus "_"'), '13[:xdigit:]' => Wx::gettext('Hexadecimal digits'), } }, '02' => { label => Wx::gettext('&Quantifiers'), value => { '00*' => Wx::gettext('Match 0 or more times'), '01+' => Wx::gettext('Match 1 or more times'), '02?' => Wx::gettext('Match 1 or 0 times'), '03{m}' => Wx::gettext('Match exactly m times'), '05{n,}' => Wx::gettext('Match at least n times'), '05{m,n}' => Wx::gettext('Match at least m but not more than n times'), } }, '03' => { label => Wx::gettext('Miscellaneous'), value => { '00|' => Wx::gettext('Alternation'), '01[ ]' => Wx::gettext('Character set'), '02^' => Wx::gettext('Beginning of line'), '03$' => Wx::gettext('End of line'), '04\b' => Wx::gettext('A word boundary'), '05\B' => Wx::gettext('Not a word boundary'), '06(?# )' => Wx::gettext('A comment'), } }, '04' => { label => Wx::gettext('Grouping constructs'), value => { '00( )' => Wx::gettext('A group'), '01(?: )' => Wx::gettext('Non-capturing group'), '02(?= )' => Wx::gettext('Positive lookahead assertion'), '03(?! )' => Wx::gettext('Negative lookahead assertion'), '04(?<=)' => Wx::gettext('Positive lookbehind assertion'), '05(? Wx::gettext('Negative lookbehind assertion'), '06\n' => Wx::gettext('Backreference to the nth group'), } }, # next list taken from perldoc perlre # most of these are not interesting for beginners so I am not sure we need to show them '05' => { label => Wx::gettext('Escape characters'), value => { '00\t' => Wx::gettext('Tab'), '01\n' => Wx::gettext('Newline'), '02\r' => Wx::gettext('Return'), '03\f' => Wx::gettext('Form feed'), '04\a' => Wx::gettext('Alarm'), '05\e' => Wx::gettext('Escape (Esc)'), '06\033' => Wx::gettext('Octal character'), '07\x1B' => Wx::gettext('Hex character'), '08\x{263a}' => Wx::gettext('Long hex character'), '09\cK' => Wx::gettext('Control character'), '10\N{name}' => Wx::gettext("Unicode character 'name'"), '11\l' => Wx::gettext('Lowercase next character'), '12\u' => Wx::gettext('Uppercase next character'), '13\L' => Wx::gettext('Lowercase till \E'), '14\U' => Wx::gettext('Uppercase till \E'), '15\E' => Wx::gettext('End case modification/metacharacter quoting'), '16\Q' => Wx::gettext('Quote (disable) pattern metacharacters till \E'), } }, ); } sub _create_controls { my ( $self, $sizer ) = @_; # Dialog Controls, created in keyboard navigation order # Regex text field my $regex_label = Wx::StaticText->new( $self, -1, Wx::gettext('&Regular expression:') ); $self->{regex} = Wx::TextCtrl->new( $self, -1, '', Wx::DefaultPosition, Wx::DefaultSize, Wx::RE_MULTILINE | Wx::WANTS_CHARS # Otherwise arrows will not work on win32 ); my %regex_groups = $self->_regex_groups; foreach my $code ( sort keys %regex_groups ) { my %sub_group = %{ $regex_groups{$code} }; my $button_name = $code . '_button'; $self->{$button_name} = Wx::Button->new( $self, -1, $sub_group{label}, ); my $menu_name = $code . '_menu'; Wx::Event::EVT_BUTTON( $self, $self->{$button_name}, sub { my @pos = $self->{$button_name}->GetPositionXY; my @size = $self->{$button_name}->GetSizeWH; $self->PopupMenu( $self->{$menu_name}, $pos[0], $pos[1] + $size[1] ); }, ); $self->{$menu_name} = Wx::Menu->new; my %sub_group_value = %{ $sub_group{value} }; foreach my $element ( sort keys %sub_group_value ) { my $label = $element; $label =~ s/^\d{2}//; my $menu_item = $self->{$menu_name}->Append( -1, $label . ' ' . $sub_group_value{$element} ); Wx::Event::EVT_MENU( $self, $menu_item, sub { $_[0]->{regex}->WriteText($label); }, ); } } # Optionally toggle the visibility of the description field $self->{description_checkbox} = Wx::CheckBox->new( $self, -1, Wx::gettext('Show &Description'), ); # Describe-the-regex text field $self->{description_text} = Wx::TextCtrl->new( $self, -1, '', Wx::DefaultPosition, Wx::DefaultSize, Wx::TE_MULTILINE | Wx::NO_FULL_REPAINT_ON_RESIZE ); # Description is hidden by default $self->{description_text}->Hide; # Original input text field my $original_label = Wx::StaticText->new( $self, -1, Wx::gettext('&Original text:') ); $self->{original_text} = Wx::TextCtrl->new( $self, -1, '', Wx::DefaultPosition, Wx::DefaultSize, Wx::TE_MULTILINE | Wx::NO_FULL_REPAINT_ON_RESIZE ); # Matched readonly text field my $matched_label = Wx::StaticText->new( $self, -1, Wx::gettext('Matched text:') ); $self->{matched_text} = Wx::RichTextCtrl->new( $self, -1, '', Wx::DefaultPosition, Wx::DefaultSize, Wx::RE_MULTILINE | Wx::RE_READONLY | Wx::WANTS_CHARS # Otherwise arrows will not work on win32 ); # Toggle the visibility of the replace (substitution) fields $self->{replace_checkbox} = Wx::CheckBox->new( $self, -1, Wx::gettext('Show Subs&titution'), ); # Replace regex text field $self->{replace_label} = Wx::StaticText->new( $self, -1, Wx::gettext('&Replace text with:') ); $self->{replace_text} = Wx::TextCtrl->new( $self, -1, '', Wx::DefaultPosition, Wx::DefaultSize, Wx::TE_MULTILINE | Wx::NO_FULL_REPAINT_ON_RESIZE ); $self->{replace_label}->Hide; $self->{replace_text}->Hide; # Result from replace text field $self->{result_label} = Wx::StaticText->new( $self, -1, Wx::gettext('&Result from replace:') ); $self->{result_text} = Wx::RichTextCtrl->new( $self, -1, '', Wx::DefaultPosition, Wx::DefaultSize, Wx::RE_MULTILINE | Wx::RE_READONLY | Wx::WANTS_CHARS # otherwise arrows will not work on win32 ); $self->{result_label}->Hide; $self->{result_text}->Hide; # Insert regex into current document $self->{insert_button} = Wx::Button->new( $self, -1, Wx::gettext('Insert'), ); # Close button $self->{close_button} = Wx::Button->new( $self, Wx::ID_CANCEL, Wx::gettext('&Close'), ); my $buttons = Wx::BoxSizer->new(Wx::HORIZONTAL); $buttons->AddStretchSpacer; $buttons->Add( $self->{insert_button}, 0, Wx::ALL, 1 ); $buttons->Add( $self->{close_button}, 0, Wx::ALL, 1 ); $buttons->AddStretchSpacer; # Modifiers my %m = $self->_modifiers; foreach my $name ( $self->_modifier_keys ) { $self->{$name} = Wx::CheckBox->new( $self, -1, $m{$name}{name}, ); $self->{$name}->SetToolTip( Wx::ToolTip->new( $m{$name}{tooltip} ) ); } # Dialog Layout my $modifiers = Wx::BoxSizer->new(Wx::HORIZONTAL); $modifiers->AddStretchSpacer; $modifiers->Add( $self->{ignore_case}, 0, Wx::ALL, 1 ); $modifiers->Add( $self->{single_line}, 0, Wx::ALL, 1 ); $modifiers->Add( $self->{multi_line}, 0, Wx::ALL, 1 ); $modifiers->Add( $self->{extended}, 0, Wx::ALL, 1 ); $modifiers->Add( $self->{global}, 0, Wx::ALL, 1 ); $modifiers->AddStretchSpacer; my $regex = Wx::BoxSizer->new(Wx::VERTICAL); $regex->Add( $self->{regex}, 1, Wx::ALL | Wx::EXPAND, 1 ); my $regex_groups = Wx::BoxSizer->new(Wx::VERTICAL); foreach my $code ( sort keys %regex_groups ) { my $button_name = $code . '_button'; $regex_groups->Add( $self->{$button_name}, 0, Wx::EXPAND, 1 ); } my $combined = Wx::BoxSizer->new(Wx::HORIZONTAL); $combined->Add( $regex, 2, Wx::ALL | Wx::EXPAND, 0 ); $combined->Add( $regex_groups, 0, Wx::ALL | Wx::EXPAND, 0 ); # Vertical layout of the left hand side my $left = Wx::BoxSizer->new(Wx::VERTICAL); $left->Add( $modifiers, 0, Wx::ALL | Wx::EXPAND, 2 ); $left->AddSpacer(5); $left->Add( $regex_label, 0, Wx::ALL | Wx::EXPAND, 1 ); $left->Add( $combined, 0, Wx::ALL | Wx::EXPAND, 2 ); $left->Add( $self->{description_checkbox}, 0, Wx::ALL | Wx::EXPAND, 1 ); $left->Add( $self->{description_text}, 2, Wx::ALL | Wx::EXPAND, 1 ); $left->Add( $original_label, 0, Wx::ALL | Wx::EXPAND, 1 ); $left->Add( $self->{original_text}, 1, Wx::ALL | Wx::EXPAND, 1 ); $left->Add( $matched_label, 0, Wx::ALL | Wx::EXPAND, 1 ); $left->Add( $self->{matched_text}, 1, Wx::ALL | Wx::EXPAND, 1 ); $left->Add( $self->{replace_checkbox}, 0, Wx::ALL | Wx::EXPAND, 1 ); $left->Add( $self->{replace_label}, 0, Wx::ALL | Wx::EXPAND, 1 ); $left->Add( $self->{replace_text}, 1, Wx::ALL | Wx::EXPAND, 1 ); $left->Add( $self->{result_label}, 0, Wx::ALL | Wx::EXPAND, 1 ); $left->Add( $self->{result_text}, 1, Wx::ALL | Wx::EXPAND, 1 ); $left->AddSpacer(5); $left->Add( $buttons, 0, Wx::ALL | Wx::EXPAND, 1 ); # Main sizer $sizer->Add( $left, 1, Wx::ALL | Wx::EXPAND, 5 ); } sub _bind_events { my $self = shift; Wx::Event::EVT_TEXT( $self, $self->{regex}, sub { $_[0]->run; }, ); Wx::Event::EVT_KEY_DOWN( $self, sub { my ($key_event) = $_[1]; $self->Hide if $key_event->GetKeyCode == Wx::K_ESCAPE; return; } ); Wx::Event::EVT_TEXT( $self, $self->{replace_text}, sub { $_[0]->run; }, ); Wx::Event::EVT_TEXT( $self, $self->{original_text}, sub { $_[0]->run; }, ); # Modifiers foreach my $name ( $self->_modifier_keys ) { Wx::Event::EVT_CHECKBOX( $self, $self->{$name}, sub { $_[0]->box_clicked($name); }, ); } # Description checkbox Wx::Event::EVT_CHECKBOX( $self, $self->{description_checkbox}, sub { # toggles the visibility of the description field if ( $self->{description_checkbox}->IsChecked ) { my $regex = $self->{regex}->GetValue; $self->{description_text}->SetValue( $self->_dump_regex($regex) ); } $self->{description_text}->Show( $self->{description_checkbox}->IsChecked ); $self->{sizer}->Layout; }, ); # Replace checkbox Wx::Event::EVT_CHECKBOX( $self, $self->{replace_checkbox}, sub { $self->replace; # toggles the visibility of the replace fields foreach my $field (qw(replace_label replace_text result_label result_text)) { $self->{$field}->Show( $self->{replace_checkbox}->IsChecked ); } $self->{sizer}->Layout; }, ); Wx::Event::EVT_KEY_DOWN( $self->{matched_text}, sub { my ($key_event) = $_[1]; $self->Hide if $key_event->GetKeyCode == Wx::K_ESCAPE; return; } ); Wx::Event::EVT_KEY_DOWN( $self->{result_text}, sub { my ($key_event) = $_[1]; $self->Hide if $key_event->GetKeyCode == Wx::K_ESCAPE; return; } ); Wx::Event::EVT_BUTTON( $self, $self->{insert_button}, sub { shift->_insert_regex; }, ); } # # A private method that inserts the current regex into the current document # sub _insert_regex { my $self = shift; my $match_part = $self->{regex}->GetValue; my $replace_part = $self->{replace_text}->GetValue; my ($modifiers) = $self->_get_modifier_settings; my $editor = $self->current->editor or return; if ( $self->{replace_checkbox}->IsChecked ) { $editor->InsertText( $editor->GetCurrentPos, "s/$match_part/$replace_part/$modifiers" ); } else { $editor->InsertText( $editor->GetCurrentPos, "m/$match_part/$modifiers" ); } return; } # # A private method that returns a hash of regex modifiers # sub _modifiers { return ( ignore_case => { mod => 'i', name => Wx::gettext('Ignore case (&i)'), tooltip => Wx::gettext('Case-insensitive matching') }, single_line => { mod => 's', name => Wx::gettext('Single-line (&s)'), tooltip => Wx::gettext('"." also matches newline') }, multi_line => { mod => 'm', name => Wx::gettext('Multi-line (&m)'), tooltip => Wx::gettext('"^" and "$" match the start and end of any line inside the string') }, extended => { mod => 'x', name => Wx::gettext('Extended (&x)'), tooltip => Wx::gettext('Extended regular expressions allow free formatting (whitespace is ignored) and comments') }, global => { mod => 'g', name => Wx::gettext('Global (&g)'), tooltip => Wx::gettext('Replace all occurrences of the pattern') }, ); } # # returns the regex modifier keys in the order they appear in the GUI # sub _modifier_keys { return qw{ ignore_case single_line multi_line extended global}; } # -- public methods sub show { my $self = shift; if ( $self->IsShown ) { $self->SetFocus; } else { my $editor = $self->current->editor; if ($editor) { my $selection = $editor->GetSelectedText; my $selection_length = length $selection; if ( $selection_length > 0 ) { $self->{regex}->ChangeValue($selection); } else { $self->{regex}->ChangeValue('\w+'); } } else { $self->{regex}->ChangeValue('\w+'); } $self->{replace_text}->ChangeValue('Baz'); $self->{original_text}->SetValue('Foo Bar'); $self->Show; } $self->{regex}->SetFocus; return; } # # Private method to dump the regular expression description as text # sub _dump_regex { if ( scalar @_ == 2 ) { my ( $self, $regex ) = @_; require PPIx::Regexp; return $self->_dump_regex( PPIx::Regexp->new("/$regex/"), '', 0 ); } my ( $self, $parent, $str, $level ) = @_; $str = '' unless $str; $level = 0 unless $level; my @children = $parent->isa('PPIx::Regexp::Node') ? $parent->children : (); foreach my $child (@children) { next if $child->content eq ''; my $class_name = $child->class; $class_name =~ s/PPIx::Regexp:://; $str .= ( ' ' x ( $level * 4 ) ) . $class_name . ' (' . $child->content . ")\n"; $str = $self->_dump_regex( $child, $str, $level + 1 ); } return $str; } # # Private method to return all the parsed regex elements as an array # sub _parse_regex_elements { my ( $parent, $position, @array ) = @_; $position = 0 unless $position; @array = () unless @array; my @elements = $parent->isa('PPIx::Regexp::Node') ? $parent->elements : (); foreach my $element (@elements) { my $content = $element->content; next if $content eq ''; my $class_name = $element->class; push @array, { element => $element, offset => $position, len => length $content }; @array = _parse_regex_elements( $element, $position, @array ); $position += length $content; } return @array; } # # Returns the user input data of the dialog as a hashref # sub get_data { my $self = shift; my %data = ( text => { regex => $self->{regex}->GetValue, replace => $self->{replace_text}->GetValue, original_text => $self->{original_text}->GetValue, }, modifiers => [ $self->_get_modifier_settings ], ); return \%data; } # # Sets the user input data of the dialog given a hashref containing the results of get_data # sub set_data { my ( $self, $data_ref ) = @_; foreach my $text_field ( keys %{ $data_ref->{text} } ) { $self->{$text_field}->SetValue( $data_ref->{text}->{$text_field} ); } my $modifier_string = $data_ref->{modifiers}->[0]; my %modifiers = $self->_modifiers; foreach my $name ( keys %modifiers ) { $self->{$name}->SetValue(1) if $modifier_string =~ s/$modifiers{$name}{mod}//; } return; } # # Private method to get the modifier settings as two strings # the first strings returns the active modifiers, the second the # inactive ones # sub _get_modifier_settings { my $self = shift; my $active_modifiers = ''; my $inactive_modifiers = ''; my %modifiers = $self->_modifiers; foreach my $name ( keys %modifiers ) { if ( $self->{$name}->IsChecked ) { $active_modifiers .= $modifiers{$name}{mod}; } else { $inactive_modifiers .= $modifiers{$name}{mod}; } } return ( $active_modifiers, $inactive_modifiers ); } sub run { my $self = shift; my $regex = $self->{regex}->GetValue; my $original_text = $self->{original_text}->GetValue; # TODO what about white space only regexes? if ( $regex eq '' ) { $self->{matched_text}->BeginTextColour(Wx::RED); $self->{matched_text}->SetValue( Wx::gettext('Empty regex') ); $self->{matched_text}->EndTextColour; return; } my ( $active, $inactive ) = $self->_get_modifier_settings; $self->{matched_text}->Clear; $self->{matched_text}->BeginTextColour(Wx::BLACK); my $match; my $match_start; my $match_end; my $result; my $warning; # Ignore warnings on win32. It's ugly but it works :) local $SIG{__WARN__} = sub { $warning = $_[0] }; # TODO loop on all matches in case of /g my $code = "\$result = \$original_text =~ /\$regex/$active; (\$match_start, \$match_end) = (\$-[0], \$+[0])"; eval $code; if ($@) { $self->{matched_text}->BeginTextColour(Wx::RED); $self->{matched_text}->SetValue( sprintf( Wx::gettext('Match failure in %s: %s'), $regex, $@ ) ); $self->{matched_text}->EndTextColour; return; } if ($result) { $match = substr( $original_text, $match_start, $match_end - $match_start ); } if ($warning) { $self->{matched_text}->BeginTextColour(Wx::RED); $self->{matched_text}->SetValue( sprintf( Wx::gettext('Match warning in %s: %s'), $regex, $warning ) ); $self->{matched_text}->EndTextColour; return; } if ( defined $match ) { if ( $match_start == $match_end ) { $self->{matched_text}->BeginTextColour(Wx::RED); $self->{matched_text} ->SetValue( sprintf( Wx::gettext('Match with 0 width at character %s'), $match_start ) ); $self->{matched_text}->EndTextColour; } else { my @chars = split( //, $original_text ); my $pos = 0; foreach my $char (@chars) { if ( $pos == $match_start ) { $self->{matched_text}->BeginTextColour(Wx::BLUE); $self->{matched_text}->BeginUnderline; } elsif ( $pos == $match_end ) { $self->{matched_text}->EndTextColour; $self->{matched_text}->EndUnderline; } $self->{matched_text}->AppendText($char); $pos++; } } } else { $self->{matched_text}->BeginTextColour(Wx::RED); $self->{matched_text}->SetValue( Wx::gettext('No match') ); $self->{matched_text}->EndTextColour; } $self->{matched_text}->EndTextColour; $self->replace; $self->{description_text}->SetValue( $self->_dump_regex($regex) ) if $self->{description_text}->IsShown; # $self->{regex}->Clear; # my @elements = _parse_regex_elements; # foreach my $element (@elements) { # my $class_name = $element->element->class; # if ($class_name eq 'PPIx::Regexp::Token::CharClass::Simple') { # $self->{regex}->BeginTextColour(Wx::RED); # } elsif( $class_name eq 'PPIx::Regexp::Token::Quantifier') { # $self->{regex}->BeginTextColour(Wx::BLUE); # } elsif( $class_name eq 'PPIx::Regexp::Token::Operator') { # $self->{regex}->BeginTextColour(Wx::LIGHT_GREY); # } elsif( $class_name eq 'PPIx::Regexp::Structure::Capture') { # $self->{regex}->BeginTextColour(Wx::CYAN); # } # $self->{regex}->AppendText($element->content); # $self->{regex}->EndTextColour; # } return; } sub replace { my $self = shift; return if !$self->{replace_checkbox}->IsChecked; my $regex = $self->{regex}->GetValue; my $result_text = $self->{original_text}->GetValue; my $replace = $self->{replace_text}->GetValue; my ( $active, $inactive ) = $self->_get_modifier_settings; $self->{result_text}->Clear; my $code = "\$result_text =~ s{\$regex}{$replace}$active"; eval $code; if ($@) { $self->{result_text}->BeginTextColour(Wx::RED); $self->{result_text}->AppendText( sprintf( Wx::gettext('Replace failure in %s: %s'), $regex, $@ ) ); $self->{result_text}->EndTextColour; return; } if ( defined $result_text ) { $self->{result_text}->SetValue($result_text); } return; } sub box_clicked { my $self = shift; $self->run; return; } 1; __END__ =pod =head1 NAME Padre::Wx::Dialog::RegexEditor - dialog to make it easy to create a regular expression =head1 DESCRIPTION The C provides an interface to easily create regular expressions used in Perl. The user can insert a regular expression (the surrounding C characters are not needed) and a text. The C will automatically display the matching text in the bottom right window. At the top of the window the user can select any of the four regular expression modifiers: =over =item Ignore case (i) =item Single-line (s) =item Multi-line (m) =item Extended (x) =back Global match Allow the change/replacement of the // around the regular expression Highlight the match in the source text instead of in a separate window Display the captured groups in a tree hierarchy similar to Rx ? Group Span (character) Value Match 0 (Group 0) 4-7 the actual match Display the various Perl variable containing the relevant values e.g. the C<@-> and C<@+> arrays, the C<%+> hash C<$1>, C<$2>... point out what to use instead of C<$@> and C<$'> and C<$`> English explanation of the regular expression =head1 COPYRIGHT & LICENSE Copyright 2008-2012 The Padre development team as listed in Padre.pm. This program is free software; you can redistribute it and/or modify it under the same terms as Perl 5 itself. =cut