4 use Constants qw($IMAGEDIR $LOCAL_FORMAT $BODY_EMBED
5 $EMBED_MAX_DEPTH $HAVE_HTML_PARSER);
9 use BSE::CfgInfo qw(custom_class);
11 my $excerptSize = 300;
14 my ($class, %opts) = @_;
15 $opts{maxdepth} = $EMBED_MAX_DEPTH unless exists $opts{maxdepth};
16 $opts{depth} = 0 unless $opts{depth};
17 return bless \%opts, $class;
20 # replace commonly used characters
22 # unfortunately some browsers^W^Wnetscape don't support the entities yet <sigh>
26 $text =~ s/\226/-/g; # "--" looks ugly
29 $text =~ s/\’/'/g;
35 my ($self, $articles, $text, $acts, $length) = @_;
37 # remove any block level formatting
38 $self->remove_block($articles, $acts, \$text);
40 $text =~ tr/\n\r / /s;
42 if (length $text > $length) {
43 $text = substr($text, 0, $length);
46 # roughly balance [ and ]
48 1 while $temp =~ s/\s\[[^\]]*\]//; # eliminate matched
50 ++$count while $temp =~ s/\w\[[^\]]*$//; # count unmatched
52 $text .= ']' x $count;
56 # the formatter now adds <p></p> around the text, but we don't
58 my $result = $self->format_body({}, $articles, $text, 'tr', 1, 0);
59 $result =~ s!<p>|</p>!!g;
64 # attempts to move the given position forward if it's within a HTML tag,
65 # entity or just a word
67 my ($self, $text, $pos) = @_;
70 return $pos + length $1
71 if substr($text, 0, $pos) =~ /<[^<>]*$/
72 && substr($text, $pos) =~ /^([^<>]*>)/;
73 return $pos + length $1
74 if substr($text, 0, $pos) =~ /&[^;&]*$/
75 && substr($text, $pos) =~ /^([^;&]*;)/;
76 return $pos + length $1
77 if $pos <= length $text
78 && substr($text, $pos-1, 1) =~ /\w$/
79 && substr($text, $pos) =~ /^(\w+)/;
85 # my ($width, $height) = @_;
87 # $tag .= qq!width="$width"! if length $width;
88 # $tag .= qq!height="$height"! if length $height;
93 # # produces a table, possibly with options for the <table> and <tr> tags
95 # my ($options, $text) = @_;
99 # if ($options =~ /=/) {
100 # $tag .= " " . $options;
102 # elsif ($options =~ /\S/) {
103 # $options =~ s/\s+$//;
104 # my ($width, $bg, $pad, $fontsz, $fontface) = split /\|/, $options;
105 # for ($width, $bg, $pad, $fontsz, $fontface) {
106 # $_ = '' unless defined;
108 # $tag .= qq! width="$width"! if length $width;
109 # $tag .= qq! bgcolor="$bg"! if length $bg;
110 # $tag .= qq! cellpadding="$pad"! if length $pad;
111 # if (length $fontsz || length $fontface) {
112 # $cellstart = qq!<font!;
113 # $cellstart .= qq! size="$fontsz"! if length $fontsz;
114 # $cellstart .= qq! face="$fontface"! if length $fontface;
115 # $cellstart .= qq!>!;
116 # $cellend = "</font>";
120 # my @rows = split '\n', $text;
122 # for my $row (@rows) {
123 # my ($opts, @cols) = split /\|/, $row;
125 # if ($opts =~ /=/) {
128 # $tag .= "><td>$cellstart".join("$cellend</td><td>$cellstart", @cols)
129 # ."$cellend</td></tr>";
131 # $tag .= "</table>";
136 # sub _format_bullets {
139 # $text =~ s/^\s+|\s+$//g;
140 # my @points = split /(?:\r?\n)?\*\*\s*/, $text;
141 # shift @points if @points and $points[0] eq '';
142 # return '' unless @points;
143 # for my $point (@points) {
144 # $point =~ s!\n$!<br /><br />!;
146 # return "<ul><li>".join("<li>", @points)."</ul>";
152 # $text =~ s/^\s+|\s+$//g;
153 # my @points = split /(?:\r?\n)?##\s*/, $text;
154 # shift @points if @points and $points[0] eq '';
155 # return '' unless @points;
156 # for my $point (@points) {
157 # #print STDERR "point: ",unpack("H*", $point),"\n";
158 # $point =~ s!\n$!<br /><br />!;
160 # return "<ol><li>".join("<li>", @points)."</ol>";
163 # raw html - this has some limitations
164 # the input text has already been escaped, so we need to unescape it
165 # too bad if you want [] in your html (but you can use entities)
167 return unescape_html($_[0]);
171 my ($self, $acts, $articles, $what, $template, $maxdepth) = @_;
173 $maxdepth = $self->{maxdepth}
174 if !$maxdepth || $maxdepth > $self->{maxdepth};
175 #if ($self->{depth}) {
176 # print STDERR "Embed depth $self->{depth}\n";
178 if ($self->{depth} > $self->{maxdepth}) {
179 if ($self->{maxdepth} == $EMBED_MAX_DEPTH) {
180 return "** too many embedding levels **";
188 if ($what !~ /^\d+$/) {
189 # not an article id, assume there's an article here we can use
190 $id = $acts->{$what} && $acts->{$what}->('id');
191 unless ($id && $id =~ /^\d+$/) {
193 defined $template or $template = "-";
194 return "<:embed $what $template $maxdepth:>";
200 my $embed = $articles->getByPkey($id);
203 if (ref($self) ne $embed->{generator}) {
204 my $genname = $embed->{generator};
205 $genname =~ s#::#/#g; # broken on MacOS I suppose
211 print STDERR "Cannot load generator $embed->{generator}: $@\n";
212 return "** Cannot load generator $embed->{generator} for article $id **";
214 my $top = $self->{top} || $embed;
215 $gen = $embed->{generator}->new(admin=>$self->{admin}, cfg=>$self->{cfg},
216 request=>$self->{request}, top=>$top);
219 # a rare appropriate use of local
220 # it's a pity that it's broken before 5.8
221 #local $gen->{depth} = $self->{depth}+1;
222 #local $gen->{maxdepth} = $maxdepth;
223 #$template = "" if defined($template) && $template eq "-";
224 #return $gen->embed($embed, $articles, $template);
226 my $olddepth = $gen->{depth};
227 $gen->{depth} = $self->{depth}+1;
228 my $oldmaxdepth = $gen->{maxdepth};
229 $gen->{maxdepth} = $maxdepth;
230 $template = "" if defined($template) && $template eq "-";
231 my $result = $gen->embed($embed, $articles, $template);
232 $gen->{depth} = $olddepth;
233 $gen->{maxdepth} = $oldmaxdepth;
238 return "** Cannot find article $id to be embedded **";
243 my ($self, $acts, $articles, $which, $template, $maxdepth) = @_;
245 my $text = $self->_embed_low($acts, $articles, $which, $template, $maxdepth);
251 my ($args, $imagePos, $images) = @_;
253 my ($index, $align, $url) = split /\|/, $args, 3;
255 if ($index >=1 && $index <= @$images) {
258 # $align = $$imagePos =~ /r/ ? 'right' : 'left';
259 # $$imagePos =~ tr/rl/lr/; # I wonder
261 my $im = $images->[$index-1];
262 $text = qq!<img src="/images/$im->{image}" width="$im->{width}"!
263 . qq! height="$im->{height}" alt="! . escape_html($im->{alt}).'"'
265 $text .= qq! align="$align"! if $align && $align ne 'center';
267 $text = qq!<div align="center">$text</div>!
268 if $align && $align eq 'center';
269 if (!$url && $im->{url}) {
273 $text = qq!<a href="! . escape_html($url) . qq!">$text</a>!;
279 # replace markup, insert img tags
281 my ($self, $acts, $articles, $body, $imagePos, $abs_urls,
282 $auto_images, @images) = @_;
284 return substr($body, 6) if $body =~ /^<html>/i;
286 require BSE::Formatter;
288 my $formatter = BSE::Formatter->new($self, $acts, $articles,
289 $abs_urls, \$auto_images,
292 $body = $formatter->format($body);
294 # we don't format named images
295 @images = grep $_->{name} eq '', @images;
296 if ($auto_images && @images) {
297 # the first image simply goes where we're told to put it
298 # the imagePos is [tb][rl] (top|bottom)(right|left)
299 my $align = $imagePos =~ /r/ ? 'right' : 'left';
301 # Offset the end a bit so we don't get an image hanging as obviously
303 # Numbers determined by trial - it can still look pretty rough.
304 my $len = length $body;
312 #my $incr = @images > 1 ? 2*$len / (2*@images+1) : 0;
313 my $incr = $len / @images;
314 # inserting the image tags moves character positions around
315 # so we need the temp buffer
316 if ($imagePos =~ /b/) {
317 @images = reverse @images;
318 if (@images % 2 == 0) {
319 # starting at the bottom, swap it around
320 $align = $align eq 'right' ? 'left' : 'right';
324 for my $image (@images) {
325 # adjust to make sure this isn't in the middle of a tag or entity
326 my $pos = $self->adjust_for_html($body, $incr);
328 # assuming 5.005_03 would make this simpler, but <sigh>
329 my $img = qq!<img src="/images/$image->{image}"!
330 .qq! width="$image->{width}" height="$image->{height}" border="0"!
331 .qq! alt="$image->{alt}" align="$align" hspace="10" vspace="10" />!;
333 $img = qq!<a href="$image->{url}">$img</a>!;
336 $output .= substr($body, 0, $pos);
337 substr($body, 0, $pos) = '';
338 $align = $align eq 'right' ? 'left' : 'right';
340 $body = $output . $body; # don't forget the rest of it
343 return make_entities($body);
347 my ($self, $article, $articles, $template) = @_;
349 if (defined $template && $template =~ /\$/) {
350 $template =~ s/\$/$article->{template}/;
353 $template = $article->{template}
354 unless defined($template) && $template =~ /\S/;
357 my $html = BSE::Template->get_source($template, $self->{cfg});
359 # the template will hopefully contain <:embed start:> and <:embed end:>
361 # otherwise pull out the body content
362 if ($html =~ /<:\s*embed\s*start\s*:>(.*)<:\s*embed\s*end\s*:>/s
363 || $html =~ m"<\s*body[^>]*>(.*)<\s*/\s*body>"s) {
366 return $self->generate_low($html, $article, $articles, 1);
370 my ($args, $acts, $name, $templater) = @_;
372 my @ids = map { split } DevHelp::Tags->get_parms($args, $acts, $templater);
374 unless ($id =~ /^\d+$|^-1$/) {
375 $id = $templater->perform($acts, $id, "id");
378 @ids = grep /^\d+$|^-1$/, @ids;
379 map Articles->listedChildren($_), @ids;
382 sub iter_all_kids_of {
383 my ($args, $acts, $name, $templater) = @_;
385 my @ids = map { split } DevHelp::Tags->get_parms($args, $acts, $templater);
387 unless ($id =~ /^\d+$|^-1$/) {
388 $id = $templater->perform($acts, $id, "id");
391 @ids = grep /^\d+$|^-1$/, @ids;
392 map Articles->all_visible_kids($_), @ids;
396 my ($args, $acts, $name, $templater) = @_;
398 my @ids = map { split } DevHelp::Tags->get_parms($args, $acts, $templater);
400 unless ($id =~ /^\d+$/) {
401 $id = $templater->perform($acts, $id, "id");
404 @ids = grep /^\d+$/, @ids;
405 map Articles->getByPkey($_), @ids;
409 my ($self, $articles, $acts, $article, $embedded) = @_;
411 # used to generate the side menu
412 my $section_index = -1;
413 my @sections = $articles->listedChildren(-1);
414 #sort { $a->{displayOrder} <=> $b->{displayOrder} }
415 #grep $_->{listed}, $articles->sections;
416 my $subsect_index = -1;
417 my @subsections; # filled as we move through the sections
418 my @level3; # filled as we move through the subsections
419 my $level3_index = -1;
421 my $cfg = $self->{cfg} || BSE::Cfg->new;
422 my %extras = $cfg->entriesCS('extra tags');
423 for my $key (keys %extras) {
425 my $data = $cfg->entryVar('extra tags', $key);
426 $extras{$key} = sub { $data };
432 custom_class($cfg)->base_tags($articles, $acts, $article, $embedded, $cfg),
433 BSE::Util::Tags->static($acts, $self->{cfg}),
434 # for embedding the content from children and other sources
435 ifEmbedded=> sub { $embedded },
437 my ($what, $template, $maxdepth) = split ' ', $_[0];
438 undef $maxdepth if defined $maxdepth && $maxdepth !~ /^\d+/;
439 return $self->_embed_low($acts, $articles, $what, $template, $maxdepth);
441 ifCanEmbed=> sub { $self->{depth} <= $self->{maxdepth} },
445 my ($which, $acts, $name, $templater) = @_;
446 $which or $which = "child";
448 or return "<:summary $which Cannot find $which:>";
449 my $id = $templater->perform($acts, $which, "id")
450 or return "<:summary $which No id returned :>";
451 my $article = $articles->getByPkey($id)
452 or return "<:summary $which Cannot find article $id:>";
453 return $self->summarize($articles, $article->{body}, $acts,
454 $article->{summaryLength})
456 ifAdmin => sub { $self->{admin} },
458 # for generating the side menu
459 iterate_level1_reset => sub { $section_index = -1 },
460 iterate_level1 => sub {
462 if ($section_index < @sections) {
463 #@subsections = grep $_->{listed},
464 # $articles->children($sections[$section_index]->{id});
465 @subsections = grep { $_->{listed} != 2 }
466 $articles->listedChildren($sections[$section_index]->{id});
475 return escape_html($sections[$section_index]{$_[0]});
478 # used to generate a list of subsections for the side-menu
479 iterate_level2 => sub {
481 if ($subsect_index < @subsections) {
482 @level3 = grep { $_->{listed} != 2 }
483 $articles->listedChildren($subsections[$subsect_index]{id});
490 return escape_html($subsections[$subsect_index]{$_[0]});
494 return scalar @subsections;
497 # possibly level3 items
498 iterate_level3 => sub {
499 return ++$level3_index < @level3;
501 level3 => sub { escape_html($level3[$level3_index]{$_[0]}) },
502 ifLevel3 => sub { scalar @level3 },
504 # generate an admin or link url, depending on admin state
507 my ($name, $acts, $func, $templater) = @_;
508 my $item = $self->{admin} ? 'admin' : 'link';
509 $acts->{$name} or return "<:url $name:>";
510 return $templater->perform($acts, $name, $item);
514 $acts->{$_[0]} or return 0;
515 return $acts->{$_[0]}->('listed') == 1;
519 my ($image, $text) = split ' ', $_[0];
520 if (-e $IMAGEDIR."/titles/".$image) {
521 return qq!<img src="/images/titles/!.$image .qq!" border=0>!
524 return escape_html($text);
527 DevHelp::Tags->make_iterator2
528 ( \&iter_kids_of, 'ofchild', 'children_of' ),
529 DevHelp::Tags->make_iterator2
530 ( \&iter_all_kids_of, 'ofallkid', 'allkids_of' ),
531 DevHelp::Tags->make_iterator2
532 ( \&iter_inlines, 'inline', 'inlines' ),
535 my ($name, $align, $rest) = split ' ', $_[0], 3;
537 my $im = $self->get_gimage($name)
540 $self->_format_image($im, $align, $rest);
546 my ($body, $case_sensitive, @terms) = @_;
550 if ($case_sensitive) {
551 for my $term (@terms) {
552 if ($$body =~ /^(.*?)\Q$term/s) {
553 push(@found, [ length($1), $term ]);
558 for my $term (@terms) {
559 if ($$body =~ /^(.*?)\Q$term/is) {
560 push(@found, [ length($1), $term ]);
568 # this takes the same inputs as _make_table(), but eliminates any
571 my ($opts, $data) = @_;
572 my @lines = split /\n/, $data;
577 return join(' ', @lines);
580 # produce a nice excerpt for a found article
582 my ($self, $article, $found, $case_sensitive, @terms) = @_;
584 my $body = $article->{body};
586 # we remove any formatting tags here, otherwise we get wierd table
587 # rubbish or other formatting in the excerpt.
588 $self->remove_block('Articles', [], \$body);
589 1 while $body =~ s/[bi]\[([^\]\[]+)\]/$1/g;
591 $body = escape_html($body);
593 my @found = find_terms(\$body, $case_sensitive, @terms);
595 my @reterms = @terms;
601 # do a reverse sort so that the longer terms (and composite
602 # terms) are replaced first
603 my $re_str = join("|", reverse sort @reterms);
605 my $cfg = $self->{cfg};
606 if ($cfg->entryBool('basic', 'highlight_partial', 1)) {
607 $re = $case_sensitive ? qr/\b($re_str)/ : qr/\b($re_str)/i;
610 $re = $case_sensitive ? qr/\b($re_str)\b/ : qr/\b($re_str)\b/i;
613 # this used to try searching children as well, but it broke more
616 # we tried hard and failed
617 # return a generic article
618 if (length $body > $excerptSize) {
619 $body = substr($body, 0, $excerptSize);
620 $body =~ s/\S+\s*$/.../;
627 splice(@found, 5,-1) if @found > 5;
628 my $itemSize = $excerptSize / @found;
630 # try to combine any that are close
631 @found = sort { $a->[0] <=> $b->[0] } @found;
632 for my $i (reverse 0 .. $#found-1) {
633 if ($found[$i+1][0] - $found[$i][0] < $itemSize) {
634 my @losing = @{$found[$i+1]};
636 push(@{$found[$i]}, @losing);
637 splice(@found, $i+1, 1); # remove it
641 my $termSize = $excerptSize / @found;
643 for my $term (@found) {
644 my ($pos, @terms) = @$term;
645 my $start = $pos - $termSize/2;
649 $part = substr($body, 0, $termSize);
653 $part = substr($body, $start, $termSize);
656 if ($start + $termSize < length $body) {
657 $part =~ s/\s*\S*$/... /;
661 $result =~ s{$re}{<b>$1</b>}ig;
671 # # removes any html tags from the supplied text
675 # if ($HAVE_HTML_PARSER) {
677 # # don't forget that require is smart
678 # require "HTML/Parser.pm";
680 # # this may need to detect and skip <script></script> and stylesheets
681 # my $ignore_text = 0; # non-zero in a <script></script> or <style></style>
684 # ++$ignore_text if $_[0] eq 'script' or $_[0] eq 'style';
685 # if ($_[0] eq 'img' && $_[1]{alt} && !$ignore_text) {
686 # $out .= $_[1]{alt};
691 # --$ignore_text if $_[0] eq 'script' or $_[0] eq 'style';
695 # $out .= $_[0] unless $ignore_text
697 # my $p = HTML::Parser->new( text_h => [ $text_h, "dtext" ],
698 # start_h => [ $start_h, "tagname, attr" ],
699 # end_h => [ $end_h, "tagname" ]);
706 # # this won't work for some HTML, but it's a fallback
707 # $text =~ s/<[^>]*>//g;
713 # make whatever text $body points at safe for summarizing by removing most
714 # block level formatting
716 my ($self, $articles, $acts, $body) = @_;
718 require BSE::Formatter;
720 my $formatter = BSE::Formatter->new($self, $acts, $articles,
723 $$body = $formatter->remove_format($$body);
727 my ($self, $name) = @_;
729 unless ($self->{gimages}) {
731 my @gimages = Images->getBy(articleId => -1);
732 my %gimages = map { $_->{name} => $_ } @gimages;
733 $self->{gimages} = \%gimages;
736 return $self->{gimages}{$name};
740 my ($self, $im, $align, $rest) = @_;
742 if ($align && exists $im->{$align}) {
743 return escape_html($im->{$align});
746 my $html = qq!<img src="/images/$im->{image}" width="$im->{width}"!
747 . qq! height="$im->{height}" alt="! . escape_html($im->{alt})
749 $html .= qq! align="$align"! if $align && $align ne '-';
750 unless (defined($rest) && $rest =~ /\bborder=/i) {
751 $html .= ' border="0"';
753 $html .= " $rest" if defined $rest;
756 $html = qq!<a href="$im->{url}">$html</a>!;
768 Generate - provides base Squirel::Template actions for use in generating
775 This is probably better documented in L<templates.pod>.
779 These tags can be used anywhere, including in admin templates. It's
780 possible some admin code has been missed, if you find a place where
781 these cannot be used let us know.
788 Formats the give value in kI<whatevers>. If you have a number that
789 could go over 1000 and you want it to use the 'k' metric prefix when
790 it does, use this tag. eg. <:kb file sizeInBytes:>
792 =item date I<data tag>
794 =item date "I<format>" I<data tag>
796 Formats a date or date/time value from the database into something
797 more human readable. If you don't supply a format then the default
798 format of "%d-%b-%Y" is used ("20-Mar-2002").
800 The I<format> is a strftime() format specification, if that means
801 anything to you. If it doesn't, each code starts with % and are
808 abbreviated weekday name
816 abbreviated month name
824 "preferred" date and time representation
828 day of the month as a 2 digit number
840 day of year as a 3-digit number
844 month as a 2 digit number
848 minute as a 2 digit number
852 AM or PM or their equivalents
856 seconds as a 2 digit number
860 week number as a 2 digit number (first Sunday as the first day of week 1)
864 weekday as a decimal number (0-6)
868 week number as a 2 digit number (first Monday as the first day of week 1)
872 the locale's appropriate date representation
876 the locale's appropriate time representation
880 2-digit year without century
888 time zone name or abbreviation
896 Your local strftime() implementation may implement some extensions to
897 the above, if your server is on a Unix system try running "man
898 strftime" for more information.
900 =item bodytext I<data tag>
902 Formats the text from the given tag in the same way that body text is.
904 =item ifEq I<data1> I<data2>
906 Checks if the 2 values are exactly equal. This is a string
909 The 2 data parameters can either be a tag reference in [], a literal
910 string inside "" or a single word.
912 =item ifMatch I<data1> I<data2>
914 Treats I<data2> as a perl regular expression and attempts to match
917 The 2 data parameters can either be a tag reference in [], a literal
918 string inside "" or a single word.
920 =item cfg I<section> I<key>
922 =item cfg I<section> I<key> I<default>
924 Retrieves a value from the BSE configuration file.
926 If you don't supply a default then a default will be the empty string.
930 The release number of BSE.
940 Conditional tag, true if generating in admin mode.
942 =item iterator ... level1
944 Iterates over the listed level 1 articles.
948 The value of the I<name> field of the current level 1 article.
950 =item iterator ... level2
952 Iterates over the listed level 2 children of the current level 1 article.
956 The value of the I<name> field of the current level 2 article.
958 =item ifLevel2 I<name>
960 Conditional tag, true if the current level 1 article has any listed
963 =item iterator ... level3
965 Iterates over the listed level 3 children of the current level 2 article.
969 The value of the I<name> field of the current level 3 article.
971 =item ifLevel3 I<name>
973 Conditional tag, true if the current level 2 article has any listed
978 Returns a link to the specified article . Due to the way the action
979 list is built, this can be article types defined in derived classes of
980 Generate, like the C<parent> article in Generate::Article.
982 =item money I<data tag>
984 Formats the given value as a monetary value. This does not include a
985 currency symbol. Internally BSE stores monetary values as integers to
986 prevent the loss of accuracy inherent in floating point numbers. You
987 need to use this tag to display any monetary value.
989 =item ifInMenu I<which>
991 Conditional tag, true if the given item can appear in a menu.
993 =item titleImage I<imagename> I<text>
995 Generates an IMG tag if the given I<imagename> is in the title image
996 directory ($IMAGEDIR/titles). If it doesn't exists, produces the
1001 =item embed I<which> I<template>
1003 =item embed I<which> I<template> I<maxdepth>
1007 Embeds the article specified by which using either the specified
1008 template or the articles template.
1010 In this case I<which> can also be an article ID.
1012 I<template> is a filename relative to the templates directory. If
1013 this is "-" then the articles template is used (so you can set
1014 I<maxdepth> without setting the template.) If I<template> contains a
1015 C<$> sign it will be replaced with the name of the original template.
1017 If I<maxdepth> is supplied and is less than the current maximum depth
1018 then it becomes the new maximum depth. This can be used with ifCanEmbed.
1020 =item embed start ... embed end
1022 Marks the range of text that would be embedded in a parent that used
1027 Conditional tag, true if the current article is being embedded.
1033 Needs more documentation.