package Vee::BBCode; use strict; use warnings; =head1 NAME Vee::BBCode - Two-step bbcode parser =head1 SYNOPSIS use Vee::BBCode; Vee::BBCode::validate_bbcode('I [i]really[/i] think you should [b]stop[/b].'); # success Vee::BBCode::validate_bbcode('Nested [b]tags [i]are not[/b] that[/i] complicated'); # failure Vee::BBCode::apply_bbcode('[i]Always[/i] [code]use strict;[/code]!'); # HTMLized =head1 DESCRIPTION Quick authorization module, to check whether the currently logged-in user has been given a particular permission. =head1 METHODS =cut # keys are the tag names # start/end subs take one parameter: the tag's parameter # replace subs take two parameters: the tag's contents and its parameter # TODO: # - docs? # BUGS: # - replace won't work with no_close our %tags = ( b => { param => 0, start => '', end => '' }, i => { param => 0, start => '', end => '' }, u => { param => 0, start => '', end => '' }, s => { param => 0, start => '', end => '' }, h => { param => 0, start => '', end => '' }, color => { param => 2, strip_quotes => 1, start => sub { my $color = shift; if ($color !~ / \#[0-9a-f]{3,6} | (?:rgb|hsl) \( \s* \d{,3}% \s* , \s* \d{,3}% \s* , \s* \d{,3}% \s* \) | (?:rgb|hsl) \( \s* \d{,3} \s* , \s* \d{,3} \s* , \s* \d{,3} \s* \) | (?:rgb|hsl)a \( \s* -?\d+ \s* , \s* \d{,3}% \s* , \s* \d{,3}% \s* , \s* \d(\.\d+)? \s* \) | [a-z]+[0-9]* /ix) { $color = 'inherit' } qq'' }, end => '' }, # size => { param => 2, }, url => { param => 1, strip_quotes => 1, replace => sub { qq'$_[1]' } }, img => { param => 1, strip_quotes => 1, raw => 1, start => sub { $_[0] ? qq'' : '<img alt=' }, code => { param => 1, raw => 1, start => sub { '
' . ($_[0] ? "Code for $_[0]" : 'Code') . ':
' }, end => '
' }, quote => { param => 1, start => sub { '
' . ($_[0] ? "Quote from $_[0]" : 'Quote') . ':
' }, end => '
' }, mono => { param => 1, start => '', end => '' }, raw => { param => 1, raw => 1, start => '', end => '' }, hr => { param => 0, no_close => 1, start => '
' }, center => { param => 0, no_close => 0, start => '
', end => '
' }, # list => { param => 1, contents => { '*' => 1 }, }, # '*' => { param => 0, container => { list => 1 }, self_closing => 1, }, ); # change start, end, and replaces to subroutines for ease of runnage for my $tag_data (values %tags) { for my $replacement (qw/start end replace/) { my $format = $tag_data->{ $replacement }; next unless defined $format; next if ref $format; $tag_data->{ $replacement } = sub { sprintf $format, @_ }; } } =head2 apply_bbcode Applies bbcode formatting to a text string. This makes no attempt to verify the well-formed-ness of the input. =cut sub apply_bbcode { my $input = shift; for my $tag (keys %tags) { if (exists $tags{$tag}{replace}) { while ($input =~ / \[ $tag ( = .+? )? \] /sx) { # .* at the beginning ensures we catch innermost nestings first $input =~ s: (.*) \[ $tag \] (.+?) \[ / $tag \] : $1 . $tags{$tag}{replace}->(undef, $2) :gexis; $input =~ s: (.*) \[ $tag = (.+?) \] (.+?) \[ / $tag \] : $1 . $tags{$tag}{replace}->($2, $3) :gexis; } } else { $input =~ s: \[ $tag \] : $tags{$tag}{start}->() :gexi; $input =~ s: \[ $tag = (.+?) \] : $tags{$tag}{start}->($1) :gexi; $input =~ s: \[ / $tag \] : $tags{$tag}{end}->() :gexi; } } $input =~ s:\x0D\x0A:
:g; $input =~ s:\x0D|\x0A:
:g; return $input; } =head2 apply_bbcode Attempts to gauge whether a provided string of bbcode is valid. Returns the string, possibly modified (I forget), and a list of errors if applicable. This method is in transition at the moment, sorry. =cut sub validate_bbcode { my $input = shift; $input =~ s/&/&/g; $input =~ s/&(#x?[0-9a-f]{1,10});/&$1;/gi; $input =~ s/\"/"/g; $input =~ s//>/g; my @tag_stack; # my @closed_tag_stack; my $lb_entity = '['; my $rb_entity = ']'; my @output = (''); my @errors; my @replace_params; my $pos = 0; my ($left_bracket, $right_bracket); PARSE: while ($input) { $left_bracket = index $input, '['; last if $left_bracket == -1; $right_bracket = index $input, ']', $left_bracket + 1; last if $right_bracket == -1; $left_bracket = rindex $input, '[', $right_bracket; my $tag = substr $input, $left_bracket + 1, $right_bracket - $left_bracket - 1; # tack any plaintext before the next tag onto the output my $raw = substr $input, 0, $left_bracket; $raw =~ s/ \[ /$lb_entity/gx; $raw =~ s/ \] /$rb_entity/gx; $output[-1] .= $raw; # then remove the preceding plaintext and the tag from the input substr($input, 0, $right_bracket + 1) = ''; $left_bracket = 0; $right_bracket -= $left_bracket; my $closing = 0; if ('/' eq substr $tag, 0, 1) { $closing = 1; $tag = substr $tag, 1; } my ($tag_name, $param) = split /=/, $tag, 2; # check: skip the tag if it's not one of ours if (!exists $tags{$tag_name}) { $output[-1] .= $lb_entity . ($closing ? '/' : '') . $tag . $rb_entity; next; } if ($param and $tags{$tag_name}{strip_quotes}) { $param =~ s/^"|"$//g; } # check: can't have a param with a closing tag if ($param && $closing) { push @errors, [ "Parameter given on closing tag '$tag_name'", $left_bracket + 1 + length $tag ]; substr($input, $left_bracket + length($tag) + 2, length($param) + 1) = ''; $param = ''; } # check if parameter should be here if (!$closing and $param and $tags{$tag_name}{param} == 0) { push @errors, [ "Parameter given for parameter-less tag '$tag_name'", $left_bracket + 1 + length $tag ]; $param = undef; } if (!$closing and!$param and $tags{$tag_name}{param} == 2) { push @errors, [ "Parameter required for tag '$tag_name'", $left_bracket + 1 + length $tag ]; $param = ''; } if ($tags{$tag_name}{no_close}) { # don't do anything } elsif (!$closing) { push @tag_stack, $tag_name; } else { # check tag closing order my $next_tag; while ($next_tag = pop @tag_stack) { last if $next_tag eq $tag_name; push @errors, [ "Tag '$tag_name' not closed before parent tag '$next_tag'", $left_bracket ]; $output[-1] .= qq'[/$tag_name]'; } if (!defined $next_tag) { # we have run out of initial closing tags! replace this with something temporary for now push @errors, [ "Tag '$tag_name' closed but not open", $left_bracket ]; $output[-1] .= qq'[/$tag_name]'; next PARSE; } } $output[-1] .= '['; $output[-1] .= '/' if $closing; $output[-1] .= $tag; $output[-1] .= ']'; # if the tag is marked as 'raw', then its entire contents should be moved directly to output if ($tags{$tag_name}{raw} and !$closing) { my $raw_end = index $input, "[/$tag_name]"; my $raw = substr $input, 0, $raw_end; $raw =~ s/ \[ /$lb_entity/gx; $raw =~ s/ \] /$rb_entity/gx; $output[-1] .= $raw; $input = substr $input, $raw_end; } } for (@tag_stack) { push @errors, [ "Tag '$_' never closed" ]; } if ($input) { $input =~ s/ \[ /$lb_entity/gx; $input =~ s/ \] /$rb_entity/gx; $output[-1] .= $input; } return $output[-1], @errors; } =head1 AUTHOR Maintainer: Alex "Eevee" Munroe (C) See the included F file for a full list of contributers. =head1 LICENSE See the included F file. =cut 1;