<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;"># 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-&gt;register_format(
    'HTML',
    suffix         =&gt; 'shtml',
    level_filename =&gt; 'index'
);

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

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

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

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

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

$HTML-&gt;add_section_flag(qw(html_nohome html_nonav html_noparent));
$HTML-&gt;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-&gt;add_group( name =&gt; 'pagefooter' );
$HTML-&gt;add_group( name =&gt; 'navigation' );
$HTML-&gt;add_group( name =&gt; '0document' );
$HTML-&gt;add_group( name =&gt; 'email' );
$HTML-&gt;add_group( name =&gt; 'search' );

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

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

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

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

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

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

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

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

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

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

        my $class = q{bma_head};
        my $sub   = $pg-&gt;subtype;
        if ( ( $sub eq 'download' || $sub eq 'av' || $sub eq 'podcast' )
            &amp;&amp; $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-&gt;suffix;
        }

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

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

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

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

sub inline_rich_text {    # @_ = [0]class, [1]unfiltered_text, [2]context
    return $HTML-&gt;stash_pref( $_[2], 'html_links_window' )
      ? _add_new_window_links( $_[0]-&gt;SUPER::inline_rich_text( $_[1] ),
        $_[2] )
      : $_[0]-&gt;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{(&lt;a([^&gt;]+)href\s*=\s*"([^"]+)"([^&gt;]*)&gt;)}{
        _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-&gt;site;
    my $section = $context-&gt;section;
    my $sec_id =
      ( !$section || $section-&gt;is_homepage )
      ? $HTML-&gt;allowed_on_home( $context, $page )
      : $HTML-&gt;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 &amp;&amp; $sec_id == $section-&gt;id;
    my $sec      = $site-&gt;section_obj_by_id($sec_id) or return q{};
    my $sec_name = $sec-&gt;name;
    my $wrapper  = $HTML-&gt;stash_pref( $context, 'html_links_textsection' )
      || q{};
    $wrapper =~ s/&amp;lt;\%section\%&amp;gt;/$sec_name/msig;
    return ( $wrapper, _section_page_url( $site, $sec ) );
}

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

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

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

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

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

sub _link_builder {
    my ( $widget, $context, $obj, $roption ) = @_;
    $roption ||= {};
    my $name =
        $widget-&gt;is_overflow ? 'links'
      : index( $widget-&gt;name, 'spotlight' ) == 0 ? 'spotlight'
      :                                            $widget-&gt;name;
    my $site = $context-&gt;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-&gt;{use_section};
    if ( !$use_sec_id ) {
        my $sec = $roption-&gt;{no_section} ? 0 : $context-&gt;section;
        $use_sec_id =
          ( !$sec || $sec-&gt;is_homepage )
          ? $HTML-&gt;allowed_on_home( $context, $obj )
          : $HTML-&gt;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-&gt;active_page_url(
            $site,
            {   section =&gt; $site-&gt;section_obj_by_id($use_sec_id),
                rcache  =&gt; $context-&gt;relation_cache,
                rkids   =&gt; $context-&gt;active_descendants,
            }
          )
          || q{#}
      );
    $url = $HTML-&gt;escape_xml($url);

    my $new_win = q{};
    if ( $obj-&gt;subtype eq 'link' ) {
        my $link = (
            $obj-&gt;load_related_objects(
                'link_url', $context-&gt;relation_cache
            )
        )[0];
        if ( !$link || $link-&gt;link_to eq 'page' || $link-&gt;new_win eq 'no' ) {
            $new_win = q{};
        }
        elsif ( $link-&gt;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-&gt;stash("html_${element_name}_LINK_ELEMENTS");
    if ( !$rchunks ) {
        my @elements = $site-&gt;get_pref_value( "html_${element_name}_elements",
            $context-&gt;section );
        my @chunks;
        foreach my $elem (@elements) {
            next if !$link_element{$elem};
            push @chunks, $link_element{$elem};
        }
        $rchunks = \@chunks;
        $context-&gt;set_stash( "html_${element_name}_LINK_ELEMENTS", $rchunks );
    }

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

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

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

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

sub _load_related_links {
    my ( $context, $obj ) = @_;
    my @related =
      $obj-&gt;related_links( $context-&gt;site, $context-&gt;relation_cache );
    foreach my $link (@related) {
        $link-&gt;{new_win} ||= q{};
        $link-&gt;{new_window} =
            $link-&gt;{new_win} eq 'no'  ? q{}
          : $link-&gt;{new_win} eq 'yes' ? $NEW_WIN
          : is_new_window_url( $context, $link-&gt;{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-&gt;metadata;

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

    my $magnify = q{};
    my $msize = $HTML-&gt;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-&gt;stash_pref( $context, 'html_image_textmagnify' );
    my $caption = $HTML-&gt;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-&gt;escape_xml($hotlink);
        $hotlink_window =
          is_new_window_url( $context, $hotlink ) ? $NEW_WIN : q{};
    }

    my $alt_text    = $HTML-&gt;strip_html_tags( $obj-&gt;title );
    my $esc_caption = $HTML-&gt;escape_xml($caption);
    $esc_caption =~ s/&amp;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          =&gt; $obj-&gt;title,
            alt_text       =&gt; $alt_text,
            caption        =&gt; $caption,
            esc_caption    =&gt; $esc_caption,
            src            =&gt; $src,
            type_image     =&gt; 1,
            max_width      =&gt; $max_width,
            align          =&gt; $align,
            hotlink        =&gt; $hotlink,
            hotlink_window =&gt; $hotlink_window,
            magnify        =&gt; $magnify,
            magnify_text   =&gt; $magnify_text,
            close          =&gt; tag_closer($context),
        }
    );
}

sub _link_media_image {
    my ( $widget, $context, $pair, $page ) = @_;
    my ( $pointer, $obj ) = @{$pair};
    my $name =
        $widget-&gt;is_overflow ? 'links'
      : index( $widget-&gt;name, 'spotlight' ) == 0 ? 'spotlight'
      :                                            $widget-&gt;name;
    my $element_name = $name eq 'links' ? 'link' : $name;
    my $align =
      $HTML-&gt;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-&gt;active_page_url(
        $context-&gt;site,
        {   section =&gt; $context-&gt;section,
            rcache  =&gt; $context-&gt;relation_cache,
            rkids   =&gt; $context-&gt;active_descendants,
        }
    );
    $page_url ||= q{#};
    $rimage-&gt;{page_url} = $HTML-&gt;escape_xml($page_url);
    $rimage-&gt;{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-&gt;filename or return ();
    my $site     = $context-&gt;site;
    my $filesize = $obj-&gt;filesize($site);
    $filesize =~ s/ /&amp;#160;/ms;
    my $suffix = ( $file =~ /.*[.](\S+)$/ms )[0];
    my %meta = $pointer-&gt;metadata;
    my $align;

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

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

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

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

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

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

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

    return $context-&gt;build_markup(
        'wi_links_overflow.tmpl',
        links       =&gt; $rpage_links,
        div_class   =&gt; 'bmw_overflowLinks',
        widget_name =&gt; 'overflow',
        navigation  =&gt; $widget-&gt;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 &lt;= 1 &amp;&amp; !$context-&gt;defer_overflow;

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

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

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

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

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

    push @buttons, { text =&gt; $next, url =&gt; $next_url };
    return $context-&gt;build_markup(
        'wi_overflow_nav.tmpl',
        pages =&gt; \@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 =&gt; $total_num,
        this_page   =&gt; $pnum,
    );
}

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

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

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

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

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

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

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

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

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

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

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

    return $context-&gt;build_markup(
        'wi_links_generic.tmpl',
        links       =&gt; \@links,
        div_class   =&gt; 'bmw_sections',
        widget_name =&gt; '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-&gt;widget_map;
    $widget{spotlight}-&gt;add_to_collection(
        _link_builder( $widget{spotlight}, $context, $obj ) )
      if $widget{spotlight};

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

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

sub _gather_spotlight_images {
    my ( $context, $obj ) = @_;
    my @images;
    my $rcache = $context-&gt;relation_cache;
    foreach my $pair ( $obj-&gt;sorted_related_objects( 'media', $rcache ) ) {
        next if $pair-&gt;[1]-&gt;data_label ne 'image';
        my %meta = $pair-&gt;[0]-&gt;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-&gt;build_markup( 'wi_spotlightimage.tmpl',
        pages =&gt; $widget-&gt;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-&gt;build_markup( 'wi_spotlightimage_page.tmpl',
        images =&gt; \@images, );
}

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

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

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

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

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

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

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

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

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

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

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

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

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

    if ($is_home) {
        return 0
          if $flag{html_nohome}
              || $flag{html_nospothome}
              || !$HTML-&gt;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 { $_ =&gt; 1 } $obj-&gt;sections;
            return 0 if !$belongs{ $sec-&gt;id };
        }
        return 0 if !$HTML-&gt;allowed_on_section( $context, $obj );
    }

    if ( $HTML-&gt;stash_pref( $context, 'html_spotlight_needimage' ) ) {
        my $has_spotlight_image;
        my $rcache = $context-&gt;relation_cache;
        foreach my $pair ( $obj-&gt;sorted_related_objects( 'media', $rcache ) )
        {
            next if $pair-&gt;[1]-&gt;data_label ne 'image';
            my %meta = $pair-&gt;[0]-&gt;metadata;
            $has_spotlight_image = 1, last
              if $meta{link_position}
                  &amp;&amp; (   $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-&gt;flags;
    my $hideonhome =
      $HTML-&gt;stash_pref( $context, 'html_quicktease_hideonhome' );
    my $hideonmain =
      $HTML-&gt;stash_pref( $context, 'html_quicktease_hideonmain' );
    return 0
      if $flag{html_noqt}
          || ( $hideonhome &amp;&amp; $context-&gt;on_homepage($obj) )
          || ( $hideonmain &amp;&amp; $context-&gt;on_section($obj) );
    return collect_links_generic( $self, $context, $obj );
}

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

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

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

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

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

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

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

}

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

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

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

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

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

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

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

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

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

    #discover the relationships that embed content
    my @embedders;
    my $rembedders = $context-&gt;stash( 'EMBEDDERS_' . ref $obj );
    if ( !$rembedders ) {
        foreach my $relation ( $obj-&gt;data_relationships ) {
            my %info = $obj-&gt;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-&gt;set_stash( 'EMBEDDERS_' . ref $obj, [@embedders] );
    }
    else {
        @embedders = @{$rembedders};
    }

    #collect embedded relationship objects by paragraph
    my %embed_obj;
    my $rcache = $context-&gt;relation_cache;
    foreach my $embed ( map { $obj-&gt;load_related_objects( $_, $rcache ) }
        @embedders )
    {
        my ( $pos, $priority, $mod_time );
        if ( ref $embed eq 'ARRAY' ) {
            my %meta = $embed-&gt;[0]-&gt;metadata();
            $pos      = $meta{position};
            $priority = $meta{priority} || 500;
            $mod_time = $embed-&gt;[0]-&gt;mod_time;
        }
        else {
            $pos = $embed-&gt;position;
            $priority =
              $embed-&gt;can('priority') ? ( $embed-&gt;priority || 0 ) : 500;
            $mod_time = $embed-&gt;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}]*&lt;[^&gt;]+&gt;[\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 { $_-&gt;[0] }
              sort { $b-&gt;[1] &lt;=&gt; $a-&gt;[1] || $b-&gt;[2] cmp $a-&gt;[2] }
              @{ $embed_obj{$graf} };
            my @html;
            foreach my $item (@embed) {
                my $label =
                  ref $item eq 'ARRAY'
                  ? $item-&gt;[1]-&gt;data_label
                  : $item-&gt;data_label;
                push( @html,
                    $embed_handler{$label}-&gt;( $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 &gt; @grafs;
                $grafs[$num - 1] = join( q{}, @html, $grafs[$num - 1] );
            }
        }
        $text = join( q{}, @grafs );
    }
    my $html = $context-&gt;build_markup(
        'wi_content.tmpl',
        __alt_path =&gt; $rparam-&gt;{alt_path},
        content    =&gt; $text,
        above      =&gt; $above,
        below      =&gt; $below,
        class      =&gt; $divclass || 'bmw_pageContent',
    );
    return $callback ? $callback-&gt;( $obj, $context, $html ) : $html;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

sub _encode_email {    #from Markdown: http://www.daringfireball.com/
    my $addr = shift;
    srand;
    my @encode = (
        sub { q{&amp;#} . ord(shift) . q{;} },
        sub { q{&amp;#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]-&gt;($char);
        } elsif ( $char ne ':' ) {
            # leave ':' alone (to spot mailto: later)
            my $r = rand;
            # roughly 10% raw, 45% hex, 45% dec
            $char = (
                $r &gt; .9   ?  $encode[2]-&gt;($char)  :
                $r &lt; .45  ?  $encode[1]-&gt;($char)  :
                             $encode[0]-&gt;($char)
            );
        }
        $char;
    }msgex;
    return $addr;
}

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

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

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

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

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

sub wi_modified {
    my ( $context, $obj ) = @_;
    my $offset = $context-&gt;site-&gt;time_offset;
    if ( $HTML-&gt;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-&gt;bigmed_time(
            bigmed_time =&gt; $obj-&gt;pub_time,
            offset      =&gt; $offset,
        );
        my $mod_day = BigMed-&gt;bigmed_time(
            bigmed_time =&gt; $obj-&gt;mod_time,
            offset      =&gt; $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]-&gt;mod_time,
        $HTML-&gt;stash_pref( $_[0], 'html_pubdate_includetime' ) );
    my $modified_text =
      $HTML-&gt;stash_pref( $_[0], 'html_modified_textmodified' );
    $modified_text =~ s/&amp;lt;%date%&amp;gt;/$time/msg;
    return $_[0]-&gt;build_markup( 'wi_modified.tmpl', date =&gt; $modified_text, );
}

sub wi_today {
    my $site = $_[0]-&gt;site;
    my $fmt = $site-&gt;date_format || '%b %e, %Y';
    if ( index( $OSNAME, 'MSWin' ) &gt;= 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{&lt;!--#config timefmt="$fmt" --&gt;&lt;!--#echo var="DATE_LOCAL" --&gt;};
    return $_[0]-&gt;build_markup( 'wi_today.tmpl', date =&gt; $date );
}

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

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

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

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

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

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

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

sub wi_gallery {
    my ( $context, $obj, $rparam ) = @_;
    my %param = $rparam ? %{$rparam} : ();
    my $size = $param{size}
      || $HTML-&gt;stash_pref( $context, 'html_gallery_imagesize' );
    $size = $IMAGE_SIZE{$size} || $size;
    my $align = $param{direction}
      || $HTML-&gt;stash_pref( $context, 'html_gallery_direction' );
    my $caption =
      defined $param{caption}
      ? $param{caption}
      : $HTML-&gt;stash_pref( $context, 'html_gallery_caption' );
    my @images = _get_gallery_images(
        'gallery',
        $context, $obj,
        {   size      =&gt; $size,
            direction =&gt; $align,
            caption   =&gt; $caption,
            enlarge   =&gt; $param{enlarge},
            limit     =&gt; 'gallery',
            slideshow =&gt; 'gallery',
        }
    );
    return q{} if !@images;
    return $context-&gt;build_markup(
        'wi_gallery.tmpl',
        images  =&gt; \@images,
        heading =&gt; $HTML-&gt;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'
          &amp;&amp; $limit ne 'above'
          &amp;&amp; $limit ne 'below'
          &amp;&amp; $limit ne 'body'
          &amp;&amp; $limit ne 'other'
          &amp;&amp; $limit ne 'gallery';
    $limit = 'block' if $limit eq 'body';
    my $align =
      ( !$param{direction} || $param{direction} ne 'vertical' )
      ? 'left'
      : 'center';
    my $want_caption = $param{caption} &amp;&amp; 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}
      &amp;&amp; ( !$param{enlarge} || lc $param{enlarge} eq 'no' );

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

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

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

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

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

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

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

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

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

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

    my @tags;
    my $dot      = BigMed-&gt;bigmed-&gt;env('DOT');
    my $base_url = $context-&gt;site-&gt;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-&gt;[1]-&gt;name, $rpair-&gt;[1]-&gt;slug );
        push @tags,
          { tag =&gt; $name,
            url =&gt; "$base_url/$slug/",
            rel =&gt; ( $name eq $slug )
          };
    }
    return q{} if !@tags;
    return $context-&gt;build_markup(
        'wi_tags.tmpl',
        tags  =&gt; \@tags,
        label =&gt; $HTML-&gt;stash_pref( $context, 'html_tags_tagtext' ),
    );
}

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

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

    #make sure there's an include file
    my $comment_file = BigMed::Comment-&gt;comment_file_path( $site, $obj );
    if ( !-e $comment_file ) {
        BigMed::Comment-&gt;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-&gt;flags;
    my $enabled = $HTML-&gt;stash_pref( $context, 'html_comments_enabled' )
      &amp;&amp; !$flag{html_dcomments};
    if ( !$enabled ) {
        return $context-&gt;build_markup(
            'wi_comments.tmpl',
            COMMENT_PATH =&gt; $include,
            DISABLED =&gt;
              $HTML-&gt;stash_pref( $context, 'html_comments_disabled' ),
        );
    }

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

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

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

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

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

sub comment_tally_include {
    my ( $context, $obj ) = @_;
    my $site = $context-&gt;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-&gt;active_page_url(
        $site,
        {   section =&gt; $context-&gt;section,
            rcache  =&gt; $context-&gt;relation_cache,
            rkids   =&gt; $context-&gt;active_descendants,
        }
    );
    if ($url
        &amp;&amp; (   index( $url, $site-&gt;html_url ) != 0
            || index( $url, $site-&gt;doc_url ) == 0 )
      )
    {
        return qq{&lt;!-- external link, no comments --&gt;};
    }

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

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

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

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

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

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

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

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

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

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

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

}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

sub wi_sitemap {
    my $context = shift;

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

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

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

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

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

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

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

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

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

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

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

$HTML-&gt;add_widget(
    name        =&gt; 'sectionlink',
    sectionwide =&gt; 1,
    handler     =&gt; sub {
        my ( $context, $obj, $rparam ) = @_;
        my $site = $context-&gt;site;
        my $sec;
        if ( defined $rparam-&gt;{slug} ) {
            $sec = $site-&gt;section_obj_by_slug( $rparam-&gt;{slug} );
            if ( !$sec ) {
                my $slug = $HTML-&gt;escape_xml( $rparam-&gt;{slug} );
                return "&lt;!-- unknown section slug: $slug --&gt;";
            }
        }
        else {
            $sec = $context-&gt;section || $site-&gt;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]-&gt;build_markup(
            'wi_sectionlink.tmpl',
            url          =&gt; $url,
            new_window   =&gt; $new_win,
            section_name =&gt; $sec-&gt;name
        );
    },
);

$HTML-&gt;add_widget(
    name        =&gt; 'parentlink',
    sectionwide =&gt; 1,
    handler     =&gt; sub {
        my $site    = $_[0]-&gt;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]-&gt;build_markup(
            'wi_parentlink.tmpl',
            new_window   =&gt; $new_win,
            url          =&gt; $url,
            section_name =&gt; $parent-&gt;name
        );
    },
);

$HTML-&gt;add_widget(
    name        =&gt; 'mainsectionlink',
    sectionwide =&gt; 1,
    handler     =&gt; sub {
        my $site    = $_[0]-&gt;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]-&gt;build_markup(
            'wi_mainsectionlink.tmpl',
            url          =&gt; $url,
            new_window   =&gt; $new_win,
            section_name =&gt; $main-&gt;name,
        );
    },
);

sub wi_navigation {
    my ( $context, $obj, $rparam ) = @_;
    my $site  = $context-&gt;site;
    my $cache = $site-&gt;stash('html_navigation_cache')
      || _cache_full_navigation(@_);
    my $sec = $context-&gt;section;
    my $active;
    if ( !$sec || $sec-&gt;is_homepage ) {
        $active = '__HOME';
    }
    else {
        my @parents = ( ( $sec-&gt;parents ), $sec-&gt;id );
        shift @parents;    #homepage
        my @active = map {
            my $s    = $site-&gt;section_obj_by_id($_);
            my $slug = $s-&gt;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-&gt;{direction} || q{};
    my $align =
        $direction eq 'horizontal' ? 'h'
      : $direction eq 'vertical'   ? 'v'
      :   $HTML-&gt;stash_pref( $context, 'html_navigation_direction' );
    return $context-&gt;build_markup(
        'wi_navigation.tmpl',
        direction_class =&gt; "bmn_${align}nav",
        all_nav         =&gt; $cache,
        skipid          =&gt; _skip_link(),
    );
}

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

    #sort out the slug and/or main section
    if ( !$slug &amp;&amp; $rparam-&gt;{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-&gt;{slug} = $slug = $main-&gt;slug;
    }
    elsif (( !defined $slug &amp;&amp; ( !$sec || $sec-&gt;is_homepage ) )
        || ( $slug &amp;&amp; $slug eq '@all' ) )
    {                                     #on homepage or requesting homepage
        return wi_navigation(@_);
    }

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

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

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

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

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

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

sub _cache_full_navigation {
    my ( $context, $obj, $rparam ) = @_;
    my $site  = $context-&gt;site;
    my %param = (
        parent    =&gt; undef,                                          #homepage
        max_depth =&gt; $site-&gt;get_pref_value('html_navigation_depth'),
        direction =&gt; $site-&gt;get_pref_value('html_navigation_direction'),
        type =&gt; q{},    #main navigation
    );
    my $html = _navigation_tree( $context, %param );
    $site-&gt;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-&gt;_descendant_levels_wanted( $context, \%param );
    if ( !defined $child_levels_wanted ) {
        return;    #error
    }
    elsif ( !$child_levels_wanted ) {
        return q{};
    }

    my $site       = $context-&gt;site;
    my $parent_obj = $param{parent};
    my $show_home  = $parent_obj-&gt;is_homepage
      &amp;&amp; $site-&gt;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 =&gt; $parent_obj-&gt;name,
            url          =&gt; $home_url,
            new_window   =&gt; $home_new_win,
            slug         =&gt; '__HOME',
          };
    }

    foreach my $child_id ( $parent_obj-&gt;kids ) {
        my $child_obj = $site-&gt;section_obj_by_id($child_id) or return ();
        my %flag = $child_obj-&gt;flags;
        next if !$child_obj-&gt;active || $flag{html_nonav};
        $param{parent} = $child_obj;
        my $subnav =
          $child_levels_wanted &gt; 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 =&gt; $child_obj-&gt;name,
            url          =&gt; $url,
            new_window   =&gt; $new_win,
            subnav       =&gt; $subnav,
            slug         =&gt; $child_obj-&gt;slug,
          };
    }
    return q{} if !@kids;
    return $context-&gt;build_markup( 'wi_navigation_level.tmpl',
        sections =&gt; \@kids );
}

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

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

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

    return 0 if $this_depth &lt; $rparam-&gt;{base_depth};
    return $rparam-&gt;{base_depth} - $this_depth + $rparam-&gt;{max_depth};
}

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

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

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

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

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

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

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

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

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

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

    foreach my $sid (@path) {
        next if !$sid;
        my $sec = $site-&gt;section_obj_by_id($sid) or next;
        my $name = $lc ? lc $sec-&gt;name : $sec-&gt;name;
        push @breadcrumbs,
          { crumb     =&gt; $name,
            url       =&gt; _section_page_url( $site, $sec ),
            separator =&gt; $sep
          };
    }
    $_[0]-&gt;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-&gt;site      or return q{};
    my $homesec = $site-&gt;homepage_obj or return q{};
    my $sep         = $site-&gt;get_pref_value('html_breadcrumbs_separator');
    my $lc          = $site-&gt;get_pref_value('html_breadcrumbs_lc');
    my $url         = $site-&gt;homepage_url . '/index.' . $HTML-&gt;suffix();
    my @breadcrumbs = (
        {   crumb =&gt; ( $lc ? lc $homesec-&gt;name : $homesec-&gt;name ),
            url =&gt; $url,
            separator =&gt; $sep,
        },
        { crumb =&gt; ( $lc ? lc $page_title : $page_title ) }
    );
    return $context-&gt;build_markup( 'wi_breadcrumbs.tmpl',
        breadcrumbs =&gt; \@breadcrumbs );
}

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

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

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

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

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

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

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

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

                '&lt;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" '
                  . '"http://www.w3.org/TR/html4/strict.dtd"&gt;',

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

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

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

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

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

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

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

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

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

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

    my $icon     = $context-&gt;stash('html_htmlhead_icon');
    my $icontype = $context-&gt;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-&gt;homepage_dir, "favicon.$ext" ) ) {
                $icon = $site-&gt;homepage_url . "/favicon.$ext";
                $icontype = $ext eq 'ico' ? 'image/x-icon' : "image/$ext";
                last;
            }
        }
        $icon ||= q{};
        $context-&gt;set_stash( 'html_htmlhead_icon',     $icon );
        $context-&gt;set_stash( 'html_htmlhead_icontype', $icontype );
    }

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

        foreach ( $site-&gt;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) &gt; 128 ) {
            $title_text = substr( $title_text, 0, 125 ) . '...';
        }
        $title_text = $HTML-&gt;strip_html_tags($title_text);
    }

    my $description = $page-&gt;meta_description;
    if ( !$description ) {
        $description = $HTML-&gt;rich_text( $page-&gt;description, $context );
        $description = $HTML-&gt;strip_html_tags($description);
        $description =~ s/"/&amp;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-&gt;stash('html_htmlhead_rss');
    my $rss_title = $context-&gt;stash('html_htmlhead_rsstitle');
    my $dot       = BigMed-&gt;bigmed-&gt;env('DOT');

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

sub _section_feed_params {
    my ( $context, $type ) = @_;
    my $site   = $context-&gt;site;
    my $siteid = $site-&gt;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-&gt;bigmed-&gt;env('DOT');
    my @feeds;
    foreach my $secid ( $site-&gt;all_active_descendants_ids ) {
        my $sec = $site-&gt;section_obj_by_id($secid) or next;
        my %flags = $sec-&gt;flags;
        next if $flags{$flag};
        my @p = $sec-&gt;parents;
        shift @p;    #homepage
        my $ptrail =
          join( ' &amp;gt; ', map { $site-&gt;section_obj_by_id($_)-&gt;name } @p );
        $ptrail = $ptrail ? "$ptrail &amp;gt; " : q{};
        my $suff = $type eq 'rss' ? $ext : "$ext?$siteid-$secid";
        push @feeds,
          { title =&gt; $ptrail . $sec-&gt;name,
            url   =&gt; $site-&gt;directory_url($sec) . "/bm${dot}feed.$suff",
          };
    }
    return @feeds;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    my ( $rsort, $rorder ) =
      _parse_sort_order( $site-&gt;get_pref_value( $sort_pref, $sec ) );
    return $class-&gt;select( \%search, { sort =&gt; $rsort, order =&gt; $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-&gt;add_widget(
    name         =&gt; 'announcements',
    handler      =&gt; \&amp;wi_announce,
    build_always =&gt; 1,
    sectionwide  =&gt; 1,
    group        =&gt; 'tips_annc',
    prefs        =&gt; {
        'html_annc_sort_order' =&gt; {
            edit_type   =&gt; 'sort_order',
            priority    =&gt; 95,
            default     =&gt; 'priority:pub_time:mod_time|d:d:d',
            edit_params =&gt; {
                numfields =&gt; 3,
                required  =&gt; 1,
            },
        }
    }
);

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

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

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

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

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

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

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

    build_tagcloud_include($context) or return;

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

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

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

    my $site = $builder-&gt;site;
    my $sid  = $site-&gt;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-&gt;fetch, $builder-&gt;force_tags ) {
        next if $done{ $tag-&gt;id };
        $builder-&gt;log(
            notice =&gt; 'HTML: Building tag ' . $builder-&gt;log_data_tag($tag) );
        $builder-&gt;update_statusbar(
            message =&gt; ['BUILDER_Building tags', $tag-&gt;name], );
        _do_pages_for_tag( $builder, $tmpl, $tag, $tag_pointers, $rwvalue )
          or return;
        $done{ $tag-&gt;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-&gt;limited_pages();
    my @force       = $builder-&gt;force_tags();
    my $do_build    = @limit_pages || !$builder-&gt;limit_sections || @force;
    return 1 if !$do_build;

    #winnow pointers to tag-specific pointers
    my $site         = $builder-&gt;site;
    my $sid          = $site-&gt;id;
    my $tag_pointers = $builder-&gt;relation_cache-&gt;{'BigMed::Pointer'}-&gt;select(
        {   site         =&gt; $sid,
            target_table =&gt; BigMed::Tag-&gt;data_source,
            source_table =&gt; BigMed::Content::Page-&gt;data_source,
        }
    ) or return;
    return 1 if !$tag_pointers-&gt;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-&gt;select( { site =&gt; $sid } ) or return;
    my $build_tags = $all_tags-&gt;join_pointed_from(
        {   'join'    =&gt; 'BigMed::Content::Page',
            'pselect' =&gt; $tag_pointers,
            'unique'  =&gt; 1,
        },
        { 'site' =&gt; $sid, 'id' =&gt; \@limit_pages },
    ) or return;
    return ( $tag_pointers, $build_tags );
}

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

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

    #collect the html for each link
    my $tag_widget = $HTML-&gt;widget('taglinks');
    my $context    = $builder-&gt;context;
    my $ractive    = $context-&gt;active_descendants;
    my @links;
    my $page;
    my $ping_count = 0;
    while ( $page = $tag_pages-&gt;next ) {
        my %flag = $page-&gt;flags;
        next if $flag{hideall};
        my $secid = $page-&gt;effective_active_section( $site, undef, $ractive )
          or next;
        push @links,
          _link_builder( $tag_widget, $context, $page,
            { use_section =&gt; $secid } );
        $ping_count++;
        $builder-&gt;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-&gt;get_pref_value('html_tag_numdisplay') || 15;
    my $tname    = $tag-&gt;name;
    my $title    = $site-&gt;get_pref_value('html_tag_indiv_headline');
    $title =~ s/&amp;lt;%tag%&amp;gt;/$tname/msg;
    my $headline = qq{&lt;h2 class="bmw_headline"&gt;$title&lt;/h2&gt;};
    my $dot      = BigMed-&gt;bigmed-&gt;env('DOT');
    my $base_url =
      $site-&gt;homepage_url . qq{/bm${dot}tags/} . $tag-&gt;slug . '/index';

    #breadcrumbs
    my @breadcrumbs;
    my $sep       = $site-&gt;get_pref_value('html_breadcrumbs_separator');
    my $lc_bread  = $site-&gt;get_pref_value('html_breadcrumbs_lc');
    my $home      = $site-&gt;homepage_obj-&gt;name;
    my $main_tags = $site-&gt;get_pref_value('html_tag_headline');
    @breadcrumbs = (
        {   crumb =&gt; ( $lc_bread ? lc $home : $home ),
            url       =&gt; $site-&gt;homepage_url . '/index.' . $HTML-&gt;suffix,
            separator =&gt; $sep,
        },
        {   crumb =&gt; ( $lc_bread ? lc $main_tags : $main_tags ),
            url =&gt; $site-&gt;homepage_url
              . qq{/bm${dot}tags/index.}
              . $HTML-&gt;suffix,
            separator =&gt; $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'   =&gt; $context,
            'base_url'  =&gt; $base_url,
            'pnum'      =&gt; $count,
            'total_num' =&gt; $npages,
        );

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

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

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

    return 1;
}

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

    require BigMed::Tag;
    my $rtag_count =
      BigMed::Tag-&gt;tag_counts( $site,
        $context-&gt;relation_cache-&gt;{'BigMed::Pointer'},
        $context-&gt;content, )
      or return;
    my %tag_count = %{ $rtag_count-&gt;{count} };
    my @tiers;
    if (%tag_count) {
        my ( $low, $high ) = ( $rtag_count-&gt;{low}, $rtag_count-&gt;{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-&gt;select( { site =&gt; $sid },
            { sort =&gt; 'name', order =&gt; 'ascend' } )
          or return;
        while ( my $rindex = $all_tags-&gt;next_index ) {
            push @tags, [$rindex-&gt;{id}, $rindex-&gt;{name}, $rindex-&gt;{slug}]
              if $tag_count{ $rindex-&gt;{id} };
        }
    }
    my @items;
    my $dot     = BigMed-&gt;bigmed-&gt;env('DOT');
    my $tag_url = $site-&gt;homepage_url . "/bm${dot}tags";
    foreach my $rtag (@tags) {
        my $count = $tag_count{ $rtag-&gt;[0] };
        my $size =
            $count &lt; $tiers[0]  ? 'xsmall'
          : $count &lt; $tiers[1]  ? 'small'
          : $count &lt;= $tiers[2] ? 'medium'   #&lt;= helps hit med for tiny ranges
          : $count &lt;= $tiers[3] ? 'large'
          :                       'xlarge';
        my ( $name, $slug ) = ( $rtag-&gt;[1], $rtag-&gt;[2] );
        push @items,
          { name  =&gt; $name,
            slug  =&gt; $slug,
            count =&gt; $count,
            size  =&gt; $size,
            $size =&gt; 1,
            url   =&gt; "$tag_url/$slug/",
          };
    }

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

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

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

my $BMAPP;

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

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

sub is_new_window_url {
    my ( $context, $url ) = @_;
    return 0 if !$url || $url eq 'http://' || index( $url, 'http' ) != 0;
    my $new_window = $HTML-&gt;stash_pref( $context, 'html_links_window' )
      or return 0;
    my $rdomains = $context-&gt;stash('html_links_window_intdomains');
    my @domains;
    if ( !$rdomains ) {
        @domains =
          $context-&gt;site-&gt;get_pref_value('html_links_window_intdomains');
        $context-&gt;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-&gt;active_page_url(
        $context-&gt;site,
        {   section =&gt; $context-&gt;section,
            rcache  =&gt; $context-&gt;relation_cache,
            rkids   =&gt; $context-&gt;active_descendants,
        }
    );
    $url ||= q{#};
    my $suffix = $HTML-&gt;suffix;

    #include query string for overridden urls like e.g. google news plugin
    my $dot = BigMed-&gt;bigmed-&gt;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 &amp; 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&lt;http://globalmoxie.com/&gt;.

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

=cut

</pre></body></html>