allow use of the new template system from static pages
[bse.git] / site / cgi-bin / modules / Generate / Article.pm
1 package Generate::Article;
2 use strict;
3 use BSE::Template;
4 use Constants qw(%LEVEL_DEFAULTS $CGI_URI $ADMIN_URI $IMAGES_URI 
5                  $UNLISTED_LEVEL1_IN_CRUMBS);
6 use BSE::TB::Images;
7 use vars qw(@ISA);
8 use Generate;
9 use BSE::Regen qw(generate_button);
10 use BSE::Util::Tags qw(tag_article);
11 use BSE::TB::ArticleFiles;
12 @ISA = qw/Generate/;
13 use BSE::Util::HTML;
14 use BSE::Arrows;
15 use Carp 'confess';
16 use BSE::Util::Iterate;
17
18 our $VERSION = "1.004";
19
20 my $excerptSize = 300;
21
22 my %level_names = map { $_, $LEVEL_DEFAULTS{$_}{display} }
23   grep { $LEVEL_DEFAULTS{$_}{display} } keys %LEVEL_DEFAULTS;
24
25 sub new {
26   my ($class, %opts) = @_;
27
28   $opts{top} or confess "Please supply 'top' to $class->new";
29
30   return $class->SUPER::new(%opts);
31 }
32
33 sub edit_link {
34   my ($self, $id) = @_;
35   return "$CGI_URI/admin/add.pl?id=$id";
36 }
37
38 sub link_to_form {
39   my ($self, $link, $text, $target) = @_;
40
41   my ($url, $query) = split /\?/, $link;
42   my $form = qq!<form action="$url"!;
43   $form .= qq! target="$target"! if $target;
44   $form .= '>';
45   if (defined $query && length $query) {
46     for my $attr (split /&/, $query) {
47       my ($name, $value) = split /=/, $attr, 2;
48       # I'm assuming none of the values are uri escaped
49       $value = escape_html($value);
50       $form .= qq!<input type=hidden name="$name" value="$value" />!
51     }
52   }
53   $form .= qq!<input type=submit value="!.escape_html($text).'" />';
54   $form .= "</form>";
55
56   return $form;
57 }
58
59 sub generate_low {
60   my ($self, $template, $article, $articles, $embedded) = @_;
61   my %acts;
62   %acts = $self -> baseActs($articles, \%acts, $article, $embedded);
63
64   my $page = BSE::Template->replace($template, $self->{cfg}, \%acts,
65                                     $self->variables);
66
67   %acts = (); # try to destroy any circular refs
68
69   return $page;
70 }
71
72 sub tag_title {
73   my ($cfg, $article, $images, $args, $acts, $funcname, $templater) = @_;
74
75   my $which = $args || 'article';
76
77   exists $acts->{$which} 
78     or return "** no such object $which **";
79
80   my $title = $templater->perform($acts, $which, 'title');
81   my $imagename = $which eq 'article' ? $article->{titleImage} : 
82     $templater->perform($acts, $which, 'titleImage');
83   my $xhtml = $cfg->entry("basic", "xhtml", 1);
84   if ($imagename) {
85     my $html = qq!<img src="/images/titles/$imagename"!;
86     $html .= ' border="0"' unless $xhtml;
87     $html .= qq! class="bse_image_title" alt="$title" />!;
88   }
89   my $im;
90   if ($which eq 'article') {
91     ($im) = grep lc $_->{name} eq 'bse_title', @$images;
92   }
93   else {
94     my $id = $templater->perform($acts, $which, 'id');
95     require BSE::TB::Images;
96     my @images = BSE::TB::Images->getBy(articleId=>$id);
97     ($im) = grep lc $_->{name} eq 'bse_title', @$images;
98   }
99
100   if ($im) {
101     my $src = $im->{src} || "/images/$im->{image}";
102     $src = escape_html($src);
103     return qq!<img src="$src" width="$im->{width}"!
104       . qq! height="$im->{height}" alt="$title" class="bse_image_title" />!;
105   }
106   else {
107     return $title;
108   }
109 }
110
111 sub _default_admin {
112   my ($self, $article, $embedded) = @_;
113
114   my $req = $self->{request};
115   my $html = <<HTML;
116 <table><tr>
117 <td><form action="$CGI_URI/admin/add.pl" name="edit">
118 <input type=submit value="Edit $level_names{$article->{level}}">
119 <input type=hidden name=id value="$article->{id}">
120 </form></td>
121 <td><form action="$ADMIN_URI">
122 <input type=submit value="Admin menu">
123 </form></td>
124 HTML
125   if (exists $level_names{1+$article->{level}}
126       && $req->user_can(edit_add_child=>$article)) {
127     $html .= <<HTML;
128 <td><form action="$CGI_URI/admin/add.pl" name="addchild">
129 <input type=submit value="Add $level_names{1+$article->{level}}">
130 <input type=hidden name=parentid value="$article->{id}">
131 </form></td>
132 HTML
133   }
134   if (generate_button() && $req->user_can(regen_article=>$article)) {
135     $html .= <<HTML;
136 <td><form action="$CGI_URI/admin/generate.pl" name="regen">
137 <input type=hidden name=id value="$article->{id}">
138 <input type=submit value="Regenerate">
139 </form></td>
140 HTML
141   }
142   $html .= "<td>".$self->link_to_form($article->{admin}."&admin=0",
143                                       "Display", "_blank")."</td>";
144   my $parent = $article->parent;
145   if ($article->{link}) {
146     $html .= "<td>"
147       . $self->link_to_form($article->{link}, "On site", "_blank")
148         . "</td>";
149   } elsif ($parent && $parent->{link}) {
150     $html .= "<td>"
151       . $self->link_to_form($parent->{link}, "On site", "_blank")
152         . "</td>";
153   }
154   if ($parent && $parent->{admin} ne $article->{admin} && !$embedded) {
155     $html .= "<td>"
156       .$self->link_to_form($parent->{admin}, "Parent")."</td>";
157   }
158   $html .= <<HTML;
159 </tr></table>
160 HTML
161   return $html;
162 }
163
164 sub abs_urls {
165   my ($self, $article) = @_;
166
167   my $top = $self->{top} || $article;
168
169   $article->{link} =~ /^\w+:/ || $top->{link} =~ /^\w+:/;
170 }
171
172 sub tag_admin {
173   my ($self, $article, $default, $embedded, $arg) = @_;
174
175   $self->{admin} or return '';
176   $self->{request} or return '';
177   my $cfg = $self->{cfg};
178
179   my $name = $arg || $default;
180   my $template = "admin/adminmenu/$name";
181
182   unless (BSE::Template->find_source($template, $cfg)) {
183     return $self->_default_admin($article, $embedded);
184   }
185
186   my $parent = $article->parent;
187   my %acts;
188   %acts =
189     (
190      $self->{request}->admin_tags,
191      article => [ \&tag_article, $article, $cfg ],
192      parent => [ \&tag_article, $parent, $cfg ],
193      ifParent => $parent,
194      ifEmbedded => $embedded,
195     );
196
197   return BSE::Template->get_page($template, $cfg, \%acts);
198 }
199
200 sub tag_thumbimage {
201   my ($self, $rcurrent, $images, $args, $acts, $funcname, $templater) = @_;
202
203   my ($geometry_id, $id, $field) = 
204     DevHelp::Tags->get_parms($args, $acts, $templater);
205
206   return $self->do_thumbimage($geometry_id, $id, $field, $images, $$rcurrent);
207 }
208
209 sub iter_images {
210   my ($self, $images, $arg) = @_;
211
212   if ($arg eq 'all') {
213     return @$images;
214   }
215   elsif ($arg eq 'named') {
216     return grep $_->{name} ne '', @$images;
217   }
218   elsif ($arg =~ m!^named\s+/([^/]+)/$!) {
219     my $re = $1;
220     return grep $_->{name} =~ /$re/i, @$images;
221   }
222   else {
223     return grep $_->{name} eq '', @$images;
224   }
225 }
226
227 =item filen name
228
229 =item filen name field
230
231 =item filen -
232
233 =item filen - field
234
235 Reference an article attached file by name.
236
237 C<filen name> will display a link to the file.
238
239 C<<filen name I<field> >> will display the given field from the file
240 record.  A I<field> of C<url> will be a URL to the file.
241
242 If the file identifier given doesn't exist for the current article the
243 empty string is returned, allowing use as ifFilen.
244
245 The result is unspecified if the I<field> specified isn't one of the
246 image record field names and isn't C<url>.
247
248 =cut
249
250 sub tag_filen {
251   my ($self, $files, $current, $arg, $acts, $funcname, $templater) = @_;
252
253   my ($name, $field, @rest) = 
254     DevHelp::Tags->get_parms($arg, $acts, $templater);
255
256   length $name
257     or return '* name cannot be an empty string *';
258
259   my $file;
260   if ($name eq '-') {
261     $$current
262       or return "* filen - can only be used inside a files iterator *";
263
264     $file = $$current;
265   }
266   else {
267     ($file) = grep $_->{name} eq $name, @$files
268       or return '';
269   }
270
271   return $self->_format_file($file, $field, "@rest");
272 }
273
274 =item iterator begin files
275
276 =item iterator begin files named /foo/
277
278 =item iterator begin files filter: FILE[file_handler] eq 'flv'
279
280 =item file field
281
282 Iterate over files attached to the current article.
283
284 <:file field:> can only access simple attributes.
285
286 <:filen - field:> can also access any inline representations.
287
288 =cut
289
290 sub iter_files {
291   my ($self, $files, $arg, $acts, $funcname, $templater) = @_;
292
293   $arg =~ /\S/
294     or return @$files;
295
296   if ($arg =~ m(^named\s+/([^/]+)/$)) {
297     my $re = $1;
298     return grep $_->{name} =~ /$re/i, @$files;
299   }
300   if ($arg =~ m(^filter: (.*)$)s) {
301     my $expr = $1;
302     $expr =~ s/FILE\[(\w+)\]/\$file->$1/g;
303     my $sub = eval 'sub { my $file = shift; ' . $expr . '; }';
304     $sub
305       or die "* Cannot compile sub from filter $expr: $@ *";
306     return grep $sub->($_), @$files;
307   }
308
309   die "* Unknown type of file filter expression *";
310 }
311
312 =item iterator: crumbs/crumb
313
314 Iterators over the ancestor tree from the article parent to the root.
315
316 Parameters include:
317
318 =over
319
320 =item *
321
322 showtop - the top level article is included even if unlisted
323
324 =item *
325
326 listedonly - only listed articles in the tree are included
327
328 =back
329
330 The default depends on the value of $Constants::UNLISTED_LEVEL1_IN_CRUMBS.
331
332 =cut
333
334 sub iter_crumbs {
335   my ($self, $crumbs, $args) = @_;
336
337   $args ||= $UNLISTED_LEVEL1_IN_CRUMBS ? 'showtop' : 'listedonly';
338   if ($args eq 'showtop') {
339     return @$crumbs;
340   }
341   else {
342     return grep $_->{listed}, @$crumbs;
343   }
344 }
345
346 sub tag_ifUnderThreshold {
347   my ($self, $article, $args) = @_;
348
349   my $count;
350   my $what = $args || '';
351   if ($self->{kids}{$article->{id}}{$what}) {
352     $count = @{$self->{kids}{$article->{id}}{$what}};
353   }
354   else {
355     $count = @{$self->{kids}{$article->{id}}{children}};
356   }
357
358   return $count <= $article->{threshold};
359 }
360
361 sub baseActs {
362   my ($self, $articles, $acts, $article, $embedded) = @_;
363
364   my $cfg = $self->{cfg} || BSE::Cfg->single;
365
366   $self->set_variable(article => $article);
367   $self->set_variable(embedded => $embedded);
368   # used to generate the list (or not) of children to this article
369   my $child_index = -1;
370   my @children = $articles->listedChildren($article->{id});
371
372   # used to generate a navigation list for the article
373   # generate a list of ancester articles/sections
374   # Jason calls these breadcrumbs
375   my @crumbs;
376   my @ancestors;
377   my $temp = $article;
378   while ($temp->{parentid} > 0
379         and my $crumb = $articles->getByPkey($temp->{parentid})) {
380     unshift(@ancestors, $crumb);
381     unshift(@crumbs, $crumb) if $crumb->{listed} == 1 || $crumb->{level} == 1;
382     $temp = $crumb;
383   }
384   #my $crumb_index = -1;
385   #my @work_crumbs; # set by the crumbs iterator
386   my $current_crumb;
387
388   my $parent = $articles->getByPkey($article->{parentid});
389   my $section = @crumbs ? $crumbs[0] : $article;
390
391   my @images = BSE::TB::Images->getBy('articleId', $article->{id});
392   my @unnamed_images = grep $_->{name} eq '', @images;
393   my @iter_images;
394   my $image_index = -1;
395   my $had_image_tags = 0;
396   my @all_files = sort { $b->{displayOrder} <=> $a->{displayOrder} }
397     BSE::TB::ArticleFiles->getBy(articleId=>$article->{id});
398   my @files = grep !$_->{hide_from_list}, @all_files;
399   
400   my $blank = qq!<img src="$IMAGES_URI/trans_pixel.gif"  width="17" height="13" border="0" align="absbottom" alt="" />!;
401
402   my $top = $self->{top} || $article;
403   my $abs_urls = $self->abs_urls($article);
404
405   my $dynamic = $self->{force_dynamic}
406     || (UNIVERSAL::isa($top, 'Article') ? $top->is_dynamic : 0);
407
408   my @stepkids;
409   my @allkids;
410   my @stepparents;
411   if (UNIVERSAL::isa($article, 'Article')) {
412     @stepkids     = $article->visible_stepkids;
413     @allkids      = $article->all_visible_kids;
414     @stepparents  = $article->visible_step_parents;
415   }
416   $self->{kids}{$article->{id}}{stepkids} = \@stepkids;
417   $self->{kids}{$article->{id}}{allkids} = \@allkids;
418   $self->{kids}{$article->{id}}{children} = \@children;
419
420   my $allkids_index;
421   my $current_image;
422   my $current_file;
423   my $art_it = BSE::Util::Iterate::Article->new(cfg =>$cfg, admin => $self->{admin}, top => $self->{top});
424   my $it = BSE::Util::Iterate->new;
425   # separate these so the closures can see %acts
426   my %acts =
427     (
428      $self->SUPER::baseActs($articles, $acts, $article, $embedded),
429      article=>[ \&tag_article, $article, $cfg ],
430      ifTitleImage => 
431      sub { 
432        my $which = shift || 'article';
433        return $acts->{$which} && $acts->{$which}->('titleImage')
434      },
435      title => [ \&tag_title, $cfg, $article, \@images ],
436      thumbnail =>
437      sub {
438        my ($args, $acts, $name, $templater) = @_;
439        my ($which, $class) = split ' ', $args;
440        $which ||= 'article';
441        if ($acts->{$which} && 
442            (my $image = $templater->perform($acts, $which, 'thumbImage'))) {
443          my $width = $templater->perform($acts, $which, 'thumbWidth');
444          my $height = $templater->perform($acts, $which, 'thumbHeight');
445          my $result = '<img src="/images/'.$image
446            .'" width="'.$width
447              .'" height="'.$height.'"';
448          $result .= qq! class="$class"! if $class;
449          $result .= ' border="0" alt="" />';
450          return $result;
451        }
452        else {
453          return '';
454        }
455      },
456      ifThumbnail =>
457      sub {
458        my ($which, $acts, $name, $templater) = @_;
459        $which ||= 'article';
460        return $acts->{$which} && 
461          $templater->perform($acts, $which, 'thumbImage');
462      },
463      ifUnderThreshold => 
464      [ tag_ifUnderThreshold => $self, $article ],
465      ifChildren => sub { scalar @children },
466      iterate_children_reset => sub { $child_index = -1; },
467      iterate_children =>
468      sub {
469        return ++$child_index < @children;
470      },
471      child =>
472      sub {
473        return tag_article($children[$child_index], $cfg, $_[0]);
474      },
475
476      section=> [ \&tag_article, $section, $cfg ],
477
478      # these are mostly obsolete, use moveUp and moveDown instead
479      # where possible
480      ifPrevChild => sub { $child_index > 0 },
481      ifNextChild => sub { $child_index < $#children },
482
483      # generate buttons for administration (only for admin generation)
484      admin=> [ tag_admin=>$self, $article, 'article', $embedded ],
485
486      # transform the article or response body (entities, images)
487      body=>sub {
488        my ($args, $acts, $funcname, $templater) = @_;
489        return $self->format_body(acts => $acts, 
490                                  article => $articles, 
491                                  text => $article->{body},
492                                  imagepos => $article->{imagePos}, 
493                                  abs_urls => $abs_urls,
494                                  auto_images => !$had_image_tags, 
495                                  templater => $templater, 
496                                  images => \@images,
497                                  files => \@all_files,
498                                  articles => $articles);
499      },
500
501      # used to display a navigation path of parent sections
502      $art_it->make_iterator([ iter_crumbs => $self, \@crumbs ],
503                             'crumb', 'crumbs', undef, undef,
504                             'nocache', \$current_crumb),
505      crumbs =>
506      sub {
507        # this is obsolete
508        $cfg->entry('basic', 'warn_obsolete', 0)
509          and print STDERR "* crumbs tag obsolete *\n";
510        return tag_article($current_crumb, $cfg, $_[0]);
511      },
512      
513      # access to parent
514      ifParent => sub { $parent },
515      parent =>
516      sub { return $parent && tag_article($parent, $cfg, $_[0]) },
517      # for rearranging order in admin mode
518      moveDown=>
519      sub {
520        @children > 1 or return '';
521        if ($self->{admin} && $child_index < $#children) {
522          my $html = <<HTML;
523 <a href="$CGI_URI/admin/move.pl?id=$children[$child_index]{id}&amp;d=down"><img src="$IMAGES_URI/admin/move_down.gif" width="17" height="13" border="0" alt="Move Down" align="bottom" /></a>
524 HTML
525          chop $html;
526          return $html;
527        } else {
528          return $blank;
529        }
530      },
531      moveUp=>
532      sub {
533        @children > 1 or return '';
534        if ($self->{admin} && $child_index > 0) {
535          my $html = <<HTML;
536 <a href="$CGI_URI/admin/move.pl?id=$children[$child_index]{id}&amp;d=up"><img src="$IMAGES_URI/admin/move_up.gif" width="17" height="13" border="0" alt="Move Up" align="bottom" /></a>
537 HTML
538          chop $html;
539          return $html;
540        } else {
541          return $blank;
542        }
543      },
544      movekid => [ \&tag_movekid, $self, \$child_index, \@children, $article ],
545      movestepkid =>
546      sub {
547        my ($arg, $acts, $funcname, $templater) = @_;
548        my $html = '';
549        return '' unless $self->{admin};
550        return '' unless @allkids > 1;
551        defined $allkids_index && $allkids_index >= 0 && $allkids_index < @allkids
552          or return '** movestepkid must be inside iterator allkids **';
553        my ($img_prefix, $urladd) = 
554          DevHelp::Tags->get_parms($arg, $acts, $templater);
555        $img_prefix = '' unless defined $img_prefix;
556        $urladd = '' unless defined $urladd;
557        my $top = $self->{top} || $article;
558        my $refreshto = $ENV{SCRIPT_NAME} . "?id=$top->{id}$urladd";
559        my $down_url = "";
560        if ($allkids_index < $#allkids) {
561          $down_url = "$CGI_URI/admin/move.pl?stepparent=$article->{id}&d=swap&id=$allkids[$allkids_index]{id}&other=$allkids[$allkids_index+1]{id}";
562        }
563        my $up_url = "";
564        if ($allkids_index > 0) {
565          $up_url = "$CGI_URI/admin/move.pl?stepparent=$article->{id}&d=swap&id=$allkids[$allkids_index]{id}&other=$allkids[$allkids_index-1]{id}";
566        }
567        
568        return make_arrows($self->{cfg}, $down_url, $up_url, $refreshto, $img_prefix);
569      },
570      ifCurrentPage=>
571      sub {
572        my $arg = shift;
573        $arg && $acts->{$arg} && $acts->{$arg}->('id') == $article->{id};
574      },
575      ifAncestor =>
576      sub {
577        my ($arg, $acts, $name, $templater) = @_;
578        unless ($arg =~ /^\d+$/) {
579          $acts->{$arg} or die "ENOIMPL\n";
580          $arg = $acts->{$arg} && $templater->perform($acts, $arg, 'id')
581            or return;
582        }
583        scalar grep $_->{id} == $arg, @ancestors, $article;
584      },
585      ifStepAncestor => [ \&tag_ifStepAncestor, $article ],
586      # access to images, if any
587      $it->make_iterator([ iter_images => $self, \@images ], 'image', 'images', \@iter_images, \$image_index, 'nocache', \$current_image),
588      # override the generated image tag
589      image =>
590      sub {
591        my ($which, $align, $rest) = split ' ', $_[0], 3;
592
593        $had_image_tags = 1;
594        my $im;
595        if (defined $which && $which =~ /^\d+$/ && $which >=1 
596            && $which <= @images) {
597          $im = $images[$which-1];
598        }
599        else {
600          $im = $current_image;
601        }
602
603        return $self->_format_image($im, $align, $rest);
604      },
605      imagen => 
606      sub {
607        my ($arg, $acts, $funcname, $templater) = @_;
608        my ($name, $align, @rest) =
609          DevHelp::Tags->get_parms($arg, $acts, $templater);
610        my $rest = "@rest";
611
612        my ($im) = grep lc $name eq lc $_->{name}, @images
613          or return '';
614
615        $self->_format_image($im, $align, $rest);
616      },
617      ifImage => sub { $_[0] >= 1 && $_[0] <= @images },
618      thumbimage => [ tag_thumbimage => $self, \$current_image, \@images ],
619      $it->make
620      (
621       plural => "files",
622       single => "file",
623       code => [ iter_files => $self, \@files ],
624       nocache => 1,
625       store => \$current_file,
626      ),
627      filen => [ tag_filen => $self, \@files, \$current_file ],
628      BSE::Util::Tags->make_iterator(\@stepkids, 'stepkid', 'stepkids'),
629      $art_it->make_iterator(undef, 'allkid', 'allkids', \@allkids, \$allkids_index),
630      $art_it->make_iterator(undef, 'stepparent', 'stepparents', \@stepparents),
631      top => [ \&tag_article, $self->{top} || $article, $cfg ],
632      ifDynamic => $dynamic,
633      ifStatic => !$dynamic,
634      ifAccessControlled => [ \&tag_ifAccessControlled, $article ],
635     );
636
637   if ($abs_urls) {
638     my $oldurl = $acts{url};
639     my $urlbase = $cfg->entryErr('site', 'url');
640     $acts{url} =
641       sub {
642         my $value = $oldurl->(@_);
643         return $value if $value =~ /^<:/; # handle "can't do it"
644         unless ($value =~ /^\w+:/) {
645           # put in the base site url
646           $value = $urlbase . $value;
647         }
648         return $value;
649       };
650   }
651   if ($dynamic && $cfg->entry('basic', 'ajax', 0)) {
652     # make sure the ajax tags are left until we do dynamic replacement
653     delete @acts{qw/ajax ifAjax/};
654   }
655
656   return %acts;
657 }
658
659 sub tag_ifStepAncestor {
660   my ($article, $arg, $acts, $name, $templater) = @_;
661
662   unless ($arg =~ /^\d+$/) {
663     $acts->{$arg} or die "ENOIMPL\n";
664     $arg = $acts->{$arg} && $templater->perform($acts, $arg, 'id')
665       or return;
666   }
667   return 0 if $article->{id} < 0;
668   return $article->{id} == $arg || $article->is_step_ancestor($arg);
669 }
670
671 sub tag_ifDynamic {
672   my ($self, $top) = @_;
673
674   # this is to support pregenerated pages being handled as dynamic pages
675   $self->{force_dynamic} and return 1;
676
677   UNIVERSAL::isa($top, 'Article') ? $top->is_dynamic : 0;
678 }
679
680 sub tag_ifAccessControlled {
681   my ($article, $arg, $acts, $funcname, $templater) = @_;
682
683   if ($arg) {
684     if ($acts->{$arg}) {
685       my $id = $templater->perform($acts, $arg, 'id');
686       $article = Articles->getByPkey($id);
687       unless ($article) {
688         print STDERR "** Unknown article $id from $arg in ifAccessControlled\n";
689         return 0;
690       }
691     }
692     else {
693       print STDERR "** Unknown article $arg in ifAccessControlled\n";
694       return 0;
695     }
696   }
697
698   return UNIVERSAL::isa($article, 'Article') ? 
699     $article->is_access_controlled : 0;
700 }
701
702 sub get_image {
703   my ($self, $image_id, $images) = @_;
704
705   my $im;
706   if ($image_id =~ /^\d+$/) {
707     $image_id >= 1 && $image_id <= @$images
708       or return ( undef, "* Out of range image index '$image_id' *" );
709     
710     $im = $images->[$image_id-1];
711   }
712   elsif ($image_id =~ /^[^\W\d]\w*$/) {
713     ($im) = grep $_->{name} eq $image_id, @$images
714       or return ( undef, "* Unknown image identifier '$image_id' *" );
715   }
716   else {
717     return ( undef, "* Unrecognized image '$image_id' *" );
718   }
719   
720   return $im;
721 }
722
723 sub do_popimage {
724   my ($self, $image_id, $class, $images) = @_;
725
726   my ($im, $msg) = $self->get_image($image_id, $images);
727   $im
728     or return $msg;
729
730   return $self->do_popimage_low($im, $class);
731 }
732
733 # note: this is called by BSE::Formatter::thumbimage(), update that if
734 # this is changed
735 sub do_thumbimage {
736   my ($self, $geo_id, $image_id, $field, $images, $current) = @_;
737
738   my $im;
739   if ($image_id eq '-' && $current) {
740     $im = $current
741       or return "** No current image in images iterator **"
742   }
743   else {
744     ($im, my $msg) = $self->get_image($image_id, $images);
745     $im
746       or return $msg;
747   }
748
749   return $self->_sthumbimage_low($geo_id, $im, $field);
750 }
751
752 sub generate {
753   my ($self, $article, $articles) = @_;
754
755   my $html = BSE::Template->get_source($article->{template}, $self->{cfg});
756   $html =~ s/<:\s*embed\s+(?:start|end)\s*:>//g;
757
758   return $self->generate_low($html, $article, $articles, 0);
759 }
760
761 sub tag_movekid {
762   my ($self, $rindex, $rchildren, $article, $args, $acts, 
763       $funcname, $templater) = @_;
764
765   $self->{admin} or return '';
766   @$rchildren or return '';
767
768   my ($img_prefix, $urladd) = 
769     DevHelp::Tags->get_parms($args, $acts, $templater);
770   defined $img_prefix or $img_prefix = '';
771   defined $urladd or $urladd = '';
772
773   my $top = $self->{top} || $article;
774   my $refreshto = $ENV{SCRIPT_NAME} . "?id=$top->{id}$urladd";
775   my $down_url = "";
776   if ($$rindex < $#$rchildren) {
777     $down_url = "$CGI_URI/admin/move.pl?id=$rchildren->[$$rindex]{id}&d=down";
778   }
779   my $up_url = "";
780   if ($$rindex > 0) {
781     $up_url = "$CGI_URI/admin/move.pl?id=$rchildren->[$$rindex]{id}&d=up";
782   }
783
784   return make_arrows($self->{cfg}, $down_url, $up_url, $refreshto, $img_prefix);
785 }
786
787 sub _find_articles {
788   my ($self, $article_id, $article, @rest) = @_;
789
790   if ($article_id eq 'article') {
791     return $article;
792   }
793   elsif ($article_id eq 'children') {
794     return $article->all_visible_kids;
795   }
796   elsif ($article_id eq 'parent') {
797     return $article->parent;
798   }
799   else {
800     return $self->SUPER::_find_articles($article_id, $article, @rest);
801   }
802 }
803
804 1;
805
806 __END__
807
808 =head1 NAME
809
810   Generate::Article - generates articles.
811
812 =head1 SYNOPSIS
813
814 =head1 DESCRIPTION
815
816 =head1 TAGS
817
818
819 =head2 Tag notes
820
821 In your HTML each tag will be preceded by <: and followed by :>
822
823 Tags marked as conditional will require a little more.  Conditional
824 tags can be used in two ways:
825
826 <:ifName args:>true text<:or:>false text<:eif:>
827
828 or:
829
830 <:if Name args:>true text<:or Name:>false text<:eif Name:>
831
832 Tags starting iterator ... are used as iterators, like:
833
834 <:iterator begin name:>
835 repeated text
836 <:iterator separator name:>
837 separator text
838 <:iterator end name:>
839
840 In general, a parameter I<which> can be any one of 'article', 'parent'
841 or 'section'.  In a child iterator it can also be 'child'.  In a
842 crumbs iterator it can also be 'crumbs'.  If I<which> is missing it
843 means the current article.
844
845 =head2 Normal tags
846
847 =over 4
848
849 =item article I<name>
850
851 Access to fields of the article. See L<Article attributes>.
852
853 =item parent I<name>
854
855 Access to fields of the parent article. See L<Article attributes>.
856
857 =item ifParent
858
859 Conditional tag, true if there is a parent.
860
861 =item section I<name>
862
863 Access to the fields of the section containing the article.  See
864 L<Article attributes>.
865
866 =item title I<which>
867
868 The title of the article presented as an image if there is a
869 titleImage or as text.  See L<Tag notes> for values for which.
870
871 =item ifTitleImage I<which>
872
873 Conditional tag, true if the given article has a titleImage,
874
875 =item thumbnail I<which> I<class>
876
877 The thumbnail image as an <img> tag for object I<which> where I<which>
878 is one of the article objects defined.  The optional I<class> adds a
879 class attribute to the tag with that class.
880
881 =item ifThumbnail I<which>
882
883 Conditional tag, true if the article specified by I<which> has a
884 thumbnail.
885
886 =item ifUnderThreshold
887
888 =item ifUnderThreshold stepkids
889
890 =item ifUnderThreshold allkids
891
892 Conditional tag, true if the number of children/stepkids/allkids is
893 less or equal to the article's threshold.
894
895 =item body
896
897 The formatted body of the article.
898
899 =item keywords
900
901 Ignore this one.
902
903 =item iterator ... crumbs [option]
904
905 Iterates over the ancestors of the article.  See the L</item crumbs>.
906
907 I<option> can be empty, "listedonly" or "showtop".  If empty the
908 display of an unlisted level1 ancestor is controlled by
909 $UNLISTED_LEVEL1_IN_CRUMBS, if "listedonly" then an unlisted level1
910 article isn't shown in the crumbs, and it is if "showtop" is the
911 I<option>.  This can be used in <: ifCrumbs :> too.
912
913 =item crumbs I<name>
914
915 Access to the fields of the specific ancestor.  I<name> can be any of
916 the L<Article attributes>.
917
918 =item ifCrumbs [options]
919
920 Conditional tag, true if there are any crumbs.
921
922 See L</iterator ... crumbs [option]> for information on I<option>.
923
924 =item ifChildren
925
926 Conditional tag, true if the article has any children.
927
928 =item iterator ... children
929
930 Iterates over the children of the article.  See the L</item child>.
931
932 =item child I<name>
933
934 Access to the fields of the current child.
935
936 =item summary
937
938 Produces a processed summary of the current child's body.
939
940 =item ifPrevChild
941
942 Conditional tag, true if there is a previous child.  Originally used
943 for generating a move up link, but you can use the moveUp tag for
944 that now.
945
946 =item ifNextChild
947
948 Conditional tag, true if there is a next child.  Originally used to
949 generating a move down link, but you can use the moveDown tag for that
950 now.
951
952 =item ifCurrentPage I<which>
953
954 Conditional tag, true if the given I<which> is the page currently
955 being generated.  This can be used to apply special formatting if a
956 C<level1> or C<level2> article is the current page.
957
958 =item iterator ... images
959
960 Iterates over the unnamed images for the given article.
961
962 =item iterator ... images all
963
964 Iterates over all images for the article.
965
966 =item iterator ... images named
967
968 Iterates over the named images for the article.
969
970 =item iterator ... images named /regexp/
971
972 Iterates over images with names matching the given regular expression.
973 Note that if the expression matches an empty string then unnamed
974 images will be included.
975
976 =item image which field
977
978 Extracts the given field from the specified image.
979
980 I<which> in this can be either an image index to access a specific
981 image, or "-" to access the current image in the images iterator.
982
983 The image fields are:
984
985 =over
986
987 =item articleId
988
989 The identifier of the article the image belongs to.
990
991 =item image
992
993 A partial url of the image, relative to /images/.
994
995 =item alt
996
997 Alternative text of the image.
998
999 =item width
1000
1001 =item height
1002
1003 dimensions of the image.
1004
1005 =item url
1006
1007 the url if any associated with the image
1008
1009 =back
1010
1011 =item image which align
1012
1013 =item image which
1014
1015 =item image
1016
1017 Produces HTML to render the given image.
1018
1019 I<which> can be an image index (1, 2, 3...) to select one of the
1020 images from the current article, or '-' or omitted to select the
1021 current image from the images iterator.  If align is present then the
1022 C<align> attribute of the image is set.
1023
1024 If the image has a URL that <a href="...">...</a> will also be
1025 generated.
1026
1027 =item ifImage imageindex
1028
1029 Condition tag, true if an image exists at the given index.
1030
1031 =item ifImages
1032
1033 =item ifImages all
1034
1035 Conditional tag, true if the article has any images.
1036
1037 =item ifImages named
1038
1039 Conditional tag, true if the article has any named images.
1040
1041 =item ifImages named /regexp/
1042
1043 Conditional tag, true if the article has any named images, where the
1044 name matches the regular expression.
1045
1046 =item ifImages unnamed
1047
1048 Conditional tag, true if the article has any unnamed images.
1049
1050 =item embed child
1051
1052 This has been made more general and been moved, see L<Generate/embed child>.
1053
1054 =item ifDynamic
1055
1056 Tests if the article is dynamically generated.
1057
1058 =item top I<field>
1059
1060 Toplevel article being generated.  This is the page that any articles
1061 are being embedded in.
1062
1063 =item iterator ... files
1064
1065 Iterates over the files attached to the article, setting the file tag.
1066
1067 =item file I<field>
1068
1069 Information from the current file in the files iterator.
1070
1071 The file fields are:
1072
1073 =over
1074
1075 =item *
1076
1077 id - identifier for this file
1078
1079 =item *
1080
1081 articleId - article this file belongs to
1082
1083 =item *
1084
1085 displayName - the filename of the file as displayed
1086
1087 =item *
1088
1089 filename - filename of the file as stored on disk,
1090
1091 =item *
1092
1093 sizeInBytes - size of the file in bytes.
1094
1095 =item *
1096
1097 description - the entered description of the file
1098
1099 =item *
1100
1101 contentType - the MIME content type of the file
1102
1103 =item *
1104
1105 displayOrder - number used to control the listing order.
1106
1107 =item *
1108
1109 forSale - non-zero if the file needs to be paid for to be downloaded.
1110
1111 =item *
1112
1113 download - if this is non-zero BSE will attempt to make the browser
1114 download the file rather than display it.
1115
1116 =item *
1117
1118 whenUploaded - date/time when the file was uploaded.
1119
1120 =item *
1121
1122 requireUser - if non-zero the user must be logged on to download this
1123 file.
1124
1125 =item *
1126
1127 notes - longer descriptive text.
1128
1129 =item *
1130
1131 name - identifier for the file for filelink[]
1132
1133 =item *
1134
1135 hide_from_list - if non-zero the file won't be listed by the files
1136 iterator, but will still be available to filelink[].
1137
1138 =back
1139
1140 =back
1141
1142 =head2 Article attributes
1143
1144 =over 4
1145
1146 =item id
1147
1148 Identifies the article.
1149
1150 =item parentId
1151
1152 The identifier of the parent article.
1153
1154 =item title
1155
1156 The title of the article.  See the title tag
1157
1158 =item titleImage
1159
1160 The name of the title image for the article, if any.  See the title
1161 and ifTitleImage tags.
1162
1163 =item body
1164
1165 The body of the article.  See the body tag.
1166
1167 =item thumbImage
1168
1169 =item thumbWidth
1170
1171 =item thumbHeight
1172
1173 The thumbnail image for the article, if any.  See the thumbnail tag.
1174
1175 =item release
1176
1177 =item expire
1178
1179 The release and expiry dates of the article.
1180
1181 =item keyword
1182
1183 Any keywords for the article.  Indexed by the search engine.
1184
1185 =item link
1186
1187 =item admin
1188
1189 Links to the normal and adminstrative versions of the article.  The
1190 url tag defined by Generate.pm will select the appropriate tag for the
1191 current mode.
1192
1193 =item threshold
1194
1195 The maximum number of articles that should be embeded into the current
1196 article for display.  See the ifUnderThreshold tag.
1197
1198 =item summaryLength
1199
1200 The maximum amount of text displayed in the summary of an article.
1201 See the summary tag.
1202
1203 =item generator
1204
1205 The class used to generate the article.  Should be one of
1206 Generate::Article, Generate::Catalog or Generate::Product.
1207
1208 =item level
1209
1210 The level of the article.  Sections are level1, etc
1211
1212 =item listed
1213
1214 How the article is listed.  If zero then the article can only be found
1215 in a search.  If 1 then the article is listed in menus and article
1216 contents, if 2 then the article is only listed in article contents.
1217
1218 =item lastModified
1219
1220 When the article was last modified.  Currently only used for display
1221 in search results.
1222
1223 =item lastModifiedBy
1224
1225 Set the the current admin user logon if access_control is enabled in the cfg.
1226
1227 =item created
1228
1229 Set to the current date time when a new article is created.
1230
1231 =item createdBy
1232
1233 Set to the current admin user logon if access_control is enabled in
1234 the cfg.
1235
1236 =item author
1237
1238 A user definable field for attributing the article author.
1239
1240 =item pageTitle
1241
1242 An alternate article title which can be used to make search engine
1243 baited oage titles.
1244
1245 =item metaDescription
1246
1247 Article metadata description, used as metadata in the generated HTML
1248 output.
1249
1250 =item metaKeywords
1251
1252 Article metadata keywords, used as metadata in the generated HTML
1253 output.
1254
1255 =back
1256
1257 The following attributes are unlikely to be used in a page:
1258
1259 =over 4
1260
1261 =item displayOrder
1262
1263 Used internally to control the ordering of articles within a section.
1264
1265 =item imagePos
1266
1267 The position of the first image in the body.  The body tag will format
1268 images into the body as specified by this tag.
1269
1270 =item template
1271
1272 The template used to format the article.
1273
1274 =item flags
1275
1276 Flags which can be checked by code or template tags to control behaviours 
1277 specific the the article.
1278
1279 =item force_dynamic
1280
1281 Forces a page to be displayed dynamically with page.pl regardless of
1282 access control.
1283
1284 =item inherit_siteuser_rights
1285
1286 Controls whether the article inherits its parents access controls.
1287
1288 =back
1289
1290 =head2 Admin tags
1291
1292 =over 4
1293
1294 The following tags produce output only in admin mode.
1295
1296 =item admin
1297
1298 Produces buttons and links used for administering the article.
1299
1300 =item moveUp
1301
1302 Generates a move up link if there is a previous child for the current
1303 child.
1304
1305 =item moveDown
1306
1307 Generates a move down link if there is a next child for the current child.
1308
1309 =item admin
1310
1311 Produces buttons and links used for administering the article.
1312
1313 This tag can use a specialized template if it's available.  If you
1314 call it with a parameter, <:admin I<template>:> then it will use
1315 template C<< admin/adminmenu/I<template>.tmpl >>.  When used in an
1316 article template C<< <:admin:> >> behaves like C<< <:admin article:>
1317 >>, when used in a catalog template C<< <:admin:> >> behaves like C<<
1318 <:admin catalog:> >>, when used in a product template C<< <:admin:> >>
1319 behaves like C<< <:admin product:> >>.  See L<Admin Menu Templates>
1320 for the tags available in admin menu templates.
1321
1322 If the template doesn't exist then the old behaviour applies.
1323
1324 =back
1325
1326 =head2 Admin Menu Templates
1327
1328 These tags can be used in the templates included by the C<< <:admin:>
1329 >> tag.
1330
1331 The basic template tags and ifUserCan tag can be used in admin menu
1332 templates.
1333
1334 =over
1335
1336 =item article field
1337
1338 Retrieves a field from the current article.
1339
1340 =item parent field
1341
1342 Retrieves a field from the parent of the current article.
1343
1344 =item ifParent
1345
1346 Conditional tag, true if the current article has a parent.
1347
1348 =item ifEmbedded
1349
1350 Conditional tag, true if the current article is embedded in another
1351 article, in this context.
1352
1353 =back
1354
1355 =cut