# 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: Builder.pm 3325 2008-09-12 08:12:29Z josh $

package BigMed::Builder;
use strict;
use warnings;
use utf8;
use Carp;
use BigMed;
use BigMed::Plugin;
use BigMed::Template;
use BigMed::Context;
use BigMed::Error;
use BigMed::Trigger;    #currently used only for filter_content_selection
use BigMed::DiskUtil (
    'bm_file_path', 'bm_confirm_dir', 'bm_check_space', 'bm_write_file',
    'bm_delete_file', 'bm_load_file',
);
use BigMed::PageUtils;
use BigMed::Log;
use BigMed::Status;
use BigMed::JanitorNote;

my $ERR = 'BigMed::Error';
my $DEFER_MARGIN = 50;


###########################################################
# CONSTRUCTOR
###########################################################

sub new {
    my $class = shift;
    croak 'usage: $class->new(%params)' if @_ % 2;
    my %param = @_;
    my $site  = $param{site};
    if ( !$site || !$param{site}->isa('BigMed::Site') ) {
        croak 'site object required in BigMed::Builder constructor';
    }

    my $moxiedata = BigMed->bigmed->env('MOXIEDATA');
    BigMed::Plugin->load_content_types(); #capture any plugin changes

    my $self = bless {
        site    => $site,
        tmpl    => BigMed::Template->new($site),
        tmpldir => bm_file_path( $moxiedata, 'templates', 'site_templates' ),
        tmpldir_custom =>
          bm_file_path( $moxiedata, 'templates_custom', 'site_templates' ),
    }, $class;
    if ( $param{preview} ) {
        $self->load_content( $param{preview}->{section_id},
            $param{preview}->{page_id} );
        
        #don't load pointers; only doing preview for a single page,
        #so there's no performance gain for loading them ahead of time,
        #except for when doing a preview of a section page. In that
        #case, the preview routine handles it.
    }
    else {
        $self->load_content() or return;
        my $all_pointers = BigMed::Pointer->select( { site => $site->id } )
          or return;
        $all_pointers->tune('source_id');
        $self->set_relation_cache( { 'BigMed::Pointer' => $all_pointers });
    }

    my @formats = $self->formats;
    my %sitewide_value;
    my %marked;
    foreach my $format (@formats) {
        $sitewide_value{$format} = {};
        $marked{$format} = { on_section => {}, on_homepage => {} };
    }
    $self->{sitewide_values} = \%sitewide_value;
    $self->{marked}          = \%marked;

    return $self;
}

sub load_content {
    my $self    = shift;
    my $sec_id  = shift;
    my $page_id = shift;
    my $sections;
    $sections = [$sec_id, $self->site->all_descendants_ids($sec_id)]
      if $sec_id;
    my $content = BigMed::Content::Page->select(
        {   site       => $self->site->id,
            id         => $page_id,
            sections   => $sections,
            pub_status => 'published',
        }
      )
      or return;
    $self->set_content($content);
    return $content;
}

###########################################################
# ACCESSORS
###########################################################

sub site {
    return $_[0]->{site};
}

my @Formats;

sub formats {
    @Formats = BigMed::Plugin->load_formats() if ( !@Formats );
    return @Formats;
}

sub content {
    return $_[0]->{content};
}

sub set_content {
    return $_[0]->{content} = $_[1];
}

sub relation_cache {
    return $_[0]->{relation_cache};
}

sub set_relation_cache {
    return $_[0]->{relation_cache} = $_[1];
}

sub context {
    return $_[0]->{context};
}

sub set_context {
    ( $_[1] && $_[1]->isa('BigMed::Context') )
      or croak 'No BigMed::Context object supplied';
    $_[0]->{context} = $_[1];
    return $_[1];
}

sub set_limit_page_builds {
    return $_[0]->{limit_pages} = $_[1];
}

sub limit_page_builds {
    return defined $_[1]
      ? $_[0]->{limit_pages}->{ $_[1] }
      : defined $_[0]->{limit_pages};
}

sub limited_pages {
    return $_[0]->{limit_pages} ? keys %{ $_[0]->{limit_pages} } : ();
}

sub limit_sections {
    return $_[0]->{limit_sections};
}

sub set_force_tags {
    return $_[0]->{force_tags} = $_[1];
}

sub force_tags {
    return $_[0]->{force_tags} ? @{ $_[0]->{force_tags} } : ();
}


sub sitewide_values {
    croak 'sitewide_values requires a format class name argument' if !$_[1];
    croak "unknown format class '$_[1]' in sitewide_values request"
      if !$_[0]->{sitewide_values}->{ $_[1] };
    return %{ $_[0]->{sitewide_values}->{ $_[1] } };
}

sub set_sitewide_value {

    # @_ = ($builder, $format_class, $stringified_widget_tag, $value)
    return $_[0]->{sitewide_values}->{ $_[1] }->{ $_[2] } = $_[3];
}

sub defer_overflow {
    return $_[0]->{defer_overflow};
}

sub set_defer_overflow {
    return $_[0]->{defer_overflow} = $_[1];
}

###########################################################
# CONTEXT HELPERS
###########################################################

sub init_section_context {
    my ($self, $sec_id, $progress) = @_;

    #Routine sets the site, section, filtered section content and level
    #in the context object.

    #About content winnowing
    #Where possible, if the previously processed section was the parent
    #section, then carve out a little shortcut by using that section's
    #content selection to winnow the current content.

    #Only ever want content for current section and below (for top level,
    #that's all sections below the homepage). Always want *all* content for
    #current section, even if section is inactive, because that info is used
    #to remove detail pages in the _do_section_info_and_detail_pages routine.

    #For normal, active sections, only want subsection content from *active*
    #subsections. For inactive sections, want active and inactive content
    #because need to pass inactive content down to child section. In that
    #case, can't trust that we're getting everything from the parent
    #object, since active sections omit inactive content. So:

    #Inherit sections from parent when section is active or immediate parent
    #is inactive. Otherwise, get fresh content from builder object.

    my $this_context;
    my $site = $self->site;
    if ( !$sec_id ) {    #building top-level
            #include homepage id in the section list; if no active sections at
            #all, it would otherwise return *all* content when we really want
            #no content at all.
        my @secs = ( $site->homepage_id, $site->all_active_descendants_ids );
        my $content =
          $self->content->select( { site => $site->id, sections => \@secs } );
        my $home = $site->homepage_obj or return q{}; #doesn't exist?!?
        $this_context = BigMed::Context->new(
            level   => 'top',
            section => $home,
            content => $content,
            site    => $site,
            relation_cache => $self->relation_cache,
        );
        shift(@secs);
        $this_context->set_ordered_descendants(\@secs);
        my %sec = map { $_ => 1 } @secs;
        $this_context->set_active_descendants(\%sec);
    }
    else {    #get section object and collect section-specific content
        defined( my $section = $self->site->section_obj_by_id($sec_id) )
          or return;
        return q{} if !$section;    #doesn't exist?! fail quietly and move on

        my $remove = $self->{remove_pages} || {};
        $this_context = BigMed::Context->new(
            level         => 'section',
            section       => $section,
            site          => $site,
            remove_detail => $remove->{$sec_id},    #undef or a hash ref
            relation_cache => $self->relation_cache,
        );

        #get content from *old* context object if it's the immediate parent
        my $content;
        my $prev_context = $self->context;
        if (   $prev_context
            && $prev_context->section
            && $prev_context->section->id == ( $section->parents )[-1]
            && ( $this_context->is_active || !$prev_context->is_active ) )
        {

            #TODO:
            #this doesn't save us lots yet. only works when the immediately
            #preceding section was the parent. At best, we get a savings
            #on a single section per taxonomy level. Might take a look
            #at caching so that all siblings get the same savings.

            $content = $self->context->content;
        }
        else {
            $content = $self->content;
        }

        my @secs;
        if ( $this_context->is_active ) {
            @secs = $site->all_active_descendants_ids($sec_id);
            my %sec = map { $_ => 1 } @secs;
            $this_context->set_active_descendants(\%sec);
            $this_context->set_ordered_descendants([@secs]); # *copy* @secs
            unshift @secs, $sec_id;
        }
        else {
            @secs = ( $sec_id, $site->all_descendants_ids($sec_id) );
        }

        $content =
          $content->select( { site => $site->id, sections => \@secs } )
          or return;
        $this_context->set_content($content);
    }

    $this_context->set_stash('BUILDER_PROGRESS', $progress);
    $self->set_context($this_context);
    $self->call_trigger('fresh_section_context');
    return $self->context;
}

sub init_format_context {
    my $self         = shift;
    my $format_class = shift
      or croak 'init_format_context requires format class argument';

    #Gather all templates for the format, pluck out the widget tags,
    #and set the context object's format info, inherited widget values
    #and template text.

    #POSSIBLE ISSUE: We're consolidating all of the widgets from all
    #templates so that we can do them all in one pass for performance
    #savings. Works fine unless templates vary the widgets from the same
    #collector group. If one template for the level has the
    #spotlight widget and others do not, the spotlight widget will
    #be built for all templates of that level even if not displayed,
    #throwing off the display of other widgets in the group.
    #this may be more of a problem in theory than in practice, since
    #it seems unlikely that templates at the same level will
    #mix and match group widgets like this. For now, keep it as-is.

    #gather all template text for this format/level and identify widgets
    my $context          = $self->context;
    my $section          = $context->section;
    my $site             = $context->site;
    my @level_templates  = $format_class->templates( $context->level );
    my @detail_templates;
    @detail_templates = $format_class->templates('detail')
      if $context->level ne 'top';
    my @extras           = $format_class->templates('extras');
    my %all_widget;
    my $format_name = $format_class->name;

    #gather widgets and template text for all templates; if a template is
    #suppressed or its section is not active, mark it as suppressed and
    #skip the text/widget gathering (the build routines will also delete
    #any detail/level files for that template).
    my $is_active = $format_class->is_active($context);
    my $dot = BigMed->bigmed->env('DOT');
    foreach my $tmpl ( @level_templates, @detail_templates, @extras ) {
        $tmpl->{suppress} = !$is_active
          || ( $tmpl->{option_pref}
            && !$site->get_pref_value( $tmpl->{option_pref}, $section ) );
        next if $tmpl->{suppress};

        #handle bm~foo "dot" replacement for ~ or .
        $tmpl->{extender} &&= $dot . $tmpl->{extender};
        if ($tmpl->{filename} && $dot ne '~') {
            $tmpl->{filename} =~ s/~/$dot/msg;
        }
        $tmpl->{text} = $self->template_text(
            format  => $format_name,
            type    => $tmpl->{name},
            section => $section,
        );
        my @stringify = stringify_widget_params( $tmpl->{text} );
        @all_widget{@stringify} = (1) x @stringify;
    }
    $context->set_format_name($format_name);
    $context->set_format_class($format_class);
    $context->set_defer_overflow( $self->defer_overflow );
    $context->set_inherit( { $self->sitewide_values($format_class) } );
    $context->set_detail_templates( \@detail_templates );
    $context->set_level_templates( \@level_templates );
    $context->set_extras_templates( \@extras );
    $context->set_widget_template_path(
        [   bm_file_path( $self->{tmpldir_custom}, $format_name ),
            bm_file_path( $self->{tmpldir},        $format_name ),
        ]
    );

    my $dirpath =
      $format_class->directory_path( $context->site, $context->section )
      or return;
    bm_confirm_dir( $dirpath, { build_path => 1 } );
    bm_check_space( $dirpath, 512 );
    $context->set_dirpath($dirpath);

    $context->set_on_homepage(
        $self->{marked}->{$format_class}->{on_homepage} );

    my @parents = $section ? $section->parents : ();
    if ( @parents == 1 ) {    #main section, reset the on_section tally
        $context->set_on_section( {} );
    }
    else {
        $context->set_on_section(
            $self->{marked}->{$format_class}->{on_main} );
    }
    $self->init_format_widgets( keys %all_widget );
    return $context;
}

sub init_format_widgets {
    my $self    = shift;
    my @widgets = @_;               #stringified widgets from templates
    my $context = $self->context;
    my $format_class = $context->format_class;

    #Routine scans widgets to identify the valid widget tags, identifies
    #collector widgets and associated collector groups, adds any
    #must-build widgets and creates widget objects for all.
    #Sets the context object's widget map (keys are stringified widget
    #tags and values are widget objects), collector array and collector
    #group array. Collectors are sorted in descending order by priority.

    #gather all widget objects to build and identify the collectors
    my %must_build;
    foreach my $wtag ( $format_class->widget_list ) {
        my $w = $format_class->widget($wtag);
        $must_build{$wtag} = $w if $w->build_always;
    }
    my %widget_map;
    my %collector_map;
    foreach my $found (@widgets) {
        my ($wtag) = unstringify_widget_params($found);
        next if !$format_class->widget_exists($wtag);

        #key the full, stringified, parameterized widget to a generic
        #widget object for this tag type
        my $w = $format_class->widget($wtag);
        $widget_map{$found} = $w;

        #collectors do not get any parameters; as they collect, it's
        #just a one-size-fits-all deal for the section (otherwise
        #there could be trouble with multiple versions of collectors
        #collecting for the same group). so just add the plain widget obj.
        $collector_map{$wtag} ||= $w if $w->is_collector;
        delete $must_build{$wtag};
    }
    while ( my ( $wtag, $w ) = each(%must_build) ) {   #add unseen must-builds
        $widget_map{$wtag} = $w;
        $collector_map{$wtag} ||= $w if $w->is_collector;
    }

    #get collector groups and add any overflow collectors
    my %cgroup;
    foreach my $w ( values %collector_map ) {
        $cgroup{ $w->collects_for } = 1 if $w->collects_for;
    }
    foreach my $cg ( keys %cgroup ) {
        my %cg_info = $format_class->collector_group($cg);
        next if !$cg_info{overflow};
        my $cg_widget = $format_class->overflow_widget($cg);
        $collector_map{ $cg_widget->name } = $cg_widget;
    }

    #go through *all* groups this time and add overflow navbar widgets;
    #these are just empty placeholders
    #to make sure that any empty/unused navbar widgets get replaced
    #with an empty string, even if the collector group is not in use on
    #the page.
    foreach my $cg ( $format_class->collector_groups ) {
        my %info = $format_class->collector_group($cg);
        next if !$info{overflow} || !$info{overflow}->{nav_widget};
        my $w = $info{overflow}->{nav_widget};
        $widget_map{$w} = $format_class->empty_widget($w)
          if !$collector_map{$w};
    }

    $context->set_widget_map( \%widget_map );
    $context->set_collectors(
        [sort { $b->priority <=> $a->priority } values %collector_map] );
    $context->set_collector_groups( [keys %cgroup] );
    return $context;
}

sub _gather_format_info {
    my $self         = shift;
    my $context      = $self->context;
    my $format_class = $context->format_class;

    my %on_homepage = $context->on_homepage();
    my %on_main     = $context->on_section();
    $self->{marked}->{$format_class}->{on_homepage} = \%on_homepage;
    $self->{marked}->{$format_class}->{on_main}     = \%on_main;
    return;
}

###########################################################
# BUILD METHODS
###########################################################

sub build {
    my $self = shift;
    croak 'Usage: $builder->build(%param)' if @_ % 2;
    my %param = @_;
    my $site  = $self->site;

    #make sure that sections are ordered in site order to help builder
    #process most efficiently
    $self->log(
        notice => 'Builder: Starting build for ' . $self->log_data_tag($site) );
    my @sections;
    if ( $param{sections} ) {
        @sections =
          ref $param{sections} eq 'ARRAY'
          ? @{ $param{sections} }
          : ( $param{sections} );
        my @include;
        foreach my $sec_id (@sections) {
            defined( my $sec = $site->section_obj_by_id($sec_id) )
              or return ();
            push @include, $sec_id, $sec->parents if $sec;
        }
        my %final = map { $_ => 1 } @include;
        @sections = grep { $final{$_} } $site->all_descendants_ids;
    }
    elsif ( !$param{top_only} ) {
        @sections = $site->all_descendants_ids;
    }
    
    #useful to know if we've requested specific sections
    #(including homepage), e.g. when building tag pages
    $self->{limit_sections} = ( $param{sections} || $param{top_only} );

    my @force_tags =
        ref $param{force_tags} eq 'ARRAY' ? @{ $param{force_tags} }
      : $param{force_tags} ? ( $param{force_tags} )
      : ();
    my @pages =
        ref $param{pages} eq 'ARRAY' ? @{ $param{pages} }
      : $param{pages} ? ( $param{pages} )
      : ();
    my $build_only =
        @pages ? { map { $_ => 1 } @pages }
      : $param{no_detail} || $param{defer_overflow} ? {}
      : undef;
    $self->set_limit_page_builds($build_only);
    $self->set_defer_overflow( $param{defer_overflow} );
    $self->set_force_tags(\@force_tags);
    $self->_gather_removal_info( %{ $param{remove_detail} } )
      if $param{remove_detail};

    my $rerror;
    my $rdone;
    if ($param{statusbar}) {
        $self->{statusbar} = BigMed::Status->new( { steps => @sections + 1 });        
        $self->add_trigger('fresh_section_context', \&_update_statusbar_section);
        $self->add_trigger('level_midbuild', \&ping_statusbar);

        $rerror = sub {
            $self->{statusbar}->send_error($_[0]);
        };
        $rdone = sub {
            $self->{statusbar}->mark_done();
            exit(0);
        };
    }
    else {
        delete $self->{statusbar};
        $rdone = sub {1};
        $rerror = sub{return};
    }

    my $progress = 1;    
    $self->_build_level() or return $rerror->();    #top-level build
    foreach my $sec_id (@sections) {    #individual-section build
        $progress++;
        next if $sec_id && $sec_id == $self->site->homepage_id;  #already done
        $self->_build_level($sec_id, $progress) or return $rerror->();
    }
    $self->log(
        notice => 'Builder: Completed build for ' . $self->log_data_tag($site) );
    delete $self->{remove_pages};
    return $rdone->();
}

sub update_statusbar {
    my $self = shift;
    $self->{statusbar}->update_status(@_) if $self->{statusbar};
    return 1;
}

sub ping_statusbar {
    my $self = shift;
    $self->{statusbar}->ping if $self->{statusbar};
    return 1;
}

sub _update_statusbar_section {
    my $self = shift;
    my $context = $self->context;
    my $count = $context->content->count;
    my $name = $context->section->name;
    return $self->update_statusbar(
        progress => $context->stash('BUILDER_PROGRESS'),
        message => ['BUILDER_Building section', $name, $count],
    );
}

sub _gather_removal_info {
    my $self = shift;
    croak 'Usage: $builder->_gather_removal_info(%param)' if @_ % 2;
    my %param    = @_;
    my @sections =
        ref $param{sections} eq 'ARRAY' ? @{ $param{sections} }
      : $param{sections} ? ( $param{sections} )
      : ();
    my @pages =
        ref $param{pages} eq 'ARRAY' ? @{ $param{pages} }
      : $param{pages} ? ( $param{pages} )
      : ();
    my %section_map;
    foreach my $sid (@sections) {
        $section_map{$sid}->{$_} = 1 for @pages;
    }
    $self->{remove_pages} = \%section_map;
    return;
}

sub _build_level {
    my ($self, $sec_id, $progress) = @_;

    defined( my $context = $self->init_section_context($sec_id, $progress) )
      or return;
    return 1 if !$context;    #indicates the section could not be found... ?!?
    my $defer = $context->defer_overflow;
    my $confirm_defer_overflow = 0;
    
    foreach my $format_class ( $self->formats ) {
        $self->init_format_context($format_class) or return;
        $self->_build_format_level()              or return;
        $self->_gather_format_info();

        #context's defer overflow gets reset to reflect whether it
        #still requires defer_overflow
        $confirm_defer_overflow = 1 if $context->defer_overflow;
        $context->set_defer_overflow($defer);
    }

    if ($confirm_defer_overflow) {    #tell janitor to build section overflow
        $self->log( info => 'Builder: Deferred overflow links for '
              . $self->log_data_tag( $context->section ) );
        my $jnote = BigMed::JanitorNote->new();
        $jnote->set_site( $self->site->id );
        $jnote->set_action('build_overflow');
        $sec_id ||= $self->site->homepage_id;
        $jnote->set_target($sec_id);
        $jnote->save or return;
    }
    return 1;
}

sub _build_format_level {
    my $self    = shift;
    my $context = $self->context;
    my $site_id = $self->site->id;

    my $page_type = $context->section->is_homepage ? 'homepage' : 'section';
    $self->log( info => 'Builder: Starting '
          . $context->format_name
          . " for $page_type "
          . $self->log_data_tag( $context->section ) );
    $self->_do_section_info_and_detail_pages() or return;

    #load the section page object and run its widget handlers/assemblers
    my $section = $context->section || $context->site->homepage_obj;
    if ( !$section ) {    #no section, no homepage; warn and move on
        warn 'Could not locate section to build site. No homepage section?';
        return 1;
    }

    my $spage_id = $section->page or return 1;    #no page, skip building
    defined(
        my $spage = BigMed::Content::Page->fetch(
            { site => $site_id, id => $spage_id }
        )
      )
      or return;
    return 1 if !$spage;    #no such section page, continue along
    $spage->set_slug('index')   if !$spage->slug;
    $section->set_slug('index') if $section->is_homepage;
    my %w_value = $self->level_widget_values($spage);

    foreach my $tmpl ( $context->level_templates ) {
        $tmpl->{filename} ||= $context->format_class->level_filename;
        my $path = $self->builder_file_path( $tmpl, $section );
        if ( $tmpl->{suppress} ) {
            bm_delete_file($path);
            next;
        }
        $self->call_trigger('before_level_build', $section, $tmpl, \%w_value);
        my $page = $self->replace_widgets( $tmpl->{text}, \%w_value );
        $self->call_trigger( 'before_level_save', $section, $tmpl, \$page );
        bm_write_file( $path, $page, {build_path => 1} ) or return;
    }
    foreach my $tmpl ( $context->extras_templates ) {
        foreach my $callback ( @{ $tmpl->{level_extras} } ) {
            my $rparam = $callback->($context) or next;
            my $filename = $rparam->{filename} || $tmpl->{filename} or next;
            my $path = $self->builder_file_path_slug( $tmpl, $filename );
            if ( $tmpl->{suppress} ) {
                bm_delete_file($path);
                next;
            }
            $self->call_trigger(
                'before_extras_build', $section, $tmpl, \%w_value
            );
            my $page = $self->replace_widgets(
              $tmpl->{text}, { %w_value, %{$rparam} }
            );
            $self->call_trigger('before_extras_save', $section, $tmpl, \$page);
            bm_write_file( $path, $page, {build_path => 1} ) or return;
        }
    }
    return 1;
}

sub _do_section_info_and_detail_pages {
    my $self             = shift;
    my $context          = $self->context;
    my $site_id          = $self->site->id;
    my @detail_templates = $context->detail_templates;

    #check if the section is inactive and handle it by deleting detail pages
    #and moving along before gathering the collection widgets
    return $self->_delete_all_context_detail if !$context->is_active;

    #remove detail pages if any
    my %remove_detail = $context->remove_detail;
    foreach my $id ( keys %remove_detail ) {
        my $obj =
          BigMed::Content::Page->fetch( { site => $site_id, id => $id } );
        return if !defined $obj;
        next   if !$obj;

        foreach my $tmpl (@detail_templates) {
            my $path = $self->builder_file_path( $tmpl, $obj );
            bm_delete_file($path) or return;
        }
    }

    #organize collectors by sort order
    #(TODO: Couldn't this be done further upriver?)
    my %sort_scheme;
    foreach my $col ( $context->collectors ) {
        push @{ $sort_scheme{ $col->sort_order($context) } }, $col;
    }
    my $content      = $context->content;
    my %widget_map   = $context->widget_map;
    my $format_class = $context->format_class;
    my $sec = $context->section;
    my $is_home = !$sec || $sec->is_homepage;
    my $built_detail_pages = !@detail_templates || $is_home;
    my $limit_builds = $self->limit_page_builds;
    my $confirm_defer_overflow = 0;

    #go through the content in each sort scheme, gathering all of the
    #collectors for each and, on the first pass only, the detail pages for
    #each content object

    foreach my $sort_order ( keys %sort_scheme ) {
        my $sorted = _sort_content($content, $sort_order) or return;

        #collector widgets for this sort order
        my @c_widgets     = @{ $sort_scheme{$sort_order} };
        my %hungry_widget = map { $_->name => 1 } @c_widgets;
        my $raccept_only; #identifies when only limiting collectors are left
        my $coll_count = 0;
        my $skip_overflow;
        my $local_defer = $context->defer_overflow;

        #To think about: Would be ideal if we only touch each object
        #once. Right now we're loading every object at least once for
        #each format. May want to think through how to make the content
        #the outer loop and the format the inner loop instead of vice versa.
        my $obj;
        my $ping_count = 0;
      PAGES: while ( $obj = $sorted->next ) {
            next if $remove_detail{ $obj->id };

            #Handle link collectors
            if (%hungry_widget) {    #do collection
                _run_collection( $obj, $context, \@c_widgets,
                    \%hungry_widget )
                  or return;

                #check for newly empty collector widgets and, if found,
                #check for speed opportunities (skipping overflow and
                #fast-forwarding for accept-only collector widgets)
                if ( $coll_count != @c_widgets
                    && ( $coll_count = @c_widgets ) )
                {
                    #if deferring overflow, check to see if we're going
                    #to start skipping here. cancel deferment for
                    #this section if there are only a few pages left.
                    $skip_overflow ||= $local_defer
                      && _only_overflow_left(@c_widgets)
                      && ( ( $sorted->count - $sorted->index ) > $DEFER_MARGIN
                        || ( $local_defer = 0 ) );
                    if ($skip_overflow) { #we're done with all collectors
                        %hungry_widget = ();
                        @c_widgets = ();
                    }

                    $raccept_only = _check_for_speed_mode(@c_widgets)
                      if $built_detail_pages;
                }
                
                if ($raccept_only) {    #fast-forward to matching item
                    my $rindex;
                    while ( $rindex = $sorted->next_index ) {
                        $sorted->set_index( $sorted->index - 1 ), last
                          if ( $raccept_only->{ $rindex->{subtype} } );
                    }
                }
                
            }
            elsif ($built_detail_pages) {    #nothing left
                last PAGES;
            }
            elsif ($skip_overflow && !$self->limit_page_builds( $obj->id ) ) {
                #in speed mode, fast-forward to next matching detail page
                my $rindex;
                while ( $rindex = $sorted->next_index ) {
                    $sorted->set_index( $sorted->index - 1 ), last
                      if $self->limit_page_builds( $rindex->{id} )
                }
                next PAGES;
            }
            
            #run a trigger every xxx pages for processes (like status bar)
            #that want a ping to stay alive
            $ping_count++;
            $self->call_trigger('level_midbuild') if !( $ping_count % 100);
            
            next PAGES
              if $built_detail_pages
              || $obj->subtype eq 'section'
              || ( $limit_builds && !$self->limit_page_builds( $obj->id ) );

            #build detail page only if page belongs to this specific section
            my $sec_id = $sec->id;
            my $this_section;
            foreach my $sid ( $obj->sections ) {
                $this_section = 1, last if $sid == $sec_id;
            }
            next PAGES if !$this_section;

            #get the widget values and pour into each detail template, save.
            my %w_value = $self->detail_widget_values($obj);
            foreach my $tmpl (@detail_templates) {
                my $path = $self->builder_file_path( $tmpl, $obj );
                if ( $tmpl->{suppress} ) {
                    bm_delete_file($path);
                    next;
                }
                $self->call_trigger(
                    'before_detail_build', $obj, $tmpl, \%w_value
                );
                my $page = $self->replace_widgets( $tmpl->{text}, \%w_value );
                $self->call_trigger('before_detail_save', $obj, $tmpl, \$page);
                bm_write_file( $path, $page ) or return;
            }
            return if $ERR->error;    #in case widgets triggered error
        }
        return if !defined $obj;      #caught an i/o error
        $built_detail_pages = 1;
        $confirm_defer_overflow = 1 if $skip_overflow;
    }
    $context->set_defer_overflow($confirm_defer_overflow);
    return 1;
}

sub _only_overflow_left {
    foreach my $cw (@_) {
        return 0 if !$cw->is_overflow;
    }
    return 1;
}

sub _check_for_speed_mode {
    my @widgets = @_ or return;
    my %accept;
    foreach my $cw( @widgets ) {
        my @accepts = $cw->accepts or return;
        @accept{@accepts} = (1) x @accepts;
    }
    return \%accept;
}

sub _sort_content {
    my ($content, $sort_order) = @_;
    my ( $sort, $order ) = split( /\|/ms, $sort_order );
    my @sort = split( /:/ms, $sort );
    my @order =
      map { $_ eq 'a' ? 'ascend' : 'descend' } split( /:/ms, $order );
    return $content->select(
        undef,
        { sort => \@sort, order => \@order },
    );
}

sub _delete_all_context_detail {
    my $self    = shift;
    my $context = $self->context;
    my $content = $context->content;

    $context->set_defer_overflow(0); #no overflow for inactive sections

    #nothing to delete if no detail templates
    my @detail_templates = $context->detail_templates or return 1;

    #this tries to delete files that belong to subsections and thus
    #would never exist in the current directory, but it's (probably)
    #faster than scanning all of the sections in each object.
    my $obj;
    while ( $obj = $content->next ) {
        next if $obj->subtype eq 'section';
        foreach my $tmpl (@detail_templates) {
            my $path = $self->builder_file_path( $tmpl, $obj );
            bm_delete_file($path) or return;
        }
    }
    return if !defined $obj;
    return 1;
}

sub remove_old_files {
    my $self      = shift;
    my $old_slug  = shift;
    my $rsections = shift;
    croak 'Usage: $builder->remove_old_files($old_slug, \@orig_sections);'
      if !$old_slug || !$rsections;
    foreach my $sid ( @{$rsections} ) {
        my $context = $self->init_section_context($sid) or next;
        foreach my $format_class ( $self->formats ) {
            $self->init_format_context($format_class) or return;
            foreach my $tmpl ( $context->detail_templates ) {
                my $path = $self->builder_file_path_slug( $tmpl, $old_slug );
                bm_delete_file($path) or return;
            }
        }

    }
    return 1;
}

sub preview {
    my $self = shift;
    croak 'Usage: $builder->preview(%param)' if @_ % 2;
    my %param = @_;
    foreach my $req qw(format_class template level content_obj) {
        croak "preview requires $req parameter" if !$param{$req};
    }
    $self->set_limit_page_builds( {} );    #no page builds
    $self->set_defer_overflow( 1 );    #no overflow pages
    undef $param{section} if $param{section} == $self->site->homepage_id;
    $self->init_section_context( $param{section} );

    #probably some optimization that can be done for format_context,
    #since it goes to the trouble of loading all templates and parsing
    #for widgets, and we need only one template...
    $self->init_format_context( $param{format_class} );
    my $text = $self->template_text(
        format  => $param{format_class}->name,
        type    => $param{template},
        section => $param{section},
      )
      or return;

    #if inactive section or template, the templates were never scanned.
    #scan the one template that we need.
    if ( !$param{format_class}->is_active( $self->context ) ) {
        my @stringify = stringify_widget_params($text);
        my %all_widget = map { $_ => 1 } @stringify;
        $self->init_format_widgets( keys %all_widget );
    }

    $self->context->set_stash('_BUILD_PREVIEW', 1);
    my %w_value;
    if ( $param{level} eq 'detail' ) {
        %w_value = $self->detail_widget_values( $param{content_obj} );
    }
    else {
        #for inactive sections, links are not included in page because
        #inactive sections do not do collection in _do_section_info...
        #method. I think this is reasonable. Section preview for inactive
        #sections then means that you're only getting the page-specific
        #text.
        
        #we'll be collecting pointers for multiple pages, load and cache
        #all if we don't have it; previews don't automatically load
        #at object creation as other requests do
        if (!$self->relation_cache) {
            my $all_pointers = BigMed::Pointer->select(
                {   site         => $self->site->id, },
              )
              or return undef;
            $all_pointers->tune('source_id');
            my $rcache = { 'BigMed::Pointer' => $all_pointers };
            $self->set_relation_cache($rcache);
            $self->context->set_relation_cache($rcache);
        }

        #defer_overflow flag is on, so we won't build the overflow pages;
        #since we're not running this through format_level, the janitor
        #won't build them either (no janitor note is left here).
        $self->_do_section_info_and_detail_pages() or return;
        %w_value = $self->level_widget_values( $param{content_obj} );
    }
    $self->context->set_stash('_BUILD_PREVIEW', undef);
    
    #find include files and replace with current text
    $self->call_trigger(
        'before_preview_build', $param{content_obj}, \$text, \%w_value
    );
    my $html =  $self->replace_widgets( $text, \%w_value );
    $self->call_trigger(
        'before_preview_show', $param{content_obj}, \$html
    );
    return BigMed::PageUtils::page_to_cgi( $self->site, $html,
        $param{content_obj}->page_url($self->site) );
}

sub builder_file_path {
    my $self            = shift;
    my $tmpl            = shift;
    my $page_or_section = shift;
    my $filename        = $tmpl->{filename} || $page_or_section->slug
      or croak 'could not determine file name for item '
      . $page_or_section->id . ' in '
      . $self->context->format_class . q{ }
      . $tmpl->{name}
      . ' template';
    return $self->builder_file_path_slug( $tmpl, $filename );
}

sub builder_file_path_slug {
    my ( $self, $tmpl, $filename ) = @_;
    my @filepath = ref $filename eq 'ARRAY' ? @{$filename} : $filename;
    $filename = pop @filepath;
    $filename .= $tmpl->{extender} if $tmpl->{extender};
    $filename .= q{.} . $tmpl->{suffix};
    return bm_file_path( $self->context->dirpath, @filepath, $filename );
}

sub detail_widget_values {
    my $self = shift;
    my $obj  = shift;

    my $context      = $self->context;
    my %widget_map   = $context->widget_map;
    my $format_class = $context->format_class;

    #build all widget values for this object
    my %w_value;
    foreach my $stringified ( keys %widget_map ) {
        my $def = $context->widget_value($stringified);
        if ( defined $def ) {
            $w_value{$stringified} = $def;
            next;
        }

        #build widget and cache if appropriate
        my ( $wtag, $rparam ) = unstringify_widget_params($stringified);
        my $widget = $widget_map{$stringified};
        my $value = $widget->page_string( $context, $obj, $rparam );
        $self->set_sitewide_value( $format_class, $stringified, $value )
          if $widget->sitewide;
        $context->set_widget_value( $stringified, $value )
          if $widget->sectionwide;    #always true if sitewide, too
        $w_value{$stringified} = $value;
    }
    return %w_value;
}

sub level_widget_values {
    my $self    = shift;
    my $spage   = shift;
    my $context = $self->context;
    my ( %w_value, %navbar, @overflow );
    foreach my $w ( $context->collectors ) {
        if ( $w->is_overflow ) {      #OVERFLOW WIDGET
            my $navbar;
            if ( !$w->count ) {
                $navbar = q{};
            }
            else {
                my $rpages;
                ( $rpages, $navbar ) =
                  $self->_assemble_overflow_content( $w, $spage );
                push @overflow, [$w, $rpages];
            }
            my $nav_wtag = $w->nav_widget or next;
            $w_value{$nav_wtag} = $navbar;
            $navbar{$nav_wtag}  = 1;
        }
        else {    #REGULAR COLLECTOR
            $w_value{ $w->name } = $w->assemble($context);
        }
    }

    my $format_class = $context->format_class;
    my %widget_map   = $context->widget_map;

    foreach my $stringified ( keys %widget_map ) {
        my ( $wtag, $rparam ) = unstringify_widget_params($stringified);
        next if $navbar{$wtag};    #already got it

        my $widget = $widget_map{$stringified};
        if ( $widget->is_collector && !$widget->use_handler ) {
            $w_value{$stringified} = $w_value{$wtag};
            next;
        }

        #otherwise, it's just a regular handler
        my $cache = $context->widget_value($stringified);
        $w_value{$stringified} = $cache, next if defined $cache;

        my $value = $widget->page_string( $context, $spage, $rparam );
        $self->set_sitewide_value( $format_class, $stringified, $value )
          if $widget->sitewide;
        $context->set_widget_value( $stringified, $value )
          if $widget->sectionwide;    #always true if sitewide, too
        $w_value{$stringified} = $value;
    }
    
    if (!$context->defer_overflow) {
        foreach my $oflow (@overflow) {
            $self->_build_overflow_pages( @{$oflow}, \%w_value ) or return;
        }
    }
    return %w_value;
}

sub template_text {
    my $self = shift;
    return $self->{tmpl}->template_text(@_);
}

###########################################################
# BUILD HELPERS
###########################################################

sub _run_collection {
    my ( $obj, $context, $rc_widgets, $rhungry_widget ) = @_;
    my %cgrouped;    #marks if object already collected for a group
    my $delete;
    foreach my $cw ( @{$rc_widgets} ) {
        next if !$cw->is_accepted( $obj->subtype );
        my $wname  = $cw->name;
        if ( !$cw->is_hungry($context) ) {
            delete $rhungry_widget->{$wname};
            $delete = 1;
            next;
        }

        #try to collect it and, if accepted, add the group info
        my $cgroup = $cw->collects_for;
        next if $cgroup && $cgrouped{$cgroup};
        if ( $cw->collect( $context, $obj ) && $cgroup ) {
            $cgrouped{$cgroup} = 1;

            #take note if it belongs to a non-overflow member of the main
            #list on either the homepage or a main section page
            if ( $cgroup eq 'main_list'
                && index( $wname, '___overflow' ) < 0 )
            {
                if ( $context->section->is_homepage ) {
                    $context->mark_homepage($obj);
                }
                else {
                    my @p = $context->section->parents;
                    $context->mark_section($obj) if @p == 1;
                }
            }
        }
    }
    return if $ERR->error;
    if ($delete) { #finished with one or more widgets, update the list
        @{$rc_widgets} = grep { $rhungry_widget->{$_->name} } @{$rc_widgets};
    }
    return 1;
}

sub _assemble_overflow_content {
    my ( $self, $oflow, $spage ) = @_;
    my $context = $self->context;

    #get number of overflow pages, not including main page
    my $per_page = $oflow->page_limit($context);
    my $rlinks   = $oflow->collection;
    my $npages;
    if ($per_page) {
        $npages = int( @{$rlinks} / $per_page );
        $npages++ if @{$rlinks} % $per_page;
    }
    else {
        $npages = 1;
    }

    #build the overflow in pages if a page limit is specified; otherwise in
    #one dash
    my @pages;
    my $count = 0;
    while ( @{$rlinks} ) {
        $count++;
        my $total = @{$rlinks};
        my $take = ( !$per_page || $per_page > $total ) ? $total : $per_page;
        my @page_links = splice( @{$rlinks}, 0, $take );
        push @pages,
          $oflow->assemble( $context, \@page_links, $count, $npages );
    }
    my $navbar = $oflow->nav_widget
      ? $oflow->build_nav( $context, 0, scalar @pages )    #nav for main page
      : q{};
    return ( \@pages, $navbar );
}

sub _build_overflow_pages {
    my ( $self, $oflow, $rpages, $rw_value ) = @_;
    my $context = $self->context;
    my $site_id = $self->site->id;
    my $section = ( $context->section || $context->site->homepage_obj )
      or return;
    my $dot = BigMed->bigmed->env('DOT');

    my %tmpl = $context->format_class->template_info( $oflow->template );
    $tmpl{filename} = $oflow->filename;
    $tmpl{filename} =~ s/~/$dot/msg if $dot ne '~';
    my $tmpl_text = $self->template_text(
        format  => $context->format_name,
        type    => $oflow->template,
        section => $section,
    );
    my $count = 1;
    foreach my $content ( @{$rpages} ) {
        $count++;    #we start with 2
        $tmpl{extender} = "${dot}p$count";
        my $page =
          $self->replace_widgets( $tmpl_text,
            { %{$rw_value}, content => $content } );
        my $path = $self->builder_file_path( \%tmpl, $section );
        bm_write_file( $path, $page ) or return;
    }
    return 1;
}

###########################################################
# WIDGET TAG MANIPULATION
###########################################################

my $Widget_Regex = qr/<%([a-zA-Z0-9\-_]+)(\s*[^%>]+)?%>/;

sub stringify_widget_params {
    my $text = shift;
    my @w_params;

    #be careful with using $1,$2 directly because of utf8 parsing issues
    while ( $text =~ m{$Widget_Regex}msg ) {
        my ($widget, $attr) = ($1, $2);
        push @w_params, _build_widget_string( $widget, $attr );
    }
    return @w_params;
}

sub replace_widgets {
    my ($self, $text, $rwidgets) = @_;

    #be careful with using $1,$2,$3 directly because of utf8 parsing issues
    $text =~ s{($Widget_Regex)}{
        my ($tag, $widget, $attr) = ($1, $2, $3);
        my $string = _build_widget_string($widget, $attr);
        defined $rwidgets->{$string} ? $rwidgets->{$string} : $tag
    }msge;
    return $text;
}

sub _build_widget_string {
    my ( $wtag, $addl ) = @_;
    my %param;

    #be careful with using $1,$2 directly because of utf8 parsing issues
    if ($addl) {
        while ( $addl =~ /(\w+?)\s*=\s*(["'])(.*?)\2\s*/msg ) {
            my ($name, $quote, $val) = ($1, $2, $3);
            $param{ lc $name } = $quote . $val . $quote;
        }
    }
    my $string = lc $wtag;
    my $p_string = join( '!!!!', map { "$_=$param{$_}" } sort keys %param );
    $string .= "!!!!$p_string" if $p_string;
    return $string;
}

sub unstringify_widget_params {
    my $text = shift;
    my ( $wtag, @w_params ) = split( /!!!!/ms, $text );
    my %param = map { /\A(\w+?)=["'](.*?)["']\z/ms } @w_params;
    return ( $wtag, \%param );
}

1;

__END__

=head1 BigMed::Builder

Generates files for all BigMed::Format formats.

=head1 DESCRIPTION

A BigMed::Builder object generates all of the files in all of the various
formats to create a Big Medium-generated site.

=head1 SYNOPSIS

    my $builder = BigMed::Builder->new($site_obj);
    $builder->build;    #build all pages of the site

    #build only specific sections
    $builder->build( sections => \@section_ids );

    #build all section pages but only the detail pages for specific pages
    $builder->build( pages => \@page_ids );

    #build only specific sections and only specific detail pages
    $builder->build( sections => \@section_ids, pages => \@page_ids );

=head1 Methods

=head2 Construction and Build Methods

=over 4

=item * new

    my $builder = BigMed::Builder->new(
        site => $site_obj,
        preview => { section_id => $sid, page_id => $pid }
    );

Returns a BigMed::Builder object, loaded with the site's content pages and
ready to build.

The optional preview parameter is a hash reference containing a section_id
and/or page_id value. If this parameter is supplied, the pool of content
that the builder object has to work with is winnowed down to pages
belonging to the section specified in the section_id (and its children)
and/or to the page specified in the page ID. This helps performance
for preview operations.

=item * build

    #build all pages of the site
    $builder->build;

    #build only specific sections (the homepage and the parent sections of
    #the specified sections are also rebuilt)
    $builder->build( sections => \@section_ids );

    #build all section pages but only the detail pages for specific pages
    $builder->build( pages => \@page_ids );

    #build only specific sections and only specific detail pages
    $builder->build( sections => \@section_ids, pages => \@page_ids );
    
    #build specific pages and front-pages only of sections
    $builder->build(
        sections       => \@sections,
        pages          => \@pages,
        defer_overflow => 1
    );
    
    #build and remove detail files for specific pages in specific
    #sections:
    $builder->build(
        remove_detail => { sections => \@section_ids, pages => \@page_ids }
    );
    
    #build only section indices, no detail pages:
    $builder->build( no_detail => 1 );

Builds the files in all formats (defined via BigMed::Format) for the specified
sections and pages.

The routine accepts an optional hash of parameters:

=over 4

=item * pages => \@pages

An array reference of page IDs to build. If unspecified, all pages will be
built.

=item * sections => \@sections

An array reference of section IDs to builod. If unspecified, all sections
will be built (unless top_only flag is on).

=item * top_only => 1

If true and no sections are specified, only the top level (homepage) will
be built.

=item * remove_detail => { sections => \@section_ids, pages => \@page_ids }

A hash reference indicating the detail pages and links to these pages
should be removed. The value is a hash reference with keys 'sections'
and 'pages'.  The pages value is an array reference of page IDs to remove.
The sections value is a set of section IDs specifying the section IDs from
which the pages should be removed (if not specified, the pages will be
removed from all sections where they are found).

=item * statusbar => 1

If true, the method will print a HTML response to STDOUT appropriate
for returning as response to Big Medium's StatusDriver JavaScript
object for displaying status bars.

Default is false.

=item * no_detail => 1

If true, no detail pages will be built. (This parameter is ignored if
a value is specified in the pages parameter). Default is false.

=item * defer_overflow => 1

If true, overflow pages for sections will be deferred if there are
over 50 pages to process for the links. Instead only section front
pages will be built immediately, along with any detail pages specified
in the pages parameter. A note is left for the janitor process to build
the overflow pages during the regular maintenance routine.

Default is false.

=item * force_tags => \@tag_objects

An optional array reference of BigMed::Tag objects. If provided, the
tag pages for these tags will be built, in addition to tag pages
already queued for building.

=back

=item * preview

    my $result = $builder->preview(
        content_obj => $page_obj,
        format_class => 'BigMed::Format::HTML',
        template => 'page',
        level => 'detail',
        section => $section_id,
    }

Returns the final page text for the BigMed::Content::Page object specified
in the content_obj parameter, with the template specified by
the format_class, template, level and section parameters (section
should be undefined for top-level/homepage previews).

=back

=head2 Accessor Methods

Unless you're working with the builder's trigger callbacks, you probably won't
need to use any of these accessors directly, since the build method juggles
all this stuff for you...

=over 4

=item * context

    my $context_obj = $builder->context;

Returns the context object, chock full of info about the current stage of the
build process. See BigMed::Context for details.

=item * set_context

    $builder->set_context($context_obj);

Sets the builder's context object.

=item * formats

    my @formats = $builder->formats;

Loads and returns the BigMed::Format class names.

=item * content

Returns a BigMed::Data selection for all of the site's page objects.

=item * set_content

    $builder->set_content($selection);

Sets the builder's content selection.

=item * relation_cache

Returns a hash reference containing the relationship class selections
cached so far, suitable for passing to the Page and Content methods
that expect a relationship cache (e.g., page_url, active_page_url,
load_related_objects, sorted_related_objects).

Called internally by the constructor method, where the value of the
relation_cache is initialized to:

    { 'BigMed::Pointer' => $all_site_pointers }

=item * set_relation_cache

    $builder->set_relation_cache($rcache);

Sets the builder's relation cache to the hash reference in the argument.

=item * limit_page_builds

    $boolean = $builder->limit_page_builds( $page_id );
    $boolean = $builder->limit_page_builds();

If an argument is provided, returns true if the page ID in the argument
is one of a limited number of pages to build.

If no argument is provided returns true if page builds are limited to a
discrete set of pages.

=item * set_limit_page_builds

    $builder->set_limit_page_builds(\%map);

Sets the pages to build, using a reference to a hash whose keys are IDs of
pages to build and whose values are true.

=item * limited_pages

    my @page_ids = $builder->limited_pages();

Returns an array of the pages to which the build has been limited via
C<set_limit_page_builds>.

=item * limit_sections

    $boolean = $builder->limit_sections();

Returns true if a specific set of sections have been requested to build
(including homepage-only via the top_only flag). Used, for example,
when checking whether tag pages should be built.

=item * force_tags

    @tag_objects = $builder->force_tags();

Returns the array of BigMed::Tag objects, if any, set via the C<build>
method's force_tags parameter or the C<set_force_tags> method.

=item * set_force_tags

    $builder->set_force_tags(\@tag_objects);

Sets the array of BigMed::Tag objects whose tag pages should be built
in addition to tags that would naturally be built based on the
limited_pages and limit_sections values.

=item * set_sitewide_value

    $builder->set_sitewide_value($format_class, $stringified, $value);

Caches a default value to use for a stringified widget name/param in the
named format class.

=item * defer_overflow

Returns true if builder has been set to defer building overflow link pages.

=item * set_defer_overflow

Sets the value of the defer_overflow flag.

=back

=head2 Context Initialization Methods

These methods are used internally by BigMed::Builder.

The builder object maintains a BigMed::Context object that reflects the
current status of the building process, including a variety of information
about the current section, its content, its templates, etc. These methods
update the context object when switching sections and formats. (See
BigMed::Context for more details about the context object and its
methods).

=over 4

=item * init_section_context

    $builder->init_section_context($section_id); #returns undef on error

Sets the site, section, filtered content and level attributes of the context
object. If callback triggers for the "filter_content_selection" trigger
moment have been registered, they're called as part of this method.

Returns the context object.

=item * init_format_context

    $builder->init_format_context($format_class); #returns undef on error

Gathers all templates for the format, identifies all requested widgets,
sets the context object's format info and any inherited widget values.
This method should be called after the init_section_context method
has been called for the section.

Returns the context object.

=item * init_format_widgets

    $builder->init_format_widgets(@stringified_widget_tags);

Called internally by init_format_context to validate and organize the
widgets in the current context's templates.

Specifically, the method scans the stringified widget tags in the argument
array to identify valid widgets, cherry-pick the collector widgets and their
associated collector groups, and add any must-build widgets to the list.

=back

=head2 Utility Methods

=over 4

=item * template_text

    my $text = $builder->template_text(
        format => $format_name, #not the class, the name
        type => $template_name,
        section => $section;
    );

Returns a string with the full text of the template specified by the format,
type and section.

=item * builder_file_path

    my $path = $builder->builder_file_path( $rtemplate, $page_or_section );

Returns the path of the file to build for the template and page/section info
in the argument. The $rtemplate argument is a template reference, like
those retrieved via BigMed::Context's detail_template and level_template
methods. The $page_or_section argument is an object for the content object
to build.

=item * builder_file_path_slug

    my $path = $builder->builder_file_path( $rtemplate, $base_slug_name );

Like builder_file_path, but instead of providing a page or section object
in the second argument, you provide the specific slug name that you want
to use for the path.

If you provide an array reference for the slug name, it will be expanded
into a directory path.

=item * detail_widget_values

    my %widget_value = $builder->detail_widget_values($page_obj);

Returns a hash of all widget values for the page object argument. Keys are
stringified widget tags and values are the corresponding string values.

=item * remove_old_files

    $self->remove_old_files( $old_slug, \@sections );

Removes all detail pages for the slug name $old_slug and in the sections
whose IDs are in the array reference in the second argument.

Returns true on success, undef if there's a problem (in which case an
error message is added to the Big Medium error queue).

=back

=head1 Widget Manipulation Routines

Note that these are not class/object methods, but plain old-fashioned
procedural routines.

=over 4

=item * stringify_widget_params

    my @stringified = stringify_widget_params($template_text);

Returns an array of stringified widget tags (tags and parameters shmooshed
into a string).

=item * unstringify_widget_params

    my ($tag, $rparam) = unstringify_widget_params($stringified);

Accepts a stringified widget tag value and Rrturns a two-member array
consisting of the widget tag name and a hash reference of parameters.

=item * $builder->replace_widgets

    my $new_text = replace_widgets($text, \%widget_value);

Pours the widget values in the \%widget_value hash reference
(generated by the c<detail_widget_values> method) into the template
text in the first argument.

=back

=head1 Callback Triggers

=head2 add_trigger

    BigMed::Builder->add_trigger('filter_content_selection', \&filter);

Adds a callback for the specified moment in the page-building process.
The first argument is the label for the trigger moment and the second
argument is the code reference.

Available trigger moments are:

=over 4

=item * fresh_section_context

Called at each build level (e.g. "top" or homepage level, and for each
section of the site) before the building begins and allows code refs to
manipulate or filter the content in the builder's context object, for
example. The routine gets the builder object as the only object.

=item * before_level_build

Called just before generating the text for each format-level template
(e.g. homepage and section pages). The routines get four arguments:

=over 4

=item 1. The BigMed::Builder object

=item 2. The section object

=item 3. The current template hash reference

=item 4. The hash reference of widget values (used as an argument for
the C<replace_widgets> method).

=back

=item * level_midbuild

Called after every 100 pages built for any given section. Useful for
long-running processes that need to keep a connection alive. The status bar
for example, uses this trigger to ping the http connection. The callback
routine gets the builder object as its argument.

=item * before_level_save

Called just before saving a format-level page to disk. The routines get
four arguments:

=over 4

=item 1. The BigMed::Builder object

=item 2. The section object

=item 3. The current template hash reference

=item 4. A scalar reference to the string about to be saved

=back

=item * before_level_build

Called just before generating the text for each detail page file. The
routines get four arguments:

=over 4

=item 1. The BigMed::Builder object

=item 2. The page object

=item 3. The current template hash reference

=item 4. The hash reference of widget values (used as an argument for
the C<replace_widgets> method).

=back

=item * before_detail_save

Called just before saving a detail page file to disk. The routines get
four arguments:

=over 4

=item 1. The BigMed::Builder object

=item 2. The page object

=item 3. The current template hash reference

=item 4. A scalar reference to the string about to be saved

=back

=item * before_extras_build

Called just before generating the text for each level's extras template,
if any. The routines get four arguments:

=over 4

=item 1. The BigMed::Builder object

=item 2. The section object

=item 3. The current template hash reference

=item 4. The hash reference of widget values (used as an argument for
the C<replace_widgets> method).

=back

=item * before_extras_save

Called just before saving an extras file to disk. The routines get
four arguments:

=over 4

=item 1. The BigMed::Builder object

=item 2. The BigMed::Section object

=item 3. The current template hash reference

=item 4. A scalar reference to the string about to be saved

=back

=item * before_preview_build

Called just before generating the text for a detail preview (used by
BigMed::Editor, for example, to preview unpublished pages).

=over 4

=item 1. The BigMed::Builder object

=item 2. The BigMed::Content::Page object

=item 3. A scalar reference of the template text before getting replaced

=item 4. The hash reference of widget values (used as an argument for
the C<replace_widgets> method).

=back

=item * before_preview_show

Called just before returning the final preview html.

=over 4

=item 1. The BigMed::Builder object

=item 2. The BigMed::Content::Page object

=item 4. A scalar reference to the preview page string about to be returned

=back

=back

=head1 AUTHOR & COPYRIGHTS

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

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

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

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

=cut

