root/veekun/trunk/lib/Vee/BBCode.pm @ 25

Revision 1, 8.4 KB (checked in by eevee, 3 years ago)

Initial import and cleanup. Older commits are available upon request.

Line 
1package Vee::BBCode;
2
3use strict;
4use warnings;
5
6=head1 NAME
7
8Vee::BBCode - Two-step bbcode parser
9
10=head1 SYNOPSIS
11
12    use Vee::BBCode;
13    Vee::BBCode::validate_bbcode('I [i]really[/i] think you should [b]stop[/b].');       # success
14    Vee::BBCode::validate_bbcode('Nested [b]tags [i]are not[/b] that[/i] complicated');  # failure
15   
16    Vee::BBCode::apply_bbcode('[i]Always[/i] [code]use strict;[/code]!');  # HTMLized
17
18=head1 DESCRIPTION
19
20Quick authorization module, to check whether the currently logged-in user has
21been given a particular permission.
22
23=head1 METHODS
24
25=cut
26
27# keys are the tag names
28# start/end subs take one parameter: the tag's parameter
29# replace subs take two parameters: the tag's contents and its parameter
30# TODO:
31#  - docs?
32# BUGS:
33#  - replace won't work with no_close
34our %tags = (
35    b       => { param => 0, start => '<strong class="bbcode_b">', end => '</strong>' },
36    i       => { param => 0, start => '<em class="bbcode_i">', end => '</em>' },
37    u       => { param => 0, start => '<span class="bbcode_u">', end => '</span>' },
38    s       => { param => 0, start => '<span class="bbcode_s">', end => '</span>' },
39    color   => { param => 2, strip_quotes => 1, start => sub {
40        my $color = shift;
41        if ($color !~ /
42            \#[0-9a-f]{3,6}
43            | (?:rgb|hsl) \( \s* \d{,3}% \s* , \s* \d{,3}% \s* , \s* \d{,3}% \s* \)
44            | (?:rgb|hsl) \( \s* \d{,3}  \s* , \s* \d{,3}  \s* , \s* \d{,3}  \s* \)
45            | (?:rgb|hsl)a \( \s* -?\d+  \s* , \s* \d{,3}% \s* , \s* \d{,3}% \s* , \s* \d(\.\d+)? \s* \)
46            | [a-z]+[0-9]*
47         /ix) { $color = 'inherit' }
48        qq'<span class="bbcode_color" style="color: $color;">'
49    }, end => '</span>' },
50#    size    => { param => 2, },
51    url     => { param => 1, strip_quotes => 1, replace => sub { qq'<a href="@{[ $_[0] ? $_[0] : $_[1] ]}" class="bbcode_url">$_[1]</a>' }  },
52    img     => { param => 1, strip_quotes => 1, raw => 1, start => sub { $_[0] ? qq'<img src="$_[0]" alt="' : '<img alt="" src="' }, end => '" class="bbcode_img"/>' },
53    code    => { param => 1, raw => 1, start => sub { '<span class="bbcode_code_header">' . ($_[0] ? "Code for $_[0]" : 'Code') . ':</span><span class="bbcode_code">' }, end => '</span>' },
54    mono    => { param => 1, start => '<span class="bbcode_prop">', end => '</span>' },
55    raw     => { param => 1, raw => 1, start => '', end => '' },
56    hr      => { param => 0, no_close => 1, start => '<hr class="bbcode_hr"/>' },
57#    list    => { param => 1, contents => { '*' => 1 }, },
58#    '*'     => { param => 0, container => { list => 1 }, self_closing => 1, },
59);
60
61# change start, end, and replaces to subroutines for ease of runnage
62for my $tag_data (values %tags) {
63    for my $replacement (qw/start end replace/) {
64        my $format = $tag_data->{ $replacement };
65        next unless defined $format;
66        next if ref $format;
67        $tag_data->{ $replacement } = sub { sprintf $format, @_ };
68    }
69}
70
71=head2 apply_bbcode
72
73Applies bbcode formatting to a text string.  This makes no attempt to verify
74the well-formed-ness of the input.
75
76=cut
77
78sub apply_bbcode {
79    my $input = shift;
80
81    for my $tag (keys %tags) {
82        if (exists $tags{$tag}{replace}) {
83            while ( $input =~ /\[$tag[]=]/ ) {
84                $input =~ s: (.*) \[ $tag \] (.+?) \[ / $tag \] : $1 . $tags{$tag}{replace}->(undef, $2) :gexi;
85                $input =~ s: (.*) \[ $tag = (.+?) \] (.+?) \[ / $tag \] : $1 . $tags{$tag}{replace}->($2, $3) :gexi;
86            }
87        } else {
88            $input =~ s: \[ $tag \] : $tags{$tag}{start}->() :gexi;
89            $input =~ s: \[ $tag = (.+?) \] : $tags{$tag}{start}->($1) :gexi;
90            $input =~ s: \[ / $tag \] : $tags{$tag}{end}->() :gexi;
91        }
92    }
93   
94    $input =~ s:\x0D\x0A:<br/>:g;
95    $input =~ s:\x0D|\x0A:<br/>:g;
96   
97    return $input;
98}
99
100=head2 apply_bbcode
101
102Attempts to gauge whether a provided string of bbcode is valid.  Returns the
103string, possibly modified (I forget), and a list of errors if applicable.
104
105This method is in transition at the moment, sorry.
106
107=cut
108
109sub validate_bbcode {
110    my $input = shift;
111
112    $input =~ s/&/&amp;/g;
113    $input =~ s/&amp;(#x?[0-9a-f]{1,10});/&$1;/gi;
114    $input =~ s/\"/&quot;/g;
115    $input =~ s/</&lt;/g;
116    $input =~ s/>/&gt;/g;
117
118    my @tag_stack;
119#    my @closed_tag_stack;
120
121    my $lb_entity = '&#91;';
122    my $rb_entity = '&#93;';
123    my @output = ('');
124    my @errors;
125    my @replace_params;
126    my $pos = 0;
127    my ($left_bracket, $right_bracket);
128    PARSE: while ($input) {
129        $left_bracket  = index $input, '[';                      last if $left_bracket  == -1;
130        $right_bracket = index $input, ']', $left_bracket + 1;   last if $right_bracket == -1;
131        $left_bracket = rindex $input, '[', $right_bracket;
132        my $tag = substr $input, $left_bracket + 1, $right_bracket - $left_bracket - 1;
133       
134        # tack any plaintext before the next tag onto the output
135        my $raw = substr $input, 0, $left_bracket;
136        $raw =~ s/ \[ /$lb_entity/gx;
137        $raw =~ s/ \] /$rb_entity/gx;
138        $output[-1] .= $raw;
139        # then remove the preceding plaintext and the tag from the input
140        substr($input, 0, $right_bracket + 1) = '';
141
142        $left_bracket = 0;
143        $right_bracket -= $left_bracket;
144
145        my $closing = 0;
146        if ('/' eq substr $tag, 0, 1) {
147            $closing = 1;
148            $tag = substr $tag, 1;
149        }
150
151        my ($tag_name, $param) = split /=/, $tag, 2;
152
153        # check: skip the tag if it's not one of ours
154        if (!exists $tags{$tag_name}) {
155            $output[-1] .= $lb_entity . ($closing ? '/' : '') . $tag . $rb_entity;
156            next;
157        }
158
159        if ($param and $tags{$tag_name}{strip_quotes}) {
160            $param =~ s/^&quot;|&quot;$//g;
161        }
162
163        # check: can't have a param with a closing tag
164        if ($param && $closing) {
165            push @errors, [ "Parameter given on closing tag '$tag_name'", $left_bracket + 1 + length $tag ];
166            substr($input, $left_bracket + length($tag) + 2, length($param) + 1) = '';
167            $param = '';
168        }
169
170        # check if parameter should be here
171        if (!$closing and $param and $tags{$tag_name}{param} == 0) {
172            push @errors, [ "Parameter given for parameter-less tag '$tag_name'", $left_bracket + 1 + length $tag ];
173            $param = undef;
174        }
175        if (!$closing and!$param and $tags{$tag_name}{param} == 2) {
176            push @errors, [ "Parameter required for tag '$tag_name'", $left_bracket + 1 + length $tag ];
177            $param = '';
178        }
179
180        if ($tags{$tag_name}{no_close}) {
181            # don't do anything
182        } elsif (!$closing) {
183            push @tag_stack, $tag_name;
184        } else {
185            # check tag closing order
186            my $next_tag;
187            while ($next_tag = pop @tag_stack) {
188                last if $next_tag eq $tag_name;
189
190                push @errors, [ "Tag '$tag_name' not closed before parent tag '$next_tag'", $left_bracket ];
191                $output[-1] .= qq'<span class="bbcode_error_wrongorder">[/$tag_name]</span>';
192            }
193
194            if (!defined $next_tag) {
195                # we have run out of initial closing tags!  replace this with something temporary for now
196                push @errors, [ "Tag '$tag_name' closed but not open", $left_bracket ];
197                $output[-1] .= qq'<span class="bbcode_error_notopen">[/$tag_name]</span>';
198                next PARSE;
199            }
200        }
201
202        $output[-1] .= '[';
203        $output[-1] .= '/' if $closing;
204        $output[-1] .= $tag;
205        $output[-1] .= ']';
206
207        # if the tag is marked as 'raw', then its entire contents should be moved directly to output
208        if ($tags{$tag_name}{raw} and !$closing) {
209            my $raw_end = index $input, "[/$tag_name]";
210            my $raw = substr $input, 0, $raw_end;
211            $raw =~ s/ \[ /$lb_entity/gx;
212            $raw =~ s/ \] /$rb_entity/gx;
213            $output[-1] .= $raw;
214            $input = substr $input, $raw_end;
215        }
216    }
217   
218    for (@tag_stack) {
219        push @errors, [ "Tag '$_' never closed" ];
220    }
221
222    if ($input) {
223        $input =~ s/ \[ /$lb_entity/gx;
224        $input =~ s/ \] /$rb_entity/gx;
225        $output[-1] .= $input;
226    }
227   
228    return $output[-1], @errors;
229}
230
231=head1 AUTHOR
232
233Maintainer: Alex "Eevee" Munroe (C<veekun@veekun.com>)
234
235See the included F<AUTHORS> file for a full list of contributers.
236
237=head1 LICENSE
238
239See the included F<LICENSE> file.
240
241=cut
242
2431;
Note: See TracBrowser for help on using the browser.