4 use Constants qw($IMAGEDIR $LOCAL_FORMAT $BODY_EMBED
5 $EMBED_MAX_DEPTH $HAVE_HTML_PARSER);
8 use BSE::Util::Tags qw(tag_article);
9 use BSE::CfgInfo qw(custom_class);
10 use BSE::Util::Iterate;
13 use base 'BSE::ThumbLow';
14 use base 'BSE::TagFormats';
16 our $VERSION = "1.008";
18 my $excerptSize = 300;
21 my ($class, %opts) = @_;
24 Carp->import('confess');
25 confess("cfg missing on generator->new call");
27 $opts{maxdepth} = $EMBED_MAX_DEPTH unless exists $opts{maxdepth};
28 $opts{depth} = 0 unless $opts{depth};
32 bse => BSE::Variables->variables(%opts),
34 my $self = bless \%opts, $class;
35 $self->set_variable_class(articles => "Articles");
45 my ($self, $name, $value) = @_;
47 $self->{vars}{$name} = $value;
52 sub set_variable_class {
53 my ($self, $name, $class) = @_;
55 require Squirrel::Template;
56 $self->set_variable($name => Squirrel::Template::Expr::WrapClass->new($class));
65 # replace commonly used characters
67 # unfortunately some browsers^W^Wnetscape don't support the entities yet <sigh>
71 $text =~ s/\226/-/g; # "--" looks ugly
74 $text =~ s/\’/'/g;
80 my ($self, $articles, $text, $acts, $length) = @_;
82 # remove any block level formatting
83 $self->remove_block($articles, $acts, \$text);
85 $text =~ tr/\n\r / /s;
87 if (length $text > $length) {
88 $text = substr($text, 0, $length);
91 # roughly balance [ and ]
93 1 while $temp =~ s/\s\[[^\]]*\]//; # eliminate matched
95 ++$count while $temp =~ s/\w\[[^\]]*$//; # count unmatched
97 $text .= ']' x $count;
101 # the formatter now adds <p></p> around the text, but we don't
103 my $result = $self->format_body(articles => $articles,
105 $result =~ s!<p>|</p>!!g;
110 # attempts to move the given position forward if it's within a HTML tag,
111 # entity or just a word
112 sub adjust_for_html {
113 my ($self, $text, $pos) = @_;
115 # advance if in a tag
116 return $pos + length $1
117 if substr($text, 0, $pos) =~ /<[^<>]*$/
118 && substr($text, $pos) =~ /^([^<>]*>)/;
119 return $pos + length $1
120 if substr($text, 0, $pos) =~ /&[^;&]*$/
121 && substr($text, $pos) =~ /^([^;&]*;)/;
122 return $pos + length $1
123 if $pos <= length $text
124 && substr($text, $pos-1, 1) =~ /\w$/
125 && substr($text, $pos) =~ /^(\w+)/;
130 # raw html - this has some limitations
131 # the input text has already been escaped, so we need to unescape it
132 # too bad if you want [] in your html (but you can use entities)
134 return unescape_html($_[0]);
138 my ($self, $acts, $articles, $what, $template, $maxdepth, $templater) = @_;
140 $maxdepth = $self->{maxdepth}
141 if !$maxdepth || $maxdepth > $self->{maxdepth};
142 #if ($self->{depth}) {
143 # print STDERR "Embed depth $self->{depth}\n";
145 if ($self->{depth} > $self->{maxdepth}) {
146 if ($self->{maxdepth} == $EMBED_MAX_DEPTH) {
147 return "** too many embedding levels **";
155 if ($what =~ /^alias:([a-z]\w*)$/) {
157 ($embed) = $articles->getBy(linkAlias => $alias)
158 or return "** Cannot find article aliased $alias to be embedded **";;
162 if ($what !~ /^\d+$/) {
163 # not an article id, assume there's an article here we can use
164 $id = $acts->{$what} && $templater->perform($acts, $what, 'id');
165 unless ($id && $id =~ /^\d+$/) {
167 defined $template or $template = "-";
168 return "<:embed $what $template $maxdepth:>";
175 $embed = $articles->getByPkey($id)
176 or return "** Cannot find article $id to be embedded **";;
180 if (ref($self) ne $embed->{generator}) {
181 my $genname = $embed->{generator};
182 $genname =~ s#::#/#g; # broken on MacOS I suppose
188 print STDERR "Cannot load generator $embed->{generator}: $@\n";
189 return "** Cannot load generator $embed->{generator} for article $embed->{id} **";
191 my $top = $self->{top} || $embed;
192 $gen = $embed->{generator}->new
194 admin=>$self->{admin},
195 admin_links => $self->{admin_links},
197 request=>$self->{request},
202 my $olddepth = $gen->{depth};
203 $gen->{depth} = $self->{depth}+1;
204 my $oldmaxdepth = $gen->{maxdepth};
205 $gen->{maxdepth} = $maxdepth;
206 $template = "" if defined($template) && $template eq "-";
207 my $result = $gen->embed($embed, $articles, $template);
208 $gen->{depth} = $olddepth;
209 $gen->{maxdepth} = $oldmaxdepth;
215 my ($self, $acts, $articles, $which, $template, $maxdepth) = @_;
217 my $text = $self->_embed_low($acts, $articles, $which, $template, $maxdepth);
222 sub formatter_class {
223 require BSE::Formatter::Article;
224 return 'BSE::Formatter::Article'
227 # replace markup, insert img tags
241 my $acts = $opts{acts};
242 my $articles = $opts{articles};
243 my $body = $opts{text};
244 my $imagePos = $opts{imagepos};
245 my $abs_urls = $opts{abs_urls};
246 my $auto_images = $opts{auto_images};
247 my $templater = $opts{templater};
248 my $images = $opts{images};
249 my $files = $opts{files};
251 return substr($body, 6) if $body =~ /^<html>/i;
253 my $formatter_class = $self->formatter_class;
255 my $formatter = $formatter_class->new(gen => $self,
257 articles => $articles,
258 abs_urls => $abs_urls,
259 auto_images => \$auto_images,
262 templater => $templater);
264 $body = $formatter->format($body);
266 my $xhtml = $self->{cfg}->entry('basic', 'xhtml', 1);
268 # we don't format named images
269 my @images = grep $_->{name} eq '', @$images;
272 && $self->{cfg}->entry('basic', 'auto_images', 1)
273 && $imagePos ne 'xx') {
274 # the first image simply goes where we're told to put it
275 # the imagePos is [tb][rl] (top|bottom)(right|left)
276 my $align = $imagePos =~ /r/ ? 'right' : 'left';
278 # Offset the end a bit so we don't get an image hanging as obviously
280 # Numbers determined by trial - it can still look pretty rough.
281 my $len = length $body;
289 #my $incr = @images > 1 ? 2*$len / (2*@images+1) : 0;
290 my $incr = $len / @images;
291 # inserting the image tags moves character positions around
292 # so we need the temp buffer
293 if ($imagePos =~ /b/) {
294 @images = reverse @images;
295 if (@images % 2 == 0) {
296 # starting at the bottom, swap it around
297 $align = $align eq 'right' ? 'left' : 'right';
301 for my $image (@images) {
302 # adjust to make sure this isn't in the middle of a tag or entity
303 my $pos = $self->adjust_for_html($body, $incr);
305 my $img = $image->inline(cfg => $self->{cfg}, align => $align);
307 $output .= substr($body, 0, $pos);
308 substr($body, 0, $pos) = '';
309 $align = $align eq 'right' ? 'left' : 'right';
311 $body = $output . $body; # don't forget the rest of it
314 return make_entities($body);
318 my ($self, $article, $articles, $template) = @_;
320 if (defined $template && $template =~ /\$/) {
321 $template =~ s/\$/$article->{template}/;
324 $template = $article->{template}
325 unless defined($template) && $template =~ /\S/;
328 my $html = BSE::Template->get_source($template, $self->{cfg});
330 # the template will hopefully contain <:embed start:> and <:embed end:>
332 # otherwise pull out the body content
333 if ($html =~ /<:\s*embed\s*start\s*:>(.*)<:\s*embed\s*end\s*:>/s
334 || $html =~ m"<\s*body[^>]*>(.*)<\s*/\s*body>"s) {
337 return $self->generate_low($html, $article, $articles, 1);
341 my ($self, $state, $args, $acts, $name, $templater) = @_;
343 my $filter = $self->_get_filter(\$args);
345 $state->{parentid} = undef;
346 my @ids = map { split } DevHelp::Tags->get_parms($args, $acts, $templater);
348 unless ($id =~ /^\d+$|^-1$/) {
349 $id = $templater->perform($acts, $id, "id");
352 @ids = grep /^\d+$|^-1$/, @ids;
354 $state->{parentid} = $ids[0];
356 $self->_do_filter($filter, map Articles->listedChildren($_), @ids);
359 my $cols_re; # cache for below
365 my ($self, $rargs) = @_;
367 if ($$rargs =~ s/filter:\s+(.*)\z//s) {
369 my $orig_expr = $expr;
371 my $cols_expr = '(' . join('|', Article->columns) . ')';
372 $cols_re = qr/\[$cols_expr\]/;
374 $expr =~ s/$cols_re/\$article->{$1}/g;
375 $expr =~ s/ARTICLE/\$article/g;
376 #print STDERR "Expr $expr\n";
377 my $filter = $expr_cache{$expr};
379 $filter = eval 'sub { my $article = shift; '.$expr.'; }';
381 print STDERR "** Failed to compile filter expression >>$expr<< built from >>$orig_expr<<\n";
384 $expr_cache{$expr} = $filter;
396 my ($self, $filter, @articles) = @_;
401 return grep $filter->($_), @articles;
404 sub iter_all_kids_of {
405 my ($self, $state, $args, $acts, $name, $templater) = @_;
407 my $filter = $self->_get_filter(\$args);
409 $state->{parentid} = undef;
410 my @ids = map { split } DevHelp::Tags->get_parms($args, $acts, $templater);
412 unless ($id =~ /^\d+$|^-1$/) {
413 $id = $templater->perform($acts, $id, "id");
416 @ids = grep /^\d+$|^-1$/, @ids;
417 @ids == 1 and $state->{parentid} = $ids[0];
419 $self->_do_filter($filter, map Articles->all_visible_kids($_), @ids);
423 my ($self, $state, $args, $acts, $name, $templater) = @_;
425 my $filter = $self->_get_filter(\$args);
427 $state->{parentid} = undef;
428 my @ids = map { split } DevHelp::Tags->get_parms($args, $acts, $templater);
430 unless ($id =~ /^\d+$/) {
431 $id = $templater->perform($acts, $id, "id");
434 @ids = grep /^\d+$/, @ids;
435 @ids == 1 and $state->{parentid} = $ids[0];
437 $self->_do_filter($filter, map Articles->getByPkey($_), @ids);
441 my ($self, $args) = @_;
443 unless ($self->{gimages}) {
444 require BSE::TB::Images;
445 my @gimages = BSE::TB::Images->getBy(articleId => -1);
446 my %gimages = map { $_->{name} => $_ } @gimages;
447 $self->{gimages} = \%gimages;
451 sort { $a->{name} cmp $b->{name} } values %{$self->{gimages}};
452 if ($args =~ m!^named\s+/([^/]+)/$!) {
454 return grep $_->{name} =~ /$re/i, @gimages;
462 my ($self, $args) = @_;
464 unless ($self->{gfiles}) {
465 my @gfiles = Articles->global_files;
466 my %gfiles = map { $_->{name} => $_ } @gfiles;
467 $self->{gfiles} = \%gfiles;
471 sort { $a->{name} cmp $b->{name} } values %{$self->{gfiles}};
472 if ($args =~ m!^named\s+/([^/]+)/$!) {
474 return grep $_->{name} =~ /$re/i, @gfiles;
476 elsif ($args =~ m(^filter: (.*)$)s) {
478 $expr =~ s/FILE\[(\w+)\]/\$file->$1/g;
479 my $sub = eval 'sub { my $file = shift; ' . $expr . '; }';
481 or die "* Cannot compile sub from filter $expr: $@ *";
482 return grep $sub->($_), @gfiles;
492 $self->{admin} or return;
494 return BSE::Util::Tags->secure($self->{request});
500 my $static = $self->{cfg}->entry('basic', 'static_thumbnails', 1);
501 $self->{admin} and $static = 0;
502 $self->{dynamic} and $static = 0;
507 # implements popimage and gpopimage
508 sub do_popimage_low {
509 my ($self, $im, $class) = @_;
515 static => $self->_static_images,
521 my ($self, $image_id, $class) = @_;
523 my $im = $self->get_gimage($image_id)
524 or return "* Unknown global image '$image_id' *";
526 return $self->do_popimage_low($im, $class);
529 sub _sthumbimage_low {
530 my ($self, $geometry, $im, $field) = @_;
532 return $self->_thumbimage_low($geometry, $im, $field, $self->{cfg}, $self->_static_images);
535 sub tag_gthumbimage {
536 my ($self, $rcurrent, $args, $acts, $name, $templater) = @_;
538 my ($geometry_id, $id, $field) = DevHelp::Tags->get_parms($args, $acts, $templater);
540 return $self->do_gthumbimage($geometry_id, $id, $field, $$rcurrent);
544 my ($self, $acts, $templater, $article_id, $image_tags, $msg) = @_;
547 if ($article_id =~ /^\d+$/) {
549 $article = Articles->getByPkey($article_id);
551 $$msg = "* no article $article_id found *";
555 elsif ($acts->{$article_id}) {
556 my $id = $templater->perform($acts, $article_id, "id");
557 $article = Articles->getByPkey($id);
559 $$msg = "* article $article_id/$id not found *";
564 ($article) = Articles->getBy(linkAlias => $article_id);
566 $$msg = "* no article $article_id found *";
573 my @images = $article->images;
575 for my $tag (split /,/, $image_tags) {
576 if ($tag =~ m!^/(.*)/$!) {
578 ($im) = grep $_->{name} =~ /$re/i, @images
581 elsif ($tag =~ /^\d+$/) {
582 if ($tag >= 1 && $tag <= @images) {
583 $im = $images[$tag-1];
587 elsif ($tag =~ /^[^\W\d]\w*$/) {
588 ($im) = grep $_->{name} eq $tag, @images
593 $$msg = "* no image matching $image_tags found *";
600 sub tag_sthumbimage {
601 my ($self, $args, $acts, $name, $templater) = @_;
603 my ($article_id, $geometry, $image_tags, $field) = split ' ', $args;
606 my $im = $self->_find_image($acts, $templater, $article_id, $image_tags, \$msg)
609 return $self->_sthumbimage_low($geometry, $im, $field);
613 my ($self, $args, $acts, $name, $templater) = @_;
615 my ($article_id, $image_tags, $field, $rest) = split ' ', $args, 4;
618 my $im = $self->_find_image($acts, $templater, $article_id, $image_tags, \$msg)
621 return $self->_format_image($im, $field, $rest);
624 =item iterator vimages I<articles> I<filter>
626 =item iterator vimages I<articles>
628 Iterates over the images belonging to the articles specified.
630 I<articles> can be any of:
636 article - the current article
640 children - all visible children (including stepkids) of the current
645 parent - the parent of the current article
649 I<number> - a numeric article id, such as C<10>.
653 alias(I<alias>) - a link alias of an article
657 childrenof(I<articles>) - an articles that are children of
658 I<articles>. I<articles> can be any normal article spec, so
659 C<childrenof(childrenof(-1))> is valid.
663 I<tagname> - a tag name referring to an article.
667 I<articles> has [] replacement done before parsing.
669 I<filter> can be missing, or either of:
675 named /I<regexp>/ - images with names matching the given regular
680 numbered I<number> - images with the given index.
684 Items for this iterator are vimage and vthumbimage.
689 my ($self, $article, $args, $acts, $name, $templater) = @_;
693 if ($args =~ s!\s+named\s+/([^/]+)/$!!) {
696 elsif ($args =~ s!\s+numbered\s+(\d+)$!!) {
699 my @args = DevHelp::Tags->get_parms($args, $acts, $templater);
701 for my $article_id (map { split /[, ]/ } @args) {
702 my @articles = $self->_find_articles($article_id, $article, $acts, $name, $templater);
703 for my $article (@articles) {
704 my @aimages = $article->images;
706 push @images, grep $_->{name} =~ /$re/, @aimages;
708 elsif (defined $num) {
709 if ($num >= 0 && $num <= @aimages) {
710 push @images, $aimages[$num-1];
714 push @images, @aimages;
726 Retrieve the given field from the current vimage, or display the image.
731 my ($self, $rvimage, $args) = @_;
733 $$rvimage or return '** no current vimage **';
735 my ($field, $rest) = split ' ', $args, 2;
737 return $self->_format_image($$rvimage, $field, $rest);
740 =item vthumbimage geometry field
742 =item vthumbimage geometry
744 Retrieve the given field from the thumbnail of the current vimage or
745 display the thumbnail.
749 sub tag_vthumbimage {
750 my ($self, $rvimage, $args) = @_;
752 $$rvimage or return '** no current vimage **';
753 my ($geo, $field) = split ' ', $args;
755 return $self->_sthumbimage_low($geo, $$rvimage, $field);
759 my ($self, $article_id, $article, $acts, $name, $templater) = @_;
761 if ($article_id =~ /^\d+$/) {
762 my $result = Articles->getByPkey($article_id);
763 $result or print STDERR "** Unknown article id $article_id **\n";
764 return $result ? $result : ();
766 elsif ($article_id =~ /^alias\((\w+)\)$/) {
767 my $result = Articles->getBy(linkAlias => $1);
768 $result or print STDERR "** Unknown article alias $article_id **\n";
769 return $result ? $result : ();
771 elsif ($article_id =~ /^childrenof\((.*)\)$/) {
774 return Articles->all_visible_kids(-1);
777 my @parents = $self->_find_articles($id)
779 return map $_->all_visible_kids, @parents;
782 elsif ($acts->{$article_id}) {
783 my $id = $templater->perform($acts, $article_id, 'id');
784 if ($id && $id =~ /^\d+$/) {
785 return Articles->getByPkey($id);
788 print STDERR "** Unknown article identifier $article_id **\n";
794 my ($self, $articles, $acts, $article, $embedded) = @_;
796 # used to generate the side menu
797 my $section_index = -1;
798 my @sections = $articles->listedChildren(-1);
799 #sort { $a->{displayOrder} <=> $b->{displayOrder} }
800 #grep $_->{listed}, $articles->sections;
801 my $subsect_index = -1;
802 my @subsections; # filled as we move through the sections
803 my @level3; # filled as we move through the subsections
804 my $level3_index = -1;
806 my $cfg = $self->{cfg} || BSE::Cfg->single;
807 my %extras = $cfg->entriesCS('extra tags');
808 for my $key (keys %extras) {
810 my $data = $cfg->entryVar('extra tags', $key);
811 $extras{$key} = sub { $data };
816 my $it = BSE::Util::Iterate->new;
817 my $art_it = BSE::Util::Iterate::Article->new(cfg => $cfg,
818 admin => $self->{admin},
819 top => $self->{top});
824 custom_class($cfg)->base_tags($articles, $acts, $article, $embedded, $cfg),
826 BSE::Util::Tags->static($acts, $self->{cfg}),
827 # for embedding the content from children and other sources
828 ifEmbedded=> sub { $embedded },
830 my ($args, $acts, $name, $templater) = @_;
831 my ($what, $template, $maxdepth) = split ' ', $args;
832 undef $maxdepth if defined $maxdepth && $maxdepth !~ /^\d+/;
833 return $self->_embed_low($acts, $articles, $what, $template, $maxdepth, $templater);
835 ifCanEmbed=> sub { $self->{depth} <= $self->{maxdepth} },
839 my ($args, $acts, $name, $templater) = @_;
840 my ($which, $limit) = DevHelp::Tags->get_parms($args, $acts, $templater);
841 $which or $which = "child";
842 $limit or $limit = $article->{summaryLength};
844 or return "<:summary $which Cannot find $which:>";
845 my $id = $templater->perform($acts, $which, "id")
846 or return "<:summary $which No id returned :>";
847 my $article = $articles->getByPkey($id)
848 or return "<:summary $which Cannot find article $id:>";
849 return $self->summarize($articles, $article->{body}, $acts, $limit);
851 ifAdmin => sub { $self->{admin} },
852 ifAdminLinks => sub { $self->{admin_links} },
854 # for generating the side menu
855 iterate_level1_reset => sub { $section_index = -1 },
856 iterate_level1 => sub {
858 if ($section_index < @sections) {
859 #@subsections = grep $_->{listed},
860 # $articles->children($sections[$section_index]->{id});
861 @subsections = grep { $_->{listed} != 2 }
862 $articles->listedChildren($sections[$section_index]->{id});
871 return tag_article($sections[$section_index], $cfg, $_[0]);
874 # used to generate a list of subsections for the side-menu
875 iterate_level2 => sub {
877 if ($subsect_index < @subsections) {
878 @level3 = grep { $_->{listed} != 2 }
879 $articles->listedChildren($subsections[$subsect_index]{id});
886 return tag_article($subsections[$subsect_index], $cfg, $_[0]);
890 return scalar @subsections;
893 # possibly level3 items
894 iterate_level3 => sub {
895 return ++$level3_index < @level3;
898 tag_article($level3[$level3_index], $cfg, $_[0])
900 ifLevel3 => sub { scalar @level3 },
902 # generate an admin or link url, depending on admin state
905 my ($name, $acts, $func, $templater) = @_;
906 my $item = $self->{admin_links} ? 'admin' : 'link';
909 my $url = $templater->perform($acts, $name, $item);
910 if (!$self->{admin} && $self->{admin_links}) {
911 $url .= $url =~ /\?/ ? "&" : "?";
912 $url .= "admin=0&admin_links=1";
918 $acts->{$_[0]} or return 0;
919 return $acts->{$_[0]}->('listed') == 1;
923 my ($image, $text) = split ' ', $_[0];
924 if (-e $IMAGEDIR."/titles/".$image) {
925 return qq!<img src="/images/titles/!.$image .qq!" border=0>!
928 return escape_html($text);
931 $art_it->make( code => [ iter_kids_of => $self ],
933 plural => 'children_of',
936 $art_it->make( code => [ iter_kids_of => $self ],
937 single => 'ofchild2',
938 plural => 'children_of2',
941 $art_it->make( code => [ iter_kids_of => $self ],
942 single => 'ofchild3',
943 plural => 'children_of3',
946 $art_it->make( code => [ iter_all_kids_of => $self ],
947 single => 'ofallkid',
948 plural => 'allkids_of',
950 $art_it->make( code => [ iter_all_kids_of => $self ],
951 single => 'ofallkid2',
952 plural => 'allkids_of2',
955 $art_it->make( code => [ iter_all_kids_of => $self ],
956 single => 'ofallkid3',
957 plural => 'allkids_of3',
960 $art_it->make( code => [ iter_all_kids_of => $self ],
961 single => 'ofallkid4',
962 plural => 'allkids_of4',
965 $art_it->make( code => [ iter_all_kids_of => $self ],
966 single => 'ofallkid5',
967 plural => 'allkids_of5',
970 $art_it->make( code => [ iter_inlines => $self ],
977 my ($args, $acts, $func, $templater) = @_;
978 my ($name, $align, @rest) =
979 DevHelp::Tags->get_parms($args, $acts, $templater);
984 $im = $current_gimage
988 $im = $self->get_gimage($name)
992 $self->_format_image($im, $align, $rest);
994 $it->make_iterator( [ \&iter_gimages, $self ], 'gimagei', 'gimages',
995 undef, undef, undef, \$current_gimage),
998 my ($name, $field) = split ' ', $_[0], 3;
1000 my $file = $self->get_gfile($name)
1003 $self->_format_file($file, $field);
1005 $it->make_iterator( [ \&iter_gfiles, $self ], 'gfilei', 'gfiles'),
1006 gthumbimage => [ tag_gthumbimage => $self, \$current_gimage ],
1007 sthumbimage => [ tag_sthumbimage => $self ],
1008 simage => [ tag_simage => $self ],
1009 $it->make_iterator( [ iter_vimages => $self, $article ], 'vimage', 'vimages', undef, undef, undef, \$current_vimage),
1010 vimage => [ tag_vimage => $self, \$current_vimage ],
1011 vthumbimage => [ tag_vthumbimage => $self, \$current_vimage ],
1016 my ($body, $case_sensitive, $terms) = @_;
1020 if ($case_sensitive) {
1021 for my $term (@$terms) {
1022 if ($$body =~ /^(.*?)\Q$term/s) {
1023 push(@found, [ length($1), $term ]);
1028 for my $term (@$terms) {
1029 if ($$body =~ /^(.*?)\Q$term/is) {
1030 push(@found, [ length($1), $term ]);
1038 # this takes the same inputs as _make_table(), but eliminates any
1040 sub _cleanup_table {
1041 my ($opts, $data) = @_;
1042 my @lines = split /\n/, $data;
1047 return join(' ', @lines);
1050 # produce a nice excerpt for a found article
1052 my ($self, $article, $found, $case_sensitive, $terms, $type, $body) = @_;
1055 $body = $article->{body};
1057 # we remove any formatting tags here, otherwise we get wierd table
1058 # rubbish or other formatting in the excerpt.
1059 my @files = $article->files;
1060 $self->remove_block('Articles', [], \$body, \@files);
1061 1 while $body =~ s/[bi]\[([^\]\[]+)\]/$1/g;
1064 $body = escape_html($body);
1068 my @found = find_terms(\$body, $case_sensitive, $terms);
1070 my @reterms = @$terms;
1076 # do a reverse sort so that the longer terms (and composite
1077 # terms) are replaced first
1078 my $re_str = join("|", reverse sort @reterms);
1080 my $cfg = $self->{cfg};
1081 if ($cfg->entryBool('search', 'highlight_partial', 1)) {
1082 $re = $case_sensitive ? qr/\b($re_str)/ : qr/\b($re_str)/i;
1085 $re = $case_sensitive ? qr/\b($re_str)\b/ : qr/\b($re_str)\b/i;
1088 # this used to try searching children as well, but it broke more
1091 # we tried hard and failed
1092 # return a generic article
1093 if (length $body > $excerptSize) {
1094 $body = substr($body, 0, $excerptSize);
1095 $body =~ s/\S+\s*$/.../;
1102 splice(@found, 5,-1) if @found > 5;
1103 my $itemSize = $excerptSize / @found;
1105 # try to combine any that are close
1106 @found = sort { $a->[0] <=> $b->[0] } @found;
1107 for my $i (reverse 0 .. $#found-1) {
1108 if ($found[$i+1][0] - $found[$i][0] < $itemSize) {
1109 my @losing = @{$found[$i+1]};
1111 push(@{$found[$i]}, @losing);
1112 splice(@found, $i+1, 1); # remove it
1116 my $highlight_prefix =
1117 $cfg->entry('search highlight', "${type}_prefix", "<b>");
1118 my $highlight_suffix =
1119 $cfg->entry('search highlight', "${type}_suffix", "</b>");
1120 my $termSize = $excerptSize / @found;
1122 for my $term (@found) {
1123 my ($pos, @terms) = @$term;
1124 my $start = $pos - $termSize/2;
1128 $part = substr($body, 0, $termSize);
1132 $part = substr($body, $start, $termSize);
1135 if ($start + $termSize < length $body) {
1136 $part =~ s/\s*\S*$/... /;
1140 $result =~ s{$re}{$highlight_prefix$1$highlight_suffix}ig;
1151 # make whatever text $body points at safe for summarizing by removing most
1152 # block level formatting
1154 my ($self, $articles, $acts, $body, $files) = @_;
1156 my $formatter_class = $self->formatter_class;
1160 my $formatter = $formatter_class->new(gen => $self,
1162 article => $articles,
1163 articles => $articles,
1166 $$body = $formatter->remove_format($$body);
1170 my ($self, $name) = @_;
1172 unless ($self->{gimages}) {
1173 require BSE::TB::Images;
1174 my @gimages = BSE::TB::Images->getBy(articleId => -1);
1175 my %gimages = map { $_->{name} => $_ } @gimages;
1176 $self->{gimages} = \%gimages;
1179 return $self->{gimages}{$name};
1183 my ($self, $name) = @_;
1185 unless ($self->{gfiles}) {
1186 my @gfiles = Articles->global_files;
1187 my %gfiles = map { $_->{name} => $_ } @gfiles;
1188 $self->{gfiles} = \%gfiles;
1191 return $self->{gfiles}{$name};
1194 # note: this is called by BSE::Formatter::thumbimage(), update that if
1196 sub do_gthumbimage {
1197 my ($self, $geo_id, $image_id, $field, $current) = @_;
1200 if ($image_id eq '-' && $current) {
1204 $im = $self->get_gimage($image_id);
1207 or return '** unknown global image id **';
1209 return $self->_sthumbimage_low($geo_id, $im, $field);
1212 sub get_real_article {
1213 my ($self, $article) = @_;
1224 Generate - provides base Squirel::Template actions for use in generating
1231 This is probably better documented in L<templates.pod>.
1235 These tags can be used anywhere, including in admin templates. It's
1236 possible some admin code has been missed, if you find a place where
1237 these cannot be used let us know.
1242 =item kb I<data tag>
1244 Formats the give value in kI<whatevers>. If you have a number that
1245 could go over 1000 and you want it to use the 'k' metric prefix when
1246 it does, use this tag. eg. <:kb file sizeInBytes:>
1248 =item date I<data tag>
1250 =item date "I<format>" I<data tag>
1252 Formats a date or date/time value from the database into something
1253 more human readable. If you don't supply a format then the default
1254 format of "%d-%b-%Y" is used ("20-Mar-2002").
1256 The I<format> is a strftime() format specification, if that means
1257 anything to you. If it doesn't, each code starts with % and are
1258 replaced as follows:
1264 abbreviated weekday name
1272 abbreviated month name
1280 "preferred" date and time representation
1284 day of the month as a 2 digit number
1288 hour (24-hour clock)
1292 hour (12-hour clock)
1296 day of year as a 3-digit number
1300 month as a 2 digit number
1304 minute as a 2 digit number
1308 AM or PM or their equivalents
1312 seconds as a 2 digit number
1316 week number as a 2 digit number (first Sunday as the first day of week 1)
1320 weekday as a decimal number (0-6)
1324 week number as a 2 digit number (first Monday as the first day of week 1)
1328 the locale's appropriate date representation
1332 the locale's appropriate time representation
1336 2-digit year without century
1344 time zone name or abbreviation
1352 Your local strftime() implementation may implement some extensions to
1353 the above, if your server is on a Unix system try running "man
1354 strftime" for more information.
1356 =item bodytext I<data tag>
1358 Formats the text from the given tag in the same way that body text is.
1360 =item ifEq I<data1> I<data2>
1362 Checks if the 2 values are exactly equal. This is a string
1365 The 2 data parameters can either be a tag reference in [], a literal
1366 string inside "" or a single word.
1368 =item ifMatch I<data1> I<data2>
1370 Treats I<data2> as a perl regular expression and attempts to match
1371 I<data1> against it.
1373 The 2 data parameters can either be a tag reference in [], a literal
1374 string inside "" or a single word.
1376 =item cfg I<section> I<key>
1378 =item cfg I<section> I<key> I<default>
1380 Retrieves a value from the BSE configuration file.
1382 If you don't supply a default then a default will be the empty string.
1386 The release number of BSE.
1396 Conditional tag, true if generating in admin mode.
1398 =item iterator ... level1
1400 Iterates over the listed level 1 articles.
1402 =item level1 I<name>
1404 The value of the I<name> field of the current level 1 article.
1406 =item iterator ... level2
1408 Iterates over the listed level 2 children of the current level 1 article.
1410 =item level2 I<name>
1412 The value of the I<name> field of the current level 2 article.
1414 =item ifLevel2 I<name>
1416 Conditional tag, true if the current level 1 article has any listed
1419 =item iterator ... level3
1421 Iterates over the listed level 3 children of the current level 2 article.
1423 =item level3 I<name>
1425 The value of the I<name> field of the current level 3 article.
1427 =item ifLevel3 I<name>
1429 Conditional tag, true if the current level 2 article has any listed
1434 Returns a link to the specified article . Due to the way the action
1435 list is built, this can be article types defined in derived classes of
1436 Generate, like the C<parent> article in Generate::Article.
1438 =item money I<data tag>
1440 Formats the given value as a monetary value. This does not include a
1441 currency symbol. Internally BSE stores monetary values as integers to
1442 prevent the loss of accuracy inherent in floating point numbers. You
1443 need to use this tag to display any monetary value.
1445 =item ifInMenu I<which>
1447 Conditional tag, true if the given item can appear in a menu.
1449 =item titleImage I<imagename> I<text>
1451 Generates an IMG tag if the given I<imagename> is in the title image
1452 directory ($IMAGEDIR/titles). If it doesn't exists, produces the
1455 =item embed I<which>
1457 =item embed I<which> I<template>
1459 =item embed I<which> I<template> I<maxdepth>
1463 Embeds the article specified by which using either the specified
1464 template or the articles template.
1466 In this case I<which> can also be an article ID.
1468 I<template> is a filename relative to the templates directory. If
1469 this is "-" then the articles template is used (so you can set
1470 I<maxdepth> without setting the template.) If I<template> contains a
1471 C<$> sign it will be replaced with the name of the original template.
1473 If I<maxdepth> is supplied and is less than the current maximum depth
1474 then it becomes the new maximum depth. This can be used with ifCanEmbed.
1476 =item embed start ... embed end
1478 Marks the range of text that would be embedded in a parent that used
1483 Conditional tag, true if the current article is being embedded.
1489 Needs more documentation.