package Kwiki::Formatter; use Spoon::Formatter -Base; use mixin 'Kwiki::Installer'; const config_class => 'Kwiki::Config'; const class_id => 'formatter'; const class_title => 'Kwiki Formatter'; const top_class => 'Kwiki::Formatter::Top'; const class_prefix => 'Kwiki::Formatter::'; const all_blocks => [qw(comment wafl_block hr heading ul ol pre table p)]; const all_phrases => [qw( asis wafl_phrase forced titlehyper titlewiki titlemailto hyper wiki mailto ndash mdash strong em u tt del )]; const css_file => 'formatter.css'; sub init { $self->hub->css->add_file($self->css_file); } sub formatter_classes { qw( Spoon::Formatter::WaflPhrase Spoon::Formatter::WaflBlock Line Heading Paragraph Preformatted Comment Ulist Olist Item Table TableRow TableCell Strong Emphasize Underline Delete Inline MDash NDash Asis ForcedLink HyperLink TitledHyperLink TitledMailLink MailLink TitledWikiLink WikiLink ); } ################################################################################ # Blocks ################################################################################ package Kwiki::Formatter::Top; use base 'Spoon::Formatter::Container'; const formatter_id => 'top'; ################################################################################ package Kwiki::Formatter::Comment; use base 'Spoon::Formatter::Unit'; const formatter_id => 'comment'; const html_start => "\n"; sub match { return unless $self->text =~ /^((?:#[\ \t].*\n)+)/m; $self->set_match; } sub text_filter { my $comment = shift; $comment =~ s/^# //gm; $comment =~ s/-/-/g; return $comment; } ################################################################################ package Kwiki::Formatter::Line; use base 'Spoon::Formatter::Unit'; const formatter_id => 'hr'; const pattern_block => qr/^----+\s*\n/m; const html => "
\n"; ################################################################################ package Kwiki::Formatter::Heading; use base 'Spoon::Formatter::Block'; const formatter_id => 'heading'; field 'level'; sub html_start { 'level . '>' } sub html_end { 'level . ">\n" } sub match { return unless $self->text =~ /^(={1,6})\s+(.*?)(\s+=+)?\s*\n+/m; $self->level(length($1)); $self->set_match($2); } ################################################################################ package Kwiki::Formatter::Paragraph; use base 'Spoon::Formatter::Block'; const formatter_id => 'p'; const pattern_block => qr/((?:^(?!(?:[\=\*\0]+ |[\#\|\s]|\.\w+\s*\n|-{4,}\s*\n)).*\S.*\n)+(^\s*\n)*)/m; const html_start => "

\n"; const html_end => "

\n"; ################################################################################ package Kwiki::Formatter::List; use base 'Spoon::Formatter::Container'; const contains_blocks => [qw(li)]; field 'level'; field 'start_level'; field 'tag_stack' => []; sub match { my $bullet = $self->bullet; return unless $self->text =~ /((?:^($bullet).*\n)(?:^\2(?!$bullet).*\n)*)/m; $self->set_match; ($bullet = $2) =~ s/\s//g; $self->level(length($bullet)); return 1; } sub html_start { my $next = $self->next_unit; my $tag_stack = $self->tag_stack; $next->tag_stack($tag_stack) if ref($next) and $next->isa('Kwiki::Formatter::List'); my $level = defined $self->start_level ? $self->start_level : $self->level; push @$tag_stack, ($self->html_end_tag) x $level; return ($self->html_start_tag x $level) . "\n"; } sub html_end { my $level = $self->level; my $tag_stack = $self->tag_stack; my $next = $self->next_unit; my $newline = "\n"; if (ref($next) and $next->isa('Kwiki::Formatter::List')) { my $next_level = $next->level; if ($level < $next_level) { $next->start_level($next_level - $level); $level = 0; } else { $next->start_level(0); $level = $level - $next_level; $newline = ''; } if ($self->level - $level == $next->level and $self->formatter_id ne $next->formatter_id ) { $level++; $next->start_level($next->start_level + 1); } } return join('', reverse splice(@$tag_stack, 0 - $level, $level)) . $newline; } ################################################################################ package Kwiki::Formatter::Ulist; use base 'Kwiki::Formatter::List'; const formatter_id => 'ul'; const html_start_tag => ''; const bullet => '\*+\ +'; ################################################################################ package Kwiki::Formatter::Olist; use base 'Kwiki::Formatter::List'; const formatter_id => 'ol'; const html_start_tag => '
    '; const html_end_tag => '
'; const bullet => '0+\ +'; ################################################################################ package Kwiki::Formatter::Item; use base 'Spoon::Formatter::Block'; const formatter_id => 'li'; const html_start => "
  • "; const html_end => "
  • \n"; const bullet => '[0\*]+\ +'; sub match { my $bullet = $self->bullet; return unless $self->text =~ /^$bullet(.*)\n/m; $self->set_match; } ################################################################################ package Kwiki::Formatter::Preformatted; use base 'Spoon::Formatter::Unit'; const formatter_id => 'pre'; const html_start => qq{
    };
    const html_end => "
    \n"; sub match { return unless $self->text =~ /((?:^ +\S.*?\n|^ *\n)+)/m; my $text = $1; $self->set_match; return unless $text =~ /\S/; return 1; } sub text_filter { my $text = shift; $text =~ s/(?<=\n)\s*$//mg; my $indent; for ($text =~ /^( +)/gm) { $indent = length() if not defined $indent or length() < $indent; } $text =~ s/^ {$indent}//gm; $text; } ################################################################################ # XXX Support colspan package Kwiki::Formatter::Table; use base 'Spoon::Formatter::Container'; const formatter_id => 'table'; const contains_blocks => [qw(tr)]; const pattern_block => qr/((^\|.*?\|\n)+)/sm; const html_start => qq{\n}; const html_end => "
    \n"; ################################################################################ package Kwiki::Formatter::TableRow; use base 'Spoon::Formatter::Container'; const formatter_id => 'tr'; const contains_blocks => [qw(td)]; const pattern_block => qr/(^\|.*?\|\n)/sm; const html_start => "\n"; const html_end => "\n"; ################################################################################ package Kwiki::Formatter::TableCell; use base 'Spoon::Formatter::Unit'; const formatter_id => 'td'; field contains_blocks => []; field contains_phrases => []; const table_blocks => [qw(wafl_block hr heading ul ol pre p)]; sub table_phrases { $self->hub->formatter->all_phrases } const html_start => ""; const html_end => "\n"; sub match { return unless $self->text =~ /(\|(\s*.*?\s*)\|)(.*)/sm; $self->start_offset($-[1]); $self->end_offset($3 eq "\n" ? $+[3] : $+[2]); my $text = $2; $text =~ s/^[ \t]*\n?(.*?)[ \t]*$/$1/; $self->text($text); if ($text =~ /\n/) { $self->contains_blocks($self->table_blocks); } else { $self->contains_phrases($self->table_phrases); } return 1; } ################################################################################ # Phrase Classes ################################################################################ package Kwiki::Formatter::Strong; use base 'Spoon::Formatter::Phrase'; use Kwiki ':char_classes'; const formatter_id => 'strong'; const pattern_start => qr/(^|(?<=[^$ALPHANUM]))\*(?=\S)/; const pattern_end => qr/\*(?=[^$ALPHANUM]|\z)/; const html_start => ""; const html_end => ""; ################################################################################ package Kwiki::Formatter::Emphasize; use base 'Spoon::Formatter::Phrase'; use Kwiki ':char_classes'; 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 Kwiki::Formatter::Underline; use base 'Spoon::Formatter::Phrase'; use Kwiki ':char_classes'; const formatter_id => 'u'; const pattern_start => qr/(^|(?<=[^$ALPHANUM]))_(?=\S)/; const pattern_end => qr/_(?=[^$ALPHANUM]|\z)/; const html_start => ""; const html_end => ""; ################################################################################ package Kwiki::Formatter::Inline; use base 'Spoon::Formatter::Unit'; use Kwiki ':char_classes'; const formatter_id => 'tt'; const pattern_start => qr/(^|(?<=[^$ALPHANUM]))\[\=/; const pattern_end => qr/\](?=[^$ALPHANUM]|\z)/; const html_start => ""; const html_end => ""; ################################################################################ package Kwiki::Formatter::Delete; use base 'Spoon::Formatter::Phrase'; use Kwiki ':char_classes'; const formatter_id => 'del'; const pattern_start => qr/(^|(?<=[^$ALPHANUM]))-(?=[^\-\s])/; const pattern_end => qr/-(?=[^$ALPHANUM]|\z)/; const html_start => ''; const html_end => ''; ################################################################################ # Empty Phrases (search & replace) ################################################################################ package Kwiki::Formatter::MDash; use base 'Spoon::Formatter::Unit'; const formatter_id => 'mdash'; const pattern_start => qr/\-{3}(?=[^-])/; const html => '—'; ################################################################################ package Kwiki::Formatter::NDash; use base 'Spoon::Formatter::Unit'; const formatter_id => 'ndash'; const pattern_start => qr/\-{2}(?=[^-])/; const html => '–'; ################################################################################ # Much Ado about Linking ################################################################################ package Kwiki::Formatter::ForcedLink; use base 'Spoon::Formatter::Unit'; use Kwiki ':char_classes'; const formatter_id => 'forced'; const pattern_start => qr/\[([$WORD]+)\]/; sub html { $self->matched =~ $self->pattern_start; my $target = $1; my $script = $self->hub->config->script_name; my $text = $self->escape_html( $target ); my $page = $self->hub->pages->new_from_name($target); return $target unless $page; my $class = $page->exists ? '' : ' class="empty"'; return qq($target); } ################################################################################ package Kwiki::Formatter::HyperLink; use base 'Spoon::Formatter::Unit'; const formatter_id => 'hyper'; our $pattern = qr{\w+:(?://|\?)\S+?(?=[),.:;]?\s|$)}; const pattern_start => qr/$pattern|!$pattern/; sub html { my $text = $self->escape_html($self->matched); return $text if $text =~ s/^!//; return qq() if $text =~ /(?:jpe?g|gif|png)$/i; return qq($text); } ################################################################################ package Kwiki::Formatter::TitledHyperLink; use base 'Spoon::Formatter::Unit'; const formatter_id => 'titlehyper'; const pattern_start => qr{\[(?:\s*([^\]]+)\s+)?(\w+:(?://|\?)[^\]\s]+)(?:\s+([^\]]+)\s*)?\]}; sub html { my $text = $self->escape_html($self->matched); my ($title1, $target, $title2) = ($text =~ $self->pattern_start); $title1 = '' unless defined $title1; $title2 = '' unless defined $title2; $target =~ s{^\w+:(?!//)}{}; my $title = $title1 . ' ' . $title2; $title =~ s/^\s*(.*?)\s*$/$1/; $title = $target unless $title =~ /\S/; return qq($title); } ################################################################################ package Kwiki::Formatter::WikiLink; use base 'Spoon::Formatter::Unit'; use Kwiki ':char_classes'; const formatter_id => 'wiki'; our $pattern = qr/[$UPPER](?=[$WORD]*[$UPPER])(?=[$WORD]*[$LOWER])[$WORD]+/; const pattern_start => qr/$pattern|!$pattern/; sub html { my $page_name = $self->escape_html($self->matched); return $page_name if $page_name =~ s/^!//; my $page = $self->hub->pages->new_from_name($page_name); return $page_name unless $page; return $page->kwiki_link; } ################################################################################ package Kwiki::Formatter::TitledWikiLink; use base 'Spoon::Formatter::Unit'; use Kwiki ':char_classes'; const formatter_id => 'titlewiki'; const pattern_start => qr/\[([^\]]*)\s+([$UPPER](?=[$WORD]*[$UPPER])(?=[$WORD]*[$LOWER])[$WORD]+)\]/; sub html { my $text = $self->escape_html($self->matched); my ($label, $page_name) = ($text =~ $self->pattern_start); my $page = $self->hub->pages->new_from_name($page_name); return $label unless $page; return $page->kwiki_link($label); } ################################################################################ package Kwiki::Formatter::MailLink; use base 'Spoon::Formatter::Unit'; use Kwiki ':char_classes'; const formatter_id => 'mailto'; our $pattern = qr/[$ALPHANUM][$WORD\+\-\.]*@[$WORD][$WORD\-\.]+/; const pattern_start => qr/$pattern|!$pattern/; sub html { my $text = $self->escape_html( $self->matched ); return $text if $text =~ s/^!//; my $dot = ($text =~ s/(\.+)$//) ? $1 : ''; return qq($text$dot); } ################################################################################ package Kwiki::Formatter::TitledMailLink; use base 'Spoon::Formatter::Unit'; use Kwiki ':char_classes'; const formatter_id => 'titlemailto'; const pattern_start => qr/\[([^\]]+)\s+([$ALPHANUM][$WORD\+\-\.]*@[$WORD][$WORD\-\.]+)\]/; sub html { my $text = $self->escape_html($self->matched); my ($title, $addr) = ($text =~ $self->pattern_start); my $dot = ($addr =~ s/(\.+)$//) ? $1 : ''; return qq($title$dot); } ################################################################################ package Kwiki::Formatter::Asis; use base 'Spoon::Formatter::Unit'; const formatter_id => 'asis'; const pattern_start => qr/\{\{/; const pattern_end => qr/\}\}/; package Kwiki::Formatter; __DATA__ =head1 NAME Kwiki::Formatter - Kwiki Formatter Base Class =head1 SYNOPSIS =head1 DESCRIPTION =head1 AUTHOR Brian Ingerson =head1 COPYRIGHT Copyright (c) 2004. Brian Ingerson. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See http://www.perl.com/perl/misc/Artistic.html =cut __css/formatter.css__ pre.formatter_pre { font-family: monospace; background-color: #eee; padding: 2px; padding-left: 10px; margin-left: 20px; margin-right: 20px; } table.formatter_table { border-collapse: collapse; margin-bottom: .2em; } table.formatter_table td { border: 1px; border-style: solid; padding: .2em; vertical-align: top; } span.wafl_error { color: #f00; text-decoration: underline;