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

Revision 111, 8.7 KB (checked in by eevee, 3 years ago)

Fixed: bbcode stylesheet was not being loaded.
Changed: Stylesheet variables are now available to all stylesheets.
Styled forum reply form.

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    h       => { param => 0, start => '<span class="bbcode-h">', end => '</span>' },
40    color   => { param => 2, strip_quotes => 1, start => sub {
41        my $color = shift;
42        if ($color !~ /
43            \#[0-9a-f]{3,6}
44            | (?:rgb|hsl) \( \s* \d{,3}% \s* , \s* \d{,3}% \s* , \s* \d{,3}% \s* \)
45            | (?:rgb|hsl) \( \s* \d{,3}  \s* , \s* \d{,3}  \s* , \s* \d{,3}  \s* \)
46            | (?:rgb|hsl)a \( \s* -?\d+  \s* , \s* \d{,3}% \s* , \s* \d{,3}% \s* , \s* \d(\.\d+)? \s* \)
47            | [a-z]+[0-9]*
48         /ix) { $color = 'inherit' }
49        qq'<span class="bbcode-color" style="color: $color;">'
50    }, end => '</span>' },
51#    size    => { param => 2, },
52    url     => { param => 1, strip_quotes => 1, replace => sub { qq'<a href="@{[ $_[0] ? $_[0] : $_[1] ]}" class="bbcode-url">$_[1]</a>' }  },
53    img     => { param => 1, strip_quotes => 1, raw => 1, start => sub { $_[0] ? qq'<img src="$_[0]" alt="' : '<img alt="" src="' }, end => '" class="bbcode-img"/>' },
54    code    => { param => 1, raw => 1, start => sub { '<div class="bbcode-code-heading">' . ($_[0] ? "Code for $_[0]" : 'Code') . ':</div><div class="bbcode-code">' }, end => '</div>' },
55    quote   => { param => 1, start => sub { '<div class="bbcode-quote-heading">' . ($_[0] ? "Quote from $_[0]" : 'Quote') . ':</div><div class="bbcode-quote">' }, end => '</div>' },
56    mono    => { param => 1, start => '<span class="bbcode-prop">', end => '</span>' },
57    raw     => { param => 1, raw => 1, start => '', end => '' },
58    hr      => { param => 0, no_close => 1, start => '<hr class="bbcode-hr"/>' },
59    center  => { param => 0, no_close => 0, start => '<div class="bbcode-center">', end => '</div>' },
60#    list    => { param => 1, contents => { '*' => 1 }, },
61#    '*'     => { param => 0, container => { list => 1 }, self_closing => 1, },
62);
63
64# change start, end, and replaces to subroutines for ease of runnage
65for my $tag_data (values %tags) {
66    for my $replacement (qw/start end replace/) {
67        my $format = $tag_data->{ $replacement };
68        next unless defined $format;
69        next if ref $format;
70        $tag_data->{ $replacement } = sub { sprintf $format, @_ };
71    }
72}
73
74=head2 apply_bbcode
75
76Applies bbcode formatting to a text string.  This makes no attempt to verify
77the well-formed-ness of the input.
78
79=cut
80
81sub apply_bbcode {
82    my $input = shift;
83
84    for my $tag (keys %tags) {
85        if (exists $tags{$tag}{replace}) {
86            while ( $input =~ /\[$tag[]=]/ ) {
87                $input =~ s: (.*) \[ $tag \] (.+?) \[ / $tag \] : $1 . $tags{$tag}{replace}->(undef, $2) :gexi;
88                $input =~ s: (.*) \[ $tag = (.+?) \] (.+?) \[ / $tag \] : $1 . $tags{$tag}{replace}->($2, $3) :gexi;
89            }
90        } else {
91            $input =~ s: \[ $tag \] : $tags{$tag}{start}->() :gexi;
92            $input =~ s: \[ $tag = (.+?) \] : $tags{$tag}{start}->($1) :gexi;
93            $input =~ s: \[ / $tag \] : $tags{$tag}{end}->() :gexi;
94        }
95    }
96   
97    $input =~ s:\x0D\x0A:<br/>:g;
98    $input =~ s:\x0D|\x0A:<br/>:g;
99   
100    return $input;
101}
102
103=head2 apply_bbcode
104
105Attempts to gauge whether a provided string of bbcode is valid.  Returns the
106string, possibly modified (I forget), and a list of errors if applicable.
107
108This method is in transition at the moment, sorry.
109
110=cut
111
112sub validate_bbcode {
113    my $input = shift;
114
115    $input =~ s/&/&amp;/g;
116    $input =~ s/&amp;(#x?[0-9a-f]{1,10});/&$1;/gi;
117    $input =~ s/\"/&quot;/g;
118    $input =~ s/</&lt;/g;
119    $input =~ s/>/&gt;/g;
120
121    my @tag_stack;
122#    my @closed_tag_stack;
123
124    my $lb_entity = '&#91;';
125    my $rb_entity = '&#93;';
126    my @output = ('');
127    my @errors;
128    my @replace_params;
129    my $pos = 0;
130    my ($left_bracket, $right_bracket);
131    PARSE: while ($input) {
132        $left_bracket  = index $input, '[';                      last if $left_bracket  == -1;
133        $right_bracket = index $input, ']', $left_bracket + 1;   last if $right_bracket == -1;
134        $left_bracket = rindex $input, '[', $right_bracket;
135        my $tag = substr $input, $left_bracket + 1, $right_bracket - $left_bracket - 1;
136       
137        # tack any plaintext before the next tag onto the output
138        my $raw = substr $input, 0, $left_bracket;
139        $raw =~ s/ \[ /$lb_entity/gx;
140        $raw =~ s/ \] /$rb_entity/gx;
141        $output[-1] .= $raw;
142        # then remove the preceding plaintext and the tag from the input
143        substr($input, 0, $right_bracket + 1) = '';
144
145        $left_bracket = 0;
146        $right_bracket -= $left_bracket;
147
148        my $closing = 0;
149        if ('/' eq substr $tag, 0, 1) {
150            $closing = 1;
151            $tag = substr $tag, 1;
152        }
153
154        my ($tag_name, $param) = split /=/, $tag, 2;
155
156        # check: skip the tag if it's not one of ours
157        if (!exists $tags{$tag_name}) {
158            $output[-1] .= $lb_entity . ($closing ? '/' : '') . $tag . $rb_entity;
159            next;
160        }
161
162        if ($param and $tags{$tag_name}{strip_quotes}) {
163            $param =~ s/^&quot;|&quot;$//g;
164        }
165
166        # check: can't have a param with a closing tag
167        if ($param && $closing) {
168            push @errors, [ "Parameter given on closing tag '$tag_name'", $left_bracket + 1 + length $tag ];
169            substr($input, $left_bracket + length($tag) + 2, length($param) + 1) = '';
170            $param = '';
171        }
172
173        # check if parameter should be here
174        if (!$closing and $param and $tags{$tag_name}{param} == 0) {
175            push @errors, [ "Parameter given for parameter-less tag '$tag_name'", $left_bracket + 1 + length $tag ];
176            $param = undef;
177        }
178        if (!$closing and!$param and $tags{$tag_name}{param} == 2) {
179            push @errors, [ "Parameter required for tag '$tag_name'", $left_bracket + 1 + length $tag ];
180            $param = '';
181        }
182
183        if ($tags{$tag_name}{no_close}) {
184            # don't do anything
185        } elsif (!$closing) {
186            push @tag_stack, $tag_name;
187        } else {
188            # check tag closing order
189            my $next_tag;
190            while ($next_tag = pop @tag_stack) {
191                last if $next_tag eq $tag_name;
192
193                push @errors, [ "Tag '$tag_name' not closed before parent tag '$next_tag'", $left_bracket ];
194                $output[-1] .= qq'<span class="bbcode-error-wrongorder">[/$tag_name]</span>';
195            }
196
197            if (!defined $next_tag) {
198                # we have run out of initial closing tags!  replace this with something temporary for now
199                push @errors, [ "Tag '$tag_name' closed but not open", $left_bracket ];
200                $output[-1] .= qq'<span class="bbcode-error-notopen">[/$tag_name]</span>';
201                next PARSE;
202            }
203        }
204
205        $output[-1] .= '[';
206        $output[-1] .= '/' if $closing;
207        $output[-1] .= $tag;
208        $output[-1] .= ']';
209
210        # if the tag is marked as 'raw', then its entire contents should be moved directly to output
211        if ($tags{$tag_name}{raw} and !$closing) {
212            my $raw_end = index $input, "[/$tag_name]";
213            my $raw = substr $input, 0, $raw_end;
214            $raw =~ s/ \[ /$lb_entity/gx;
215            $raw =~ s/ \] /$rb_entity/gx;
216            $output[-1] .= $raw;
217            $input = substr $input, $raw_end;
218        }
219    }
220   
221    for (@tag_stack) {
222        push @errors, [ "Tag '$_' never closed" ];
223    }
224
225    if ($input) {
226        $input =~ s/ \[ /$lb_entity/gx;
227        $input =~ s/ \] /$rb_entity/gx;
228        $output[-1] .= $input;
229    }
230   
231    return $output[-1], @errors;
232}
233
234=head1 AUTHOR
235
236Maintainer: Alex "Eevee" Munroe (C<veekun@veekun.com>)
237
238See the included F<AUTHORS> file for a full list of contributers.
239
240=head1 LICENSE
241
242See the included F<LICENSE> file.
243
244=cut
245
2461;
Note: See TracBrowser for help on using the browser.