# @COPYRIGHT@ package Socialtext::Formatter::Phrase; use strict; use warnings; my $ALPHANUM = '\p{Letter}\p{Number}\pM'; use base 'Socialtext::Formatter::Unit'; sub match { my $self = shift; my $text = shift; return unless $text =~ $self->pattern_start; $self->start_offset( $-[0] ); $self->start_end_offset( $+[0] ); $self->matched( substr( $text, $-[0], $+[0] - $-[0] ) ); my $pattern_end = $self->pattern_end or return 1; return substr( $text, $+[0] ) =~ $pattern_end; } sub contains_phrases { my $self = shift; my $id = $self->formatter_id; [ grep { $_ ne $id } @{ Socialtext::Formatter->all_phrases } ]; } ################################################################################ package Socialtext::Formatter::Teletype; use base 'Socialtext::Formatter::Phrase'; use Class::Field qw( const ); const formatter_id => 'tt'; const pattern_start => qr/(^|(?<=[^$ALPHANUM]))`(?=\S)/; const pattern_end => qr/`(?=[^$ALPHANUM]|\z)/; const html_start => ""; const html_end => ""; sub contains_phrases { [] } ################################################################################ package Socialtext::Formatter::Strong; use base 'Socialtext::Formatter::Phrase'; use Class::Field qw( const ); const formatter_id => 'strong'; const pattern_start => qr/(^|(?<=[^{$ALPHANUM}\*]))\*(?=\S)(?!\*)/; const pattern_end => qr/\*(?=[^{$ALPHANUM}\*]|\z)/; const html_start => ""; const html_end => ""; ################################################################################ package Socialtext::Formatter::Emphasize; use base 'Socialtext::Formatter::Phrase'; use Class::Field qw( const ); const formatter_id => 'em'; const pattern_start => qr/(^|(?<=[^${ALPHANUM}_]))_(?=\S[^_]*_(?=\W|\z))/; const pattern_end => qr/_(?=[^{$ALPHANUM}_]|\z)/; const html_start => ""; const html_end => ""; ################################################################################ package Socialtext::Formatter::Delete; use base 'Socialtext::Formatter::Phrase'; use Class::Field qw( const ); const formatter_id => 'del'; const pattern_start => qr/(^|(?<=\s))-(?=[^\-\s])/; const pattern_end => qr/-(\z|(?=\s))/; const html_start => ''; const html_end => ''; ################################################################################ package Socialtext::Formatter::Asis; use base 'Socialtext::Formatter::Phrase'; use Class::Field qw( const field ); use Socialtext::BrowserDetect (); const formatter_id => 'asis'; const pattern_start => qr/\{\{/; # The extra space here is to work around the fact that IE innerHTML eats # spaces after a SPAN which means in order to preserve the space after an asis # we need to store it manually. ugly ugly ugly. const pattern_end => qr/\}\}( ?)/; field 'asis_text'; field 'extra_space'; sub contains_phrases { [] } sub get_text { my $self = shift; return $self->asis_text . $self->extra_space; } # Use our own match routine to properly consume the text instead of creating # child units of contained text. sub match { my $self = shift; my $text = shift; # Match the start, save the opening match tag return unless $text =~ $self->pattern_start; my $match_start = $+[0]; $self->start_offset( $-[0] ); $self->matched( substr( $text, $-[0], $+[0] - $-[0] ) ); # Match the end, save matched body in the title field return unless $text =~ $self->pattern_end; Socialtext::BrowserDetect::ie() ? $self->extra_space( $1 ? " " : '' ) : $self->extra_space( $1 || '' ); $self->asis_text( substr( $text, $match_start, $-[0] - $match_start ) ); $self->start_end_offset( $-[0] ); return 1; } sub html { my $self = shift; my $text = $self->escape_html( $self->asis_text ); my $space = $self->extra_space; return qq($text$space); } ################################################################################ package Socialtext::Formatter::FreeLink; use base 'Socialtext::Formatter::Phrase'; use Class::Field qw( const field ); const formatter_id => 'wiki'; const pattern_start => qr/(?:"([^"]*)"\s*)?(?:^|(?<=[^$ALPHANUM]))\[(?=[^\s\[\]])/; const pattern_end => qr/\](?=[^$ALPHANUM]|\z)/; field 'label'; field 'title'; sub contains_phrases { [] } sub get_text { my $self = shift; return $self->label || $self->title; } # Use our own match routine to properly consume the text instead of creating # child units of contained text. sub match { my $self = shift; my $text = shift; my $rt_19458_hack; # Match the start, save the opening match tag return unless $text =~ $self->pattern_start; my $match_start = $+[0]; $self->start_offset( $-[0] ); $self->label($1) if $1; $self->matched( substr( $text, $-[0], $+[0] - $-[0] ) ); # Match the end, save matched body in the title field my $end = substr( $text, $match_start ); return unless $end =~ $self->pattern_end; $text = $rt_19458_hack = $text; # To work around a Perl bug, see {rt 19458} $self->title( substr( $text, $match_start, $-[0] ) ); $self->start_end_offset( $-[0] + $match_start ); return 1; } sub html { my $self = shift; my $label = $self->label; my $page_title = $self->title; my ( $page_disposition, $page_uri ) # XXX hub remainder = $self->hub->pages->title_to_disposition($page_title); if ($page_uri) { my $escaped_page_title = $page_title; $escaped_page_title =~ s/-/=-/g; my $hint = defined $label ? "" : ''; $label = $page_title unless defined $label; $self->_freelink_url($page_uri, $page_disposition, $label, $hint); } else { return $page_title; } } sub _freelink_url { my $self = shift; my $page_uri = shift; my $page_disposition = shift; my $label = shift; my $hint = shift; my $link = $self->hub->viewer->link_dictionary->format_link( link => 'free', page_uri => $page_uri, url_prefix => $self->url_prefix, workspace => $self->current_workspace_name, # for page inclusion ); return '$label$hint}; } ################################################################################ package Socialtext::Formatter::HyperLink; use base 'Socialtext::Formatter::Phrase'; use Class::Field qw( const ); # These are all stolen from URI.pm my $reserved = q{;/?:@&=+$,[]#}; my $mark = q{-_.!~*'()}; my $unreserved = "A-Za-z0-9\Q$mark\E"; my $uric = quotemeta($reserved) . $unreserved . "%"; const formatter_id => 'hyper'; const pattern_start => qr{ (?:http|https|ftp|irc|file): (?://)? # this is optional to allow things like http:base/docs/search.png [$uric]+ [A-Za-z0-9/#] # we expect links to end with a slash or some alphanumeric, not punctuation }x; sub html { my $self = shift; my $match = $self->matched; my $target = $self->current_workspace->external_links_open_new_window ? qq{target="_blank"} : ''; my $wrap_start = ''; my $wrap_finish = ''; if ( $match =~ m{^\w+:(?!//)} ) { my $original = $match; $match = $self->_special_http_link($match); # XXX need to figure out if we can remove this and let # Socialtext::Formatter::WaflPhrase::wikiwyg_to_html take care of it $wrap_start = ''; $wrap_finish = ""; } my $href = $self->html_escape($match); my $output = $match =~ /\.(gif|jpg|jpeg|jpe|png|pbm)(?:\?\S+)?$/i ? qq{$href} : $match =~ /^irc:/ ? qq{$href} : qq{$href}; return $wrap_start . $output . $wrap_finish; } sub _special_http_link { my $self = shift; my $match = shift; $match =~ s/^\w+://; return $self->hub->viewer->link_dictionary->format_link( link => 'special_http', arg1 => $match, url_prefix => $self->url_prefix, workspace => $self->current_workspace_name, ); } ################################################################################ package Socialtext::Formatter::BracketHyperLink; use base 'Socialtext::Formatter::HyperLink'; use Class::Field qw( const ); const formatter_id => 'b_hyper'; const pattern_start => qr{("[^"]*"\s*)?<(?:http|https|ftp|irc|file):.+?>}; sub html { my $self = shift; my $match = $self->matched; (my $escaped_match = $match) =~ s/-/=-/g; $match =~ s/<(.*)>$/$1/; my ($text, $hint) = ($match, ''); if ($match =~ s/^"(.*?)"\s*//) { my $quoted = $1; $text = $quoted; if ($match =~ m!^\w+://!) { $hint = ""; } } my $target = $self->current_workspace->external_links_open_new_window ? qq{target="_blank"} : ''; my $wrap_start = ''; my $wrap_finish = ''; if ( $match =~ m{^\w+:(?!//)} ) { my $original = $match; $match = $self->_special_http_link($match); $wrap_start = ''; $wrap_finish = ''; } my $href = $self->html_escape($match); my $escaped_text = $self->html_escape($text); my $output = $match =~ /\.(gif|jpg|jpeg|jpe|png|pbm)$/i ? qq{$escaped_text} : $match =~ /^irc:/ ? qq{$escaped_text} : qq{$escaped_text$hint}; return $wrap_start . $output . $wrap_finish; } ################################################################################ package Socialtext::Formatter::IMLink; use base 'Socialtext::Formatter::Phrase'; use Class::Field qw( const ); const formatter_id => 'im'; my %im_types = ( yahoo => 'yahoo', ymsgr => 'yahoo', callto => 'callto', skype => 'callto', aim => 'aim', msn => 'msn', asap => 'asap', ); my $im_re = join '|', keys %im_types; const pattern_start => qr/\b(?:$im_re)\:[^\s\>\)]+/; sub html { my $self = shift; my $match = $self->matched; my ( $scheme, $recipient ) = $match =~ /\b($im_re):([^\s\>\)]+)/; my $method = "_$im_types{$scheme}_link"; $self->_enspan_nlw_link( $scheme, $recipient, $self->$method( $self->escape_html($recipient), $match ), ); } # XXX other option here is to canonicalize these things # to wafl, which seems fishy and leaves out http:base/foo/bar.png sub _enspan_nlw_link { my $self = shift; my $scheme = shift; my $recipient = shift; my $text = shift; qq{$text}; } sub _yahoo_link { my $self = shift; return qq{$_[0]$_[0]}; } sub _aim_link { my $self = shift; return qq{$_[0]$_[0]}; } sub _msn_link { my $self = shift; $self->escape_html( $_[1] ); } sub _callto_link { my $self = shift; return qq{$_[0]$_[0]}; } sub _asap_link { my $self = shift; return qq{$_[0]}; } ################################################################################ package Socialtext::Formatter::MailLink; use base 'Socialtext::Formatter::Phrase'; use Class::Field qw( const ); const formatter_id => 'mail'; const pattern_start => qr/[\w+%\-\.]+@(?:[\w\-]+\.)+[\w\-]+/; sub html { my $self = shift; '' . $self->matched . ''; } ################################################################################ package Socialtext::Formatter::BracketMailLink; use base 'Socialtext::Formatter::Phrase'; use Class::Field qw( const ); const formatter_id => 'b_mail'; # XXX this should reuse the regexp from MailLink, rather than copy const pattern_start => qr/("[^"]*"\s*)?/; sub html { my $self = shift; my $match = $self->matched; $match =~ s/$/$1/; my $text = ( $match =~ s/^"(.*?)"\s*// ) ? $1 : $match; '' . $text . ''; } ################################################################################ package Socialtext::Formatter::FileLink; use base 'Socialtext::Formatter::Phrase'; use Class::Field qw( const ); const formatter_id => 'file'; const pattern_start => qr/("[^"]*")?<\\\\[^\s\>\)]+>/; sub html { my $self = shift; my $match = $self->matched; $match =~ s/<(\\\\.*)>$/$1/; my $text = ( $match =~ s/^"(.*?)"// ) ? $1 : $match; $match =~ s/^\\\\//; qq{$text}; } 1;