File Coverage

blib/lib/Text/Chump.pm
Criterion Covered Total %
statement 156 164 95.1
branch 55 62 88.7
condition 32 48 66.6
subroutine 25 26 96.1
pod 4 4 100.0
total 272 304 89.4


line stmt bran cond sub pod time code
1             package Text::Chump;
2              
3 8     8   232023 use strict;
  8         23  
  8         355  
4 8     8   50 use vars qw($VERSION);
  8         16  
  8         462  
5 8     8   7665 use Text::DelimMatch;
  8         57381  
  8         510  
6 8     8   7581 use Tie::IxHash;
  8         55114  
  8         378  
7 8     8   9612 use URI::Find;
  8         108374  
  8         717  
8 8     8   90 use Carp;
  8         18  
  8         24485  
9              
10             $VERSION = "1.02";
11              
12              
13              
14             =pod
15              
16             =head1 NAME
17              
18             Text::Chump - a module for parsing Chump like syntax
19              
20              
21             =head1 SYNOPSIS
22              
23             use Text::Chump;
24              
25             my $tc = Text::Chump->new();
26              
27             $tc->chump('[all mine!|http://thegestalt.org]');
28             # returns all mine!
29              
30             $tc->chump('+[all mine!|http://thegestalt.org]');
31             # returns all mine!
32              
33             $tc->chump('http://thegestalt.org');
34             # returns http;//thegestalt.org
35              
36             my $tc = Text::Chump->new({images=>0});
37              
38             $tc->chump('+[all mine!|http://thegestalt.org]');
39             # returns '+[all mine!|http://thegestalt.org]'
40              
41              
42             sub foo {
43             my ($url, $label) = @_;
44              
45             return "$label ($url)";
46             }
47              
48             $tc->install('link',\&foo);
49             $tc->chump('[foo|http://bar.com]');
50             # returns 'foo (http://bar.com)'
51              
52             sub quirka {
53             my ($opts, $match, $label) = @_;
54              
55             return "$label";
56              
57             }
58              
59              
60              
61             $tc->install('link',\$quirka,'\d+');
62             $tc->chump('[stuff|4444]');
63             # returns "stuff"
64              
65              
66              
67             =head1 DESCRIPTION
68              
69             Chump is an IRC bot that allows people to post links and comments
70             onto a website from within an IRC Channel. Some people call this a blog
71             but I hate that term. Hate it. *HATE IT*! ... *cough* ... so I'll avoid
72             it from now on.
73              
74             The Chump is based on an original idea by Bijan Parsia. Bijan wrote a bot in Squeak
75             called DiaWebLogBot, which powers the Monkeyfist Daily Churn and subsequently
76             Useful Inc. "stole all his good ideas". Therefore The Chump syntax is derived
77             and extended from diaweblogbot.
78              
79             The bot is available from B and the original page
80             that uses this form of markup is B.
81              
82             The items which are displayed on the page can have a special format. These,
83             in turn get marked up as HTML (by default). Essentially this provides a simple
84             markup language. Yes - they could have used XML and been fully buzzword compliant
85             (it uses XML in the backend if that's any help) but they didn't.
86              
87             Since then the syntax has been appropriated by a number of projects including one
88             of my own, so, like the good little code that I am, it all went in a module.
89              
90             Which I may as well release because somebody else wants to release a module which
91             depends on it and it might be useful to someone else.
92              
93             Alternatives to this module include B and B
94             although they do subtly different things. In fact you could probably chain them
95             together - especially B with uri set to 0.
96              
97             =head1 SYNTAX
98              
99             As described here
100              
101             B
102              
103              
104              
105             =over 4
106              
107             =item * Links :
108              
109             [|url]
110              
111             This creates an inline link (i.e. turning a word into a link). So, for example
112              
113             They also have [another site|http://foobar.com]
114              
115             will make the words "another site" appear as a hyperlink to
116             the URL http://foobar.com.
117              
118             =item * Images :
119              
120             +[http://url.of.image.com/image.jpg]
121              
122             This creates an inline image in some text. By providing some text you can provide
123             an alt tag which is considered a good thing to do.
124              
125             +[This is the alt text|http://url.of.image.com/image.jpg]
126              
127             By providing a url in the middle
128              
129             +[This is the alt text|http://foobar.com|http://url.of.image.com/image.jpg]
130              
131             You can turn the image into a clickable link.
132              
133              
134             =item * Urls :
135              
136             http://foobar.com
137              
138             this will be turned into a clicable link.
139              
140             =back
141              
142              
143             =head1 METHODS
144              
145             =head2 new
146              
147             Can take an hashref of options (target defaults to nothing, border defaults to 0,
148             everything else defaults to 1 == yes)
149              
150             =over 4
151              
152             =item * target :
153              
154             A default target for a URL (such as _blank)
155              
156             =item * border :
157              
158             Whether inline images should have a border
159              
160             =item * images :
161              
162             Whether to process image markup
163              
164             =item * links :
165              
166             Whether to process link markup
167              
168             =item * urls :
169              
170             Whether to process urls
171              
172             =back
173              
174              
175             =cut
176              
177              
178             # standard set up stuff
179             sub new {
180 8     8 1 114 my $class = shift;
181 8   100     38 my $self = shift || {};
182              
183 8         25 $self->{plugins} = {};
184 8         53 $self->{types} = {
185             'link' => 'link',
186             '+' => 'image',
187             'url' => 'url',
188             };
189              
190              
191 8         20 bless $self, $class;
192              
193             # we'll be macthing between '[' and ']'
194 8         38 $self->{_mc} = $self->_make_matcher();
195              
196             # Default handlers.
197 8     10   1627 $self->install('link', sub { $self->_chump_link(@_) } );
  10         39  
198 8     22   40 $self->install('image', sub { $self->_chump_image(@_) } );
  22         58  
199 8     85   43 $self->install('url', sub { $self->_chump_url(@_) } );
  85         205  
200              
201 8         50 return $self;
202              
203             }
204              
205              
206             =pod
207              
208             =head2 new_type [name] [char] [coderef]
209              
210             Installs a new type so that if the parser comes across
211              
212             $char[stuff|nonsense]
213              
214             then the parts will be passed to the coderef in the
215             normal way. If you pass in a regexp then that will be
216             used to determine the match, just like if you install a
217             new handler.
218              
219             In order to turn off handling of the new type pass in
220              
221             $opt->{"${name}s"} = 0;
222              
223             as the options to I. So
224              
225             my $text = 'foo bar %[foo|http://quux.com]';
226              
227             $mc->new_type('percent','%', sub { return $_[1] });
228             $mc->chump($text);
229            
230             returns
231              
232             'foo bar http://quux.com'
233              
234             but
235              
236             my $text = 'foo bar %[foo|http://quux.com]';
237              
238             $mc->new_type('percent','%', sub { return $_[1] }, 'foo');
239             $mc->chump($text);
240              
241             returns
242              
243             'foo bar foo'
244              
245             but
246              
247             my $text = 'foo bar %[foo|http://quux.com]';
248              
249             $mc->new_type('percent','%', sub { return $_[1] }, 'foo');
250             $mc->chump($text, { 'percents' => 0 });
251              
252             returns
253              
254             'foo bar %[foo|http://quux.com]'
255              
256              
257             So that's all clear then :)
258              
259              
260             =cut
261              
262              
263             sub new_type {
264 0     0 1 0 my ($self, $name, $char, $code, $regexp) = @_;
265              
266              
267 0         0 $self->{types}->{$char} = $name;
268 0         0 $self->{_mc} = $self->_make_matcher();
269 0         0 $self->{"${name}s"} = 1;
270 0         0 $self->install($name, $code, $regexp);
271              
272             }
273              
274              
275             sub _make_matcher {
276 8     8   19 my ($self) = @_;
277              
278 8         18 my $regexp = "";
279 8         17 foreach my $key (keys %{$self->{types}}) {
  8         61  
280 24 100       68 next if length $key != 1;
281 8 50       53 next if $key =~ m!^[a-z\d]$!m;
282 8         24 $regexp .= '\\'.$key;
283             }
284              
285              
286 8         84 return Text::DelimMatch->new("[$regexp]{0,1}\\[","\\]");
287              
288              
289             }
290              
291              
292             =pod
293              
294             =head2 chump [text]
295              
296             Takes some text to munge and returns it, fully chumped. Can optionally take
297             a hashref with the same options as I except that these options will only
298             apply to this bit of text.
299              
300             =cut
301              
302             # the real work
303             sub chump {
304             # get the text, remembering that we may not actually be passed anything
305 53     53 1 21277 my $self = shift;
306 53   50     166 my $text = shift || "";
307 53   100     184 my $opts = shift || {};
308              
309              
310              
311             # set up options
312 53 100       162 my $border = (defined $self->{border})? $self->{border} : 0;
313 53 100       203 $opts->{border} = $border unless defined $opts->{border};
314 53 100       342 $opts->{border} = "border='$opts->{border}'" unless $opts->{border} =~ /border/i;
315             # (urgh)
316              
317              
318              
319 53         83 foreach my $val (values %{$self->{types}})
  53         169  
320             {
321 159 100       373 my $tmp = (defined $self->{"${val}s"})? $self->{"${val}s"} : 1;
322 159 100       497 $opts->{"${val}s"} = $tmp unless defined $opts->{"${val}s"};
323             }
324              
325              
326              
327             # curse the tedious URI::Find interface
328             $self->{_finder} = URI::Find->new(
329             sub {
330 53     53   219097 my($uri, $orig_uri) = @_;
331 53         205 return $self->_make_link($uri,$orig_uri,$opts);
332             },
333 53         467 );
334              
335              
336              
337             # get all our tokens
338 53         1188 my @tokens = $self->_get_tokens($text);
339              
340              
341              
342             # pre declare
343 53         72 my $return;
344              
345             # for each token we've got, decide ...
346 53         87 TOKEN: foreach my $token (@tokens) {
347              
348 151         362 my $orig = $token;
349              
350             # is it a bracket match? and if so is it an image ...
351 151 100       547 if ($token =~ s/^([^\[]{0,1})\[(.*)\]$/$2/) {
352              
353 49   100     1342 my $type = $1 || 'link';
354              
355 49         161 my $typename = $self->{types}->{$type};
356            
357 49 100 66     319 unless (defined $opts->{"${typename}s"} && $opts->{"${typename}s"}) {
358 4         8 $return .= $orig;
359 4         11 next TOKEN;
360             }
361              
362            
363 45         160 my @parts = split /\|/, $token, 3;
364            
365             # check to see if there's a user defined regexp
366 45 100       128 if (my $tmp = $self->_do_regexp_plugins($typename, $opts,@parts)) {
367 4         6 $return .= $tmp;
368 4         14 next TOKEN;
369             }
370              
371             # stick it back on
372             # $return .= $type unless (defined $typename);
373              
374             # if not then work out which one is the image url,
375             # the label and the optional link url
376 41     62   191 my ($url, $label, $link) = $self->_order_params(sub { $self->_is_url($_[0]) }, @parts);
  62         1364  
377              
378             # check to see if there's a user defined regexp
379 41 100       178 if (my $tmp = $self->_do_normal_plugins($typename, $opts, $url, $label, $link)) {
380 36         54 $return .= $tmp;
381 36         127 next TOKEN;
382             }
383              
384             # otherwise return the original
385 5         19 $return .= $orig;
386              
387             # otherwise it's plain text
388             } else {
389             # check to see if there's a user defined regexp
390 102 50       271 if (my $tmp = $self->_do_regexp_plugins('url', $opts, $orig)) {
391 0         0 $return .= $tmp;
392 0         0 next TOKEN;
393             }
394            
395             # check to see if there's a user defined regexp
396 102 100       297 if (my $tmp = $self->_do_normal_plugins('url', $opts, $orig)) {
397 18         34 $return .= $tmp;
398 18         65 next TOKEN;
399             }
400            
401 84         213 $return .= $orig;
402              
403             }
404             }
405            
406             # return the whole caboodle
407 53         277 return $return;
408             }
409              
410             =head2 install [type] [coderef]
411              
412             if you pass in either 'image', 'link' or 'url' and a valid coderef
413             then that code ref will be called on the original sting instead of the
414             default behaviour.
415              
416             This is useful for outputting something other than HTML.
417              
418             And, in a special, one time only offer, if optionally you pass in
419             a regexp then you can add your own handlers. So, for example, if you
420             did :
421              
422             $tc->install('link', sub { return 'foo' }, '\d{4}');
423             print $tc->chump('[test|1234]'); # prints "foo"
424              
425             However you regexps are checked in reverse order they're put in so if
426             you then do :
427              
428             $tc->install('link', sub { return 'bar' }, '\d{5}');
429              
430             then :
431              
432             print $tc->chump('[test|1234]'); # prints "foo"
433             print $tc->chump('[test|12345]'); # prints "bar"
434              
435              
436             Note: all regexps are assumed to be case insensitive.
437              
438             If you want to monkey around with the ordering post install then the IxHash
439             object that they're installed in can be found in
440              
441             $tc->{plugins}->{[name]}->{regexp}
442              
443              
444              
445             For a link or and image the values passed to the coderef are a hashref of
446             options then the match then the label and then optionally a middle value.
447              
448             If no label is passed then it will be set to the same value as the link.
449              
450             So for these
451              
452             [foo|bar|http://thegestalt.org]
453             [http://thegestaltorg|bar|foo]
454              
455             a sub will be passed
456              
457             my ($opt, $link, $label, $middle) = @_;
458            
459             # $opt = hashref of options
460             # $link = http://thegestalt.org
461             # $label = foo
462             # $middle = bar
463              
464              
465             and for
466              
467             [http://thegestalt.org]
468              
469             you'll get
470              
471             # $opt = hashref of options
472             # $link = http://thegestalt.org
473             # $label = http://thegestalt.org
474             # $middle = undef
475              
476              
477              
478             For a url you'll only get passed an opt and the original string.
479              
480              
481              
482             =cut
483              
484             sub install {
485              
486 32   33 32 1 1465 my $self = shift || carp "Must be called in an OO manner\n";
487 32   33     73 my $name = shift || carp "Must pass a name\n";
488 32   33     63 my $code = shift || carp "Must pass a coderef\n";
489 32         36 my $regexp = shift;
490              
491              
492 32 100       70 if (defined $regexp) {
493 5 100       32 $self->{plugins}->{$name}->{regexp} = Tie::IxHash->new()
494             unless defined $self->{plugins}->{$name}->{regexp};
495              
496 5         58 $self->{plugins}->{$name}->{regexp}->Unshift($regexp => $code);
497             } else {
498 27         97 $self->{plugins}->{$name}->{default} = $code;
499             }
500             }
501              
502              
503              
504              
505             sub _get_tokens
506             {
507 53     53   82 my $self = shift;
508 53   50     132 my $text = shift || "";
509              
510              
511             # we'll be matching stuff between '[' and ']'
512 53         86 my $mc = $self->{_mc};
513              
514             # pre declare
515 53         66 my @tokens;
516              
517             # loop through all the matches
518             # Why isn't this a standard method in Text::DelimMatch?
519             # And if it is then why is it badly documented?
520 53         216 while (my $match = $mc->match($text))
521             {
522             # if we've got anything from before the match then whack it in
523 49   100     7684 my $pre = $mc->pre_matched() || "";
524 49         765 push @tokens, $pre;
525              
526             # push the match in
527 49         62 push @tokens, $match;
528              
529             # and reset $text so that we don't loop infinitely
530 49   100     146 $text = $mc->post_matched() || "";
531             }
532             # push anything left onto the tokens. This also catches the case
533             # of there being no matches
534 53         5844 push @tokens, $text;
535              
536 53         211 return @tokens;
537              
538             }
539              
540              
541              
542             =pod
543              
544             =head2 _order_params [function] [@params]
545              
546             Given a function and an array of params it will return the first
547             parameter that matches the function.
548              
549             The order that it checks in is last element of the array and then
550             the first element.
551              
552             Why this weird order? Because it's more natural to write
553              
554             [foo|http://bar.com]
555              
556             or, at least, that seems to be the behaviour I've observed.
557              
558             A typical function would look like this
559              
560              
561             sub {
562             return $_[0] =~ /\d+/;
563             }
564              
565              
566             =cut
567              
568             sub _order_params
569             {
570 73     73   155 my ($self, $function,@parts) = @_;
571            
572 73 50       153 return unless @parts;
573            
574 73         279 my $one = shift @parts;
575 73         96 my $two = pop @parts;
576            
577              
578 73         78 my ($first, $second);
579              
580 73 100       135 if ($function->($one)) {
    100          
581 21         635 $first = $one;
582 21         31 $second = $two;
583             } elsif ($function->($two)) {
584 19         376 $first = $two;
585 19         34 $second = $one;
586             } else {
587 33         295 return undef;
588             }
589              
590 40         155 return ($first, $second, @parts);
591             }
592              
593              
594             sub _do_regexp_plugins
595             {
596 147     147   305 my ($self, $type, $opts, @parts) = @_;
597              
598 147 100       1067 return undef unless defined $self->{plugins}->{$type}->{regexp};
599              
600 25         130 foreach my $re ($self->{plugins}->{$type}->{regexp}->Keys())
601             {
602 32     63   474 my ($a, $b, $c) = $self->_order_params(sub { return $_[0] =~ m!$re!i }, @parts );
  63         534  
603 32 100       341 next unless defined $a;
604 4 50       9 $b = $a unless defined $b;
605            
606            
607 4         6 my $tmp;
608 4         6 eval {
609 4         20 $tmp = $self->{plugins}->{$type}->{regexp}->FETCH($re)->($opts, $a, $b, $c);
610             };
611 4 50       54 unless ($@) {
612 4         18 return $tmp;
613             }
614             }
615            
616 21         244 return undef;
617              
618              
619             }
620              
621              
622              
623             sub _do_normal_plugins {
624 143     143   405 my ($self, $type, $opts, $a, $b, $c) = @_;
625              
626              
627              
628 143 100       282 return undef unless defined $a;
629 138 50       391 return undef unless defined $self->{plugins}->{$type}->{default};
630            
631              
632              
633              
634              
635 138 100       272 $b = $a unless defined $b;
636 138         139 my $tmp;
637 138         266 eval {
638 138         596 $tmp = $self->{plugins}->{$type}->{default}($opts, $a, $b, $c);
639             };
640 138 50       587 unless ($@) {
641 138         587 return $tmp;
642             }
643              
644 0         0 return undef;
645             }
646              
647              
648             =pod
649              
650             =head2 _chump_link [opts] [url] [labe]
651              
652             Just incase you want to call this from your own plugin,
653             this is the default action for links.
654              
655             Calls, I<_make_link> internally.
656              
657             =cut
658            
659             sub _chump_link
660             {
661 10     10   21 my ($self, $opts, $url, $label) = @_;
662             # We don't do a lot here, but I wanted a nice, easy-to-override
663             # function name.
664 10         27 return $self->_make_link($url, $label, $opts);
665             }
666              
667             =pod
668              
669             =head2 _chump_image [opts] [url] [labe]
670              
671             Ditto, but for images.
672              
673             Returns
674              
675             $label{border} />
676              
677             optionally wrapping it in an href to
678              
679             =cut
680              
681             sub _chump_image
682             {
683 22     22   34 my ($self, $opts, $url, $label, $link) = @_;
684              
685            
686 22   50     54 $opts->{border} ||= "";
687 22   50     37 $url ||= "";
688 22   50     35 $label ||= "";
689 22   100     59 $link ||= "";
690              
691 22         57 my $img = "$label{border} />";
692 22 100 100     61 $img = $self->_make_link($link, $img, $opts) if $link and $self->_is_url($link);
693 22         190 return $img;
694             }
695              
696              
697             =pod
698              
699             =head2 _chump_url [opts] [text]
700              
701             Does a call to to I<_make_link> for each URL it finds.
702              
703             =cut
704              
705             sub _chump_url
706             {
707 85     85   124 my ($self, $opts, $text) = @_;
708 85 100 66     598 $self->{_finder}->find(\$text) if ($opts->{urls} && $text !~ /^\+?\[.*\]$/);
709 85         73897 return $text;
710             }
711              
712              
713             =pod
714              
715             =head2 _make_link [link] [label]
716              
717             returns
718            
719             $text
720              
721             =cut
722              
723             # create a link including setting the target
724             sub _make_link
725             {
726 72     72   345 my ($self, $link, $text) = @_;
727              
728 72   50     168 $link ||= "";
729 72   50     1326 $text ||= "";
730              
731 72   50     164 my $opts = $_[3] || {};
732              
733 72 100       171 my $target = (defined $self->{target})? $self->{target} : undef;
734 72 100       164 $target = $opts->{target} if defined $opts->{target};
735              
736 72 100       6915 $target = (defined $target)? " target='$target'" : "";
737              
738 72         226 return "$text";
739              
740             }
741              
742              
743             =pod
744              
745             =head2 _is_url [text]
746              
747             Returns 1 if the text is a url or 0 if it isn't.
748              
749             =cut
750              
751             sub _is_url {
752 74     74   106 my ($self, $url) = @_;
753 74   100     156 $url ||= "";
754              
755 74         124 my $copy = "$url";
756 74         233 return $self->{_finder}->find(\$copy);
757             }
758              
759              
760             1;
761              
762              
763             =pod
764              
765             =head1 BUGS
766              
767             Not that I know of.
768              
769             Oh, wait - maybe it should URL escape any entities in the text but you
770             should probably do that yourself.
771              
772             =head1 COPYING
773              
774             (c)opyright 2002, Simon Wistow
775              
776             Distributed under the same terms as Perl itself.
777              
778             This software is under no warranty and will probably ruin your life, kill your friends, burn your house and bring about the apocalypse
779              
780              
781             =head1 AUTHOR
782              
783             Copyright 2003, Simon Wistow
784              
785             =head1 SEE ALSO
786              
787             B, L,
788             L, L, L,
789             L
790              
791             =cut
792