| 1 | package Vee::BBCode; |
|---|
| 2 | |
|---|
| 3 | use strict; |
|---|
| 4 | use warnings; |
|---|
| 5 | |
|---|
| 6 | =head1 NAME |
|---|
| 7 | |
|---|
| 8 | Vee::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 | |
|---|
| 20 | Quick authorization module, to check whether the currently logged-in user has |
|---|
| 21 | been 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 |
|---|
| 34 | our %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 |
|---|
| 65 | for 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 | |
|---|
| 76 | Applies bbcode formatting to a text string. This makes no attempt to verify |
|---|
| 77 | the well-formed-ness of the input. |
|---|
| 78 | |
|---|
| 79 | =cut |
|---|
| 80 | |
|---|
| 81 | sub apply_bbcode { |
|---|
| 82 | my $input = shift; |
|---|
| 83 | |
|---|
| 84 | for my $tag (keys %tags) { |
|---|
| 85 | if (exists $tags{$tag}{replace}) { |
|---|
| 86 | while ($input =~ / \[ $tag ( = .+? )? \] /sx) { |
|---|
| 87 | # .* at the beginning ensures we catch innermost nestings first |
|---|
| 88 | $input =~ s: (.*) \[ $tag \] (.+?) \[ / $tag \] : $1 . $tags{$tag}{replace}->(undef, $2) :gexis; |
|---|
| 89 | $input =~ s: (.*) \[ $tag = (.+?) \] (.+?) \[ / $tag \] : $1 . $tags{$tag}{replace}->($2, $3) :gexis; |
|---|
| 90 | } |
|---|
| 91 | } else { |
|---|
| 92 | $input =~ s: \[ $tag \] : $tags{$tag}{start}->() :gexi; |
|---|
| 93 | $input =~ s: \[ $tag = (.+?) \] : $tags{$tag}{start}->($1) :gexi; |
|---|
| 94 | $input =~ s: \[ / $tag \] : $tags{$tag}{end}->() :gexi; |
|---|
| 95 | } |
|---|
| 96 | } |
|---|
| 97 | |
|---|
| 98 | $input =~ s:\x0D\x0A:<br/>:g; |
|---|
| 99 | $input =~ s:\x0D|\x0A:<br/>:g; |
|---|
| 100 | |
|---|
| 101 | return $input; |
|---|
| 102 | } |
|---|
| 103 | |
|---|
| 104 | =head2 apply_bbcode |
|---|
| 105 | |
|---|
| 106 | Attempts to gauge whether a provided string of bbcode is valid. Returns the |
|---|
| 107 | string, possibly modified (I forget), and a list of errors if applicable. |
|---|
| 108 | |
|---|
| 109 | This method is in transition at the moment, sorry. |
|---|
| 110 | |
|---|
| 111 | =cut |
|---|
| 112 | |
|---|
| 113 | sub validate_bbcode { |
|---|
| 114 | my $input = shift; |
|---|
| 115 | |
|---|
| 116 | $input =~ s/&/&/g; |
|---|
| 117 | $input =~ s/&(#x?[0-9a-f]{1,10});/&$1;/gi; |
|---|
| 118 | $input =~ s/\"/"/g; |
|---|
| 119 | $input =~ s/</</g; |
|---|
| 120 | $input =~ s/>/>/g; |
|---|
| 121 | |
|---|
| 122 | my @tag_stack; |
|---|
| 123 | # my @closed_tag_stack; |
|---|
| 124 | |
|---|
| 125 | my $lb_entity = '['; |
|---|
| 126 | my $rb_entity = ']'; |
|---|
| 127 | my @output = (''); |
|---|
| 128 | my @errors; |
|---|
| 129 | my @replace_params; |
|---|
| 130 | my $pos = 0; |
|---|
| 131 | my ($left_bracket, $right_bracket); |
|---|
| 132 | PARSE: while ($input) { |
|---|
| 133 | $left_bracket = index $input, '['; last if $left_bracket == -1; |
|---|
| 134 | $right_bracket = index $input, ']', $left_bracket + 1; last if $right_bracket == -1; |
|---|
| 135 | $left_bracket = rindex $input, '[', $right_bracket; |
|---|
| 136 | my $tag = substr $input, $left_bracket + 1, $right_bracket - $left_bracket - 1; |
|---|
| 137 | |
|---|
| 138 | # tack any plaintext before the next tag onto the output |
|---|
| 139 | my $raw = substr $input, 0, $left_bracket; |
|---|
| 140 | $raw =~ s/ \[ /$lb_entity/gx; |
|---|
| 141 | $raw =~ s/ \] /$rb_entity/gx; |
|---|
| 142 | $output[-1] .= $raw; |
|---|
| 143 | # then remove the preceding plaintext and the tag from the input |
|---|
| 144 | substr($input, 0, $right_bracket + 1) = ''; |
|---|
| 145 | |
|---|
| 146 | $left_bracket = 0; |
|---|
| 147 | $right_bracket -= $left_bracket; |
|---|
| 148 | |
|---|
| 149 | my $closing = 0; |
|---|
| 150 | if ('/' eq substr $tag, 0, 1) { |
|---|
| 151 | $closing = 1; |
|---|
| 152 | $tag = substr $tag, 1; |
|---|
| 153 | } |
|---|
| 154 | |
|---|
| 155 | my ($tag_name, $param) = split /=/, $tag, 2; |
|---|
| 156 | |
|---|
| 157 | # check: skip the tag if it's not one of ours |
|---|
| 158 | if (!exists $tags{$tag_name}) { |
|---|
| 159 | $output[-1] .= $lb_entity . ($closing ? '/' : '') . $tag . $rb_entity; |
|---|
| 160 | next; |
|---|
| 161 | } |
|---|
| 162 | |
|---|
| 163 | if ($param and $tags{$tag_name}{strip_quotes}) { |
|---|
| 164 | $param =~ s/^"|"$//g; |
|---|
| 165 | } |
|---|
| 166 | |
|---|
| 167 | # check: can't have a param with a closing tag |
|---|
| 168 | if ($param && $closing) { |
|---|
| 169 | push @errors, [ "Parameter given on closing tag '$tag_name'", $left_bracket + 1 + length $tag ]; |
|---|
| 170 | substr($input, $left_bracket + length($tag) + 2, length($param) + 1) = ''; |
|---|
| 171 | $param = ''; |
|---|
| 172 | } |
|---|
| 173 | |
|---|
| 174 | # check if parameter should be here |
|---|
| 175 | if (!$closing and $param and $tags{$tag_name}{param} == 0) { |
|---|
| 176 | push @errors, [ "Parameter given for parameter-less tag '$tag_name'", $left_bracket + 1 + length $tag ]; |
|---|
| 177 | $param = undef; |
|---|
| 178 | } |
|---|
| 179 | if (!$closing and!$param and $tags{$tag_name}{param} == 2) { |
|---|
| 180 | push @errors, [ "Parameter required for tag '$tag_name'", $left_bracket + 1 + length $tag ]; |
|---|
| 181 | $param = ''; |
|---|
| 182 | } |
|---|
| 183 | |
|---|
| 184 | if ($tags{$tag_name}{no_close}) { |
|---|
| 185 | # don't do anything |
|---|
| 186 | } elsif (!$closing) { |
|---|
| 187 | push @tag_stack, $tag_name; |
|---|
| 188 | } else { |
|---|
| 189 | # check tag closing order |
|---|
| 190 | my $next_tag; |
|---|
| 191 | while ($next_tag = pop @tag_stack) { |
|---|
| 192 | last if $next_tag eq $tag_name; |
|---|
| 193 | |
|---|
| 194 | push @errors, [ "Tag '$tag_name' not closed before parent tag '$next_tag'", $left_bracket ]; |
|---|
| 195 | $output[-1] .= qq'<span class="bbcode-error-wrongorder">[/$tag_name]</span>'; |
|---|
| 196 | } |
|---|
| 197 | |
|---|
| 198 | if (!defined $next_tag) { |
|---|
| 199 | # we have run out of initial closing tags! replace this with something temporary for now |
|---|
| 200 | push @errors, [ "Tag '$tag_name' closed but not open", $left_bracket ]; |
|---|
| 201 | $output[-1] .= qq'<span class="bbcode-error-notopen">[/$tag_name]</span>'; |
|---|
| 202 | next PARSE; |
|---|
| 203 | } |
|---|
| 204 | } |
|---|
| 205 | |
|---|
| 206 | $output[-1] .= '['; |
|---|
| 207 | $output[-1] .= '/' if $closing; |
|---|
| 208 | $output[-1] .= $tag; |
|---|
| 209 | $output[-1] .= ']'; |
|---|
| 210 | |
|---|
| 211 | # if the tag is marked as 'raw', then its entire contents should be moved directly to output |
|---|
| 212 | if ($tags{$tag_name}{raw} and !$closing) { |
|---|
| 213 | my $raw_end = index $input, "[/$tag_name]"; |
|---|
| 214 | my $raw = substr $input, 0, $raw_end; |
|---|
| 215 | $raw =~ s/ \[ /$lb_entity/gx; |
|---|
| 216 | $raw =~ s/ \] /$rb_entity/gx; |
|---|
| 217 | $output[-1] .= $raw; |
|---|
| 218 | $input = substr $input, $raw_end; |
|---|
| 219 | } |
|---|
| 220 | } |
|---|
| 221 | |
|---|
| 222 | for (@tag_stack) { |
|---|
| 223 | push @errors, [ "Tag '$_' never closed" ]; |
|---|
| 224 | } |
|---|
| 225 | |
|---|
| 226 | if ($input) { |
|---|
| 227 | $input =~ s/ \[ /$lb_entity/gx; |
|---|
| 228 | $input =~ s/ \] /$rb_entity/gx; |
|---|
| 229 | $output[-1] .= $input; |
|---|
| 230 | } |
|---|
| 231 | |
|---|
| 232 | return $output[-1], @errors; |
|---|
| 233 | } |
|---|
| 234 | |
|---|
| 235 | =head1 AUTHOR |
|---|
| 236 | |
|---|
| 237 | Maintainer: Alex "Eevee" Munroe (C<veekun@veekun.com>) |
|---|
| 238 | |
|---|
| 239 | See the included F<AUTHORS> file for a full list of contributers. |
|---|
| 240 | |
|---|
| 241 | =head1 LICENSE |
|---|
| 242 | |
|---|
| 243 | See the included F<LICENSE> file. |
|---|
| 244 | |
|---|
| 245 | =cut |
|---|
| 246 | |
|---|
| 247 | 1; |
|---|