File Coverage

lib/Kwiki/Formatter.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


\n"; \n"; \n";
line stmt bran cond sub pod time code
1             package Kwiki::Formatter;
2 1     1   2198 use Spoon::Formatter -Base;
  0            
  0            
3             use mixin 'Kwiki::Installer';
4              
5             const config_class => 'Kwiki::Config';
6             const class_id => 'formatter';
7             const class_title => 'Kwiki Formatter';
8             const top_class => 'Kwiki::Formatter::Top';
9             const class_prefix => 'Kwiki::Formatter::';
10             const all_blocks => [qw(comment wafl_block hr heading ul ol pre table p)];
11             const all_phrases => [qw(
12             asis wafl_phrase forced
13             titlehyper titlewiki titlemailto
14             hyper wiki mailto
15             ndash mdash strong em u tt del
16             )];
17             const css_file => 'formatter.css';
18              
19             sub init {
20             $self->hub->css->add_file($self->css_file);
21             }
22              
23             sub formatter_classes {
24             qw(
25             Spoon::Formatter::WaflPhrase
26             Spoon::Formatter::WaflBlock
27             Line Heading Paragraph Preformatted Comment
28             Ulist Olist Item Table TableRow TableCell
29             Strong Emphasize Underline Delete Inline MDash NDash Asis
30             ForcedLink HyperLink TitledHyperLink TitledMailLink MailLink
31             TitledWikiLink WikiLink
32             );
33             }
34              
35             ################################################################################
36             # Blocks
37             ################################################################################
38             package Kwiki::Formatter::Top;
39             use base 'Spoon::Formatter::Container';
40             const formatter_id => 'top';
41              
42             ################################################################################
43             package Kwiki::Formatter::Comment;
44             use base 'Spoon::Formatter::Unit';
45             const formatter_id => 'comment';
46              
47             const html_start => "\n";
49              
50             sub match {
51             return unless $self->text =~ /^((?:#[\ \t].*\n)+)/m;
52             $self->set_match;
53             }
54              
55             sub text_filter {
56             my $comment = shift;
57             $comment =~ s/^# //gm;
58             $comment =~ s/-/-/g;
59             return $comment;
60             }
61              
62             ################################################################################
63             package Kwiki::Formatter::Line;
64             use base 'Spoon::Formatter::Unit';
65             const formatter_id => 'hr';
66             const pattern_block => qr/^----+\s*\n/m;
67             const html => "
\n";
68              
69             ################################################################################
70             package Kwiki::Formatter::Heading;
71             use base 'Spoon::Formatter::Block';
72             const formatter_id => 'heading';
73             field 'level';
74              
75             sub html_start { 'level . '>' }
76             sub html_end { 'level . ">\n" }
77              
78             sub match {
79             return unless $self->text =~ /^(={1,6})\s+(.*?)(\s+=+)?\s*\n+/m;
80             $self->level(length($1));
81             $self->set_match($2);
82             }
83              
84             ################################################################################
85             package Kwiki::Formatter::Paragraph;
86             use base 'Spoon::Formatter::Block';
87             const formatter_id => 'p';
88             const pattern_block =>
89             qr/((?:^(?!(?:[\=\*\0]+ |[\#\|\s]|\.\w+\s*\n|-{4,}\s*\n)).*\S.*\n)+(^\s*\n)*)/m;
90              
91             const html_start => "

\n";

92             const html_end => "

\n";
93              
94             ################################################################################
95             package Kwiki::Formatter::List;
96             use base 'Spoon::Formatter::Container';
97             const contains_blocks => [qw(li)];
98             field 'level';
99             field 'start_level';
100             field 'tag_stack' => [];
101              
102             sub match {
103             my $bullet = $self->bullet;
104             return unless
105             $self->text =~ /((?:^($bullet).*\n)(?:^\2(?!$bullet).*\n)*)/m;
106             $self->set_match;
107             ($bullet = $2) =~ s/\s//g;
108             $self->level(length($bullet));
109             return 1;
110             }
111              
112             sub html_start {
113             my $next = $self->next_unit;
114             my $tag_stack = $self->tag_stack;
115             $next->tag_stack($tag_stack)
116             if ref($next) and $next->isa('Kwiki::Formatter::List');
117             my $level = defined $self->start_level
118             ? $self->start_level : $self->level;
119             push @$tag_stack, ($self->html_end_tag) x $level;
120             return ($self->html_start_tag x $level) . "\n";
121             }
122              
123             sub html_end {
124             my $level = $self->level;
125             my $tag_stack = $self->tag_stack;
126             my $next = $self->next_unit;
127             my $newline = "\n";
128             if (ref($next) and $next->isa('Kwiki::Formatter::List')) {
129             my $next_level = $next->level;
130             if ($level < $next_level) {
131             $next->start_level($next_level - $level);
132             $level = 0;
133             }
134             else {
135             $next->start_level(0);
136             $level = $level - $next_level;
137             $newline = '';
138             }
139             if ($self->level - $level == $next->level and
140             $self->formatter_id ne $next->formatter_id
141             ) {
142             $level++;
143             $next->start_level($next->start_level + 1);
144             }
145             }
146             return join('', reverse splice(@$tag_stack, 0 - $level, $level))
147             . $newline;
148             }
149              
150             ################################################################################
151             package Kwiki::Formatter::Ulist;
152             use base 'Kwiki::Formatter::List';
153             const formatter_id => 'ul';
154             const html_start_tag => '
    ';
155             const html_end_tag => '';
156             const bullet => '\*+\ +';
157              
158             ################################################################################
159             package Kwiki::Formatter::Olist;
160             use base 'Kwiki::Formatter::List';
161             const formatter_id => 'ol';
162             const html_start_tag => '
    ';
163             const html_end_tag => '';
164             const bullet => '0+\ +';
165              
166             ################################################################################
167             package Kwiki::Formatter::Item;
168             use base 'Spoon::Formatter::Block';
169             const formatter_id => 'li';
170             const html_start => "
  • ";
  • 171             const html_end => "\n";
    172             const bullet => '[0\*]+\ +';
    173              
    174             sub match {
    175             my $bullet = $self->bullet;
    176             return unless
    177             $self->text =~ /^$bullet(.*)\n/m;
    178             $self->set_match;
    179             }
    180              
    181             ################################################################################
    182             package Kwiki::Formatter::Preformatted;
    183             use base 'Spoon::Formatter::Unit';
    184             const formatter_id => 'pre';
    185             const html_start => qq{
    }; 
    186             const html_end => "\n";
    187              
    188             sub match {
    189             return unless $self->text =~ /((?:^ +\S.*?\n|^ *\n)+)/m;
    190             my $text = $1;
    191             $self->set_match;
    192             return unless $text =~ /\S/;
    193             return 1;
    194             }
    195              
    196             sub text_filter {
    197             my $text = shift;
    198             $text =~ s/(?<=\n)\s*$//mg;
    199             my $indent;
    200             for ($text =~ /^( +)/gm) {
    201             $indent = length()
    202             if not defined $indent or
    203             length() < $indent;
    204             }
    205             $text =~ s/^ {$indent}//gm;
    206             $text;
    207             }
    208              
    209             ################################################################################
    210             # XXX Support colspan
    211             package Kwiki::Formatter::Table;
    212             use base 'Spoon::Formatter::Container';
    213             const formatter_id => 'table';
    214             const contains_blocks => [qw(tr)];
    215             const pattern_block => qr/((^\|.*?\|\n)+)/sm;
    216             const html_start => qq{\n};
    217             const html_end => "
    \n";
    218              
    219             ################################################################################
    220             package Kwiki::Formatter::TableRow;
    221             use base 'Spoon::Formatter::Container';
    222             const formatter_id => 'tr';
    223             const contains_blocks => [qw(td)];
    224             const pattern_block => qr/(^\|.*?\|\n)/sm;
    225             const html_start => "
    226             const html_end => "
    227              
    228             ################################################################################
    229             package Kwiki::Formatter::TableCell;
    230             use base 'Spoon::Formatter::Unit';
    231             const formatter_id => 'td';
    232             field contains_blocks => [];
    233             field contains_phrases => [];
    234             const table_blocks => [qw(wafl_block hr heading ul ol pre p)];
    235             sub table_phrases { $self->hub->formatter->all_phrases }
    236             const html_start => "";
    237             const html_end => "
    238              
    239             sub match {
    240             return unless $self->text =~ /(\|(\s*.*?\s*)\|)(.*)/sm;
    241             $self->start_offset($-[1]);
    242             $self->end_offset($3 eq "\n" ? $+[3] : $+[2]);
    243             my $text = $2;
    244             $text =~ s/^[ \t]*\n?(.*?)[ \t]*$/$1/;
    245             $self->text($text);
    246             if ($text =~ /\n/) {
    247             $self->contains_blocks($self->table_blocks);
    248             }
    249             else {
    250             $self->contains_phrases($self->table_phrases);
    251             }
    252             return 1;
    253             }
    254              
    255             ################################################################################
    256             # Phrase Classes
    257             ################################################################################
    258             package Kwiki::Formatter::Strong;
    259             use base 'Spoon::Formatter::Phrase';
    260             use Kwiki ':char_classes';
    261             const formatter_id => 'strong';
    262             const pattern_start => qr/(^|(?<=[^$ALPHANUM]))\*(?=\S)/;
    263             const pattern_end => qr/\*(?=[^$ALPHANUM]|\z)/;
    264             const html_start => "";
    265             const html_end => "";
    266              
    267             ################################################################################
    268             package Kwiki::Formatter::Emphasize;
    269             use base 'Spoon::Formatter::Phrase';
    270             use Kwiki ':char_classes';
    271             const formatter_id => 'em';
    272             const pattern_start => qr/(^|(?<=[^$ALPHANUM]))\/(?=\S[^\/]*\/(?=\W|\z))/;
    273             const pattern_end => qr/\/(?=[^$ALPHANUM]|\z)/;
    274             const html_start => "";
    275             const html_end => "";
    276              
    277             ################################################################################
    278             package Kwiki::Formatter::Underline;
    279             use base 'Spoon::Formatter::Phrase';
    280             use Kwiki ':char_classes';
    281             const formatter_id => 'u';
    282             const pattern_start => qr/(^|(?<=[^$ALPHANUM]))_(?=\S)/;
    283             const pattern_end => qr/_(?=[^$ALPHANUM]|\z)/;
    284             const html_start => "";
    285             const html_end => "";
    286              
    287             ################################################################################
    288             package Kwiki::Formatter::Inline;
    289             use base 'Spoon::Formatter::Unit';
    290             use Kwiki ':char_classes';
    291             const formatter_id => 'tt';
    292             const pattern_start => qr/(^|(?<=[^$ALPHANUM]))\[\=/;
    293             const pattern_end => qr/\](?=[^$ALPHANUM]|\z)/;
    294             const html_start => "";
    295             const html_end => "";
    296              
    297             ################################################################################
    298             package Kwiki::Formatter::Delete;
    299             use base 'Spoon::Formatter::Phrase';
    300             use Kwiki ':char_classes';
    301             const formatter_id => 'del';
    302             const pattern_start => qr/(^|(?<=[^$ALPHANUM]))-(?=[^\-\s])/;
    303             const pattern_end => qr/-(?=[^$ALPHANUM]|\z)/;
    304             const html_start => '';
    305             const html_end => '';
    306              
    307             ################################################################################
    308             # Empty Phrases (search & replace)
    309             ################################################################################
    310             package Kwiki::Formatter::MDash;
    311             use base 'Spoon::Formatter::Unit';
    312             const formatter_id => 'mdash';
    313             const pattern_start => qr/\-{3}(?=[^-])/;
    314             const html => '—';
    315              
    316             ################################################################################
    317             package Kwiki::Formatter::NDash;
    318             use base 'Spoon::Formatter::Unit';
    319             const formatter_id => 'ndash';
    320             const pattern_start => qr/\-{2}(?=[^-])/;
    321             const html => '–';
    322              
    323             ################################################################################
    324             # Much Ado about Linking
    325             ################################################################################
    326             package Kwiki::Formatter::ForcedLink;
    327             use base 'Spoon::Formatter::Unit';
    328             use Kwiki ':char_classes';
    329             const formatter_id => 'forced';
    330             const pattern_start => qr/\[([$WORD]+)\]/;
    331              
    332             sub html {
    333             $self->matched =~ $self->pattern_start;
    334             my $target = $1;
    335             my $script = $self->hub->config->script_name;
    336             my $text = $self->escape_html( $target );
    337             my $page = $self->hub->pages->new_from_name($target);
    338             return $target unless $page;
    339             my $class = $page->exists
    340             ? '' : ' class="empty"';
    341             return qq($target);
    342             }
    343              
    344             ################################################################################
    345             package Kwiki::Formatter::HyperLink;
    346             use base 'Spoon::Formatter::Unit';
    347             const formatter_id => 'hyper';
    348             our $pattern = qr{\w+:(?://|\?)\S+?(?=[),.:;]?\s|$)};
    349             const pattern_start => qr/$pattern|!$pattern/;
    350              
    351             sub html {
    352             my $text = $self->escape_html($self->matched);
    353             return $text if $text =~ s/^!//;
    354             return qq()
    355             if $text =~ /(?:jpe?g|gif|png)$/i;
    356             return qq($text);
    357             }
    358              
    359             ################################################################################
    360             package Kwiki::Formatter::TitledHyperLink;
    361             use base 'Spoon::Formatter::Unit';
    362             const formatter_id => 'titlehyper';
    363             const pattern_start =>
    364             qr{\[(?:\s*([^\]]+)\s+)?(\w+:(?://|\?)[^\]\s]+)(?:\s+([^\]]+)\s*)?\]};
    365              
    366             sub html {
    367             my $text = $self->escape_html($self->matched);
    368             my ($title1, $target, $title2) = ($text =~ $self->pattern_start);
    369             $title1 = '' unless defined $title1;
    370             $title2 = '' unless defined $title2;
    371             $target =~ s{^\w+:(?!//)}{};
    372             my $title = $title1 . ' ' . $title2;
    373             $title =~ s/^\s*(.*?)\s*$/$1/;
    374             $title = $target
    375             unless $title =~ /\S/;
    376             return qq($title);
    377             }
    378              
    379             ################################################################################
    380             package Kwiki::Formatter::WikiLink;
    381             use base 'Spoon::Formatter::Unit';
    382             use Kwiki ':char_classes';
    383             const formatter_id => 'wiki';
    384             our $pattern = qr/[$UPPER](?=[$WORD]*[$UPPER])(?=[$WORD]*[$LOWER])[$WORD]+/;
    385             const pattern_start => qr/$pattern|!$pattern/;
    386              
    387             sub html {
    388             my $page_name = $self->escape_html($self->matched);
    389             return $page_name
    390             if $page_name =~ s/^!//;
    391             my $page = $self->hub->pages->new_from_name($page_name);
    392             return $page_name unless $page;
    393             return $page->kwiki_link;
    394             }
    395              
    396             ################################################################################
    397             package Kwiki::Formatter::TitledWikiLink;
    398             use base 'Spoon::Formatter::Unit';
    399             use Kwiki ':char_classes';
    400             const formatter_id => 'titlewiki';
    401             const pattern_start =>
    402             qr/\[([^\]]*)\s+([$UPPER](?=[$WORD]*[$UPPER])(?=[$WORD]*[$LOWER])[$WORD]+)\]/;
    403              
    404             sub html {
    405             my $text = $self->escape_html($self->matched);
    406             my ($label, $page_name) = ($text =~ $self->pattern_start);
    407             my $page = $self->hub->pages->new_from_name($page_name);
    408             return $label unless $page;
    409             return $page->kwiki_link($label);
    410             }
    411              
    412             ################################################################################
    413             package Kwiki::Formatter::MailLink;
    414             use base 'Spoon::Formatter::Unit';
    415             use Kwiki ':char_classes';
    416             const formatter_id => 'mailto';
    417             our $pattern = qr/[$ALPHANUM][$WORD\+\-\.]*@[$WORD][$WORD\-\.]+/;
    418             const pattern_start => qr/$pattern|!$pattern/;
    419              
    420             sub html {
    421             my $text = $self->escape_html( $self->matched );
    422             return $text if $text =~ s/^!//;
    423             my $dot = ($text =~ s/(\.+)$//) ? $1 : '';
    424             return qq($text$dot);
    425             }
    426              
    427             ################################################################################
    428             package Kwiki::Formatter::TitledMailLink;
    429             use base 'Spoon::Formatter::Unit';
    430             use Kwiki ':char_classes';
    431             const formatter_id => 'titlemailto';
    432             const pattern_start =>
    433             qr/\[([^\]]+)\s+([$ALPHANUM][$WORD\+\-\.]*@[$WORD][$WORD\-\.]+)\]/;
    434              
    435             sub html {
    436             my $text = $self->escape_html($self->matched);
    437             my ($title, $addr) = ($text =~ $self->pattern_start);
    438             my $dot = ($addr =~ s/(\.+)$//) ? $1 : '';
    439             return qq($title$dot);
    440             }
    441              
    442             ################################################################################
    443             package Kwiki::Formatter::Asis;
    444             use base 'Spoon::Formatter::Unit';
    445             const formatter_id => 'asis';
    446             const pattern_start => qr/\{\{/;
    447             const pattern_end => qr/\}\}/;
    448              
    449             package Kwiki::Formatter;
    450             __DATA__