# Copyright 2002-2008 Josh Clark and Global Moxie, LLC. This code cannot be
# redistributed without permission from globalmoxie.com.  For more
# information, consult your Big Medium license.
#
# $Id: HTML.pm 3334 2008-09-15 13:33:45Z josh $

package BigMed::Format::HTML;
use strict;
use warnings;
use utf8;
use Carp;
$Carp::Verbose = 1;
use English qw( -no_match_vars );

use base qw(BigMed::Format);
use BigMed::DiskUtil qw(bm_file_path bm_write_file bm_delete_file);
use BigMed::NoBots
  qw(antispam_prefs get_captcha_html clear_captcha_html antispam_display_info);
use BigMed::Comment;
use BigMed::Search;

#register HTML format
my $HTML = 'BigMed::Format::HTML';
$HTML->register_format(
    'HTML',
    suffix         => 'shtml',
    level_filename => 'index'
);

$HTML->add_trigger( 'after_saveprefs_navigation',
    sub { require BigMed::CSS; BigMed::CSS->build_sheet( $_[1] ); } );
$HTML->add_trigger(
    'after_saveprefs_spam',
    sub {
        $HTML->clear_captcha_html( $_[1] );
        $HTML->get_captcha_html( $_[1] );
    }
);

#attribute added for external links
my $NEW_WIN = ' target="newsite"';

###########################################################
# HTML TEMPLATES
###########################################################

$HTML->register_template(
    {   name        => 'home',
        description => 'HTML_TMPL_DESC_home',
        level       => 'top',
    },
    {   name        => 'utility',
        description => 'HTML_TMPL_DESC_utility',
        custom_sec  => 1,
        level_extras =>
          [\&build_tips, \&build_tags, \&build_js_page, \&build_search_page],
    },
    {   name        => 'section',
        custom_sec  => 1,
        description => 'HTML_TMPL_DESC_section',
        level       => 'section',
    },
    {   name        => 'page',
        custom_sec  => 1,
        description => 'HTML_TMPL_DESC_page',
        level       => 'detail',
    },
    {   name        => 'tool_email',
        description => 'HTML_TMPL_DESC_tool_email',
        level       => ['top', 'detail', 'section'],
        extender    => 'email',
    },
    {   name        => 'tool_feeds',
        description => 'HTML_TMPL_DESC_tool_feeds',
        level       => 'top',
        filename    => 'bm~feeds',
        option_pref => 'rss_enable_feed',
    },
    {   name        => 'tool_print',
        description => 'HTML_TMPL_DESC_tool_print',
        level       => ['top', 'detail', 'section'],
        extender    => 'print',
    },
);

###########################################################
# PREFERENCE FLAGS
###########################################################

$HTML->add_section_flag(qw(html_nohome html_nonav html_noparent));
$HTML->add_page_flag(
    qw(
      hideall html_nohome html_nospothome html_nospotsec
      html_nonews html_noqt html_nomain html_nosec html_znosearch
      html_dcomments
      )
);

###########################################################
# CONTROL PANEL GROUPS AND GROUP PREFS
###########################################################

$HTML->add_group( name => 'pagefooter' );
$HTML->add_group( name => 'navigation' );
$HTML->add_group( name => '0document' );
$HTML->add_group( name => 'email' );
$HTML->add_group( name => 'search' );

my %ELEMENTS_PREFS = ();
$HTML->add_group(
    name  => '0links',
    prefs => {
        'html_link_elements' => {
            default   => [qw(head desc)],
            priority  => 100,
            edit_type => 'value_several',
            options   => [
                q{}, qw!head unhead desc full date mod more imore
                  byline sec unsec comments sp lb : . &mdash; ( ) [ ]!
            ],
            labels => {
                'head'     => 'LINK_ELEM_head',
                'unhead'   => 'LINK_ELEM_unhead',
                'desc'     => 'LINK_ELEM_desc',
                'full'     => 'LINK_ELEM_full',
                'date'     => 'LINK_ELEM_date',
                'mod'      => 'LINK_ELEM_mod',
                'more'     => 'LINK_ELEM_more',
                'imore'    => 'LINK_ELEM_imore',
                'byline'   => 'LINK_ELEM_byline',
                'sec'      => 'LINK_ELEM_sec',
                'unsec'    => 'LINK_ELEM_unsec',
                'comments' => 'LINK_ELEM_comments',
                'sp'       => 'LINK_ELEM_space',
                'lb'       => 'LINK_ELEM_linebreak',
                '&mdash;'  => 'LINK_ELEM_emdash',
            },
            edit_params => {
                numfields   => 12,
                description => 'LINK_ELEM_DESC_default elements',
                required    => 1,
            },

        },
        'html_links_bylinelb' => {
            edit_type => 'value_list',
            priority  => 98,
            default   => q{},
            sitewide  => 1,
            options   => [q{}, 'before', 'after', 'both'],
            labels    => {
                ''       => 'LINK_LB_None',
                'before' => 'LINK_LB_Before byline',
                'after'  => 'LINK_LB_After byline',
                'both'   => 'LINK_LB_Both byline',
            },
            edit_params => { description => 'LINKS_DESC_links_bylinelb', },
        },
        'html_links_desclb' => {
            edit_type => 'value_list',
            priority  => 96,
            default   => q{before},
            sitewide  => 1,
            options   => [q{}, 'before', 'after', 'both'],
            labels    => {
                ''       => 'LINK_LB_None',
                'before' => 'LINK_LB_Before desc',
                'after'  => 'LINK_LB_After desc',
                'both'   => 'LINK_LB_Both desc',
            },
            edit_params => { description => 'LINKS_DESC_links_desclb', },
        },
        'html_link_sort_order' => {
            edit_type   => 'sort_order',
            priority    => 95,
            default     => 'priority:pub_time:mod_time|d:d:d',
            edit_params => {
                container_class => 'bmcpDividerField',
                description     => 'SORT_DESC_default sort order',
                numfields       => 3,
                required        => 1,
            },
        },
        'html_links_window' => {
            edit_type   => 'boolean',
            default     => 0,
            priority    => 88,
            sitewide    => 1,
            edit_params => {
                container_class => 'bmcpDividerField',
                option_label    => 'LINKS_DESC_open links in new window',
            },
        },
        'html_links_window_intdomains' => {
            edit_type   => 'value_freeform',
            default     => [],
            priority    => 86,
            sitewide    => 1,
            edit_params => { description => 'LINKS_DESC_internal_domains', }
        },
        'html_overflow_maxpages' => {
            edit_type => 'value_list',
            priority  => 84,
            default   => 5,
            sitewide  => 1,
            options   => [qw(0 1 2 3 4 5 10 1000)],
            labels    => {
                '0'    => 'LINKS_OFLOW_MAX_0',
                '1'    => 'LINKS_OFLOW_MAX_1',
                '2'    => 'LINKS_OFLOW_MAX_2',
                '3'    => 'LINKS_OFLOW_MAX_3',
                '4'    => 'LINKS_OFLOW_MAX_4',
                '5'    => 'LINKS_OFLOW_MAX_5',
                '10'   => 'LINKS_OFLOW_MAX_10',
                '1000' => 'LINKS_OFLOW_MAX_Unlimited',
            },
            edit_params => {
                container_class => 'bmcpDividerField',
                description     => 'LINKS_DESC_overflow_maxpages',
            },
        },
        'html_overflow_numdisplay' => {
            edit_type   => 'number_zeroplus_integer',
            priority    => 82,
            default     => 15,
            sitewide    => 1,
            edit_params => {
                required    => 1,
                description => 'LINKS_DESC_overflow_numdisplay',
            },
        },
        'html_links_includetime' => {
            edit_type   => 'boolean',
            priority    => 75,
            default     => 0,
            edit_params => {
                container_class => 'bmcpDividerField',
                option_label    => 'LINK_DESC_include_time',
            },
        },
        'html_links_exclude_self' => {
            edit_type   => 'boolean',
            priority    => 74,
            default     => 0,
            edit_params => { option_label => 'LINK_DESC_exclude_self', },
        },
        'html_links_moreicon' => {
            default     => 'moreicon_greensq.gif',
            priority    => 72,
            edit_type   => 'raw_text',
            edit_params => {
                prompt_as => 'icon',
                icon_type => 'more',
            },
        },
        'html_links_textsection' => {
            default     => '[ In &lt;%section%&gt; ]',
            edit_type   => 'rich_text_inline',
            priority    => 70,
            edit_params => {
                container_class => 'bmcpDividerField',
                description     => 'LINKS_DESC_textsection',
                required        => 1,
            },
        },
        'html_links_textnavnext' => {
            default     => 'Next',
            priority    => 68,
            edit_type   => 'rich_text_inline',
            edit_params => {
                description => 'BM_rich_text_inline_notice',
                required    => 1,
            },
        },
        'html_links_textnavprev' => {
            default     => 'Previous',
            priority    => 66,
            edit_type   => 'rich_text_inline',
            edit_params => {
                description => 'BM_rich_text_inline_notice',
                required    => 1,
            },
        },
        'html_links_textrelated' => {
            default     => 'Also:',
            priority    => 64,
            edit_type   => 'rich_text_inline',
            edit_params => { description => 'BM_rich_text_inline_notice', },
        },
        'html_links_textmore' => {
            default     => 'more...',
            priority    => 62,
            edit_type   => 'rich_text_inline',
            edit_params => {
                description => 'BM_rich_text_inline_notice',
                required    => 1,
            },
        },
        'html_links_textbyline' => {
            default     => 'By &lt;%author%&gt;.',
            priority    => 60,
            edit_type   => 'rich_text_inline',
            edit_params => {
                description => 'LINKS_DESC_textbyline',
                required    => 1,
            },
        },
    },
);

$HTML->add_group(
    name  => 'images',
    prefs => {
        'html_image_size' => {
            default     => '200x200',
            edit_type   => 'value_list',
            options     => \&_image_size_options,
            labels      => \&_image_size_labels,
            edit_params => { description => 'PREFS_IMAGE_DESC_image_size', },
            priority    => 79,
        },
        'html_annc_image_size' => {
            default   => '100x100',
            edit_type => 'value_list',
            options   => \&_image_size_options,
            labels    => \&_image_size_labels,
            edit_params =>
              { description => 'PREFS_IMAGE_DESC_annc_image_size', },
            priority => 76,
        },
        'html_tip_image_size' => {
            default   => '100x100',
            edit_type => 'value_list',
            options   => \&_image_size_options,
            labels    => \&_image_size_labels,
            edit_params =>
              { description => 'PREFS_IMAGE_DESC_tip_image_size', },
            priority => 73,
        },
        'html_image_magnify' => {
            edit_type   => 'boolean',
            default     => 1,
            edit_params => {
                container_class => 'bmcpDividerField',
                description     => 'PREFS_IMAGE_DESC_enable_mag',
                option_label    => 'PREFS_IMAGE_OPT_enable_mag',
            },
            priority => 69,
        },
        'html_image_magnifysize' => {
            default   => '600x600',
            edit_type => 'value_list',
            options   => \&_image_size_options,
            labels    => \&_image_size_labels,
            edit_params =>
              { description => 'PREFS_IMAGE_DESC_image_magnifysize', },
            priority => 65,
        },
        'html_image_textmagnify' => {
            default     => 'Click to enlarge',
            edit_type   => 'simple_text',
            edit_params => { required => 1, },
            priority    => 63,
        },
    },
);

$HTML->add_group(
    name  => 'tags',
    prefs => {
        'html_tag_headline' => {
            default     => 'Tags',
            sitewide    => 1,
            priority    => 100,
            edit_type   => 'simple_text',
            edit_params => {
                required    => 1,
                description => 'PREFS_TAG_DESC_tag_headline',
            },
        },
        'html_tag_intro' => {
            edit_type => 'rich_text_brief',
            sitewide  => 1,
            priority  => 90,
            default   => <<'TAG_INTRO',
RichText:In addition to using the main navigation, you can find
pages by browsing &ldquo;tags,&rdquo; a set of informal categories that
describe this site&rsquo;s contents.
TAG_INTRO
            edit_params => { description => 'PREFS_TAG_DESC_tag_intro', },
        },
        'html_tag_indiv_headline' => {
            default     => 'Pages tagged &ldquo;&lt;%tag%&gt;&rdquo;',
            sitewide    => 1,
            priority    => 80,
            edit_type   => 'simple_text',
            edit_params => {
                required    => 1,
                description => 'PREFS_TAG_DESC_tag_indiv_headline',
            },
        },
        'html_tag_numdisplay' => {
            edit_type   => 'number_zeroplus_integer',
            sitewide    => 1,
            priority    => 70,
            default     => 15,
            sitewide    => 1,
            edit_params => {
                required    => 1,
                description => 'PREFS_TAG_DESC_tag_numdisplay',
            },
        },
    },
);

$HTML->add_group(
    name  => 'tips_annc',
    prefs => {
        'html_micro_show_subsection' => {
            default     => 1,
            sitewide    => 1,
            priority    => 90,
            edit_type   => 'boolean',
            edit_params => { option_label => 'PREFS_TIPANNC_OPT_show_sub', },
        },
    },
);

$HTML->add_group( name => 'detail', );

$HTML->add_group(
    name  => 'spam',
    prefs => { $HTML->antispam_prefs },    #via BigMed::NoBots
);

$HTML->add_group( name => 'vcomments', );

###########################################################
# LINK BUILDER ROUTINES
###########################################################

my %link_element = (  #callbacks receive args: context, page obj, url, onclick
    head => sub {
        my ( $ct, $pg, $url, $onclick ) = @_;

        my $class = q{bma_head};
        my $sub   = $pg->subtype;
        if ( ( $sub eq 'download' || $sub eq 'av' || $sub eq 'podcast' )
            && $url =~ /[.]([a-zA-Z0-9]+)(\?|$)/ms )
        {             # not a page link; it's a document download, show icon
            $class .= qq| bm_docicon bm_${1}DocIcon| if $1 ne $HTML->suffix;
        }

        return
            qq~<a href="$url"$onclick class="$class" title="~
          . $HTML->strip_html_tags( $pg->title )
          . qq~" rel="bookmark">~
          . $pg->title . '</a>';
    },
    unhead => sub { q~<span class="bma_head">~ . $_[1]->title . '</span>' },
    desc   => sub {
        my $desc = $HTML->inline_rich_text( $_[1]->description, $_[0] )
          or return q{};
        my $lb = $HTML->stash_pref( $_[0], 'html_links_desclb' );
        if ($lb) {
            my $br = '<br' . tag_closer( $_[0] ) . '>';
            $desc =
                $lb eq 'before' ? $br . $desc
              : $lb eq 'after'  ? $desc . $br
              :                   $br . $desc . $br;
        }
        return $desc;
    },
    full   => sub { _content_builder( $_[0], $_[1] ) },
    byline => sub {
        my $authors = $_[1]->authors( $_[0]->relation_cache ) or return q{};
        my $byline = $HTML->stash_pref( $_[0], 'html_links_textbyline' );
        $byline =~ s/&lt;%author%&gt;/$authors/msig;
        $byline = qq~<span class="bma_byline">$byline</span>~;
        my $lb = $HTML->stash_pref( $_[0], 'html_links_bylinelb' );
        if ($lb) {
            my $br = '<br' . tag_closer( $_[0] ) . '>';
            $byline =
                $lb eq 'before' ? $br . $byline
              : $lb eq 'after'  ? $byline . $br
              :                   $br . $byline . $br;
        }
        return $byline;
    },
    more => sub {
        my $more = $HTML->stash_pref( $_[0], 'html_links_textmore' );
        qq~<a href="$_[2]"$_[3] class="bma_more">$more</a>~;
    },
    imore => sub {
        my $more = $HTML->stash_pref( $_[0], 'html_links_moreicon' )
          or return q{};
        my $moretext = $HTML->stash_pref( $_[0], 'html_links_textmore' );
        my $src = $_[0]->site->html_url . "/bm.assets/$more";
        qq~<a href="$_[2]"$_[3] class="bma_more"><img src="$src" alt="$moretext" ~
          . qq~title="$moretext" /></a>~;
    },
    date => sub {
        '<span class="bma_date">'
          . link_formatted_date( $_[0], $_[1]->pub_time )
          . '</span>';
    },
    comments => \&comment_tally_include,
    mod      => sub {
        '<span class="bma_date">'
          . link_formatted_date( $_[0], $_[1]->mod_time )
          . '</span>';
    },
    sec => sub {
        my ( $text, $url ) = _link_section_and_url(@_);
        return q{} if $text eq q{};
        qq~<a href="$url" class="bma_section">$text</a>~;
    },
    unsec => sub {
        my ($text) = ( _link_section_and_url(@_) )[0];
        return q{} if $text eq q{};
        qq~<span class="bma_section">$text</span>~;
    },
    'sp'       => q{ },
    q{:}       => q{:},
    q{.}       => q{.},
    q{&mdash;} => '&mdash;',
    'lb'       => sub { '<br' . tag_closer( $_[0] ) . '>' },
    '('        => '(',
    ')'        => ')',
    '['        => '[',
    ']'        => ']',
);

sub register_link_element {    #allow plugins to register new elements
    my ( $class, $name, $output ) = @_;
    $link_element{$name} = $output;

    #update preference if necessary
    my $ropt = BigMed::Prefs->pref_options('html_link_elements');
    my $found = grep { $_ eq $name } @{$ropt};
    if ( !$found ) {           #update preference
        push @{$ropt}, $name;
        my $rlabel = BigMed::Prefs->pref_labels('html_link_elements');
        $rlabel->{$name} = "LINK_ELEM_$name";
        BigMed::Prefs->register_pref(
            'html_link_elements' => {
                options => $ropt,
                labels  => $rlabel,
            }
        );
    }
    return 1;
}

sub rich_text {    # @_ = [0]class, [1]unfiltered_text, [2]context
    my $html =
      $HTML->stash_pref( $_[2], 'html_links_window' )
      ? _add_new_window_links( $_[0]->SUPER::rich_text( $_[1] ), $_[2] )
      : $_[0]->SUPER::rich_text( $_[1] );

    #rich_text should always come back as xhtml; convert to html if necessary
    $html =~ s{ />}{>}g if index( tag_closer( $_[2] ), '/' ) < 0;    #html
    return $html;
}

sub inline_rich_text {    # @_ = [0]class, [1]unfiltered_text, [2]context
    return $HTML->stash_pref( $_[2], 'html_links_window' )
      ? _add_new_window_links( $_[0]->SUPER::inline_rich_text( $_[1] ),
        $_[2] )
      : $_[0]->SUPER::inline_rich_text( $_[1] );
}

#should probably abstract this new-window code into a separate module;
#using pretty identical code in Format::HTML, Comment, and Web::WebSearch

sub _add_new_window_links {
    my ( $filtered, $context ) = @_;
    $filtered =~ s{(<a([^>]+)href\s*=\s*"([^"]+)"([^>]*)>)}{
        _update_new_win_tag($context,$3,$1,$2,$4);
      }msge;
    return $filtered;
}

sub _update_new_win_tag {
    my ( $context, $url, $tag, $int1, $int2 ) = @_;
    return $tag if !is_new_window_url( $context, $url );
    foreach ( $int1, $int2 ) {
        return $tag if /(target|onclick)\s*=\s*"/i;
    }
    $tag =~ s/(href\s*=\s*"\Q$url\E")/$1$NEW_WIN/;
    return $tag;
}

sub _link_section_and_url {
    my ( $context, $page ) = @_;
    my $site    = $context->site;
    my $section = $context->section;
    my $sec_id =
      ( !$section || $section->is_homepage )
      ? $HTML->allowed_on_home( $context, $page )
      : $HTML->allowed_on_section( $context, $page );
    return q{} if !$sec_id;

    #always return empty string for current section; links don't need
    #to advertise same section.
    return q{} if $section && $sec_id == $section->id;
    my $sec      = $site->section_obj_by_id($sec_id) or return q{};
    my $sec_name = $sec->name;
    my $wrapper  = $HTML->stash_pref( $context, 'html_links_textsection' )
      || q{};
    $wrapper =~ s/&lt;\%section\%&gt;/$sec_name/msig;
    return ( $wrapper, _section_page_url( $site, $sec ) );
}

sub _section_page_url {    #@_ = [0]site_obj [1]section_obj
    my $url = $_[1]->stash('HTML::section_url') || $_[1]->alias;
    return $url if $url;

    $url = $_[0]->directory_url( $_[1] ) . '/index.' . $HTML->suffix;
    $_[1]->set_stash( 'HTML::section_url', $url );
    return $url;
}

my %media_handler = (
    'image'    => \&_link_media_image,
    'document' => \&_link_media_document,
    'av'       => \&_link_media_av,
);

sub _link_assembler {
    my ( $widget, $context, $rheading ) = @_;
    my $name = $widget->name;
    $name = 'spotlight' if index( $name, 'spotlight' ) == 0;
    $rheading ||= {};
    my $div_class =
        index( $name, 'links' ) == 0 ? 'bmw_links'
      : index( $name, 'links' ) < 0 ? "bmw_${name}Links"
      : $name =~ /(\S+)links/ms ? "bmw_${1}Links"
      :                           "bmw_$name";

    return $context->build_markup(
        'wi_links_generic.tmpl',
        links       => $widget->collection,
        div_class   => $div_class,
        widget_name => $name,
        heading     => $rheading->{heading},    #optional
    );
}

sub _link_builder {
    my ( $widget, $context, $obj, $roption ) = @_;
    $roption ||= {};
    my $name =
        $widget->is_overflow ? 'links'
      : index( $widget->name, 'spotlight' ) == 0 ? 'spotlight'
      :                                            $widget->name;
    my $site = $context->site;

    #need to make sure url uses correct section (can't link to sections
    #which are flagged not to be included)
    my $use_sec_id = $roption->{use_section};
    if ( !$use_sec_id ) {
        my $sec = $roption->{no_section} ? 0 : $context->section;
        $use_sec_id =
          ( !$sec || $sec->is_homepage )
          ? $HTML->allowed_on_home( $context, $obj )
          : $HTML->allowed_on_section( $context, $obj );
    }

    #for url, page builder should only be processing pages in active sections
    #except for when it does a preview. So active_page_url should be empty
    #only when displaying a preview page. Use a '#' as a placeholder.
    my $url =
      !$use_sec_id
      ? q{#}
      : (
        $obj->active_page_url(
            $site,
            {   section => $site->section_obj_by_id($use_sec_id),
                rcache  => $context->relation_cache,
                rkids   => $context->active_descendants,
            }
          )
          || q{#}
      );
    $url = $HTML->escape_xml($url);

    my $new_win = q{};
    if ( $obj->subtype eq 'link' ) {
        my $link = (
            $obj->load_related_objects(
                'link_url', $context->relation_cache
            )
        )[0];
        if ( !$link || $link->link_to eq 'page' || $link->new_win eq 'no' ) {
            $new_win = q{};
        }
        elsif ( $link->new_win eq 'yes'
            || is_new_window_url( $context, $url ) )
        {
            $new_win = $NEW_WIN;
        }
    }

    my $element_name = $name eq 'links' ? 'link' : $name;
    my $rchunks = $context->stash("html_${element_name}_LINK_ELEMENTS");
    if ( !$rchunks ) {
        my @elements = $site->get_pref_value( "html_${element_name}_elements",
            $context->section );
        my @chunks;
        foreach my $elem (@elements) {
            next if !$link_element{$elem};
            push @chunks, $link_element{$elem};
        }
        $rchunks = \@chunks;
        $context->set_stash( "html_${element_name}_LINK_ELEMENTS", $rchunks );
    }

    my $link_text = join(
        q{},
        map { ref $_ eq 'CODE' ? $_->( $context, $obj, $url, $new_win ) : $_ }
          @{$rchunks}
    );

    my %link = (
        link     => $link_text,
        pagetype => $obj->subtype,
        pageid   => $obj->id,
    );
    if ( $HTML->stash_pref( $context, "html_${name}_includerelated" ) ) {
        my @related = _load_related_links( $context, $obj );
        if (@related) {
            $link{related} = \@related;
            $link{related_text} =
              $HTML->stash_pref( $context, 'html_links_textrelated' );
        }
    }

    my $rcache = $context->relation_cache;
    foreach my $pair ( $obj->sorted_related_objects( 'media', $rcache ) ) {
        next if $roption->{no_image} && $pair->[1]->data_label eq 'image';
        my %meta = $pair->[0]->metadata;
        my $pos  = $meta{link_position};
        next
          if !$pos
              || $pos eq 'none'
              || ( index( $name, 'spotlight' ) < 0  && $pos eq 'spot' )
              || ( index( $name, 'spotlight' ) == 0 && $pos eq 'links' );

        my $rel_obj = $pair->[1];
        if ( $media_handler{ $rel_obj->data_label } ) {
            my ( $position, $rparams ) =
              $media_handler{ $rel_obj->data_label }
              ->( $widget, $context, $pair, $obj );
            push( @{ $link{"media_$position"} }, $rparams ) if $position;
        }
    }
    return \%link;
}

sub _load_related_links {
    my ( $context, $obj ) = @_;
    my @related =
      $obj->related_links( $context->site, $context->relation_cache );
    foreach my $link (@related) {
        $link->{new_win} ||= q{};
        $link->{new_window} =
            $link->{new_win} eq 'no'  ? q{}
          : $link->{new_win} eq 'yes' ? $NEW_WIN
          : is_new_window_url( $context, $link->{url} ) ? $NEW_WIN
          :                                               q{};
    }
    return @related;
}

###########################################################
# IMAGE BUILDERS
###########################################################

my @MAGNIFY_SIZES = qw(600x600 400x400 800x800 orig);

sub _format_image {
    my ( $context, $pair, $size_or_prefname, $align ) = @_;
    my ( $pointer, $obj ) = @{$pair};
    my %meta = $pointer->metadata;

    my $tsize =
      BigMed::Prefs->pref_exists($size_or_prefname)
      ? $HTML->stash_pref( $context, $size_or_prefname )
      : $size_or_prefname;
    my %format  = $obj->formats;
    my $file    = $format{$tsize} || $format{custom} or return ();
    my $img_url = $context->site->image_url;

    my $magnify = q{};
    my $msize = $HTML->stash_pref( $context, 'html_image_magnifysize' );
    foreach my $try_size ( $msize, @MAGNIFY_SIZES ) {
        next if !$format{$try_size};
        $magnify =
          index( $format{$try_size}, 'url:' ) == 0
          ? substr( $format{$try_size}, 4 )
          : "$img_url/$format{$try_size}";
        last;
    }

    my $magnify_text =
      $HTML->stash_pref( $context, 'html_image_textmagnify' );
    my $caption = $HTML->inline_rich_text( $meta{caption}, $context );
    my $hotlink = $meta{hotlink_url};
    $hotlink = q{} if !$meta{hotlink_url} || $meta{hotlink_url} eq q{http://};
    my $hotlink_window;
    if ($hotlink) {
        $hotlink = $HTML->escape_xml($hotlink);
        $hotlink_window =
          is_new_window_url( $context, $hotlink ) ? $NEW_WIN : q{};
    }

    my $alt_text    = $HTML->strip_html_tags( $obj->title );
    my $esc_caption = $HTML->escape_xml($caption);
    $esc_caption =~ s/&apos;/'/g;    # escaped apostrophes display in ie as-is
    my $max_width = ( $tsize =~ /(\d+)x/ms )[0];

    my $src =
      index( $file, 'url:' ) == 0 ? substr( $file, 4 ) : "$img_url/$file";

    return (
        {   title          => $obj->title,
            alt_text       => $alt_text,
            caption        => $caption,
            esc_caption    => $esc_caption,
            src            => $src,
            type_image     => 1,
            max_width      => $max_width,
            align          => $align,
            hotlink        => $hotlink,
            hotlink_window => $hotlink_window,
            magnify        => $magnify,
            magnify_text   => $magnify_text,
            close          => tag_closer($context),
        }
    );
}

sub _link_media_image {
    my ( $widget, $context, $pair, $page ) = @_;
    my ( $pointer, $obj ) = @{$pair};
    my $name =
        $widget->is_overflow ? 'links'
      : index( $widget->name, 'spotlight' ) == 0 ? 'spotlight'
      :                                            $widget->name;
    my $element_name = $name eq 'links' ? 'link' : $name;
    my $align =
      $HTML->stash_pref( $context, "html_${element_name}_imagepos" );
    return () if !$align || $align eq 'none';
    $align = q{} if $align eq 'above';
    my $size_prefname = "html_${element_name}_imagesize";
    my $rimage = _format_image( $context, $pair, $size_prefname, $align )
      or return ();    #no such image size

    my $page_url = $page->active_page_url(
        $context->site,
        {   section => $context->section,
            rcache  => $context->relation_cache,
            rkids   => $context->active_descendants,
        }
    );
    $page_url ||= q{#};
    $rimage->{page_url} = $HTML->escape_xml($page_url);
    $rimage->{new_window} =
      is_new_window_url( $context, $page_url ) ? $NEW_WIN : q{};
    return ( 'top', $rimage );
}

###########################################################
# DOCUMENT BUILDERS
###########################################################

sub _format_document {
    my ( $context, $pair ) = @_;
    my ( $pointer, $obj )  = @{$pair};
    my $file     = $obj->filename or return ();
    my $site     = $context->site;
    my $filesize = $obj->filesize($site);
    $filesize =~ s/ /&#160;/ms;
    my $suffix = ( $file =~ /.*[.](\S+)$/ms )[0];
    my %meta = $pointer->metadata;
    my $align;

    if ( index( $meta{position}, 'block' ) < 0 ) {
        $align = q{};
    }
    elsif ( !$meta{align} || $meta{align} eq 'default' ) {
        $align = $HTML->stash_pref( $context, 'html_content_objalign' );
    }
    else {
        $align = $meta{align};
    }
    return (
        {   title   => $obj->title,
            caption => $HTML->inline_rich_text( $meta{caption}, $context ),
            url     => $site->doc_url . "/$file",
            type_document => 1,
            filesize      => $filesize,
            filetype      => $suffix,
            align         => $align,
            "ext_$suffix" => 1,
        }
    );
}

sub _link_media_document {
    my ( $widget, $context, $pair ) = @_;
    return ( 'bottom', _format_document( $context, $pair ) );
}

sub _format_av {
    my ($av) = _format_document(@_);
    $av->{type_document} = 0;
    $av->{type_av}       = 1;
    return ($av);
}

sub _link_media_av {
    my ( $widget, $context, $pair ) = @_;
    return ( 'bottom', _format_av( $context, $pair ) );
}

###########################################################
# MAIN-LIST LINK COLLECTORS
###########################################################

$HTML->add_collector_group(
    name            => 'main_list',
    sort_order_pref => 'html_link_sort_order',
    overflow        => {
        filename        => 'index',
        template        => 'utility',
        collector       => \&collect_mainlinks_overflow,
        assembler       => \&assemble_mainlinks_overflow,
        page_limit_pref => 'html_overflow_numdisplay',
        nav_widget      => 'overflow',
        nav_builder     => \&overflow_nav,
        reject_subtypes => 'section',
    }
);

sub assemble_mainlinks_overflow {
    my ( $widget, $context, $rpage_links, $pnum, $total_num ) = @_;

    return $context->build_markup(
        'wi_links_overflow.tmpl',
        links       => $rpage_links,
        div_class   => 'bmw_overflowLinks',
        widget_name => 'overflow',
        navigation  => $widget->build_nav( $context, $pnum, $total_num ),
    );
}

sub overflow_nav {
    my ( $widget, $context, $pnum, $total_num ) = @_;

    #pnum is zero-based, where 0 refers to the main page with the original
    #links; total_num reflects the total number of overflow pages, not
    #including the original main links page.
    #bump pnum and total_num up by one to use 1-based counting and
    #include the original master page in the count.
    $pnum++;
    $total_num++;

    return q{} if $total_num <= 1 && !$context->defer_overflow;

    my $base_url =
        $context->site->directory_url( $context->section ) . q{/}
      . $widget->filename;

    return navigation_bar(
        'context'   => $context,
        'base_url'  => $base_url,
        'pnum'      => $pnum,
        'total_num' => $total_num,
        'defer'     => $context->defer_overflow,
    );
}

my $NAV_NDISPLAY = 7;

sub navigation_bar {
    my %param = @_;
    my ( $context, $base_url, $pnum, $total_num, $defer ) =
      @param{qw(context base_url pnum total_num defer)};

    my $suffix   = $HTML->suffix;
    my $previous = $HTML->stash_pref( $context, 'html_links_textnavprev' );
    my $next     = $HTML->stash_pref( $context, 'html_links_textnavnext' );
    my $dot      = BigMed->bigmed->env('DOT');
    my $next_url =
      ( $pnum < $total_num || $defer )
      ? "$base_url${dot}p" . ( $pnum + 1 ) . ".$suffix"
      : q{};

    if ($defer) {
        return $context->build_markup( 'wi_overflow_nav.tmpl',
            pages => [{ text => $next, url => $next_url }], );
    }
    return q{} if $total_num <= 1;

    my @buttons;
    my $prev_url =
        $pnum == 2 ? "$base_url.$suffix"
      : $pnum > 2 ? "$base_url${dot}p" . ( $pnum - 1 ) . ".$suffix"
      :             q{};
    push @buttons, { text => $previous, url => $prev_url };

    my ( $start, $end );
    if ( $total_num <= $NAV_NDISPLAY ) {
        $start = 1;
        $end   = $total_num;
    }
    else {
        $start =
            $pnum > $NAV_NDISPLAY / 2
          ? $pnum - int( $NAV_NDISPLAY / 2 )
          : 1;
        $end = $start + $NAV_NDISPLAY - 1;
        if ( $end > $total_num ) {
            $end   = $total_num;
            $start = $end - $NAV_NDISPLAY + 1;
        }
    }
    foreach my $num ( $start .. $end ) {
        my $url =
          $num > 1 ? "$base_url${dot}p$num.$suffix" : "$base_url.$suffix";
        push @buttons,
          { text => $num, url => $url, selected => $num == $pnum };
    }

    push @buttons, { text => $next, url => $next_url };
    return $context->build_markup(
        'wi_overflow_nav.tmpl',
        pages => \@buttons,

        #provide these just for convenience if someone wants to hack the
        #basic template to include a "browser page n of total" legend.
        total_pages => $total_num,
        this_page   => $pnum,
    );
}

$HTML->add_widget(
    name            => 'spotlight',
    group           => '0links',
    collects_for    => 'main_list',
    collector       => \&collect_spotlight,
    assembler       => \&_link_assembler,
    limit_pref      => 'html_spotlight_numdisplay',
    priority        => 100,
    reject_subtypes => 'section',
    prefs           => {
        'html_spotlight_includerelated' =>
          { fallback => 'html_links_includerelated' },
        'html_spotlight_elements' => {
            fallback    => 'html_link_elements',
            edit_params => { numfields => 12 },
        },
        'html_spotlight_imagepos' => {
            default  => 'right',
            fallback => 'html_link_imagepos',
        },
        'html_spotlight_imagesize' => {
            default  => '200x200',
            fallback => 'html_link_imagesize'
        },
        'html_spotlight_numdisplay' => {
            default  => 1,
            fallback => 'html_links_numdisplay'
        },
        'html_spotlight_needimage' => {
            default     => 0,
            edit_type   => 'boolean',
            edit_params => {
                option_label => 'SPOTLIGHT_DESC_needimage',
                priority     => 105,
            },
        },
    },
);

$HTML->add_widget(
    name            => 'spotlighttext',
    group           => '0links',
    collector       => \&collect_spotlight,
    collects_for    => 'main_list',
    assembler       => \&_link_assembler,
    limit_pref      => 'html_spotlight_numdisplay',
    priority        => 99,
    reject_subtypes => 'section',
);

$HTML->add_widget(
    name            => 'spotlightimage',
    group           => '0links',
    collects_for    => 'main_list',
    collector       => \&collect_spotlight,
    assembler       => \&assemble_spotlightimage,
    handler         => \&spotlightimage_page_handler,    #for detail pages
    limit_pref      => 'html_spotlight_numdisplay',
    priority        => 98,
    reject_subtypes => 'section',
);

$HTML->add_widget(
    name            => 'links',
    group           => '0links',
    collects_for    => 'main_list',
    collector       => \&collect_links_generic,
    assembler       => \&_link_assembler,
    priority        => 90,
    limit_pref      => 'html_links_numdisplay',
    reject_subtypes => 'section',
    prefs           => {
        'html_links_numdisplay' => {
            default     => 10,
            edit_type   => 'number_zeroplus_integer',
            edit_params => { required => 1 },
            priority    => 110,
        },
        'html_link_imagepos' => {
            default   => 'left',
            priority  => 90,
            edit_type => 'value_list',
            options   => ['left', 'right', 'above', 'none'],
            labels    => {
                'left'  => 'LINK_THUMB_left',
                'right' => 'LINK_THUMB_right',
                'above' => 'LINK_THUMB_above',
                'none'  => 'LINK_THUMB_none',
            },
            edit_params => { container_class => 'bmcpDividerField', }
        },
        'html_link_imagesize' => {
            default   => '60x60',
            priority  => 85,
            edit_type => 'value_list',
            options   => \&_image_size_options,
            labels    => \&_image_size_labels,
        },
        'html_links_includerelated' => {
            default     => 0,
            priority    => 80,
            edit_type   => 'boolean',
            edit_params => { option_label => 'LINK_DESC_include_related', },
        },
    },
);

$HTML->add_widget(
    name            => 'morelinks',
    group           => '0links',
    collects_for    => 'main_list',
    collector       => \&collect_links_generic,
    assembler       => \&_link_assembler,
    priority        => 80,
    limit_pref      => 'html_morelinks_numdisplay',
    reject_subtypes => 'section',
    prefs           => {
        'html_morelinks_elements' => {
            fallback    => 'html_link_elements',
            edit_params => { numfields => 12 },
        },
        'html_morelinks_imagepos' => {
            default  => 'none',
            fallback => 'html_link_imagepos',
        },
        'html_morelinks_imagesize' => { fallback => 'html_link_imagesize', },
        'html_morelinks_includerelated' => {
            default  => 0,
            fallback => 'html_links_includerelated',
        },
        'html_morelinks_numdisplay' => {
            default  => 5,
            fallback => 'html_links_numdisplay'
        },
    },
);

$HTML->add_widget(   #must be collected after mainlist links but before others
    name      => 'quicktease',
    group     => '0links',
    collector => \&collect_quicktease,
    assembler => sub { _assemble_section_include( @_, 'quicktease' ) },
    handler   => sub { _handle_section_include( @_, 'quicktease' ) },
    priority   => 75,                          ## must be just below morelinks
    limit_pref => 'html_quicktease_numdisplay',
    sort_order_pref => 'html_quicktease_sort_order',
    reject_subtypes => 'section',
    sectionwide     => 1,
    use_handler     => 1,
    build_always    => 1,
    prefs           => {
        'html_quicktease_elements' => {
            fallback    => 'html_link_elements',
            edit_params => { numfields => 12 },
        },
        'html_quicktease_imagepos' => {
            default  => 'none',
            fallback => 'html_link_imagepos',
        },
        'html_quicktease_imagesize' => { fallback => 'html_link_imagesize', },
        'html_quicktease_includerelated' => {
            default  => 0,
            fallback => 'html_links_includerelated',
        },
        'html_quicktease_numdisplay' => {
            default  => 3,
            fallback => 'html_links_numdisplay'
        },
        'html_quicktease_hideonhome' => {
            default     => 0,
            edit_type   => 'boolean',
            edit_params => { option_label => 'QUICKTEASE_DESC_hideonhome', }
        },
        'html_quicktease_hideonmain' => {
            default     => 0,
            edit_type   => 'boolean',
            edit_params => { option_label => 'QUICKTEASE_DESC_hideonmain', }
        },
        'html_quicktease_textheading' => {
            default     => 'More from &lt;%section%&gt;',
            edit_type   => 'rich_text_inline',
            edit_params => { description => 'LINKS_DESC_textsection', },
            priority    => 6,
        },
        'html_quicktease_homeheading' => {
            default     => 0,
            edit_type   => 'boolean',
            edit_params => { option_label => 'PREFS_LINKS_homeheading', },
            sitewide    => 1,
            priority    => 5,
        },
        'html_quicktease_sort_order' => {
            fallback    => 'html_link_sort_order',
            edit_params => { numfields => 3, },
        },
    },
);

$HTML->add_widget(
    name            => 'latest',
    group           => '0links',
    collector       => \&collect_links_generic,
    assembler       => sub { _assemble_section_include( @_, 'latest' ) },
    handler         => sub { _handle_section_include( @_, 'latest' ) },
    priority        => 70,
    limit_pref      => 'html_latest_numdisplay',
    sort_order_pref => 'html_latest_sort_order',
    reject_subtypes => 'section',
    sectionwide     => 1,
    use_handler     => 1,
    build_always    => 1,
    prefs           => {
        'html_latest_elements' => {
            fallback    => 'html_link_elements',
            edit_params => { numfields => 12 },
        },
        'html_latest_imagepos' => {
            default  => 'none',
            fallback => 'html_link_imagepos',
        },
        'html_latest_imagesize' => { fallback => 'html_link_imagesize', },
        'html_latest_includerelated' => {
            default  => 0,
            fallback => 'html_links_includerelated',
        },
        'html_latest_numdisplay' => {
            default  => 5,
            fallback => 'html_links_numdisplay'
        },
        'html_latest_textheading' => {
            default     => 'More from &lt;%section%&gt;',
            edit_type   => 'rich_text_inline',
            edit_params => { description => 'LINKS_DESC_textsection', },
            priority    => 6,
        },
        'html_latest_homeheading' => {
            default     => 0,
            edit_type   => 'boolean',
            edit_params => { option_label => 'PREFS_LINKS_homeheading', },
            sitewide    => 1,
            priority    => 5,
        },
        'html_latest_sort_order' => {
            fallback    => 'html_link_sort_order',
            edit_params => { numfields => 3, },
        },
    },
);

$HTML->add_widget(
    name            => 'news',
    group           => '0links',
    collector       => \&collect_news,
    assembler       => \&assemble_news,
    handler         => \&handle_news,
    priority        => 40,
    limit_pref      => 'html_news_numdisplay',
    sort_order_pref => 'html_news_sort_order',
    reject_subtypes => 'section',
    sectionwide     => 1,
    use_handler     => 1,
    build_always    => 1,
    prefs           => {
        'html_news_elements' => {
            fallback    => 'html_link_elements',
            edit_params => { numfields => 12 },
            sitewide    => 1,
        },
        'html_news_imagepos' => {
            default  => 'none',
            fallback => 'html_link_imagepos',
            sitewide => 1,
        },
        'html_news_imagesize' => {
            fallback => 'html_link_imagesize',
            sitewide => 1,
        },
        'html_news_includerelated' => {
            default  => 0,
            fallback => 'html_links_includerelated',
            sitewide => 1,
        },
        'html_news_numdisplay' => {
            default  => 5,
            fallback => 'html_links_numdisplay',
            sitewide => 1,
        },
        'html_news_section' => {
            default     => q{},
            edit_type   => 'select_section',
            edit_params => { description => 'NEWS_DESC_news_section', },
            sitewide    => 1,
        },
        'html_news_sort_order' => {
            fallback    => 'html_link_sort_order',
            edit_params => { numfields => 3, },
            sitewide    => 1,
        },
    },
);

$HTML->add_widget(
    name    => 'sections',
    group   => '0links',
    handler => \&wi_sections,  #has all the prefs of a collector, but it's not
    sectionwide => 1,
    prefs       => {
        'html_sections_elements' => {
            fallback    => 'html_link_elements',
            edit_params => { numfields => 12 },
        },
        'html_sections_imagepos' => {
            default  => 'none',
            fallback => 'html_link_imagepos',
        },
        'html_sections_imagesize' => { fallback => 'html_link_imagesize', },
        'html_sections_includerelated' => { default => 0 },    #always off
    },
);

sub wi_sections {    # it looks like a collector, but it's not.
    my ( $context, $obj ) = @_;
    return q{} if !$context->is_active;

    my $site = $context->site;
    my $sec = $context->section || $site->homepage_obj();
    my @links;
    my $widget = $HTML->widget('sections');
    foreach my $sec_id ( $sec->kids ) {
        my $sub = $site->section_obj_by_id($sec_id) or next;
        next if !$sub->active;
        my $page = $sub->section_page_obj or next;
        my $can_show =
          ( !$sec || $sec->is_homepage )
          ? $HTML->allowed_on_home( $context, $page )
          : $HTML->allowed_on_section( $context, $page );
        next if !$can_show;
        $page->set_slug('index');
        push @links, _link_builder( $widget, $context, $page );
    }
    return q{} if !@links;

    return $context->build_markup(
        'wi_links_generic.tmpl',
        links       => \@links,
        div_class   => 'bmw_sections',
        widget_name => 'sections',
    );
}

sub collect_spotlight {
    my ( $self, $context, $obj ) = @_;
    _is_valid_spotlight( $context, $obj ) or return 0;

    #add to any spotlight widgets that we have
    my %widget = $context->widget_map;
    $widget{spotlight}->add_to_collection(
        _link_builder( $widget{spotlight}, $context, $obj ) )
      if $widget{spotlight};

    $widget{spotlighttext}->add_to_collection(
        _link_builder(
            $widget{spotlighttext}, $context, $obj, { no_image => 1 }
        )
    ) if $widget{spotlighttext};

    if ( $widget{spotlightimage} ) {
        my @images;
        foreach my $pair ( _gather_spotlight_images( $context, $obj ) ) {
            my ( $position, $rparams ) =
              $media_handler{image}
              ->( $widget{spotlightimage}, $context, $pair, $obj );
            push @images, $rparams;
        }
        $widget{spotlightimage}->add_to_collection( { images => \@images } );
    }
    return 1;
}

sub _gather_spotlight_images {
    my ( $context, $obj ) = @_;
    my @images;
    my $rcache = $context->relation_cache;
    foreach my $pair ( $obj->sorted_related_objects( 'media', $rcache ) ) {
        next if $pair->[1]->data_label ne 'image';
        my %meta = $pair->[0]->metadata;
        my $pos  = $meta{link_position};
        next if !$pos || $pos eq 'none' || $pos eq 'links';
        push @images, $pair;
    }
    return @images;
}

sub assemble_spotlightimage {
    my ( $widget, $context ) = @_;
    return $context->build_markup( 'wi_spotlightimage.tmpl',
        pages => $widget->collection, );
}

sub spotlightimage_page_handler {
    my ( $context, $obj ) = @_;
    my @images;
    foreach my $pair ( _gather_spotlight_images( $context, $obj ) ) {
        my $rparam =
          _format_image( $context, $pair, 'html_spotlight_imagesize', q{} );
        push @images, $rparam if $rparam;

    }
    return $context->build_markup( 'wi_spotlightimage_page.tmpl',
        images => \@images, );
}

sub _assemble_section_include {
    my ( $widget, $context, $type ) = @_;
    my ( $site, $sec ) = ( $context->site, $context->section );

    my $heading =
      BigMed::Prefs->pref_exists("html_${type}_textheading")
      ? $HTML->stash_pref( $context, "html_${type}_textheading" )
      : q{};
    if ( ( !$sec || $sec->is_homepage ) && $heading ) {
        $heading = q{}
          if BigMed::Prefs->pref_exists("html_${type}_homeheading")
              && !$site->get_pref_value("html_${type}_homeheading");
    }

    if ($heading) {
        my $url = _section_page_url( $context->site, $sec );
        my $name = qq~<a href="$url">~ . $sec->name . '</a>';
        $heading =~ s/&lt;%section%&gt;/$name/msg;
    }

    my $html;
    $html = _link_assembler( $widget, $context, { heading => $heading } )
      if $context->is_active;
    $html ||= '<!-- nothing to display -->';
    my $slug_path = _slug_path( $context, $sec );
    $slug_path &&= "/$slug_path";

    my $dot  = BigMed->bigmed->env('DOT');
    my $file = $site->html_dir . "$slug_path/bm$dot$type.shtml";
    bm_write_file( $file, $html, { build_path => 1 } );
    return $html;
}

sub _handle_section_include {
    my ( $context, $obj, $rparam, $type, $ext ) = @_;
    my $this_sec = $context->section;
    my $sec      = $this_sec;
    my $slug     = $rparam->{slug} || q{};
    my $is_home  = !$sec || $this_sec->is_homepage;
    if ( $slug eq '@all' ) {
        $sec = q{};    #empty section will generate for homepage
    }
    elsif ( $slug && ( $is_home || $this_sec->slug ne $slug ) ) {
        $sec = $context->site->section_obj_by_slug( $rparam->{slug} );
        return qq~<!-- unknown slug "$rparam->{slug}" in <%$type%> -->~
          if !$sec;
    }
    my $include = _include_dir( $context, $sec );
    $ext ||= 'shtml';
    my $dot = BigMed->bigmed->env('DOT');
    return qq|<!--#include virtual="$include/bm$dot$type.$ext" -->|;
}

sub _include_dir {    #returns directory path from web root
    my ( $context, $sec ) = @_;
    return ( $sec && !$sec->is_homepage )
      ? _section_dir_path( $context, $sec )
      : pagedir_from_webroot($context);
}

sub collect_news {
    my ( $self, $context, $obj ) = @_;
    my $news = $HTML->stash_pref( $context, 'html_news_section' );
    my $sec = $context->section;
    if ( !$news || !$sec || $news != $sec->id ) { #not a news section, spam it
        $self->mark_full();
        return 1;
    }
    my %flag = $obj->flags;
    return 0 if $flag{html_nonews} || $flag{hideall};

    return $self->add_to_collection( _link_builder( $self, $context, $obj ) );
}

sub assemble_news {
    my ( $widget, $context ) = @_;
    my $news = $HTML->stash_pref( $context, 'html_news_section' );
    my $sec = $context->section;
    return q{} if !$news || !$sec || $news != $sec->id;
    return _assemble_section_include( $widget, $context, 'news' );
}

sub handle_news {
    my $context = shift;
    my $news = $HTML->stash_pref( $context, 'html_news_section' );
    return q{} if !$news;
    my $sec = $context->site->section_obj_by_id($news) or return q{};
    my $include = _include_dir( $context, $sec );
    my $dot = BigMed->bigmed->env('DOT');
    return qq|<!--#include virtual="$include/bm${dot}news.shtml" -->|;
}

sub _is_valid_spotlight {
    my ( $context, $obj ) = @_;
    my %flag = $obj->flags;
    return 0 if $flag{hideall};

    my $sec = $context->section;
    my $is_home = ( !$sec || $sec->is_homepage );

    if ($is_home) {
        return 0
          if $flag{html_nohome}
              || $flag{html_nospothome}
              || !$HTML->allowed_on_home( $context, $obj );
    }
    else {
        return 0 if $flag{html_nosec} || $flag{html_nospotsec};
        if ( $flag{html_nomain} ) {

            #no_main actually means no parent, show on any specifically
            #assigned sections
            my %belongs = map { $_ => 1 } $obj->sections;
            return 0 if !$belongs{ $sec->id };
        }
        return 0 if !$HTML->allowed_on_section( $context, $obj );
    }

    if ( $HTML->stash_pref( $context, 'html_spotlight_needimage' ) ) {
        my $has_spotlight_image;
        my $rcache = $context->relation_cache;
        foreach my $pair ( $obj->sorted_related_objects( 'media', $rcache ) )
        {
            next if $pair->[1]->data_label ne 'image';
            my %meta = $pair->[0]->metadata;
            $has_spotlight_image = 1, last
              if $meta{link_position}
                  && (   $meta{link_position} eq 'spot'
                      || $meta{link_position} eq 'all' );
        }
        return 0 if !$has_spotlight_image;
    }
    return 1;
}

sub collect_quicktease {
    my ( $self, $context, $obj ) = @_;
    my %flag = $obj->flags;
    my $hideonhome =
      $HTML->stash_pref( $context, 'html_quicktease_hideonhome' );
    my $hideonmain =
      $HTML->stash_pref( $context, 'html_quicktease_hideonmain' );
    return 0
      if $flag{html_noqt}
          || ( $hideonhome && $context->on_homepage($obj) )
          || ( $hideonmain && $context->on_section($obj) );
    return collect_links_generic( $self, $context, $obj );
}

sub collect_links_generic {
    my ( $self, $context, $obj ) = @_;
    my %flag = $obj->flags;
    return 0 if $flag{hideall} || $flag{html_nosec};
    my $sec = $context->section;
    if ( !$sec || $sec->is_homepage ) {    #homepage
        return 0
          if $flag{html_nohome} || !$HTML->allowed_on_home( $context, $obj );
    }
    else {
        if ( $flag{html_nomain} )
        {    #no parents; disallow on secs not explictly assigned
            my %belongs = map { $_ => 1 } $obj->sections;
            return 0 if !$belongs{ $sec->id };
        }
        return 0 if !$HTML->allowed_on_section( $context, $obj );
    }
    return $self->add_to_collection( _link_builder( $self, $context, $obj ) );
}

sub _allowed_on_home {    #legacy for plugins created before v2.0.4
    return $HTML->allowed_on_home(@_);
}

sub _allowed_on_section {    #legacy for plugins created before v2.0.4
    return $HTML->allowed_on_section(@_);
}

sub collect_mainlinks_overflow {
    my ( $self, $context, $obj ) = @_;
    my $sec = $context->section;
    return $self->mark_full if !$sec || $sec->is_homepage;

    #check overall limit
    my $site       = $context->site;
    my $page_limit = $site->get_pref_value('html_overflow_maxpages') + 0
      or return $self->mark_full;
    my $per_page = $site->get_pref_value('html_overflow_numdisplay') + 0
      or return $self->mark_full;
    return $self->mark_full
      if $page_limit < 1000 && $self->count >= $page_limit * $per_page;

    my $on_sec;
    my $sec_id = $sec->id;
    foreach ( $obj->sections ) {
        $on_sec = 1, last if $_ == $sec_id;
    }
    my %flag = $obj->flags;

    return 0
      if $flag{hideall}
          || $flag{html_nosec}
          || (!$on_sec
              && ( $flag{html_nomain}
                  || !$HTML->allowed_on_section( $context, $obj ) )
          );
    return $self->add_to_collection( _link_builder( $self, $context, $obj ) );

}

###########################################################
# PAGE CONTENTS
###########################################################

$HTML->add_widget(
    name     => 'content',
    handler  => \&wi_content,
    group    => 'detail',
    priority => 90,
    prefs    => {
        'html_content_addtags' => {
            edit_type   => 'boolean',
            default     => 1,
            sitewide    => 1,
            priority    => 95,
            edit_params => { option_label => 'PREFS_CONTENT_DESC_addtags', },
        },
        'html_content_objalign' => {
            default   => 'right',
            edit_type => 'body_position',
            priority  => 85,
            options   => ['left', 'center', 'right'],
            labels    => {
                'left'   => 'HTML_CONTENT_objalign_left',
                'center' => 'HTML_CONTENT_objalign_center',
                'right'  => 'HTML_CONTENT_objalign_right',
            },
            edit_params => { description => 'PREFS_CONTENT_DESC_objalign', }
        },
        'html_content_oneclick_edit' => {
            edit_type => 'simple_text',
            default   => 'Edit page',
            priority  => 78,
            edit_params =>
              { description => 'PREFS_CONTENT_DESC_oneclick_edit', },
        },
        'html_content_oneclick_new' => {
            edit_type => 'simple_text',
            default   => 'New page',
            priority  => 75,
            edit_params =>
              { description => 'PREFS_CONTENT_DESC_oneclick_new', },
        },
        'html_content_oneclick_hide' => {
            edit_type => 'simple_text',
            default   => 'Hide edit links',
            priority  => 73,
            edit_params =>
              { description => 'PREFS_CONTENT_DESC_oneclick_hide', },
        }
    },
);

sub wi_content {
    my ( $context, $obj, $rparam ) = @_;
    my $html = _content_builder(@_);
    my %oneclick = oneclick_params( $context, $obj );
    return $context->build_markup(
        'wi_content_widget.tmpl',
        content_html => $html,
        %oneclick,
    );
}

sub oneclick_params {
    my ( $context, $obj ) = @_;
    my $base_url = $context->stash('HTML_oneclick_edit_url');
    if ( !$base_url ) {
        my $bm = BigMed->bigmed;
        my $pdiv = $bm->env('USE_BMQUERY') ? '?' : '/';
        $base_url =
            $bm->env('MOXIEBIN')
          . "/bm-editor.cgi${pdiv}edit/"
          . $context->site->id
          . '/page/';
        $context->set_stash( 'HTML_oneclick_edit_url', $base_url );
    }
    my $edit_text =
      $HTML->stash_pref( $context, 'html_content_oneclick_edit' );
    my $new_text = $HTML->stash_pref( $context, 'html_content_oneclick_new' );
    my $hide_text =
      $HTML->stash_pref( $context, 'html_content_oneclick_hide' );
    my $use_js = ( index( $OSNAME, 'MSWin' ) >= 0 )
      || $context->stash('_BUILD_PREVIEW');
    if ($use_js) {
        $edit_text =~ s/'/\\'/g;
        $hide_text =~ s/'/\\'/g;
    }
    return (
        oneclick_editurl => $base_url . $obj->id,
        oneclick_newurl  => $base_url,
        oneclick_js      => $use_js,
        oneclick_edit    => $edit_text,
        oneclick_new     => $new_text,
        oneclick_hide    => $hide_text,
    );
}

#tags to consider as paragraphs
my $graf_elem = 'p | ul | ol | dl | blockquote | pre | table | h\d';
my $graf_tag  = qr/< \s* (?:$graf_elem) \s* [^>]* >/msx;

my %embed_handler = (
    'image'     => \&_embed_image,
    'pullquote' => \&_embed_pullquote,
    'document'  => \&_embed_document,
    'av'        => \&_embed_av,
);

sub _content_builder {
    my ( $context, $obj, $rparam, $divclass ) = @_;
    my $text = $HTML->rich_text( $obj->content, $context );
    my $callback = $obj->content_hook;

    if ( $HTML->stash_pref( $context, 'html_content_addtags' ) ) {
        $text .= wi_tags( $context, $obj );
    }

    if ( $rparam->{no_media} ) {    #no embed
        $text = $callback ? $callback->( $obj, $context, $text ) : $text;
        return $context->build_markup(
            'wi_content.tmpl',
            content => $text,
            class   => $divclass || 'bmw_pageContent',
        );
    }

    #discover the relationships that embed content
    my @embedders;
    my $rembedders = $context->stash( 'EMBEDDERS_' . ref $obj );
    if ( !$rembedders ) {
        foreach my $relation ( $obj->data_relationships ) {
            my %info = $obj->relationship_info($relation);
            push( @embedders, $relation ) if $info{embed};
        }

        #don't set it to a straight reference, or the values get wiped
        #after doing the map to related objects ... have no idea why.
        #so save a ref to a different anonymous array instead of \@embedders
        $context->set_stash( 'EMBEDDERS_' . ref $obj, [@embedders] );
    }
    else {
        @embedders = @{$rembedders};
    }

    #collect embedded relationship objects by paragraph
    my %embed_obj;
    my $rcache = $context->relation_cache;
    foreach my $embed ( map { $obj->load_related_objects( $_, $rcache ) }
        @embedders )
    {
        my ( $pos, $priority, $mod_time );
        if ( ref $embed eq 'ARRAY' ) {
            my %meta = $embed->[0]->metadata();
            $pos      = $meta{position};
            $priority = $meta{priority} || 500;
            $mod_time = $embed->[0]->mod_time;
        }
        else {
            $pos = $embed->position;
            $priority =
              $embed->can('priority') ? ( $embed->priority || 0 ) : 500;
            $mod_time = $embed->mod_time;
        }
        next if !$pos || $pos eq 'hidden';
        push @{ $embed_obj{$pos} }, [$embed, $priority, $mod_time];
    }

    my ( $above, $below );
    if (%embed_obj) {    #chop up the text by paragraph and insert objects
        my @grafs;

        while (
            $text =~ m{
              (.*?)         #text that appears before any grafs
              ($graf_tag)        #the tag itself
              (.*?)         #text that appears til the next graf-ish tag
              (?=($graf_tag)|\z) #next tag, or the end
          }gmsx
          )
        {
            push @grafs, $1 if $1;    #text that appears before any grafs
            push @grafs, "$2$3";      #tag and text
        }
        @grafs = ($text) if !@grafs;    #no graf tags, treat text as one graf

        #using [\s\p{Z}] to represent space helps force the string into
        #utf8; I've seen inconsistent results when getting to this stage,
        #even with the same string; this seems to keep utf8 treatment:
        if ( $grafs[0] =~ /\A[\s\p{Z}]*<[^>]+>[\s\p{Z}]*\z/ms )
        {                               #leading text is just a tag
            my $lead = shift @grafs;
            $grafs[0] = '' if !defined $grafs[0];
            $grafs[0] = $lead . $grafs[0];
        }
        foreach my $graf ( keys %embed_obj ) {
            my @embed =
              map { $_->[0] }
              sort { $b->[1] <=> $a->[1] || $b->[2] cmp $a->[2] }
              @{ $embed_obj{$graf} };
            my @html;
            foreach my $item (@embed) {
                my $label =
                  ref $item eq 'ARRAY'
                  ? $item->[1]->data_label
                  : $item->data_label;
                push( @html,
                    $embed_handler{$label}->( $context, $item, $rparam ) )
                  if $embed_handler{$label};
            }
            if ( $graf eq 'above' ) {
                $above = join( q{}, @html );
            }
            elsif ( $graf eq 'below' ) {
                $below = join( q{}, @html );
            }
            else {
                my $num = ( split( /:/ms, $graf ) )[1];    # format is block:1
                next if !$num;
                $num = @grafs if $num > @grafs;
                $grafs[$num - 1] = join( q{}, @html, $grafs[$num - 1] );
            }
        }
        $text = join( q{}, @grafs );
    }
    my $html = $context->build_markup(
        'wi_content.tmpl',
        __alt_path => $rparam->{alt_path},
        content    => $text,
        above      => $above,
        below      => $below,
        class      => $divclass || 'bmw_pageContent',
    );
    return $callback ? $callback->( $obj, $context, $html ) : $html;
}

sub _embed_document {
    my ( $context, $pair, $rparam ) = @_;
    my $alt_path = $rparam->{alt_path} if $rparam;
    my $rdocument = _format_document( $context, $pair );
    return $context->build_markup( 'wi_content_document.tmpl', %{$rdocument},
        __alt_path => $alt_path, );
}

sub _embed_av {
    my ( $context, $pair, $rparam ) = @_;
    my $rdocument = _format_av( $context, $pair );
    my $alt_path = $rparam->{alt_path} if $rparam;
    return $context->build_markup( 'wi_content_document.tmpl', %{$rdocument},
        __alt_path => $alt_path, );
}

sub _embed_image {
    my ( $context, $pair, $rparam ) = @_;
    my $alt_path = $rparam->{alt_path} if $rparam;
    my %meta = $pair->[0]->metadata;
    my $align =
      ( !$meta{align} || $meta{align} eq 'default' )
      ? $HTML->stash_pref( $context, 'html_content_objalign' )
      : $meta{align};
    my $ipref = ( $rparam && $rparam->{img_pref} ) || 'html_image_size';
    my $rimage = _format_image( $context, $pair, $ipref, $align )
      or return q{};    #no such image size
    if ( !$HTML->stash_pref( $context, 'html_image_magnify' ) ) {
        $rimage->{magnify} = q{};
    }

    return $context->build_markup( 'wi_content_image.tmpl', %{$rimage},
        __alt_path => $alt_path, );
}

sub _embed_pullquote {
    my ( $context, $pullquote, $rparam ) = @_;
    my $alt_path = $rparam->{alt_path} if $rparam;
    my $align = $pullquote->align;
    $align = $HTML->stash_pref( $context, 'html_content_objalign' )
      if !$align || $align eq 'default';
    return $context->build_markup(
        'wi_content_pullquote.tmpl',
        text => $HTML->inline_rich_text( $pullquote->text, $context ),
        size => $pullquote->size || 'big',
        align          => $align,
        type_pullquote => 1,
        __alt_path     => $alt_path,
    );
}

###########################################################
# OTHER CONTENT PAGE WIDGETS
###########################################################

# author widgets --------------------------------

$HTML->add_widget(
    name     => 'byline',
    group    => 'detail',
    handler  => \&wi_byline,
    priority => 40,
    prefs    => {
        'html_byline_textbyline' => {
            default   => 'By',
            edit_type => 'simple_text',
            priority  => 50,
        },
        'html_byline_titlecompany' => {
            default     => 1,
            edit_type   => 'boolean',
            edit_params => {
                option_label => 'BYLINE_Include title and company in byline',
            },
            priority => 40,
        },
        'html_byline_linkto' => {
            default   => 'form',
            edit_type => 'value_list',
            options   => ['email', 'form', 'url', 'none'],
            labels    => {
                'email' => 'BYLINE_E-mail Address',
                'form'  => 'BYLINE_E-Mail Form',
                'url'   => q{BYLINE_Author's website},
                'none'  => 'BYLINE_No Link',
            },
            priority => 30,
        },
    },
);

$HTML->add_widget(
    name    => 'authorname',
    group   => 'detail',
    handler => sub { $_[1]->authors( $_[0]->relation_cache ) }
);

$HTML->add_widget(
    name    => 'authoremail',
    group   => 'detail',
    handler => \&wi_authoremail,
);

$HTML->add_widget(
    name     => 'authorlink',
    group    => 'detail',
    handler  => \&wi_authorlink,
    priority => 30,
    prefs    => {
        'html_authorlink_textauthorlink' => {
            default     => 'Contact &lt;%author%&gt;',
            edit_type   => 'rich_text_inline',
            edit_params => {
                description => 'PREFS_DESC_authorlink_textauthorlink',
                required    => 1,
            },
        },
    },
);

$HTML->add_widget(
    name    => 'authorblurb',
    group   => 'detail',
    handler => \&wi_authorblurb,
);

sub wi_byline {
    my @authors = _author_info(@_);
    return q{} if !@authors;
    if ( $authors[0]->{do_title} ) {    #confirm we have at least one title
        my $have_title = 0;
        foreach my $a (@authors) {
            $have_title = 1, last if $a->{title_company};
        }
        if ( !$have_title ) {
            $_->{do_title} = 0 for @authors;
        }
    }
    return $_[0]->build_markup(
        'wi_byline.tmpl',
        authors => \@authors,
        by      => $HTML->stash_pref( $_[0], 'html_byline_textbyline' ),
    );
}

sub wi_authoremail {
    my @authors = _author_info(@_);
    foreach my $a (@authors) {
        return $a->{email} if $a->{email};
    }
    return q{};
}

sub wi_authorlink {
    my @authors = _author_info(@_) or return q{};
    my @links;
    my $linktext =
      $HTML->stash_pref( $_[0], 'html_authorlink_textauthorlink' );
    foreach my $a (@authors) {
        next if !$a->{linkto};
        my $name = $a->{name};
        ( $a->{text} = $linktext ) =~ s/(&lt;|<)%author%(&gt;|>)/$name/msg;
        push @links, $a;
    }
    return q{} if !@links;
    return $_[0]->build_markup(
        'wi_authorlink.tmpl',
        authors  => \@links,
        multiple => @links > 1,
    );
}

sub wi_authorblurb {
    my @authors = _author_info(@_);
    my @blurbs =
      grep {    #inline rich text and no empty or space-only entries
        ( $_->{blurb} = $HTML->inline_rich_text( $_->{blurb}, $_[0] ) )
          !~ /\A\s*\z/ms;
      } @authors;
    return q{} if !@blurbs;
    return $_[0]->build_markup( 'wi_authorblurb.tmpl', authors => \@blurbs, );
}

sub _author_info {
    my ( $context, $page ) = @_;
    my $form_url = tool_url( $context, $page, 'email' ) . '?author';
    my @authors;
    my $linkto   = $HTML->stash_pref( $context, 'html_byline_linkto' );
    my $do_title = $HTML->stash_pref( $context, 'html_byline_titlecompany' );
    my $rcache   = $context->relation_cache;
    foreach my $pair ( $page->sorted_related_objects( 'author', $rcache ) ) {
        my $person = $pair->[1];
        my $name = defined $person->first_name ? $person->first_name : q{};
        $name .= q{ } . $person->last_name if defined $person->last_name;
        my $website =
          ( $person->url && $person->url ne 'http://' )
          ? $HTML->escape_xml( $person->url )
          : q{};
        my $email = $person->email ? _encode_email( $person->email ) : q{};
        my $mailto = $email ? "mailto:$email" : q{};
        my $url =
            $linkto eq 'email' ? $mailto
          : $linkto eq 'url'   ? $website
          : $linkto eq 'form' && $email ? $form_url . $person->id
          :                               q{};
        my $new_window =
             $linkto eq 'url'
          && $website
          && is_new_window_url( $context, $website );
        $new_window = $new_window ? $NEW_WIN : q{};
        my $title =
          ( $person->title && $person->company )
          ? $person->title . ', ' . $person->company
          : $person->title   ? $person->title
          : $person->company ? $person->company
          :                    q{};
        my %meta = $pair->[0]->metadata;
        push @authors,
          { name          => $name,
            email         => $email,
            url           => $website,
            new_window    => $new_window,
            title_company => $title,
            title         => $person->title,
            company       => $person->company,
            linkto        => $url,
            form_url      => $form_url . $person->id,
            do_title      => $do_title,
            blurb         => $HTML->rich_text( $meta{blurb}, $context ),
            close         => tag_closer($context),
          };
    }
    return @authors;
}

sub _encode_email {    #from Markdown: http://www.daringfireball.com/
    my $addr = shift;
    srand;
    my @encode = (
        sub { q{&#} . ord(shift) . q{;} },
        sub { q{&#x} . sprintf( '%X', ord(shift) ) . q{;} },
        sub { shift },
    );
    $addr =~ s{(.)}{
        my $char = $1;
        if ( $char eq '@' ) {
            # this *must* be encoded. I insist.
            $char = $encode[int rand 1]->($char);
        } elsif ( $char ne ':' ) {
            # leave ':' alone (to spot mailto: later)
            my $r = rand;
            # roughly 10% raw, 45% hex, 45% dec
            $char = (
                $r > .9   ?  $encode[2]->($char)  :
                $r < .45  ?  $encode[1]->($char)  :
                             $encode[0]->($char)
            );
        }
        $char;
    }msgex;
    return $addr;
}

# date widgets --------------------------------

$HTML->add_widget(
    name  => 'pubdate',
    group => 'detail',
    prefs => {
        'html_pubdate_includetime' => {
            default     => 0,
            edit_type   => 'boolean',
            edit_params => {
                option_label => 'PREFS_PUBDATE_includetime',
                description  => 'PREFS_PUBDATE_DESC_includetime',
            },
        },
        'html_pubdate_textpubdate' => {
            default     => 'Published &lt;%date%&gt;',
            edit_type   => 'rich_text_inline',
            edit_params => {
                required    => 1,
                description => 'PREFS_PUBDATE_DESC_textpubdate',
            },
        },
    },
    handler  => \&wi_pubdate,
    priority => 20,
);

sub wi_pubdate {
    my $time =
      formatted_date( $_[0], $_[1]->pub_time,
        $HTML->stash_pref( $_[0], 'html_pubdate_includetime' ) );
    my $pubdate_text = $HTML->stash_pref( $_[0], 'html_pubdate_textpubdate' );
    $pubdate_text =~ s/&lt;%date%&gt;/$time/msg;
    return $_[0]->build_markup( 'wi_pubdate.tmpl', date => $pubdate_text, );
}

$HTML->add_widget(
    name  => 'modified',
    group => 'detail',
    prefs => {
        'html_modified_notpublished' => {
            default     => 1,
            edit_type   => 'boolean',
            edit_params => { option_label => 'PREFS_MODIFIED_notpublished', },
        },
        'html_modified_textmodified' => {
            default     => '(Updated &lt;%date%&gt;)',
            edit_type   => 'rich_text_inline',
            edit_params => {
                required    => 1,
                description => 'PREFS_PUBDATE_DESC_textmodified',
            },
        },
    },
    handler  => \&wi_modified,
    priority => 15,
);

$HTML->add_widget(
    name     => 'today',
    handler  => \&wi_today,
    sitewide => 1,
);

sub wi_modified {
    my ( $context, $obj ) = @_;
    my $offset = $context->site->time_offset;
    if ( $HTML->stash_pref( $context, 'html_modified_notpublished' ) ) {

        #check to see if it's the same day (in local site time)
        #as the published time
        my $pub_day = BigMed->bigmed_time(
            bigmed_time => $obj->pub_time,
            offset      => $offset,
        );
        my $mod_day = BigMed->bigmed_time(
            bigmed_time => $obj->mod_time,
            offset      => $offset,
        );
        $mod_day =~ s/[ ].*$//ms;
        $pub_day =~ s/[ ].*$//ms;
        return q{} if $mod_day eq $pub_day;    #same day
    }

    #build the formatted time, using the same time preference as
    #published time
    my $time =
      formatted_date( $_[0], $_[1]->mod_time,
        $HTML->stash_pref( $_[0], 'html_pubdate_includetime' ) );
    my $modified_text =
      $HTML->stash_pref( $_[0], 'html_modified_textmodified' );
    $modified_text =~ s/&lt;%date%&gt;/$time/msg;
    return $_[0]->build_markup( 'wi_modified.tmpl', date => $modified_text, );
}

sub wi_today {
    my $site = $_[0]->site;
    my $fmt = $site->date_format || '%b %e, %Y';
    if ( index( $OSNAME, 'MSWin' ) >= 0 )
    {    #windows has incomplete SSI support
        $fmt =~ s{\%e}{\%d}g;
        $fmt =~ s{\%h}{\%b}g;
        $fmt =~ s{\%n}{\%m}g;
        $fmt =~ s{\%r}{\%I:%M %p}g;
        $fmt =~ s{\%R}{\%H:%M}g;
        $fmt =~ s{\%T}{\%H:%M:%S}g;
    }
    my $date =
      qq{<!--#config timefmt="$fmt" --><!--#echo var="DATE_LOCAL" -->};
    return $_[0]->build_markup( 'wi_today.tmpl', date => $date );
}

# image widgets --------------------------------

$HTML->add_widget(
    name    => 'images',
    group   => 'detail',
    handler => \&wi_images,
);

$HTML->add_widget(
    name    => 'gallery',
    group   => 'images',
    handler => \&wi_gallery,
    prefs   => {
        'html_gallery_textheading' => {
            default     => 'Image Gallery',
            edit_type   => 'rich_text_inline',
            priority    => 100,
            edit_params => { description => 'BM_rich_text_inline_notice', },
        },
        'html_gallery_imagesize' => {
            default   => '100x100',
            edit_type => 'value_list',
            options   => \&_image_size_options,
            labels    => \&_image_size_labels,
            priority  => 90,
            edit_params =>
              { description => 'PREFS_IMAGE_DESC_gallery_imagesize', }
        },
        'html_gallery_direction' => {
            default   => 'horizontal',
            edit_type => 'value_list',
            options   => ['horizontal', 'vertical',],
            labels    => {
                'horizontal' => 'PREFS_GALLERY_Horizontal',
                'vertical'   => 'PREFS_GALLERY_Vertical',
            },
            priority => 80,
        },
        'html_gallery_caption' => {
            default     => 0,
            edit_type   => 'boolean',
            edit_params => { option_label => 'PREFS_GALLERY_DESC_caption', },
            priority    => 70,
        },
    }
);

my %IMAGE_SIZE = (
    'thumbnail' => '60x60',
    'xsmall'    => '100x100',
    'small'     => '200x200',
    'medium'    => '400x400',
    'large'     => '600x600',
    'xlarge'    => '800x800',
    'original'  => 'orig',
);

sub _image_size_options {
    my ( $app, $site, $sec_id ) = @_;
    require BigMed::Media::Image;
    return ( ( map { $_->[1] } BigMed::Media::Image->image_formats($site) ),
        'orig' );
}

sub _image_size_labels {
    my ( $app, $site, $sec_id ) = @_;
    require BigMed::Media::Image;
    return (
        'orig' => $app->language('PREFS_IMAGE_ORIGINAL'),
        map { $_ => $app->language("PREFS_IMAGE_$_") }
          BigMed::Media::Image->default_sizes
    );
}

sub wi_images {
    my ( $context, $obj, $rparam ) = @_;
    my @images = _get_gallery_images( 'images', $context, $obj, $rparam );
    return q{} if !@images;
    return $context->build_markup( 'wi_images.tmpl', images => \@images, );
}

sub wi_gallery {
    my ( $context, $obj, $rparam ) = @_;
    my %param = $rparam ? %{$rparam} : ();
    my $size = $param{size}
      || $HTML->stash_pref( $context, 'html_gallery_imagesize' );
    $size = $IMAGE_SIZE{$size} || $size;
    my $align = $param{direction}
      || $HTML->stash_pref( $context, 'html_gallery_direction' );
    my $caption =
      defined $param{caption}
      ? $param{caption}
      : $HTML->stash_pref( $context, 'html_gallery_caption' );
    my @images = _get_gallery_images(
        'gallery',
        $context, $obj,
        {   size      => $size,
            direction => $align,
            caption   => $caption,
            enlarge   => $param{enlarge},
            limit     => 'gallery',
            slideshow => 'gallery',
        }
    );
    return q{} if !@images;
    return $context->build_markup(
        'wi_gallery.tmpl',
        images  => \@images,
        heading => $HTML->stash_pref( $context, 'html_gallery_textheading' ),
    );
}

sub _get_gallery_images {    #handles both gallery and image widgets
    my ( $wname, $context, $obj, $rparam ) = @_;
    my %param = $rparam ? %{$rparam} : ();
    my $size = $param{size} || 'small';
    $size = $IMAGE_SIZE{$size} || $size;
    my $limit = $param{limit} || q{};
    $limit = q{}
      if $limit ne 'hidden'
          && $limit ne 'above'
          && $limit ne 'below'
          && $limit ne 'body'
          && $limit ne 'other'
          && $limit ne 'gallery';
    $limit = 'block' if $limit eq 'body';
    my $align =
      ( !$param{direction} || $param{direction} ne 'vertical' )
      ? 'left'
      : 'center';
    my $want_caption = $param{caption} && lc $param{caption} ne 'no';
    my @images;

    #choose either the magnify or hotlink url depending on widget
    my ( $prefer, $deprec ) =
      $wname eq 'gallery' ? qw(magnify hotlink) : qw(hotlink magnify);

    my $no_magnify = defined $param{enlarge}
      && ( !$param{enlarge} || lc $param{enlarge} eq 'no' );

    my $rcache = $context->relation_cache;
    foreach my $pair ( $obj->sorted_related_objects( 'media', $rcache ) ) {
        next if $pair->[1]->data_label ne 'image';
        my %format = $pair->[1]->formats;
        next if !$format{$size};
        my %meta = $pair->[0]->metadata;
        my $pos = $meta{position} || q{};
        next if $limit && index( $pos, $limit ) != 0;
        my $rimage = _format_image( $context, $pair, $size, $align )
          or next;
        $rimage->{caption} = q{} if !$want_caption;
        $rimage->{magnify} = q{} if $no_magnify;
        $rimage->{slideshow} = $rparam->{slideshow};
        delete $rimage->{$deprec} if $rimage->{$prefer};   #magnify vs hotlink
        push @images, $rimage;
    }
    return @images;
}

# content tool widgets --------------------------------

$HTML->add_widget(
    name    => 'related',
    handler => \&wi_related,
    group   => 'detail',
    prefs   => {
        'html_related_textheading' => {
            default     => 'Related links',
            edit_type   => 'rich_text_inline',
            edit_params => { description => 'BM_rich_text_inline_notice', },
        }
    },
);

$HTML->add_widget(
    name    => 'emailpage',
    handler => sub { _tool_link( $_[0], $_[1], 'email' ); },
    group   => 'detail',
    prefs   => {
        'html_tools_emailicon' => {
            default     => 'emailicon_blue.gif',
            edit_type   => 'raw_text',
            edit_params => {
                prompt_as => 'icon',
                icon_type => 'email',
            },
        },
        'html_tools_emailtext' => {
            default     => 'E-mail',
            edit_type   => 'rich_text_inline',
            edit_params => {
                description => 'BM_rich_text_inline_notice',
                required    => 1,
            },
        }
    },
    priority => 25,
);

$HTML->add_widget(
    name    => 'printpage',
    handler => sub { _tool_link( $_[0], $_[1], 'print' ); },
    group   => 'detail',
    prefs   => {
        'html_tools_printicon' => {
            default     => 'printicon_color.gif',
            edit_type   => 'raw_text',
            edit_params => {
                prompt_as => 'icon',
                icon_type => 'print',
            },
        },
        'html_tools_printtext' => {
            default     => 'Print',
            edit_type   => 'rich_text_inline',
            edit_params => {
                description => 'BM_rich_text_inline_notice',
                required    => 1,
            },
        }
    },
    priority => 25,
);

$HTML->add_widget(
    name    => 'pagetools',
    handler => \&wi_pagetools,
);

$HTML->add_widget(
    name    => 'emailform',
    handler => \&wi_emailform,
    group   => 'email',
    prefs   => {
        'html_emailform_introsend' => {
            default =>
              'A link to this page will be included with your message.',
            edit_type   => 'rich_text_inline',
            sitewide    => 1,
            priority    => 100,
            edit_params => {
                description => 'BM_rich_text_inline_notice',
                required    => 1,
            },
        },
        'html_emailform_introtoauthor' => {
            default     => 'Send an e-mail to the author of this page.',
            edit_type   => 'rich_text_inline',
            sitewide    => 1,
            priority    => 95,
            edit_params => {
                description => 'BM_rich_text_inline_notice',
                required    => 1,
            },
        },
        'html_emailform_formfrom' => {
            default     => 'Your e-mail address',
            edit_type   => 'rich_text_inline',
            sitewide    => 1,
            priority    => 60,
            edit_params => {
                container_class => 'bmcpDividerField',
                description     => 'BM_rich_text_inline_notice',
                required        => 1,
            },
        },
        'html_emailform_formto' => {
            default     => q{Recipient's e-mail address},
            edit_type   => 'rich_text_inline',
            sitewide    => 1,
            priority    => 55,
            edit_params => {
                description => 'BM_rich_text_inline_notice',
                required    => 1,
            },
        },
        'html_emailform_formsendcopy' => {
            default     => 'Send me a copy',
            edit_type   => 'rich_text_inline',
            sitewide    => 1,
            priority    => 50,
            edit_params => {
                description => 'BM_rich_text_inline_notice',
                required    => 1,
            },
        },
        'html_emailform_formmsg' => {
            default     => 'Personal message (optional)',
            edit_type   => 'rich_text_inline',
            sitewide    => 1,
            priority    => 45,
            edit_params => {
                description => 'BM_rich_text_inline_notice',
                required    => 1,
            },
        },
        'html_emailform_privacy' => {
            default => 'E-mail addresses supplied to this service will '
              . 'be used only to send the requested link.',
            edit_type   => 'rich_text_inline',
            sitewide    => 1,
            priority    => 40,
            edit_params => {
                description => 'BM_rich_text_inline_notice',
                required    => 1,
            },
        },
        'html_emailform_sent' => {
            default     => 'Thank you! Your e-mail has been sent.',
            edit_type   => 'rich_text_inline',
            sitewide    => 1,
            priority    => 35,
            edit_params => {
                container_class => 'bmcpDividerField',
                description     => 'BM_rich_text_inline_notice',
                required        => 1,
            },
        },
        'html_emailform_sentreturn' => {
            default     => 'Return to the original page.',
            edit_type   => 'rich_text_inline',
            sitewide    => 1,
            priority    => 30,
            edit_params => {
                description => 'BM_rich_text_inline_notice',
                required    => 1,
            },
        },
        'html_emailform_textsubject' => {
            default     => 'Link from <%sitename%>',
            edit_type   => 'raw_text',
            sitewide    => 1,
            priority    => 25,
            edit_params => {
                container_class => 'bmcpDividerField',
                description     => 'PREFS_EMAILFORM_DESC_textsubject',
                prompt_as       => 'simple_text',
                parse_as        => 'raw_text',
                required        => 1,
            }
        },
        'html_emailform_textintro' => {
            default   => 'This page from <%sitename%> was sent to you by:',
            edit_type => 'raw_text',
            sitewide  => 1,
            priority  => 20,
            edit_params =>
              { description => 'PREFS_EMAILFORM_DESC_textintro', }
        },
        'html_emailform_textsendermsg' => {
            default   => q{Sender's message:},
            edit_type => 'raw_text',
            sitewide  => 1,
            priority  => 15,
            edit_params =>
              { description => 'PREFS_EMAILFORM_DESC_textsendermsg', }
        },
        'html_emailform_textsubjectauthor' => {
            default     => 'Feedback from <%sitename%>',
            edit_type   => 'raw_text',
            sitewide    => 1,
            priority    => 10,
            edit_params => {
                container_class => 'bmcpDividerField',
                description     => 'PREFS_EMAILFORM_DESC_textsubject',
                prompt_as       => 'simple_text',
                parse_as        => 'raw_text',
                required        => 1,
            }
        },
        'html_emailform_textintroauthor' => {
            default   => 'This feedback message was sent to you by:',
            edit_type => 'raw_text',
            sitewide  => 1,
            priority  => 5,
            edit_params =>
              { description => 'PREFS_EMAILFORM_DESC_textintroauthor', }
        },
    },
);

sub wi_related {
    my ( $context, $obj ) = @_;
    my @links = _load_related_links( $context, $obj );
    return q{} if !@links;
    return $context->build_markup(
        'wi_related.tmpl',
        links   => \@links,
        heading => $HTML->stash_pref( $context, 'html_related_textheading' ),
    );
}

$HTML->add_widget(
    name    => 'tags',
    handler => \&wi_tags,
    group   => 'tags',
    prefs   => {
        'html_tags_tagtext' => {
            default     => 'Tags:',
            edit_type   => 'simple_text',
            sitewide    => 1,
            edit_params => { description => 'PREFS_TAGS_DESC_tagtext', }
        },
    },
);

sub wi_tags {
    my ( $context, $obj ) = @_;
    my @pairs =
      map  { $_->[0] }
      sort { $a->[1] cmp $b->[1] }
      map  { [$_, lc $_->[1]->name] }
      $obj->load_related_objects( 'tag', $context->relation_cache );

    my @tags;
    my $dot      = BigMed->bigmed->env('DOT');
    my $base_url = $context->site->homepage_url . "/bm${dot}tags";

    #rel="tag" should be added only if the slug and URL are the same,
    #per the rel-tag microformat specification
    foreach my $rpair (@pairs) {
        my ( $name, $slug ) = ( $rpair->[1]->name, $rpair->[1]->slug );
        push @tags,
          { tag => $name,
            url => "$base_url/$slug/",
            rel => ( $name eq $slug )
          };
    }
    return q{} if !@tags;
    return $context->build_markup(
        'wi_tags.tmpl',
        tags  => \@tags,
        label => $HTML->stash_pref( $context, 'html_tags_tagtext' ),
    );
}

$HTML->add_widget(
    name    => 'comments',
    handler => \&wi_comments,
    group   => 'vcomments',
    prefs   => { BigMed::Comment->comment_prefs },
);

sub wi_comments {
    my ( $context, $obj ) = @_;
    my $site    = $context->site;
    my $site_id = $site->id;
    my $sec_id  = $context->section->id;
    my $pid     = $obj->id;
    my $close   = tag_closer($context);

    #make sure there's an include file
    my $comment_file = BigMed::Comment->comment_file_path( $site, $obj );
    if ( !-e $comment_file ) {
        BigMed::Comment->build_page_comments( $site, $obj ) or return;
    }
    my $include = pagedir_from_webroot($context) . "/bm.comments/$pid.txt";

    #check sitewide and page-specific pref to see if comments enabled
    my %flag = $obj->flags;
    my $enabled = $HTML->stash_pref( $context, 'html_comments_enabled' )
      && !$flag{html_dcomments};
    if ( !$enabled ) {
        return $context->build_markup(
            'wi_comments.tmpl',
            COMMENT_PATH => $include,
            DISABLED =>
              $HTML->stash_pref( $context, 'html_comments_disabled' ),
        );
    }

    #enabled; gather the form elements
    my $captcha_html = $context->stash('CAPTCHA_HTML');
    if ( !$captcha_html ) {
        $captcha_html = $HTML->get_captcha_html( $site, $obj );
        $context->set_stash( 'CAPTCHA_HTML', $captcha_html );
    }

    my $bm       = BigMed->bigmed;
    my $pdiv     = $bm->env('USE_BMQUERY') ? '?' : '/';
    my $form_url = $bm->env('MOXIEBIN')
      . "/bm-comment.cgi${pdiv}submit/$site_id/$sec_id/$pid";
    my $self_url = $obj->page_url(
        $context->site,
        {   section => $context->section,
            rcache  => $context->relation_cache,
        }
    );
    my %display_info =
      $HTML->antispam_display_info( $obj,
        [qw(id preview name email url comment submit)] );
    my %realname = %{ $display_info{realname} };
    my %fakename = %{ $display_info{fakename} };
    my %param    = (
        FORM_HEAD =>
          $HTML->stash_pref( $context, 'html_comments_form_heading' ),
        FORM_CAPTION =>
          $HTML->stash_pref( $context, 'html_comments_form_caption' ),
        SELF_URL         => $self_url,
        FORM_URL         => $form_url,
        TIME_SETTER      => $display_info{set_time},
        LOCAL_TIME       => $display_info{local_time},
        TSTAMP_FIELD     => $display_info{tstamp},
        PID_FIELD        => $realname{id},
        PID_VALUE        => $pid,
        PREVIEWFIELD     => $realname{preview},
        NAMEFIELD        => $realname{name},
        EMAILFIELD       => $realname{email},
        URLFIELD         => $realname{url},
        COMMENT_PATH     => $include,
        CAPTCHA_HTML     => $captcha_html,
        CLOSE            => $close,
        COMMENTS_ENABLED => 1,
        REMEMBER_LABEL =>
          $HTML->stash_pref( $context, 'html_comments_remember' ),
        PREVIEW => $HTML->stash_pref( $context, 'html_comments_preview' ),
    );

    my $markdown = $HTML->stash_pref( $context, 'html_comments_format' );
    foreach my $field (qw(name email url comment submit)) {
        my $is_req = $field ne 'submit' && $field ne 'url';
        my $honeypot = {
            fieldname => $fakename{$field},
            label     => 'Leave this field blank',
            classattr => 'class="bmf_honey"',
            close     => $close,
        };
        my $label = $HTML->stash_pref( $context, "html_comments_$field" );
        my $real = {
            fieldname => $realname{$field},
            label     => $is_req ? "$label*" : $label,
            close     => $close,
            classattr => (
                  $field eq 'submit' ? 'class="bmf_auto"'
                : $is_req ? 'class="bmf_req"'
                : q{}
            ),
            markdown => $markdown,
        };

        $param{"${field}_fields"} =
          rand() > .5 ? [$honeypot, $real] : [$real, $honeypot];
    }
    return $context->build_markup( 'wi_comments.tmpl', %param );
}

$HTML->add_widget(
    name    => 'commentcount',
    handler => \&comment_tally_include,
);

sub comment_tally_include {
    my ( $context, $obj ) = @_;
    my $site = $context->site;

    # only show tally if it's a local page (not a link or document)
    # (blank url means that we're previewing, so go ahead and show tally)
    my $url = $obj->active_page_url(
        $site,
        {   section => $context->section,
            rcache  => $context->relation_cache,
            rkids   => $context->active_descendants,
        }
    );
    if ($url
        && (   index( $url, $site->html_url ) != 0
            || index( $url, $site->doc_url ) == 0 )
      )
    {
        return qq{<!-- external link, no comments -->};
    }

    my $file = BigMed::Comment->tally_file_path( $site, $obj );
    if ( !-e $file ) {
        BigMed::Comment->build_page_comments( $site, $obj );
    }
    my $include =
        pagedir_from_webroot($context)
      . '/bm.comments/'
      . $obj->id
      . '-tally.txt';
    return qq{<!--#include virtual="$include" -->};
}

sub wi_pagetools {
    my ( $context, $obj ) = @_;
    my @tools = map { _tool_info( $context, $obj, $_ ) } qw(email print);
    return $context->build_markup( 'wi_pagetools.tmpl', tools => \@tools, );
}

sub _tool_link {
    my ( $context, $obj, $tool ) = @_;
    return $context->build_markup( 'wi_tool-link.tmpl',
        %{ _tool_info( $context, $obj, $tool ) },
    );
}

sub _tool_info {
    my ( $context, $obj, $tool ) = @_;
    my $icon = $HTML->stash_pref( $context, "html_tools_${tool}icon" );
    $icon = $context->site->html_url . "/bm.assets/$icon" if $icon;
    return {
        text => $HTML->stash_pref( $context, "html_tools_${tool}text" ),
        icon => $icon,
        url    => tool_url( $context, $obj, $tool ),
        widget => "${tool}page",
        close  => tag_closer($context),
    };
}

sub wi_emailform {
    my ( $context, $obj ) = @_;
    my $site    = $context->site;
    my $section = $context->section || $site->homepage_obj;
    my $sec_id  = $section->id;
    my $site_id = $site->id;
    my $page_id = $obj->id;

    my $BM       = BigMed->bigmed;
    my $pdiv     = $BM->env('USE_BMQUERY') ? '?' : '/';
    my $form_url = BigMed->bigmed->env('MOXIEBIN')
      . "/bm-email.cgi${pdiv}email-page/$site_id/$sec_id/$page_id";
    my $page_url = $obj->page_url(
        $context->site,
        {   section => $context->section,
            rcache  => $context->relation_cache,
        }
    );

    my %display_info =
      $HTML->antispam_display_info( $obj, [qw(id formto formfrom formmsg)] );
    my %realname     = %{ $display_info{realname} };
    my %fakename     = %{ $display_info{fakename} };
    my $captcha_html = $context->stash('CAPTCHA_HTML');
    if ( !$captcha_html ) {
        $captcha_html = $HTML->get_captcha_html( $site, $obj );
        $context->set_stash( 'CAPTCHA_HTML', $captcha_html );
    }

    my $close = tag_closer($context);
    my %param = (
        FORM_URL     => $form_url,
        URL          => $page_url,
        TIME_SETTER  => $display_info{set_time},
        LOCAL_TIME   => $display_info{local_time},
        TSTAMP_FIELD => $display_info{tstamp},
        PID_FIELD    => $realname{id},
        PID_VALUE    => $page_id,
        CAPTCHA_HTML => $captcha_html,
        AUTHOR_INTRO =>
          $HTML->stash_pref( $context, 'html_emailform_introtoauthor' ),
        SEND_INTRO =>
          $HTML->stash_pref( $context, 'html_emailform_introsend' ),
        CONFIRM => $HTML->stash_pref( $context, 'html_emailform_sent' ),
        RETURN_TEXT =>
          $HTML->stash_pref( $context, 'html_emailform_sentreturn' ),
        CLOSE => $close,
        SEND_COPY =>
          $HTML->stash_pref( $context, 'html_emailform_formsendcopy' ),
        PRIVACY_POLICY =>
          $HTML->stash_pref( $context, 'html_emailform_privacy' ),
        SEND => BigMed->bigmed->language('BM_SUBMIT_LABEL_Send'),
    );

    foreach my $field (qw(formto formfrom formmsg)) {
        my $honeypot = {
            fieldname => $fakename{$field},
            label     => 'Leave this field blank',
            classattr => 'class="bmf_honey"',
            close     => $close,
        };
        my $is_req = $field ne 'formmsg';
        my $label = $HTML->stash_pref( $context, "html_emailform_$field" );
        $label .= '*' if $is_req;
        my $real = {
            fieldname => $realname{$field},
            label     => $label,
            close     => $close,
            classattr => $is_req ? 'class="bmf_req"' : undef,
        };

        $param{"${field}_fields"} =
          rand() > .5 ? [$honeypot, $real] : [$real, $honeypot];
    }

    return $context->build_markup( 'wi_emailform.tmpl', %param );

}

#big medium directories -----------------------

$HTML->add_widget(
    name     => 'pagedirpath',
    handler  => \&pagedir_from_webroot,
    sitewide => 1,
);
$HTML->add_widget(
    name     => 'pagedirurl',
    handler  => sub { $_[0]->site->html_url },
    sitewide => 1,
);

$HTML->add_widget(
    name     => 'homedirurl',
    handler  => sub { $_[0]->site->homepage_url },
    sitewide => 1,
);

$HTML->add_widget(
    name     => 'homedirpath',
    handler  => \&homedir_from_webroot,
    sitewide => 1,
);

$HTML->add_widget(
    name     => 'admindirurl',
    handler  => sub { BigMed->bigmed->env('BMADMINURL') },
    sitewide => 1,
);

sub pagedir_from_webroot {
    my $pagedir = $_[0]->stash('HTML_PAGEDIRPATH');
    return $pagedir if $pagedir;
    $pagedir = $_[0]->site->html_url;
    $pagedir =~ s{^https?://[^/]*}{}msi;
    $_[0]->set_stash( 'HTML_PAGEDIRPATH', $pagedir );
    return $pagedir;
}

sub homedir_from_webroot {
    my $homedir = $_[0]->site->homepage_url;    #path will be from web root
    $homedir =~ s{^https?://[^/]*}{}msi;
    return $homedir;
}

# section paths -- all from web root -----------

$HTML->add_widget(
    name        => 'dirpath',
    handler     => sub { _section_dir_path( $_[0], $_[0]->section ) },
    sectionwide => 1,
);

$HTML->add_widget(
    name    => 'parentpath',
    handler => sub {
        my $parent = _parent_section_obj( $_[0] ) or return q{};
        _section_dir_path( $_[0], $parent );
    },
    sectionwide => 1,
);

$HTML->add_widget(
    name    => 'mainsectionpath',
    handler => sub {
        my $main = _main_section_obj( $_[0] ) or return q{};
        _section_dir_path( $_[0], $main );
    },
    sectionwide => 1,
);

sub _section_dir_path {
    my ( $context, $sec ) = @_;
    return homedir_from_webroot($context) if !$sec || $sec->is_homepage;
    return pagedir_from_webroot( $_[0] ) . q{/}
      . _slug_path( $context, $sec );
}

sub _slug_path {
    my ( $context, $sec ) = @_;
    return q{} if !$sec || $sec->is_homepage;
    my $site    = $context->site;
    my @parents = $sec->parents;
    shift @parents;    #homepage
    my @slugs;
    foreach my $pid (@parents) {
        my $p = $site->section_obj_by_id($pid) or return q{};
        push @slugs, $p->slug;
    }
    return join( q{/}, @slugs, $sec->slug );
}

sub _main_section_obj {
    my $this_sec = $_[0]->section;
    return q{} if !$this_sec || $this_sec->is_homepage;
    my @parents = $this_sec->parents;
    my $main_id = $parents[1] or return $this_sec;    #home is first parent
    return $_[0]->site->section_obj_by_id($main_id);
}

sub _parent_section_obj {
    my $this_sec = $_[0]->section;
    return q{} if !$this_sec || $this_sec->is_homepage;
    my @parents = $this_sec->parents;
    return q{} if @parents < 2;    #main section
    return $_[0]->site->section_obj_by_id( $parents[-1] );
}

#page and section urls -----------

$HTML->add_widget(
    name    => 'url',
    handler => sub {    #use the actual url whether it's active or not
        return $_[1]->page_url(
            $_[0]->site,
            {   section => $_[0]->section,
                rcache  => $_[0]->relation_cache,
            }
        );
    },
);

$HTML->add_widget(
    name        => 'dirurl',
    handler     => sub { $_[0]->site->directory_url( $_[0]->section ) },
    sectionwide => 1,
);

$HTML->add_widget(
    name => 'sectionurl',
    handler =>
      sub { return _section_page_url( $_[0]->site, $_[0]->section ); },
    sectionwide => 1,
);

$HTML->add_widget(
    name    => 'parenturl',
    handler => sub {
        my $parent_obj = _parent_section_obj( $_[0] ) or return q{};
        return _section_page_url( $_[0]->site, $parent_obj );
    },
    sectionwide => 1,
);

$HTML->add_widget(
    name    => 'mainsectionurl',
    handler => sub {
        my $main_obj = _main_section_obj( $_[0] ) or return q{};
        return _section_page_url( $_[0]->site, $main_obj );
    },
    sectionwide => 1,
);

#page and section slugs -------------------------

$HTML->add_widget(
    name    => 'pageslug',
    handler => sub { $_[1]->subtype eq 'section' ? 'index' : $_[1]->slug },
);

$HTML->add_widget(
    name    => 'sectionslug',
    handler => sub {
        my $sec = $_[0]->section or return q{};
        $sec->is_homepage ? '__HOME' : $sec->slug;
    },
    sectionwide => 1,
);

$HTML->add_widget(
    name    => 'parentslug',
    handler => sub {
        my $parent_obj = _parent_section_obj( $_[0] ) or return q{};
        return $parent_obj->slug;
    },
    sectionwide => 1,
);

$HTML->add_widget(
    name    => 'mainsectionslug',
    handler => sub {
        my $main_obj = _main_section_obj( $_[0] ) or return q{};
        return $main_obj->slug;
    },
    sectionwide => 1,
);

#page/section names and titles -------------------------

$HTML->add_widget(
    name     => 'headline',
    handler  => \&wi_headline,
    group    => 'detail',
    priority => 100,
    prefs    => {
        'html_headline_subhead' => {
            default     => 0,
            edit_type   => 'boolean',
            edit_params => { option_label => 'PREFS_CONTENT_DESC_subhead', }
        }
    },
);

$HTML->add_widget(
    name    => 'title',
    handler => sub { $_[1]->title },
);

$HTML->add_widget(
    name    => 'description',
    handler => sub { $HTML->rich_text( $_[1]->description, $_[0] ) },
);

$HTML->add_widget(
    name    => 'sectionname',
    handler => sub {
        $_[0]->section
          ? $_[0]->section->name
          : $_[0]->site->homepage_obj->name;
    },
    sectionwide => 1,
);

$HTML->add_widget(
    name    => 'parentname',
    handler => sub {
        my $parent = _parent_section_obj( $_[0] ) or return q{};
        $parent->name;
    },
    sectionwide => 1,
);

$HTML->add_widget(
    name    => 'mainsectionname',
    handler => sub {
        my $main_obj = _main_section_obj( $_[0] ) or return q{};
        $main_obj->name;
    },
    sectionwide => 1,
);

$HTML->add_widget(
    name     => 'sitename',
    sitewide => 1,
    handler  => sub { $_[0]->site->name },
);

#sitemap link -------------------------

$HTML->add_widget(
    name     => 'sitemap',
    sitewide => 1,
    handler  => \&wi_sitemap,
);

sub wi_sitemap {
    my $context = shift;

    #don't bother if page directory is not within home dir url
    my $site = $context->site;
    my $hdir = $site->homepage_url;
    my $pdir = $site->html_url;
    if ( $pdir !~ /\A\Q$hdir\E/ ) {
        return q{<!-- sitemap: page url not inside homepage url -->};
    }

    my $dot = BigMed->bigmed->env('DOT');
    my $url = $context->site->homepage_url . "/bm${dot}sitemap_index.xml";
    return $context->build_markup( 'wi_sitemap.tmpl', url => $url, );
}

sub wi_headline {
    my $description =
        $HTML->stash_pref( $_[0], 'html_headline_subhead' )
      ? $HTML->inline_rich_text( $_[1]->description, $_[0] )
      : q{};
    my $title = $_[1]->title || q{};
    $title =~ s{\s+(\S+)\z}{&nbsp;$1}ms;
    return $_[0]->build_markup(
        'wi_headline.tmpl',
        title   => $title,
        subhead => $description,
    );
}

#section/home navigation -------------------------

$HTML->add_widget(
    name        => 'navigation',
    group       => 'navigation',
    handler     => \&wi_navigation,
    priority    => 60,
    sectionwide => 1,
    prefs       => {
        'html_navigation_direction' => {
            default   => 'v',
            edit_type => 'value_list',
            options   => ['v', 'h'],
            labels    => {
                'v' => 'PREFS_NAVIGATION_vertical',
                'h' => 'PREFS_NAVIGATION_horizontal',
            },
            priority => 60,
        },
        'html_navigation_vnavstyle' => {
            default     => 'dropdown',
            edit_type   => 'navigation_style',
            priority    => 50,
            edit_params => { prefix => 'vnav', },
            sitewide    => 1,
        },
        'html_navigation_hnavstyle' => {
            default     => 'dropdown',
            edit_type   => 'navigation_style',
            priority    => 40,
            edit_params => { prefix => 'hnav', },
            sitewide    => 1,
        },
        'html_navigation_depth' => {
            default   => 2,
            edit_type => 'value_list',
            options   => ['1', '2', '3', '4'],
            labels    => {
                '1' => 'PREFS_NAVIGATION_1',
                '2' => 'PREFS_NAVIGATION_2',
                '3' => 'PREFS_NAVIGATION_3',
                '4' => 'PREFS_NAVIGATION_4',
            },
            sitewide    => 1,
            edit_params => { description => 'PREFS_NAVIGATION_DESC_depth', },
            priority    => 30,
        },
        'html_navigation_includehome' => {
            default   => 1,
            edit_type => 'boolean',
            sitewide  => 1,
            edit_params =>
              { option_label => 'PREFS_NAVIGATION_DESC_includehome', },
            priority => 20,
        },
    },
);

$HTML->add_widget(
    name         => 'subnavigation',
    group        => 'navigation',
    handler      => \&wi_subnavigation,
    build_always => 1,
    sectionwide  => 1,
    priority     => 50,
    prefs        => {
        'html_subnavigation_direction' => {
            default   => 'v',
            edit_type => 'value_list',
            options   => ['v', 'h'],
            labels    => {
                'v' => 'PREFS_NAVIGATION_vertical',
                'h' => 'PREFS_NAVIGATION_horizontal',
            },
            priority => 60,
        },
        'html_subnavigation_vsubstyle' => {
            default     => 'dropdown',
            edit_type   => 'navigation_style',
            priority    => 50,
            edit_params => { prefix => 'vsub', },
            sitewide    => 1,
        },
        'html_subnavigation_hsubstyle' => {
            default     => 'dropdown',
            edit_type   => 'navigation_style',
            priority    => 40,
            edit_params => { prefix => 'hsub', },
            sitewide    => 1,
        },
        'html_subnavigation_depth' => {
            default   => 2,
            edit_type => 'value_list',
            options   => ['1', '2', '3', '4'],
            labels    => {
                '1' => 'PREFS_SUBNAVIGATION_1',
                '2' => 'PREFS_SUBNAVIGATION_2',
                '3' => 'PREFS_SUBNAVIGATION_3',
                '4' => 'PREFS_SUBNAVIGATION_4',
            },
            edit_params =>
              { description => 'PREFS_SUBNAVIGATION_DESC_depth', },
            priority => 30,
        },
    },
);

$HTML->add_widget(
    name     => 'breadcrumbs',
    group    => 'navigation',
    handler  => \&wi_breadcrumbs,
    priority => 40,
    prefs    => {
        'html_breadcrumbs_separator' => {
            edit_type => 'value_list',
            default   => '&gt;',
            options   => ['&gt;', q{|}, q{:}, q{::}],
            labels      => { '&gt;'   => 'PREFS_BREADCRUMBS_&gt;', },
            edit_params => { required => 1, }
        },
        'html_breadcrumbs_full' => {
            edit_type   => 'boolean',
            default     => 1,
            edit_params => { option_label => 'PREFS_BREADCRUMBS_DESC_full', }
        },
        'html_breadcrumbs_lc' => {
            edit_type   => 'boolean',
            default     => 0,
            edit_params => { option_label => 'PREFS_BREADCRUMBS_DESC_lc', }
        },
    },
);

$HTML->add_widget(
    name         => 'pulldown',
    group        => 'navigation',
    handler      => \&wi_pulldown,
    build_always => 1,
    priority     => 30,
    sectionwide  => 1,
    prefs        => {
        'html_pulldown_textjumpto' => {
            edit_type => 'simple_text',
            default   => 'Jump to:',
        },
    },
);

$HTML->add_widget(
    name     => 'sitenamelink',
    sitewide => 1,
    handler  => sub {
        my $site    = $_[0]->site;
        my $url     = _section_page_url( $site, $site->homepage_obj );
        my $new_win = is_new_window_url( $_[0], $url ) ? $NEW_WIN : q{};
        $_[0]->build_markup(
            'wi_sitenamelink.tmpl',
            url        => $url,
            new_window => $new_win,
            sitename   => $site->name
        );
    },
);

$HTML->add_widget(
    name     => 'sitelogo',
    sitewide => 1,
    handler  => sub {
        my $site    = $_[0]->site;
        my $url     = _section_page_url( $site, $site->homepage_obj );
        my $new_win = is_new_window_url( $_[0], $url ) ? $NEW_WIN : q{};
        $_[0]->build_markup(
            'wi_sitelogo.tmpl',
            url        => $url,
            new_window => $new_win,
            sitename   => $site->name
        );
    },
);

$HTML->add_widget(
    name     => 'homelink',
    sitewide => 1,
    handler  => sub {
        my $site    = $_[0]->site;
        my $home    = $site->homepage_obj;
        my $url     = _section_page_url( $site, $site->homepage_obj );
        my $new_win = is_new_window_url( $_[0], $url ) ? $NEW_WIN : q{};
        $_[0]->build_markup(
            'wi_homelink.tmpl',
            url           => $url,
            new_window    => $new_win,
            home_nav_name => $home->name
        );
    },
);

$HTML->add_widget(
    name        => 'sectionlink',
    sectionwide => 1,
    handler     => sub {
        my ( $context, $obj, $rparam ) = @_;
        my $site = $context->site;
        my $sec;
        if ( defined $rparam->{slug} ) {
            $sec = $site->section_obj_by_slug( $rparam->{slug} );
            if ( !$sec ) {
                my $slug = $HTML->escape_xml( $rparam->{slug} );
                return "<!-- unknown section slug: $slug -->";
            }
        }
        else {
            $sec = $context->section || $site->homepage_obj;
        }
        if ( !$sec ) {
            return;
        }
        my $url = _section_page_url( $site, $sec );
        my $new_win = is_new_window_url( $context, $url ) ? $NEW_WIN : q{};
        $_[0]->build_markup(
            'wi_sectionlink.tmpl',
            url          => $url,
            new_window   => $new_win,
            section_name => $sec->name
        );
    },
);

$HTML->add_widget(
    name        => 'parentlink',
    sectionwide => 1,
    handler     => sub {
        my $site    = $_[0]->site;
        my $parent  = _parent_section_obj( $_[0] ) or return q{};
        my $url     = _section_page_url( $site, $parent );
        my $new_win = is_new_window_url( $_[0], $url ) ? $NEW_WIN : q{};
        $_[0]->build_markup(
            'wi_parentlink.tmpl',
            new_window   => $new_win,
            url          => $url,
            section_name => $parent->name
        );
    },
);

$HTML->add_widget(
    name        => 'mainsectionlink',
    sectionwide => 1,
    handler     => sub {
        my $site    = $_[0]->site;
        my $main    = _main_section_obj( $_[0] ) or return q{};
        my $url     = _section_page_url( $site, $main );
        my $new_win = is_new_window_url( $_[0], $url ) ? $NEW_WIN : q{};
        $_[0]->build_markup(
            'wi_mainsectionlink.tmpl',
            url          => $url,
            new_window   => $new_win,
            section_name => $main->name,
        );
    },
);

sub wi_navigation {
    my ( $context, $obj, $rparam ) = @_;
    my $site  = $context->site;
    my $cache = $site->stash('html_navigation_cache')
      || _cache_full_navigation(@_);
    my $sec = $context->section;
    my $active;
    if ( !$sec || $sec->is_homepage ) {
        $active = '__HOME';
    }
    else {
        my @parents = ( ( $sec->parents ), $sec->id );
        shift @parents;    #homepage
        my @active = map {
            my $s    = $site->section_obj_by_id($_);
            my $slug = $s->slug;
            "\Q$slug\E";
        } @parents;
        $active = join( q{|}, @active );
    }

    #add bmn_active class to all active section classes
    $cache =~ s{class="
        (bmn_sec\-($active)       # section class
        (\s+[^"]+)?)              # other classes
      "}{class="$1 bmn_active"}msxg;

    my $direction = $rparam->{direction} || q{};
    my $align =
        $direction eq 'horizontal' ? 'h'
      : $direction eq 'vertical'   ? 'v'
      :   $HTML->stash_pref( $context, 'html_navigation_direction' );
    return $context->build_markup(
        'wi_navigation.tmpl',
        direction_class => "bmn_${align}nav",
        all_nav         => $cache,
        skipid          => _skip_link(),
    );
}

sub wi_subnavigation {
    my ( $context, $obj, $rparam ) = @_;
    my $site           = $context->site;
    my $sec            = $context->section;
    my $slug           = $rparam->{slug};
    my $no_subsections = '<!-- no subsections -->';

    #sort out the slug and/or main section
    if ( !$slug && $rparam->{main} ) {    #requesting a main section
        my $main = _main_section_obj($context);
        if ( !defined $main ) {           # an error
            return $no_subsections;
        }
        elsif ( !$main ) {                #homepage, send back the main nav
            return wi_navigation(@_);
        }
        $rparam->{slug} = $slug = $main->slug;
    }
    elsif (( !defined $slug && ( !$sec || $sec->is_homepage ) )
        || ( $slug && $slug eq '@all' ) )
    {                                     #on homepage or requesting homepage
        return wi_navigation(@_);
    }

    if (   !defined $slug
        || ( !$sec && $slug ne '@all' )
        || ( $sec && $sec->slug eq $slug ) )
    {

        my $html;
        my %flag = $sec->flags;
        if ( $site->is_section_active($sec) ) {

            #build all subnav for section and save to include
            my %param = (
                parent => $sec,
                max_depth =>
                  $HTML->stash_pref( $context, 'html_subnavigation_depth' ),
            );
            $html = _navigation_tree( $context, %param );
        }
        $html ||= $no_subsections;

        my $slug_path = _slug_path( $context, $sec );
        $slug_path &&= "/$slug_path";
        my $dot = BigMed->bigmed->env('DOT');
        my $include =
          $site->html_dir . "$slug_path/bm${dot}subnavigation.shtml";
        bm_write_file( $include, $html, { build_path => 1 } );
    }

    my $include_tag =
      _handle_section_include( $context, $obj, $rparam, 'subnavigation' );
    my $direction = $rparam->{direction} || q{};
    my $align =
        $direction eq 'horizontal' ? 'h'
      : $direction eq 'vertical'   ? 'v'
      :   $HTML->stash_pref( $context, 'html_subnavigation_direction' );
    my $slug_path = _slug_path( $context, $sec );
    my @slug_updater;
    if ($slug_path) {
        @slug_updater = map { { slug => $_ } } split( m{/}ms, $slug_path );
    }
    my $skiplink = _skip_link();
    return $context->build_markup(
        'wi_navigation.tmpl',
        direction_class => "bmn_${align}subnav",
        all_nav         => $include_tag,
        slug_updater    => \@slug_updater,
        skipid          => _skip_link(),
    );
}

sub _skip_link {
    return 'bmskip-' . ( int( rand(999999999) ) + 1 );
}

sub _cache_full_navigation {
    my ( $context, $obj, $rparam ) = @_;
    my $site  = $context->site;
    my %param = (
        parent    => undef,                                          #homepage
        max_depth => $site->get_pref_value('html_navigation_depth'),
        direction => $site->get_pref_value('html_navigation_direction'),
        type => q{},    #main navigation
    );
    my $html = _navigation_tree( $context, %param );
    $site->set_stash( 'html_navigation_cache', $html );
    return $html;
}

sub _navigation_tree {
    my $context = shift;
    my %param   = @_;

    #this routine generates the navigation html for subsections below
    #the specified parent object. returns empty string if none needed

    #param{parent} = parent object
    #param{base_depth} = top level of top parent object
    #                    0 is homepage, to display main navigation
    #                    1 is main section, to display first-level subsections
    #                    2 is subsection, to display second-level subsections
    #param{max_depth} = number of levels below top parent object to display

    my $child_levels_wanted =
      $HTML->_descendant_levels_wanted( $context, \%param );
    if ( !defined $child_levels_wanted ) {
        return;    #error
    }
    elsif ( !$child_levels_wanted ) {
        return q{};
    }

    my $site       = $context->site;
    my $parent_obj = $param{parent};
    my $show_home  = $parent_obj->is_homepage
      && $site->get_pref_value('html_navigation_includehome');
    my @kids;

    my $home_url = _section_page_url( $site, $parent_obj );
    my $home_new_win =
      is_new_window_url( $context, $home_url ) ? $NEW_WIN : q{};
    if ($show_home) {
        push @kids,
          { section_name => $parent_obj->name,
            url          => $home_url,
            new_window   => $home_new_win,
            slug         => '__HOME',
          };
    }

    foreach my $child_id ( $parent_obj->kids ) {
        my $child_obj = $site->section_obj_by_id($child_id) or return ();
        my %flag = $child_obj->flags;
        next if !$child_obj->active || $flag{html_nonav};
        $param{parent} = $child_obj;
        my $subnav =
          $child_levels_wanted > 1
          ? _navigation_tree( $context, %param )
          : q{};
        my $url = _section_page_url( $site, $child_obj );
        my $new_win = is_new_window_url( $context, $url ) ? $NEW_WIN : q{};
        push @kids,
          { section_name => $child_obj->name,
            url          => $url,
            new_window   => $new_win,
            subnav       => $subnav,
            slug         => $child_obj->slug,
          };
    }
    return q{} if !@kids;
    return $context->build_markup( 'wi_navigation_level.tmpl',
        sections => \@kids );
}

sub _descendant_levels_wanted {    #levels to display below current level
    my $class   = shift;
    my $context = shift;
    my $rparam  = shift;

    my $site   = $context->site;
    my $parent = $rparam->{parent};    #section to consider
    ( $parent ||= $site->homepage_obj ) or return ();
    $rparam->{parent} =
      ref $parent ? $parent : $site->section_obj_by_id($parent)
      or return ();

    my $this_depth =
      $rparam->{parent}->is_homepage
      ? 0
      : scalar( $rparam->{parent}->parents );
    $rparam->{max_depth} ||= 1;        #levels to display from base
    $rparam->{base_depth} = $this_depth if !defined $rparam->{base_depth};

    return 0 if $this_depth < $rparam->{base_depth};
    return $rparam->{base_depth} - $this_depth + $rparam->{max_depth};
}

sub wi_pulldown {
    my ( $context, $obj, $rparam ) = @_;
    my $this_sec       = $context->section || q{};
    my $sec            = $this_sec;
    my $slug           = $rparam->{slug} || q{};
    my $is_home        = !$sec || $this_sec->is_homepage;
    my $no_subsections = '<!-- no subsections for pulldown -->';

    #sort out the slug and/or main section
    if ( !$slug && $rparam->{main} ) {    #requesting the main section
        my $main = _main_section_obj($context);
        if ( !defined $main ) {           # an error
            return $no_subsections;
        }
        elsif ( !$main ) {                #homepage
            $rparam->{slug} = $slug = '@all';
        }
        else {                            #found the main section
            $rparam->{slug} = $slug = $main->slug;
        }
    }

    if ( $slug eq '@all' ) {
        $sec = q{};    #empty section will generate for homepage
    }
    elsif ( $slug && ( $is_home || $this_sec->slug ne $slug ) ) {
        $sec = $context->site->section_obj_by_slug( $rparam->{slug} );
        return qq~<!-- unknown slug "$slug" in <%pulldown%> -->~
          if !$sec;
    }

    my $include = _include_dir( $context, $sec );
    my $dot = BigMed->bigmed->env('DOT');
    my $ssi = qq|<!--#include virtual="$include/bm${dot}pulldown.txt" -->|;
    return $ssi if $sec ne $this_sec;

    #current section, go ahead and build the whole thing if active
    return $no_subsections if !$context->is_active;
    my $site = $context->site;
    my $base_depth = $is_home ? 1 : ( $sec->parents );
    $sec = $site->homepage_obj if !$sec;
    my @menu;
    my %omit_nonav;
    foreach my $sid ( @{ $context->ordered_descendants } ) {
        next if $omit_nonav{$sid};    #marked for suppression in navigation
        my $kid = $site->section_obj_by_id($sid);

        my %flag = $kid->flags;
        if ( $flag{html_nonav} ) {    #omit section and mark kids for same
            foreach my $id ( $sid, $site->all_descendants_ids($kid) ) {
                $omit_nonav{$id} = 1;
            }
            next;
        }
        push @menu,
          { indent => ( '&nbsp;&nbsp;' x scalar $kid->parents - $base_depth ),
            name   => $kid->name,
            url    => _section_page_url( $site, $kid ),
          };
    }
    my $html;
    if (@menu) {
        $html = $context->build_markup(
            'wi_pulldown.tmpl',
            menu => \@menu,
            jump_text =>
              $HTML->stash_pref( $context, 'html_pulldown_textjumpto' ),
        );
    }
    else {
        $html = $no_subsections;
    }

    my $slug_path = _slug_path( $context, $sec );
    $slug_path &&= "/$slug_path";
    my $file = $site->html_dir . "$slug_path/bm${dot}pulldown.txt";
    bm_write_file( $file, $html, { build_path => 1 } );
    return $ssi;
}

sub wi_breadcrumbs {
    my $want_full = $HTML->stash_pref( $_[0], 'html_breadcrumbs_full' );
    if ( !$want_full ) {
        my $section_bc = $_[0]->stash('HTML_SECTION_CRUMBS');
        return $section_bc if $section_bc;
    }
    my @breadcrumbs = section_breadcrumbs( $_[0] ) or return q{};
    if ( $HTML->stash_pref( $_[0], 'html_breadcrumbs_full' ) ) {
        my $sep = $HTML->stash_pref( $_[0], 'html_breadcrumbs_separator' );
        my $lc  = $HTML->stash_pref( $_[0], 'html_breadcrumbs_lc' );
        my $name = $lc ? lc $_[1]->title : $_[1]->title;

        #add breadcrumb only if different from section name
        push @breadcrumbs, { crumb => $name }
          if $name ne $breadcrumbs[-1]->{crumb};
    }
    my $bc =
      $_[0]
      ->build_markup( 'wi_breadcrumbs.tmpl', breadcrumbs => \@breadcrumbs );
    $_[0]->set_stash( 'HTML_SECTION_CRUMBS', $bc ) if !$want_full;
    return $bc;
}

sub section_breadcrumbs {
    my $rcrumb = $_[0]->stash('HTML_BREADCRUMBS');    #hey, it's R. Crumb!
    return @{$rcrumb} if $rcrumb;
    my $sec = $_[0]->section or return;
    return () if $sec->is_homepage;
    my @path = $sec->parents or return;
    push @path, $sec->id;
    my $site = $_[0]->site;
    my $sep  = $HTML->stash_pref( $_[0], 'html_breadcrumbs_separator' );
    my $lc   = $HTML->stash_pref( $_[0], 'html_breadcrumbs_lc' );
    my @breadcrumbs;

    foreach my $sid (@path) {
        next if !$sid;
        my $sec = $site->section_obj_by_id($sid) or next;
        my $name = $lc ? lc $sec->name : $sec->name;
        push @breadcrumbs,
          { crumb     => $name,
            url       => _section_page_url( $site, $sec ),
            separator => $sep
          };
    }
    $_[0]->set_stash( 'HTML_BREADCRUMBS', [@breadcrumbs] );    #new reference
    return @breadcrumbs;
}

#not used for homepage itself but pages like tags and search.
sub top_level_breadcrumbs {
    my ( $context, $page_title ) = @_;
    my $site    = $context->site      or return q{};
    my $homesec = $site->homepage_obj or return q{};
    my $sep         = $site->get_pref_value('html_breadcrumbs_separator');
    my $lc          = $site->get_pref_value('html_breadcrumbs_lc');
    my $url         = $site->homepage_url . '/index.' . $HTML->suffix();
    my @breadcrumbs = (
        {   crumb => ( $lc ? lc $homesec->name : $homesec->name ),
            url => $url,
            separator => $sep,
        },
        { crumb => ( $lc ? lc $page_title : $page_title ) }
    );
    return $context->build_markup( 'wi_breadcrumbs.tmpl',
        breadcrumbs => \@breadcrumbs );
}

#footer -------------------------

$HTML->add_widget(
    name  => 'footer',
    group => 'pagefooter',
    prefs => {
        'html_footer_aboutline' => {
            edit_type => 'rich_text_brief',
            default   => q{},
            sitewide  => 1,
        },
    },
    sitewide => 1,
    handler  => \&wi_footer,
);

sub wi_footer {
    my ( $context, $page ) = @_;
    my $footer = $context->stash('HTML_FOOTER');

    if ( !$footer ) {
        $footer = $context->set_stash(
            'HTML_FOOTER',
            $HTML->inline_rich_text(
                $context->site->get_pref_value('html_footer_aboutline'),
                $context
            )
        );
    }
    return $context->build_markup( 'wi_footer.tmpl', footer => $footer, );
}

$HTML->add_widget(
    name     => 'bigmedium',
    group    => 'pagefooter',
    sitewide => 1,
    handler  => \&wi_bigmedium,
);

sub wi_bigmedium {
    my $new_win =
      $HTML->stash_pref( $_[0], 'html_breadcrumbs_full' ) ? $NEW_WIN : q{};
    return $_[0]->build_markup( 'wi_bigmedium.tmpl', new_window => $new_win );
}

#header -------------------------

$HTML->add_widget(
    name    => 'htmlhead',
    handler => \&wi_htmlhead,
    group   => '0document',
    prefs   => {
        'html_htmlhead_doctype' => {
            edit_type => 'value_list',
            default   => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 '
              . 'Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/'
              . 'xhtml1-transitional.dtd">',
            options => [
                    '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 '
                  . 'Transitional//EN" "http://www.w3.org/TR/html4/'
                  . 'loose.dtd">',

                '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" '
                  . '"http://www.w3.org/TR/html4/strict.dtd">',

                '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 '
                  . 'Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/'
                  . 'xhtml1-transitional.dtd">',

                '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 '
                  . 'Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/'
                  . 'xhtml1-strict.dtd">',

                q{}
            ],
            labels => {
                    '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 '
                  . 'Transitional//EN" "http://www.w3.org/TR/html4/'
                  . 'loose.dtd">' => 'HTML_HTML 4.01 Transitional',

                '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" '
                  . '"http://www.w3.org/TR/html4/strict.dtd">' =>
                  'HTML_HTML 4.01 Strict',

                '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 '
                  . 'Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/'
                  . 'xhtml1-transitional.dtd">' =>
                  'HTML_XHTML 1.0 Transitional',

                '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 '
                  . 'Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/'
                  . 'xhtml1-strict.dtd">' => 'HTML_XHTML 1.0 Strict',

                q{} => 'HTML_None',
            },
        },
        'html_htmlhead_titlepage' => {
            edit_type => 'value_several',
            default   => [qw(t sp o s c)],
            options   => [q{}, qw!s t d md tag k sp . : - | o c [ ]!],
            labels    => {
                's'   => 'page_title_format Site name',
                't'   => 'page_title_format Page title/headline',
                'd'   => 'page_title_format Page description',
                'md'  => 'page_title_format Page meta description',
                'tag' => 'page_title_format Site title tagline',
                'k'   => 'page_title_format Page keywords',
                'sp'  => 'page_title_format Space',
                q{.}  => 'page_title_format :',
                q{:}  => 'page_title_format ::',
                q{|}  => 'page_title_format |',
                q{-}  => 'page_title_format -',
                'o'   => 'page_title_format (',
                'c'   => 'page_title_format )',
                '['   => 'page_title_format [',
                ']'   => 'page_title_format ]',
            },
            edit_params => {
                numfields   => 10,
                description => 'HTML_pagetitle_desc',
            },
        },
        'html_htmlhead_titletagline' => {
            edit_type   => 'simple_text',
            default     => q{},
            edit_params => { description => 'HTML_homepagetitletag_desc', },
            sitewide    => 1,
        },
        'html_htmlhead_titlehome' => {
            edit_type   => 'simple_text',
            default     => q{},
            edit_params => {
                description     => 'HTML_homepagetitle_desc',
                container_class => 'bmcpDividerField',
            },
            sitewide => 1,
        },
        'html_htmlhead_xtrahtml' => {
            default     => q{},
            edit_type   => 'raw_text',
            edit_params => {
                css_class       => 'bmcpMarkup',
                description     => 'HTML_htmlhead_xtrahtml_desc',
                container_class => 'bmcpDividerField',
            },
        },
        'html_htmlhead_lang' => {
            default   => 'en',
            edit_type => 'value_list',
            options   => [
                qw(
                  ab om aa af sq am ar hy as ay az ba eu bn dz bh bi
                  br bg my be km ca zh co hr cs da nl en eo et fo fj
                  fi fr fy gl ka de el kl gn gu ha he hi hu is id ia
                  ie iu ik ga it ja jv kn ks kk rw ky rn ko ku lo la
                  lv ln lt mk mg ms ml mt mi mr mo mn na ne no oc or
                  ps fa pl pt pa qu rm ro ru sm sg sa gd sr sh st tn
                  sn sd si ss sk sl so es su sw sv tl tg ta tt te th
                  bo ti to ts tr tk tw ug uk ur uz vi vo cy wo xh yi
                  yo za zu
                  )
            ],
            labels => {
                'ab' => 'HTML_Abkhazian',
                'om' => 'HTML_Afan (Oromo)',
                'aa' => 'HTML_Afar',
                'af' => 'HTML_Afrikaans',
                'sq' => 'HTML_Albanian',
                'am' => 'HTML_Amharic',
                'ar' => 'HTML_Arabic',
                'hy' => 'HTML_Armenian',
                'as' => 'HTML_Assamese',
                'ay' => 'HTML_Aymara',
                'az' => 'HTML_Azerbaijani',
                'ba' => 'HTML_Bashkir',
                'eu' => 'HTML_Basque',
                'bn' => 'HTML_Bengali',
                'dz' => 'HTML_Bhutani',
                'bh' => 'HTML_Bihari',
                'bi' => 'HTML_Bislama',
                'br' => 'HTML_Breton',
                'bg' => 'HTML_Bulgarian',
                'my' => 'HTML_Burmese',
                'be' => 'HTML_Byelorussian',
                'km' => 'HTML_Cambodian',
                'ca' => 'HTML_Catalan',
                'zh' => 'HTML_Chinese',
                'co' => 'HTML_Corsican',
                'hr' => 'HTML_Croatian',
                'cs' => 'HTML_Czech',
                'da' => 'HTML_Danish',
                'nl' => 'HTML_Dutch',
                'en' => 'HTML_English',
                'eo' => 'HTML_Esperanto',
                'et' => 'HTML_Estonian',
                'fo' => 'HTML_Faroese',
                'fj' => 'HTML_Fiji',
                'fi' => 'HTML_Finnish',
                'fr' => 'HTML_French',
                'fy' => 'HTML_Frisian',
                'gl' => 'HTML_Galician',
                'ka' => 'HTML_Georgian',
                'de' => 'HTML_German',
                'el' => 'HTML_Greek',
                'kl' => 'HTML_Greenlandic',
                'gn' => 'HTML_Guarani',
                'gu' => 'HTML_Gujarati',
                'ha' => 'HTML_Hausa',
                'he' => 'HTML_Hebrew',
                'hi' => 'HTML_Hindi',
                'hu' => 'HTML_Hungarian',
                'is' => 'HTML_Icelandic',
                'id' => 'HTML_Indonesian',
                'ia' => 'HTML_Interlingua',
                'ie' => 'HTML_Interlingue',
                'iu' => 'HTML_Inuktitut',
                'ik' => 'HTML_Inupiak',
                'ga' => 'HTML_Irish',
                'it' => 'HTML_Italian',
                'ja' => 'HTML_Japanese',
                'jv' => 'HTML_Javanese',
                'kn' => 'HTML_Kannada',
                'ks' => 'HTML_Kashmiri',
                'kk' => 'HTML_Kazakh',
                'rw' => 'HTML_Kinyarwanda',
                'ky' => 'HTML_Kirghiz',
                'rn' => 'HTML_Kurundi',
                'ko' => 'HTML_Korean',
                'ku' => 'HTML_Kurdish',
                'lo' => 'HTML_Laothian',
                'la' => 'HTML_Latin',
                'lv' => 'HTML_Latvian',
                'ln' => 'HTML_Lingala',
                'lt' => 'HTML_Lithuanian',
                'mk' => 'HTML_Macedonian',
                'mg' => 'HTML_Malagasy',
                'ms' => 'HTML_Malay',
                'ml' => 'HTML_Malayalam',
                'mt' => 'HTML_Maltese',
                'mi' => 'HTML_Maori',
                'mr' => 'HTML_Marathi',
                'mo' => 'HTML_Moldavian',
                'mn' => 'HTML_Mongolian',
                'na' => 'HTML_Nauru',
                'ne' => 'HTML_Nepali',
                'no' => 'HTML_Norwegian',
                'oc' => 'HTML_Occitan',
                'or' => 'HTML_Oriya',
                'ps' => 'HTML_Pashto',
                'fa' => 'HTML_Farsi',
                'pl' => 'HTML_Polish',
                'pt' => 'HTML_Portuguese',
                'pa' => 'HTML_Punjabi',
                'qu' => 'HTML_Quechua',
                'rm' => 'HTML_Rhaeto-Romance',
                'ro' => 'HTML_Romanian',
                'ru' => 'HTML_Russian',
                'sm' => 'HTML_Samoan',
                'sg' => 'HTML_Sangho',
                'sa' => 'HTML_Sanskrit',
                'gd' => 'HTML_Scots Gaelic',
                'sr' => 'HTML_Serbian',
                'sh' => 'HTML_Serbo-Croatian',
                'st' => 'HTML_Sesotho',
                'tn' => 'HTML_Setswana',
                'sn' => 'HTML_Shona',
                'sd' => 'HTML_Sindhi',
                'si' => 'HTML_Singhalese',
                'ss' => 'HTML_Siswati',
                'sk' => 'HTML_Slovak',
                'sl' => 'HTML_Slovenian',
                'so' => 'HTML_Somali',
                'es' => 'HTML_Spanish',
                'su' => 'HTML_Sundanese',
                'sw' => 'HTML_Swahili',
                'sv' => 'HTML_Swedish',
                'tl' => 'HTML_Tagalog',
                'tg' => 'HTML_Tajik',
                'ta' => 'HTML_Tamil',
                'tt' => 'HTML_Tatar',
                'te' => 'HTML_Telugu',
                'th' => 'HTML_Thai',
                'bo' => 'HTML_Tibetan',
                'ti' => 'HTML_Tigrinya',
                'to' => 'HTML_Tonga',
                'ts' => 'HTML_Tsonga',
                'tr' => 'HTML_Turkish',
                'tk' => 'HTML_Turkmen',
                'tw' => 'HTML_Twi',
                'ug' => 'HTML_Uigur',
                'uk' => 'HTML_Ukrainian',
                'ur' => 'HTML_Urdu',
                'uz' => 'HTML_Uzbek',
                'vi' => 'HTML_Vietnamese',
                'vo' => 'HTML_Volapuk',
                'cy' => 'HTML_Welsh',
                'wo' => 'HTML_Wolof',
                'xh' => 'HTML_Xhosa',
                'yi' => 'HTML_Yiddish',
                'yo' => 'HTML_Yoruba',
                'za' => 'HTML_Zhuang',
                'zu' => 'HTML_Zulu',
              }

        },
        'html_htmlhead_robot' => {
            edit_type => 'value_list',
            default   => 'index,follow',
            options   => ['index,follow', 'noindex,nofollow'],
            labels    => {
                'index,follow'     => 'HTML_Allow search engines',
                'noindex,nofollow' => 'HTML_Disallow search engines',
            },
        },
    },
);

sub wi_htmlhead {
    my ( $context, $page ) = @_;
    my ( $site, $sec ) = ( $context->site, $context->section );

    # several values are same sitewide, so use the context stash
    my $doctype  = $HTML->stash_pref( $context, 'html_htmlhead_doctype' );
    my $language = $HTML->stash_pref( $context, 'html_htmlhead_lang' );
    my $xtrahtml = $HTML->stash_pref( $context, 'html_htmlhead_xtrahtml' );
    my $exclude_self =
      $HTML->stash_pref( $context, 'html_links_exclude_self' );
    my $copyright =
      'Copyright ' . $site->name . ', ' . ( (localtime)[5] + 1900 );

    my $icon     = $context->stash('html_htmlhead_icon');
    my $icontype = $context->stash('html_htmlhead_icontype');
    if ( !defined $icon ) {    #find ico, png or gif favicon image
        foreach my $ext (qw(ico png gif)) {
            if ( -e bm_file_path( $site->homepage_dir, "favicon.$ext" ) ) {
                $icon = $site->homepage_url . "/favicon.$ext";
                $icontype = $ext eq 'ico' ? 'image/x-icon' : "image/$ext";
                last;
            }
        }
        $icon ||= q{};
        $context->set_stash( 'html_htmlhead_icon',     $icon );
        $context->set_stash( 'html_htmlhead_icontype', $icontype );
    }

    my $title_text = q{};
    if ( $page->subtype eq 'section' && ( !$sec || $sec->is_homepage ) ) {
        $title_text = $site->get_pref_value('html_htmlhead_titlehome')
          || $site->name;
    }
    else {
        my $tagline = $site->get_pref_value('html_htmlhead_titletagline');
        my %title   = (
            's' => $site->name,
            't' => $page->title,
            'd' => (
                $HTML->inline_rich_text( $page->description, $context ) || q{}
            ),
            'md' => ( $page->meta_description || q{} ),
            'k'  => ( $page->meta_keywords    || q{} ),
            'tag' => $tagline,
            'sp'  => q{ },
            q{.}  => q{:},
            q{:}  => q{::},
            q{|}  => q{|},
            q{-}  => q{-},
            'o'   => '(',
            'c'   => ')',
            '['   => '[',
            ']'   => ']',
        );

        foreach ( $site->get_pref_value( 'html_htmlhead_titlepage', $sec ) ) {
            $title_text .= $title{$_};
        }

        #To prevent search-engine bouncing, keep title tag to 128 chars
        if ( length($title_text) > 128 ) {
            $title_text = substr( $title_text, 0, 125 ) . '...';
        }
        $title_text = $HTML->strip_html_tags($title_text);
    }

    my $description = $page->meta_description;
    if ( !$description ) {
        $description = $HTML->rich_text( $page->description, $context );
        $description = $HTML->strip_html_tags($description);
        $description =~ s/"/&quot;/g;
    }

    #same for all pages of this level, so good to put into the context cache
    #section pages always get direct link to their own feed if available.
    #all other pages get a link to the full feed
    my $rss       = $context->stash('html_htmlhead_rss');
    my $rss_title = $context->stash('html_htmlhead_rsstitle');
    my $dot       = BigMed->bigmed->env('DOT');

    if ( !defined $rss ) {
        my %rss = $site->flags;
        if ( $rss{rss_disable_feed} ) {
            $rss = $context->set_stash( 'html_htmlhead_rss', q{} );
        }
        else {
            $rss =
              $context->set_stash( 'html_htmlhead_rss',
                $site->homepage_url . "/bm${dot}feed.xml" );
            $rss_title = $context->set_stash(
                'html_htmlhead_rsstitle',
                $HTML->strip_html_tags(
                        $site->name . q{ - }
                      . $site->get_pref_value('rss_fullfeed')
                )
            );
        }
    }
    if ( $page->subtype eq 'section' && !$sec->is_homepage ) {
        my %sflag = $sec->flags();
        $rss = $site->directory_url($sec) . "/bm${dot}feed.xml"
          if !$sflag{'rss_disable_feed'};
        $rss_title =
          $HTML->strip_html_tags( $site->name . q{ - } . $sec->name );
    }

    my %flag = $page->flags;
    my $robots =
      ( $flag{html_znosearch} || $flag{html_hideall} )
      ? 'noindex,nofollow'
      : $HTML->stash_pref( $context, 'html_htmlhead_robot' );

    BigMed::Comment->register_comment_prefs();
    my $comments_off = $flag{html_dcomments}
      || !$HTML->stash_pref( $context, 'html_comments_enabled' );

    return $context->build_markup(
        'wi_htmlhead.tmpl',
        close        => tag_closer($context),
        doctype      => $doctype,
        language     => $language,
        title        => $title_text,
        keywords     => $page->meta_keywords,
        description  => $description,
        author       => $page->authors( $context->relation_cache ),
        copyright    => $copyright,
        icon         => $icon,
        icontype     => $icontype,
        rss          => $rss,
        rss_title    => $rss_title,
        robots       => $robots,
        css          => $site->html_url . "/bm${dot}styles.css",
        css_custom   => $site->html_url . "/bm${dot}styles-custom.css",
        custom_html  => $xtrahtml,
        comments_off => $comments_off,
        exclude_self => $exclude_self,
        pageid       => $page->id,
        assets_dir   => $site->html_url . '/bm.assets',
        homeurl      => $site->homepage_url . '/index.shtml',
    );
}

$HTML->add_widget(
    name     => 'htmlend',
    handler  => sub { '</html>' },
    sitewide => 1,
);

$HTML->add_widget(
    name     => 'feeds',
    sitewide => 1,
    handler  => \&wi_feeds,
);

$HTML->add_widget(
    name     => 'feedtitle',
    sitewide => 1,
    handler  => \&wi_feedtitle,
);

$HTML->add_widget(
    name     => 'feedintro',
    sitewide => 1,
    handler  => \&wi_feedintro,
);

$HTML->add_widget(
    name     => 'fullfeedlink',
    sitewide => 1,
    handler  => \&wi_fullfeedlink,
);

$HTML->add_widget(
    name     => 'podcastlink',
    sitewide => 1,
    handler  => \&wi_podcastlink,
);

$HTML->add_widget(
    name     => 'sectionfeeds',
    sitewide => 1,
    handler  => \&wi_sectionfeeds,
);

sub wi_feeds {
    return q{} if !$_[0]->site->get_pref_value('rss_enable_feed');
    my $dot = BigMed->bigmed->env('DOT');
    return $_[0]->build_markup(
        'wi_feedlink.tmpl',
        title       => $_[0]->site->get_pref_value('rss_feed_linktext'),
        widget_name => 'feeds',
        url => $_[0]->site->homepage_url . "/bm${dot}feeds." . $HTML->suffix,
    );
}

sub wi_feedtitle {
    return $_[0]->build_markup( 'wi_feedtitle.tmpl',
        title => $_[0]->site->get_pref_value('rss_feed_title') );
}

sub wi_feedintro {
    return
        '<div class="bmw_feedintro">'
      . $HTML->rich_text( $_[0]->site->get_pref_value('rss_intro'), $_[0] )
      . '</div>';
}

sub wi_fullfeedlink {
    return q{} if !$_[0]->site->get_pref_value('rss_enable_feed');
    my $dot = BigMed->bigmed->env('DOT');
    return $_[0]->build_markup(
        'wi_feedlink.tmpl',
        title       => $_[0]->site->get_pref_value('rss_fullfeed'),
        widget_name => 'fullfeedlink',
        url         => $_[0]->site->homepage_url . "/bm${dot}feed.xml",
    );
}

sub wi_podcastlink {
    my $site = $_[0]->site;
    return q{}
      if !$site->get_pref_value('rss_enable_feed')
          || !$site->get_pref_value('rss_enable_podcast');
    my $dot = BigMed->bigmed->env('DOT');
    return $_[0]->build_markup(
        'wi_feedlink.tmpl',
        title       => $site->get_pref_value('rss_podcast'),
        widget_name => 'podcastlink',
        url         => $site->homepage_url . "/bm${dot}podcast.xml",
    );
}

sub wi_sectionfeeds {
    my $site = $_[0]->site;
    return q{} if !$site->get_pref_value('rss_enable_feed');
    my @feeds = _section_feed_params( $_[0], 'rss' );
    return q{} if !@feeds;
    return $_[0]->build_markup(
        'wi_sectionfeeds.tmpl',
        feeds   => \@feeds,
        heading => $site->get_pref_value('rss_section_title'),
    );
}

sub _section_feed_params {
    my ( $context, $type ) = @_;
    my $site   = $context->site;
    my $siteid = $site->id;
    my ( $flag, $ext ) =
      $type eq 'rss'
      ? qw(rss_disable_feed xml)
      : qw(js_disable_feed js);

    #can't use the context's cached active_descendants because we need
    #the full site list (although intended just for feeds page, could be
    #included on any page).
    my $dot = BigMed->bigmed->env('DOT');
    my @feeds;
    foreach my $secid ( $site->all_active_descendants_ids ) {
        my $sec = $site->section_obj_by_id($secid) or next;
        my %flags = $sec->flags;
        next if $flags{$flag};
        my @p = $sec->parents;
        shift @p;    #homepage
        my $ptrail =
          join( ' &gt; ', map { $site->section_obj_by_id($_)->name } @p );
        $ptrail = $ptrail ? "$ptrail &gt; " : q{};
        my $suff = $type eq 'rss' ? $ext : "$ext?$siteid-$secid";
        push @feeds,
          { title => $ptrail . $sec->name,
            url   => $site->directory_url($sec) . "/bm${dot}feed.$suff",
          };
    }
    return @feeds;
}

$HTML->add_widget(
    name     => 'newsgadget',
    handler  => \&wi_newsgadget,
    sitewide => 1,
);

sub wi_newsgadget {
    my ($context) = @_;
    my $site = $context->site;
    return q{<!-- newsgadget: news gadgets disabled -->}
      if !$site->get_pref_value('js_enable_feed');
    my $title = $site->get_pref_value('js_newsgadget_text');
    my $dot   = BigMed->bigmed->env('DOT');
    my $url   = $site->homepage_url . "/bm${dot}gadget." . $HTML->suffix;
    return $_[0]->build_markup(
        'wi_newsgadget.tmpl',
        title => $title,
        url   => $url,
    );
}

sub build_js_page {    #invoked by utility template's level_extras callback
    my $context = shift;
    return if $context->level ne 'top';
    my $site = $context->site;
    my $dot  = BigMed->bigmed->env('DOT');
    if ( !$site->get_pref_value('js_enable_feed') ) {
        my $fn = bm_file_path( $site->homepage_dir,
            "bm${dot}gadget." . $HTML->suffix );
        bm_delete_file($fn);
        return;
    }

    my $intro =
      $HTML->rich_text( $site->get_pref_value('js_feedbuilder_intro'),
        $context );
    my $step1 = $site->get_pref_value('js_feedbuilder_step1');
    my $step2 = $site->get_pref_value('js_feedbuilder_step2');
    my $step3 = $site->get_pref_value('js_feedbuilder_step3');
    my $step4 = $site->get_pref_value('js_feedbuilder_step4');
    my $step4_desc =
      $HTML->rich_text( $site->get_pref_value('js_feedbuilder_step4_desc'),
        $context );
    my $num_label    = $site->get_pref_value('js_number_label');
    my $desc_label   = $site->get_pref_value('js_desc_label');
    my $image_label  = $site->get_pref_value('js_image_label');
    my $win_label    = $site->get_pref_value('js_window_label');
    my $build_button = $site->get_pref_value('js_build_button');

    my @feeds = _section_feed_params( $context, 'js' );
    my $full_feed =
        $site->homepage_url
      . "/bm${dot}feed.js?"
      . $site->id . '-'
      . $site->homepage_id;
    unshift @feeds,
      { title => 'Full Feed',
        url   => $full_feed,
      };

    my $content = $context->build_markup(
        'wi_gadgetbuilder.tmpl',
        intro        => $intro,
        step1        => $step1,
        step2        => $step2,
        step3        => $step3,
        step4        => $step4,
        step4_desc   => $step4_desc,
        feeds        => \@feeds,
        num_label    => $num_label,
        desc_label   => $desc_label,
        image_label  => $image_label,
        win_label    => $win_label,
        build_button => $build_button,
    );

    my $title = $site->get_pref_value('js_feedbuilder_head');
    my $headline =
      $context->build_markup( 'wi_headline.tmpl', title => $title, );
    return {
        filename    => "bm${dot}gadget",
        headline    => $headline,
        title       => $title,
        content     => $content,
        breadcrumbs => top_level_breadcrumbs( $context, $title ),
    };
}

###########################################################
# SEARCH
###########################################################

$HTML->add_widget(
    name     => 'search',
    handler  => \&wi_search,
    sitewide => 1,
    group    => 'search',
    prefs    => { BigMed::Search->search_prefs },
);

sub wi_search {
    my ( $context, $obj, $rparam ) = @_;
    my $site   = $context->site;
    my $lang   = $site->get_pref_value('html_htmlhead_lang') || 'en-us';
    my $search = BigMed::Search->new( locale => $lang ) or return q{};
    return $search->form_html( $context, $obj, $rparam );
}

sub build_search_page {
    my $context = shift;
    return if $context->level ne 'top';
    my $la = $context->site->get_pref_value('html_htmlhead_lang') || 'en-us';
    my $search = BigMed::Search->new( locale => $la ) or return q{};
    return $search->result_page_html($context);
}

###########################################################
# TIPS
###########################################################

$HTML->add_widget(
    name => 'tips',
    handler =>
      sub { _handle_section_include( @_, 'tipincl', $HTML->suffix ) },
    sectionwide => 1,
    group       => 'tips_annc',
    prefs       => {
        'html_tip_randomize' => {
            default   => '1',
            edit_type => 'value_list',
            options   => ['0', '1'],
            labels    => {
                '0' => 'PREFS_TIPS_Display top tips',
                '1' => 'PREFS_TIPS_Display random',
            },
            priority => 90,
        },
        'html_tip_sort_order' => {
            edit_type   => 'sort_order',
            priority    => 95,
            default     => 'priority:pub_time:mod_time|d:d:d',
            edit_params => {
                numfields       => 3,
                required        => 1,
                description     => 'HTML_tipsort_desc',
                container_class => 'bmcpDividerField',
            },
            priority => 80,
        },
        'html_tip_numdisplay' => {
            edit_type   => 'number_positive_integer',
            default     => 3,
            priority    => 70,
            edit_params => { description => 'HTML_tipnumdisplay_desc', }
        },
        'html_tip_pagetitle' => {
            edit_type   => 'rich_text_inline',
            default     => 'Tips',
            edit_params => {
                required        => 1,
                description     => 'HTML_tippagetitle_desc',
                container_class => 'bmcpDividerField',
            },
            priority => 60,
        },
        'html_tip_linktext' => {
            edit_type   => 'rich_text_inline',
            default     => 'All tips for &lt;%section%&gt;',
            edit_params => { description => 'HTML_tiplinktext_desc', },
        }
    },
);

sub build_tips {
    my $context = shift;
    my $site    = $context->site;
    my $sec     = $context->section;

    my $tip;
    my @tips;
    if ( $context->is_active ) {
        my $all_tips =
          _gather_nonpage_content( $context, 'BigMed::Content::Tip',
            'html_tip_sort_order' );
        my $rpref = { img_pref => 'html_tip_image_size' };
        while ( $tip = $all_tips->next ) {
            my $content =
              _content_builder( $context, $tip, $rpref, 'bmw_tipContent' );
            my $index = @tips;
            push @tips,
              { title   => $tip->title,
                content => $content,
                id      => $tip->id,
                index   => $index,
              };
        }
    }

    #get the content of the tip include
    my @include_tips;
    my $randomize = $site->get_pref_value( 'html_tip_randomize', $sec );
    if ( $randomize && @tips > 1 ) {
        @include_tips = @tips > 60 ? @include_tips = @tips[0 .. 59] : @tips;
        my $count = 0;
        my $total = @include_tips;

        #ssi needs two-digit numbers for comparison
        foreach my $tip (@include_tips) {
            $tip->{low} = sprintf( '%02d', int( ( $count / $total ) * 60 ) );
            $tip->{next} =
              sprintf( '%02d', int( ( ( $count + 1 ) / $total ) * 60 ) );
            $count++;
        }
    }
    elsif ($randomize) {
        undef $randomize;
        @include_tips = @tips;
    }
    else {
        my $num_tips = $site->get_pref_value( 'html_tip_numdisplay', $sec );
        $num_tips = @tips if $num_tips > @tips;
        @include_tips = @tips[0 .. $num_tips - 1] if $num_tips;
    }
    my $include_content = '<!-- no tips to display -->';
    my $dot             = BigMed->bigmed->env('DOT');
    if ( @include_tips && $context->is_active ) {
        my $link_text = $site->get_pref_value( 'html_tip_linktext', $sec );
        my $sec_name = $sec ? $sec->name : $site->name;
        $link_text =~ s/&lt;%section%&gt;/$sec_name/msg;
        $include_content = $context->build_markup(
            'wi_tip_include.tmpl',
            randomize => $randomize,
            win_tips  => ( index( $OSNAME, 'MSWin' ) >= 0 ), #use js for win32
            tips      => \@include_tips,
            link_text => $link_text,
            url       => $site->directory_url($sec)
              . "/bm${dot}tips."
              . $HTML->suffix,
        );
    }
    my $slug_path = _slug_path( $context, $sec );
    $slug_path &&= "/$slug_path";
    my $include =
      $site->html_dir . "$slug_path/bm${dot}tipincl." . $HTML->suffix;
    bm_write_file( $include, $include_content, { build_path => 1 } );

    #get the content for the tips page and return it
    my $content = q{};
    $content = $context->build_markup( 'wi_tips.tmpl', tips => \@tips, )
      if @tips;

    my $tip_title = $site->get_pref_value( 'html_tip_pagetitle', $sec );
    return {
        filename => "bm${dot}tips",
        headline => qq{<h2 class="bmw_headline">$tip_title</h2>},
        title    => $tip_title,
        content  => $content,
    };
}

sub _gather_nonpage_content {
    my ( $context, $class, $sort_pref ) = @_;
    my $site   = $context->site;
    my $sec    = $context->section || $site->homepage_obj;
    my %search = ( site => $site->id, pub_status => 'published' );
    if ( $site->get_pref_value('html_micro_show_subsection') ) {
        $search{sections} = $context->ordered_descendants
          || [$site->all_active_descendants($sec)];
    }
    push @{ $search{sections} }, $sec->id;

    my $require = $class;
    $require =~ s{::}{/}msg;
    require "$require.pm";

    my ( $rsort, $rorder ) =
      _parse_sort_order( $site->get_pref_value( $sort_pref, $sec ) );
    return $class->select( \%search, { sort => $rsort, order => $rorder } );
}

sub _parse_sort_order {
    my $sort_order_string = shift;
    my ( $sort, $order ) = split( /\|/ms, $sort_order_string );
    my @sort = split( /:/ms, $sort );
    my @ord = map { $_ eq 'a' ? 'ascend' : 'descend' } split( /:/ms, $order );
    return ( \@sort, \@ord );
}

###########################################################
# ANNOUNCEMENTS
###########################################################

$HTML->add_widget(
    name         => 'announcements',
    handler      => \&wi_announce,
    build_always => 1,
    sectionwide  => 1,
    group        => 'tips_annc',
    prefs        => {
        'html_annc_sort_order' => {
            edit_type   => 'sort_order',
            priority    => 95,
            default     => 'priority:pub_time:mod_time|d:d:d',
            edit_params => {
                numfields => 3,
                required  => 1,
            },
        }
    }
);

sub wi_announce {
    my ( $context, $obj, $rparam ) = @_;
    my $site = $context->site;
    my $sec  = $context->section;
    my $slug = $rparam->{slug};
    my $dot  = BigMed->bigmed->env('DOT');
    if (   !$slug
        || ( !$sec && $slug ne '@all' )
        || ( $sec && $sec->slug eq $slug ) )
    {

        #build all announcements for section and save to include
        my $include_content = '<!-- no announcements -->';
        return $include_content if !$context->is_active;

        my $all_annc =
          _gather_nonpage_content( $context, 'BigMed::Content::Annc',
            'html_annc_sort_order' );
        my $annc;
        my @annc;
        my $rpref = { img_pref => 'html_annc_image_size' };
        while ( $annc = $all_annc->next ) {
            my $content =
              _content_builder( $context, $annc, $rpref, 'bmw_anncContent' );
            push @annc,
              { title   => $annc->title,
                content => $content,
              };
        }
        if (@annc) {    #update include_content
            $include_content =
              $context->build_markup( 'wi_annc.tmpl', announcements => \@annc,
              );
        }
        my $slug_path = _slug_path( $context, $sec );
        $slug_path &&= "/$slug_path";
        my $include = $site->html_dir . "$slug_path/bm${dot}annc.shtml";
        bm_write_file( $include, $include_content, { build_path => 1 } );
    }
    return _handle_section_include( $context, $obj, $rparam, 'annc' );
}

###########################################################
# TAGS
###########################################################

$HTML->add_widget(
    name     => 'tagcloud',
    handler  => \&wi_tagcloud,
    sitewide => 1,
);

#not a "real" widget, just gives us our prefs
$HTML->add_widget(
    name    => 'taglinks',
    group   => '0links',
    handler => sub { return q{}; },
    prefs   => {
        'html_taglinks_elements' => {
            fallback    => 'html_link_elements',
            sitewide    => 1,
            edit_params => { numfields => 12 },
        },
        'html_taglinks_imagepos' => {
            sitewide => 1,
            fallback => 'html_link_imagepos',
        },
        'html_taglinks_imagesize' => {
            fallback => 'html_link_imagesize',
            sitewide => 1,
        },
        'html_taglinks_includerelated' => {
            fallback => 'html_links_includerelated',
            sitewide => 1,
        },
    },
);

sub wi_tagcloud {

    #the cloud file itself is always built by the top level via build_tags
    #below

    return _handle_section_include( $_[0], $_[1], { slug => '@all' },
        'tagcloud' );
}

sub build_tags {    #invoked by utility template's level_extras callback
    my $context = shift;
    return if $context->level ne 'top';

    build_tagcloud_include($context) or return;

    #callback generates the individual tag pages
    $context->set_stash( 'HTML:build_tag_pages', 1 );
    BigMed::Builder->add_trigger( 'before_extras_build', \&build_indiv_tags );

    my $content = _handle_section_include( $context, undef, {}, 'tagcloud' );
    my $intro_text =
      $HTML->rich_text( $context->site->get_pref_value('html_tag_intro'),
        $context );
    my $title = $context->site->get_pref_value('html_tag_headline');
    my $dot   = BigMed->bigmed->env('DOT');
    return {
        filename    => ["bm${dot}tags",                 'index'],
        headline    => qq{<h2 class="bmw_headline">$title</h2>},
        title       => $title,
        content     => "$intro_text\n$content",
        breadcrumbs => top_level_breadcrumbs( $context, $title ),
    };
}

sub build_indiv_tags {
    my ( $builder, $section, $tmpl, $rwvalue ) = @_;
    my $context = $builder->context;
    return 1
      if !$context->stash('HTML:build_tag_pages')
          || $context->format_class ne $HTML;
    $context->set_stash( 'HTML:build_tag_pages', undef );

    my $site = $builder->site;
    my $sid  = $site->id;
    my ( $tag_pointers, $build_tags ) = _gather_tag_info($builder);
    return $tag_pointers if !ref $tag_pointers;    #error or none to build

    #note that because the force_tags value is provided, among other
    #times, when a page is deleted, it's possible that we could be getting
    #a tag that has been orphaned and deleted from the system between
    #the page deletion and now. So we're building pages for deleted tags
    #which will result in empty tag pages. Not the end of the world,
    #but not completely tidy either.

    my %done;
    foreach my $tag ( $build_tags->fetch, $builder->force_tags ) {
        next if $done{ $tag->id };
        $builder->log(
            notice => 'HTML: Building tag ' . $builder->log_data_tag($tag) );
        $builder->update_statusbar(
            message => ['BUILDER_Building tags', $tag->name], );
        _do_pages_for_tag( $builder, $tmpl, $tag, $tag_pointers, $rwvalue )
          or return;
        $done{ $tag->id } = 1;
    }
    return 1;
}

sub _gather_tag_info {
    my $builder = shift;

    #return ($tag_pointers, $build_tags) if we have something to build
    #return 1 if nothing to build, undef if error
    #
    #we only build tags if:
    # 1. A limited page set (we've just edited/updated pages, and we need
    #    to rebuild the tags for those pages).
    # 2. All pages for all sections (limited sections indicates that we're
    #    doing a defer-overflow build or updating a specific section of
    #    the site; tags not relevant there).

    my @limit_pages = $builder->limited_pages();
    my @force       = $builder->force_tags();
    my $do_build    = @limit_pages || !$builder->limit_sections || @force;
    return 1 if !$do_build;

    #winnow pointers to tag-specific pointers
    my $site         = $builder->site;
    my $sid          = $site->id;
    my $tag_pointers = $builder->relation_cache->{'BigMed::Pointer'}->select(
        {   site         => $sid,
            target_table => BigMed::Tag->data_source,
            source_table => BigMed::Content::Page->data_source,
        }
    ) or return;
    return 1 if !$tag_pointers->count;

    #gather the selection of tags to build; if building a limited set
    #of pages, build the tags for those pages only
    my $all_tags = BigMed::Tag->select( { site => $sid } ) or return;
    my $build_tags = $all_tags->join_pointed_from(
        {   'join'    => 'BigMed::Content::Page',
            'pselect' => $tag_pointers,
            'unique'  => 1,
        },
        { 'site' => $sid, 'id' => \@limit_pages },
    ) or return;
    return ( $tag_pointers, $build_tags );
}

sub _do_pages_for_tag {
    my ( $builder, $tmpl, $tag, $tag_pointers, $rwvalue ) = @_;
    my $site = $builder->site;

    #gather and sort the tag's pages
    my $tag_pages = $builder->content->join_points_to(
        {   'join'    => 'BigMed::Tag',
            'pselect' => $tag_pointers,
            'unique'  => 1,
        },
        { 'site' => $site->id, 'id' => $tag->id },
    ) or return;
    my ( $rsort, $rorder ) =
      _parse_sort_order( $site->get_pref_value('html_link_sort_order') );
    $tag_pages =
      $tag_pages->select( undef, { sort => $rsort, order => $rorder } )
      or return;

    #collect the html for each link
    my $tag_widget = $HTML->widget('taglinks');
    my $context    = $builder->context;
    my $ractive    = $context->active_descendants;
    my @links;
    my $page;
    my $ping_count = 0;
    while ( $page = $tag_pages->next ) {
        my %flag = $page->flags;
        next if $flag{hideall};
        my $secid = $page->effective_active_section( $site, undef, $ractive )
          or next;
        push @links,
          _link_builder( $tag_widget, $context, $page,
            { use_section => $secid } );
        $ping_count++;
        $builder->call_trigger('level_midbuild') if !( $ping_count % 100 );
    }
    return if !defined $page;

    #get prefs for overall page display; use links widget as a stand-in
    my %w_value  = %{$rwvalue};
    my $per_page = $site->get_pref_value('html_tag_numdisplay') || 15;
    my $tname    = $tag->name;
    my $title    = $site->get_pref_value('html_tag_indiv_headline');
    $title =~ s/&lt;%tag%&gt;/$tname/msg;
    my $headline = qq{<h2 class="bmw_headline">$title</h2>};
    my $dot      = BigMed->bigmed->env('DOT');
    my $base_url =
      $site->homepage_url . qq{/bm${dot}tags/} . $tag->slug . '/index';

    #breadcrumbs
    my @breadcrumbs;
    my $sep       = $site->get_pref_value('html_breadcrumbs_separator');
    my $lc_bread  = $site->get_pref_value('html_breadcrumbs_lc');
    my $home      = $site->homepage_obj->name;
    my $main_tags = $site->get_pref_value('html_tag_headline');
    @breadcrumbs = (
        {   crumb => ( $lc_bread ? lc $home : $home ),
            url       => $site->homepage_url . '/index.' . $HTML->suffix,
            separator => $sep,
        },
        {   crumb => ( $lc_bread ? lc $main_tags : $main_tags ),
            url => $site->homepage_url
              . qq{/bm${dot}tags/index.}
              . $HTML->suffix,
            separator => $sep,
        },
    );

    my $empty = !@links;    #none to display, but create an empty anyway
    my $npages = int( @links / $per_page );
    $npages++ if @links % $per_page;
    my $count = 0;
    my @link_pages;
    while ( @links || $empty ) {
        $empty = 0;         #do empties only once
        $count++;
        my $nav = navigation_bar(
            'context'   => $context,
            'base_url'  => $base_url,
            'pnum'      => $count,
            'total_num' => $npages,
        );

        my $total   = @links;
        my $take    = ( $per_page > $total ) ? $total : $per_page;
        my $rlinks  = $total ? [splice( @links, 0, $take )] : [];
        my $content = $context->build_markup(
            'wi_links_overflow.tmpl',
            links       => $rlinks,
            div_class   => 'bmw_tagLinks',
            widget_name => 'tags',
            navigation  => $nav,
        );

        my @crumbs = (
            @breadcrumbs,
            {   crumb => ( $lc_bread ? lc $tag->slug : $tag->slug ),
                url       => "$base_url." . $HTML->suffix,
                separator => $sep,
            }
        );
        my $bcrumbs = $context->build_markup( 'wi_breadcrumbs.tmpl',
            breadcrumbs => \@crumbs );

        my $file = $count == 1 ? 'index' : "index${dot}p$count";
        my $path =
          $builder->builder_file_path_slug( $tmpl,
            ["bm${dot}tags", $tag->slug, $file] );
        my $page = $builder->replace_widgets(
            $tmpl->{text},
            {   %w_value,
                content     => $content,
                title       => $title,
                headline    => $headline,
                breadcrumbs => $bcrumbs,
            }
        );
        bm_write_file( $path, $page, { build_path => 1 } ) or return;
    }
    return if !defined $tag;

    return 1;
}

sub build_tagcloud_include {
    my $context = shift;
    my $site    = $context->site;
    my $sid     = $site->id;

    require BigMed::Tag;
    my $rtag_count =
      BigMed::Tag->tag_counts( $site,
        $context->relation_cache->{'BigMed::Pointer'},
        $context->content, )
      or return;
    my %tag_count = %{ $rtag_count->{count} };
    my @tiers;
    if (%tag_count) {
        my ( $low, $high ) = ( $rtag_count->{low}, $rtag_count->{high} );
        my $step = ( $high - $low ) / 5;
        @tiers = (
            int( $low + $step ),
            int( $low + ( $step * 2 ) ),
            int( $low + ( $step * 3 ) ),
            int( $low + $step * 4 )
        );
    }

    #gather tag info; for performance, grab all values from index only
    my @tags;
    {
        my $all_tags = BigMed::Tag->select( { site => $sid },
            { sort => 'name', order => 'ascend' } )
          or return;
        while ( my $rindex = $all_tags->next_index ) {
            push @tags, [$rindex->{id}, $rindex->{name}, $rindex->{slug}]
              if $tag_count{ $rindex->{id} };
        }
    }
    my @items;
    my $dot     = BigMed->bigmed->env('DOT');
    my $tag_url = $site->homepage_url . "/bm${dot}tags";
    foreach my $rtag (@tags) {
        my $count = $tag_count{ $rtag->[0] };
        my $size =
            $count < $tiers[0]  ? 'xsmall'
          : $count < $tiers[1]  ? 'small'
          : $count <= $tiers[2] ? 'medium'   #<= helps hit med for tiny ranges
          : $count <= $tiers[3] ? 'large'
          :                       'xlarge';
        my ( $name, $slug ) = ( $rtag->[1], $rtag->[2] );
        push @items,
          { name  => $name,
            slug  => $slug,
            count => $count,
            size  => $size,
            $size => 1,
            url   => "$tag_url/$slug/",
          };
    }

    my $html =
      scalar @items
      ? $context->build_markup( 'wi_tagcloud.tmpl', tags => \@items, )
      : '<!-- no tags to display -->';
    my $include = $site->html_dir . "/bm${dot}tagcloud.shtml";
    return bm_write_file( $include, $html, { build_path => 1 } );
}

###########################################################
# WIDGET HELPERS
###########################################################

sub tag_closer {
    my $context = shift;
    return $context->stash('CLOSE') if defined $context->stash('CLOSE');
    my $doctype = $HTML->stash_pref( $context, 'html_htmlhead_doctype' );
    my $tag_closer = $doctype =~ /xhtml/msi ? q{ /} : q{};
    $context->set_stash( 'CLOSE', $tag_closer );
    return $tag_closer;
}

my $BMAPP;

sub link_formatted_date {    #@_ = [0]context, [1]time
    $BMAPP ||= BigMed->bigmed->app;
    return $BMAPP->format_time(
        $_[1],
        {   site         => $_[0]->site,
            not_relative => 1,
            no_time =>
              !( $HTML->stash_pref( $_[0], 'html_links_includetime' ) ),
        }
    );
}

sub formatted_date {         #@_ = [0]context, [1]time, [2]include_time
    $BMAPP ||= BigMed->bigmed->app;
    return $BMAPP->format_time(
        $_[1],
        {   site         => $_[0]->site,
            no_time      => !$_[2],
            not_relative => 1,
        }
    );
}

sub is_new_window_url {
    my ( $context, $url ) = @_;
    return 0 if !$url || $url eq 'http://' || index( $url, 'http' ) != 0;
    my $new_window = $HTML->stash_pref( $context, 'html_links_window' )
      or return 0;
    my $rdomains = $context->stash('html_links_window_intdomains');
    my @domains;
    if ( !$rdomains ) {
        @domains =
          $context->site->get_pref_value('html_links_window_intdomains');
        $context->set_stash( 'html_links_window_intdomains', [@domains] );
    }
    else {
        @domains = @{$rdomains};
    }
    foreach my $dom (@domains) {
        return 0 if index( $url, $dom ) == 0;
    }
    return 1;
}

sub tool_url {
    my ( $context, $page, $tool ) = @_;

    #use '#' if the page is not active (for preview)
    my $url = $page->active_page_url(
        $context->site,
        {   section => $context->section,
            rcache  => $context->relation_cache,
            rkids   => $context->active_descendants,
        }
    );
    $url ||= q{#};
    my $suffix = $HTML->suffix;

    #include query string for overridden urls like e.g. google news plugin
    my $dot = BigMed->bigmed->env('DOT');
    $url =~ s/[.](\Q$suffix\E)(\?.*)?$/$dot$tool.$1/ms;
    return $url;
}

1;

__END__

=head1 BigMed::Format::HTML

=head1 Synopsis

=head1 Description

=head1 Author & Copyrights

This module and all Big Medium modules are copyright Josh Clark
and Global Moxie. All rights reserved.

Use of this module and the Big Medium content
management system are governed by Global Moxie's software licenses
and may not be used outside of the terms and conditions outlined
there.

For more information, visit the Global Moxie website at
L<http://globalmoxie.com/>.

Big Medium and Global Moxie are service marks of Global Moxie
and Josh Clark. All rights reserved.

=cut

