# 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: CSS.pm 3233 2008-08-21 12:47:26Z josh $

package BigMed::CSS;
use strict;
use utf8;
use Carp;
use base qw(BigMed::Data);
use BigMed;
use BigMed::DiskUtil qw(bm_file_path bm_write_file bm_load_file);
use BigMed::Format::HTML;    #to load navigation prefs
use BigMed::Theme;

my %css_data   = ();
my %groups     = ();
my @selectors  = ();
my @attributes = ();

my @selector_groups =
  qw(default detail navigation footer links spotlight morelinks latest
  quicktease news announcements tips search);
my %allowed_sel_groups;
@allowed_sel_groups{@selector_groups} = (1) x @selector_groups;

my @attribute_groups = qw(text background box);
my %allowed_att_groups;
@allowed_att_groups{@attribute_groups} = (1) x @attribute_groups;

my %colors = (
    'aliceblue'            => "#f0f8ff",
    'antiquewhite'         => "#faebd7",
    'cyan'                 => "#00ffff",
    'aquamarine'           => "#7fffd4",
    'azure'                => "#f0ffff",
    'beige'                => "#f5f5dc",
    'bisque'               => "#ffe4c4",
    'black'                => '#000000',
    'blanchedalmond'       => "#ffebcd",
    'blue'                 => "#0000ff",
    'blueviolet'           => "#8a2be2",
    'brass'                => "#b5a642",
    'brown'                => "#a52a2a",
    'burlywood'            => "#deb887",
    'cadetblue'            => "#5f9ea0",
    'chartreuse'           => "#7fff00",
    'chocolate'            => "#d2691a",
    'coolcopper'           => "#d98719",
    'copper'               => "#b87333",
    'coral'                => "#ff7f50",
    'cornflowerblue'       => "#6495ed",
    'cornsilk'             => "#fff8dc",
    'crimson'              => "#dc143c",
    'aqua'                 => "#00ffff",
    'darkblue'             => "#00008b",
    'darkbrown'            => "#5c4033",
    'darkcyan'             => "#008b8b",
    'darkgoldenrod'        => "#b8860b",
    'darkgray'             => "#a9a9a9",
    'darkgreen'            => "#006400",
    'darkkhaki'            => "#b7b76b",
    'darkmagenta'          => "#8b008b",
    'darkolivegreen'       => "#556b2f",
    'darkorange'           => "#ff8c00",
    'darkorchid'           => "#9932cc",
    'darkred'              => "#8b0000",
    'darksalmon'           => "#e9967a",
    'darkseagreen'         => "#8fbc8f",
    'darkslateblue'        => "#483d8b",
    'darkslatagray'        => "#2f4f4f",
    'darkturquoise'        => "#00ced1",
    'darkviolet'           => "#9400d3",
    'deeppink'             => "#ff1493",
    'deepskyblue'          => "#00bfff",
    'dimgray'              => "#696969",
    'dodgerblue'           => "#1e90ff",
    'feldspar'             => "#d19275",
    'firebrick'            => "#b22222",
    'floralwhite'          => "#fffaf0",
    'forestgreen'          => "#228b22",
    'fuchsia'              => "#ffooff",
    'gainsboro'            => "#dcdcdc",
    'ghostwhite'           => "#f8f8f8",
    'gold'                 => "#ffd700",
    'goldenrod'            => "#daa520",
    'gray'                 => "#808080",
    'green'                => "#008000",
    'greenyellow'          => "#adff2f",
    'horneydew'            => "#f0fff0",
    'honeydewtab'          => "#00de2b",
    'hotpink'              => "#ff69b4",
    'indianred'            => "#cd5c5c",
    'indigo'               => "#4b0082",
    'ivory'                => "#fffff0",
    'khaki'                => "#f0e68c",
    'lavender'             => "#e6e6fa",
    'lavenderblush'        => "#fff0f5",
    'lawngreen'            => "#7cfc00",
    'lemonchiffon'         => "#fffacd",
    'lightblue'            => "#add8e6",
    'lightcoral'           => "#f08080",
    'lightcyan'            => "#e0ffff",
    'lightgoldenrodyellow' => "#fafad2",
    'lightgreen'           => "#90ee90",
    'lightgrey'            => "#d3d3d3",
    'lightpink'            => "#ffb6c1",
    'lightsalmon'          => "#ffa07a",
    'lightseagreen'        => "#20b2aa",
    'lightskyblue'         => "#87cefa",
    'lightslategray'       => "#778899",
    'lightsteelblue'       => "#b0c4de",
    'lightyellow'          => "#ffffe0",
    'lime'                 => "#00f000",
    'limegreen'            => "#32cd32",
    'linen'                => "#fae0e6",
    'magenta'              => "#ffooff",
    'maroon'               => "#800000",
    'mediumaquamarine'     => "#66cdaa",
    'mediumblue'           => "#0000cd",
    'mediumorchid'         => "#ba55d3",
    'mediumpurple'         => "#9370db",
    'mediumseagreen'       => "#3cb371",
    'mediumslateblue'      => "#7b68ee",
    'mediumspringgreen'    => "#00fa9a",
    'mediumturquoise'      => "#48d1cc",
    'mediumvioletred'      => "#c71585",
    'midnightblue'         => "#191970",
    'mintcream'            => "#f5fffa",
    'mistyrose'            => "#ffe4e1",
    'moccasin'             => "#ffe4b5",
    'navajowhite'          => "#ffdead",
    'navy'                 => "#000080",
    'oldlace'              => "#fdf5e6",
    'olive'                => "#808000",
    'olivedrab'            => "#6b8e23",
    'orange'               => "#ffa500",
    'orangered'            => "#ff4500",
    'orchid'               => "#da70d6",
    'palegoldenrod'        => "#eee8aa",
    'palegreen'            => "#98fb98",
    'paleturquoise'        => "#afeeee",
    'palevioletred'        => "#db7093",
    'papayawhip'           => "#ffefd5",
    'peachpuff'            => "#ffdab7",
    'peru'                 => "#cd853f",
    'pink'                 => "#ffc0cb",
    'plum'                 => "#dda0dd",
    'powderblue'           => "#b0e0e6",
    'purple'               => "#800080",
    'red'                  => "#ff0000",
    'rosybrown'            => "#bc8f8f",
    'royalblue'            => "#4169e1",
    'sadllebrown'          => "#8b4513",
    'salmon'               => "#fa8072",
    'sandybrown'           => "#f4a460",
    'seagreen'             => "#2e8b57",
    'seashell'             => "#fff5ee",
    'sienna'               => "#a0522d",
    'silver'               => "#c0c0c0",
    'skyblue'              => "#87ceeb",
    'slateblue'            => "#6a5acd",
    'slategray'            => "#708090",
    'snow'                 => "#fffafa",
    'springgreen'          => "#00ff7f",
    'steelblue'            => "#4682b4",
    'tan'                  => "#d2b48c",
    'teal'                 => "#008080",
    'thistle'              => "#d8bfd8",
    'transparent'          => "transparent",
    'tomato'               => "#ff6347",
    'turquoise'            => "#40e0d0",
    'violet'               => "#ee82ee",
    'wheat'                => "#f5deb3",
    'white'                => "#ffffff",
    'whitesmoke'           => "#f5f5f5",
    'yellow'               => "#ffff00",
    'yellowgreen'          => "#9acd32",
);

###########################################################
# SET SECTION DATA SCHEMA
###########################################################

my @css_schema = (
    {   name  => 'site',
        type  => 'system_id',
        index => 1,
    },
    {   name  => 'section',
        type  => 'system_id',
        index => 1,
    },
    {   name     => 'selector',
        type     => 'value_list',
        index    => 1,
        required => 1,
        default  => '',
    },
    {   name    => 'attributes',
        type    => 'key_value',
        default => {},
    },
    {   name             => 'custom',
        type             => 'raw_text',
        keep_line_breaks => 1,
    },
);

BigMed::CSS->set_schema(
    source   => 'styles',
    label    => 'style',
    elements => \@css_schema,
);

###########################################################
# PUBLIC METHODS: INIT CSS, ADD DEFINITIONS
###########################################################

sub init_css {
    my $class = shift;
    return if keys %css_data > 0;    #already did it
    $class->_register_default_selectors;
    $class->_register_ext_selectors;
    $class->_register_default_attributes;
    $class->_register_ext_attributes;
    $class->_register_generators_parsers;
    $class->_register_group_display;
}

sub selector_groups {
    return @selector_groups;
}

sub selectors {
    $_[0]->init_css;
    my $group = $_[1];
    if ($group) {
        return () if !$groups{$group} || !$groups{$group}->{priority};
        my @selectors;
        my %priority = %{ $groups{$group}->{priority} };
        foreach my $pri ( sort { $b <=> $a } keys %priority ) {
            push @selectors, @{ $priority{$pri} };
        }
        return @selectors;
    }
    else {
        return map { $_->[0] }
          sort     { $b->[1] <=> $a->[1] }
          map { [$_, ( $css_data{selector}->{$_}->{priority} || 0 )] }
          keys %{ $css_data{selector} };
    }
}

sub attribute_groups {
    $_[0]->init_css;
    return @attribute_groups;
}

sub parse_attribute_value {
    my ( $class, $attribute, $value ) = @_;
    my $rattr = $css_data{attribute}->{$attribute}
      or croak "Unknown CSS attribute $attribute";
    my $generator = $css_data{generator}->{ $rattr->{type} };
    return $generator->($value);
}

sub attribute_names {
    $_[0]->init_css;
    my $group = $_[1];
    my @attributes;
    my %attr = %{ $css_data{attribute} };
    foreach my $name ( keys %attr ) {
        if ( $attr{$name}->{group} eq $group ) {
            push @attributes, [$name, $attr{$name}->{priority}];
        }
    }
    return map { $_->[0] }
      sort     { $b->[1] <=> $a->[1] } @attributes;
}

sub attribute_prompt {
    $_[0]->init_css;
    return $css_data{attribute}->{ $_[1] }->{edit}
      ? %{ $css_data{attribute}->{ $_[1] }->{edit} }
      : ();
}

sub add_selector {

    ## Selectors set here will be registered after all default selectors
    ## are registered at initalization.

    my $class = shift;
    @_ % 2
      && croak "Poorly formatted add_selector request (odd "
      . "number of parameters)";
    my %param = @_;
    push( @selectors, \%param );
}

sub add_attribute {

    ## Attributes set here will be registered after all default attributes
    ## are registered at initalization.
    ##
    ## BigMed::CSS->add_attribute(
    ##     name => 'attribute_name',
    ##     type => 'length',
    ##     group => 'text',
    ##     priority => 5,
    ## );
    ##

    my $class = shift;
    @_ % 2
      && croak "Poorly formatted add_attribute request (odd "
      . "number of parameters)";
    my %param = @_;
    push( @attributes, \%param );
}

###########################################################
# PUBLIC METHODS: BUILD RULES AND SHEETS
###########################################################

sub _build_declaration {
    my ( $class_or_obj, $attribute, $value ) = @_;
    return "" if ( !defined $value || $value eq "" )    #but accept 0
      || !$css_data{attribute}->{$attribute};           #no such attribute
    my $type = $css_data{attribute}->{$attribute}->{type}
      or croak "Attribute '$attribute' has no defined type";
    $css_data{generator}->{$type}
      or croak "CSS attribute type '$type' has no css generator";
    my $result = $css_data{generator}->{$type}->($value);
    $result ? $attribute . ":" . $result . ";" : "";
}

sub build_rule {
    my $css = shift;
    ref $css eq "BigMed::CSS"
      or croak "BigMed::CSS object required to build rule";
    my $selector = $css->selector or return "";

    #custom value gets preference
    return $css->custom if defined $css->custom;

    my %attributes = $css->attributes or return "";

    #vars for collecting IE5 hack info
    my ( $font_size, $height, $width, %height_unit, %width_unit );
    my ( $height_offset, $width_offset ) = ( 0, 0 );

    $selector = $selector . ":link" if $selector =~ /^a($|(\.[^:]+$))/;
    my $declaration_block = '';
    foreach my $attr ( sort keys %attributes ) {
        my $value = $css->_build_declaration( $attr, $attributes{$attr} );

        #note values related to WinIE5 hack
        if ( $value =~ /^font-size:(\D+);$/ ) {    #font-size keyword
            $font_size = $1;
            next;    #add font size at ie5 hack parsing
        }
        elsif ( $value =~ /^height:(\-?\d+)(\D*);/ ) {
            $height_unit{$2} = 1;
            $height = $1;
            $height .= $2 if $2;
            next;    #add height at ie5 hack parsing
        }
        elsif ( $value =~ /^width:(\-?\d+)(\D*);/ ) {
            $width_unit{$2} = 1;
            $width = $1;
            $width .= $2 if $2;
            next;    #add width at ie5 hack parsing
        }
        elsif ( $value =~ /^padding\-(top|bottom):(\-?\d+)(\D+);/ ) {
            $height_unit{$3} = 1;
            $height_offset += $2;
        }
        elsif ( $value =~ /^padding\-(left|right):(\-?\d+)(\D+?);/ ) {
            $width_unit{$3} = 1;
            $width_offset += $2;
        }
        elsif ( $value =~ /^border\-width:(\-?\d+)(\D+?);/ ) {
            $height_unit{$2} = 1;
            $height_offset += ( $1 * 2 );
            $width_unit{$2} = 1;
            $width_offset += ( $1 * 2 );
        }

        $declaration_block .= $value;
    }

    #build the hack markup for this rule, if necessary
    $declaration_block
      .= _ie5_hack( $selector, $font_size, $height, $width, $height_offset,
        $width_offset, \%height_unit, \%width_unit );

    my $rule = '';
    if ($declaration_block) {
        $rule = $selector . " { $declaration_block }";
        $rule =~ s/([\{;]\s?)/$1\n    /g;
        $rule =~ s/    \}/\}\n/g;
    }
    $rule;
}

sub build_sheet {
    my $class = shift;
    my $site  = shift;
    ref $site eq "BigMed::Site"
      or croak "No site object provided to build_sheet for css styles";
    my $bigmed = BigMed->bigmed;

    $class->init_css();

    #build style sheet header
    my $sitename_label = $site->name || "";
    $sitename_label =~ s/\/?\*\/?//g;
    $sitename_label =~ s/&quot;//g;
    $sitename_label =~ s/&lt;//g;
    $sitename_label =~ s/&gt;//g;
    $sitename_label =~ s/&amp;/&/g;
    my $time    = $bigmed->bigmed_time;
    my $version = $bigmed->version;
    $sitename_label = "for $sitename_label," if $sitename_label;
    my $dot = $bigmed->env('DOT');
    my $sheet = <<"SHEET_HEADER";
/* bm${dot}styles.css -----------------------------------------------------
   CSS styles $sitename_label
   generated $time GMT by Big Medium v$version
   
   DO NOT EDIT THIS FILE MANUALLY
   This file is generated automatically, and any changes that you
   make to it will be overwritten. To add your own custom styles,
   use the "Edit Theme CSS" option in the Big Medium control panel.
   You may also remove theme styles via the "Remove Design Theme"
   option. Navigation styles may be removed via the navigation options
   in "HTML Preferences."
   ------------------------------------------------------------------- */
SHEET_HEADER

    #fetch the base styles, try the custom templates first
    my $base_path = bm_file_path( $bigmed->env('MOXIEDATA'),
        'templates_custom', 'site_templates', 'base.css' );
    if ( !-e $base_path ) {
        $base_path = bm_file_path(
            $bigmed->env('MOXIEDATA'), 'templates',
            'site_templates',          'base.css'
        );
    }
    my $base_css = join( "\n", bm_load_file($base_path) );
    if ($base_css) {
        $sheet .= "\n/* START BASE BIG MEDIUM STYLES\n"
          . "------------------------------------------------------ */\n"
          . $base_css . "\n";
    }

    #fetch the navigation styles, try the custom templates first
    my @custom_nav = (
        $bigmed->env('MOXIEDATA'),
        'templates_custom', 'site_templates', '_navcss'
    );
    my @default_nav =
      ( $bigmed->env('MOXIEDATA'), 'templates', 'site_templates', '_navcss' );
    foreach my $type qw(navigation subnavigation) {
        my $prefix = substr( $type, 0, 3 );
        foreach my $align qw(v h) {
            my $short = "$align$prefix";
            my $pref  = $site->get_pref_value("html_${type}_${short}style");
            next if !$pref || $pref eq 'none';
            my $try_file = bm_file_path( @custom_nav, "${short}_$pref.css" );
            my $try_file2 =
              bm_file_path( @default_nav, "${short}_$pref.css" );
            my @nav_css;
            if ( $pref && -e $try_file ) {
                @nav_css = bm_load_file($try_file);
            }
            elsif ( $pref && -e $try_file2 ) {
                @nav_css = bm_load_file($try_file2);
            }
            my $nav_css = join( "\n", @nav_css );
            $sheet .= "\n/* START "
              . uc("$type $short")
              . " STYLES\n"
              . "------------------------------------------------------ */\n"
              . $nav_css . "\n"
              if $nav_css;
        }
    }

    #fetch the theme styles
    my $theme     = BigMed::Theme->new();
    my $theme_css = $theme->site_css_text($site);
    if ($theme_css) {
        $sheet .= "\n/* START THEME STYLES\n"
          . "----------------------------------------------------------- */\n"
          . $theme_css . "\n";
    }

    #write the base/theme stylesheet
    my $file = bm_file_path( $site->html_dir, "bm${dot}styles.css" );
    bm_write_file( $file, $sheet, { build_path => 1 } ) or return undef;

    # CREATE THE CUSTOM SHEET -----------
    my $custom_sheet = <<"SHEET_HEADER";
/* bm${dot}styles-custom.css -----------------------------------------------------
   CSS custom styles $sitename_label
   generated $time GMT by Big Medium v$version
   
   DO NOT EDIT THIS FILE MANUALLY
   This file is generated automatically, and any changes that you
   make to it will be overwritten. The styles in this sheet may
   instead be added or edited via Big Medium's style editor.
   To create your own custom style entries, use Big Medium's
   "Edit Theme Style Sheet" option, which applies styles to the
   main bm${dot}styles.css sheet.
   ------------------------------------------------------------------- */

SHEET_HEADER

    my $custom_styles =
      $class->generate_custom_styles( $site, { comments => 1 } )
      or return;
    $custom_sheet .= $custom_styles;
    my $custom_file = bm_file_path( $site->html_dir, "bm${dot}styles-custom.css" );
    bm_write_file( $custom_file, $custom_sheet, { build_path => 1 } )
      or return;
    return 1;
}

sub generate_custom_styles {
    my ( $class, $site, $rparam ) = @_;
    ref $site eq "BigMed::Site"
      or croak 'generate_custom_styles requires site object';
    $rparam ||= {};
    $class->init_css();

    #fetch all styles for the site, and map selectors to objects
    my %selector_map;
    my $all_styles = $class->select( { site => $site->id } )
      or return;
    my $css;
    while ( $css = $all_styles->next ) {
        $selector_map{ $css->selector } = $css;
    }
    return if !defined $css;

    my $custom_sheet = q{};
    my $comments     = $rparam->{comments};
    if ($comments) {
        $custom_sheet .= "/* START BIG MEDIUM STYLE EDITOR STYLES\n"
          . "----------------------------------------------------------- */\n";
    }

    #step through each group and build style by priority
    foreach my $group (@selector_groups) {
        next unless $groups{$group}->{priority};
        $custom_sheet .= "\n/* ---------------------------------------\n   "
          . uc($group)
          . " STYLES\n"
          . "   --------------------------------------- */\n\n"
          if $comments;

        foreach my $priority (
            sort { $b <=> $a }
            keys %{ $groups{$group}->{priority} }
          )
        {
            foreach
              my $selector ( @{ $groups{$group}->{priority}->{$priority} } )
            {

                #this can result in duplicate entries of link, visited and
                #hover styles -- not a big deal, but there it is.
                foreach my $sel ( $selector, $selector . ":visited",
                    $selector . ":hover" )
                {
                    $custom_sheet .= $selector_map{$sel}->build_rule . "\n"
                      if $selector_map{$sel};
                }
            }
        }
    }
    return "$custom_sheet\n";
}

###########################################################
# INTERNAL ROUTINES: IE5 HACK GENERATORS
###########################################################

sub _ie5_hack {
    my ( $selector, $font_size, $height, $width, $height_offset,
        $width_offset, $rheight_unit, $rwidth_unit )
      = @_;
    return '' unless $font_size || $height || $width;
    my $ie5       = "";
    my $compliant = "";

    #do font hack if there's a font-size keyword
    if ($font_size) {
        my $ie5_keyword;    #the 'fake' keyword for ie5
        ( $font_size      eq "small"    and $ie5_keyword = "x-small" )
          or ( $font_size eq "x-small"  and $ie5_keyword = "xx-small" )
          or ( $font_size eq "medium"   and $ie5_keyword = "small" )
          or ( $font_size eq "large"    and $ie5_keyword = "medium" )
          or ( $font_size eq "x-large"  and $ie5_keyword = "large" )
          or ( $font_size eq "xx-large" and $ie5_keyword = "x-large" )
          or $ie5_keyword = $font_size;
        $ie5       .= "font-size:$ie5_keyword;";
        $compliant .= "font-size:$font_size;";
    }

    #do height hack if applicable
    my ( $compliant_height, $ie5_height ) =
      _ie5_dimension( 'height', $height, $height_offset, $rheight_unit );
    $compliant .= $compliant_height;
    $ie5       .= $ie5_height;

    #do width hack if applicable
    my ( $compliant_width, $ie5_width ) =
      _ie5_dimension( 'width', $width, $width_offset, $rwidth_unit );
    $compliant .= $compliant_width;
    $ie5       .= $ie5_width;

    #generate and add the ie5 hack declarations, if any
    return _build_hack_declarations( $selector, $compliant, $ie5 );
}

sub _ie5_dimension {
    my ( $dimension_name, $value, $offset, $runit ) = @_;
    my ( $compliant, $ie5 ) = ( '', '' );
    if ($value) {
        if ( $offset && keys %$runit == 1 ) {

            #need to offset dimension, and all units are same values;
            #do the hack
            $value =~ /(\d+)(\D+)/;
            my ( $num, $unit ) = ( $1, $2 );
            $num += $offset;
            $ie5       = "$dimension_name:$num$unit;";
            $compliant = "$dimension_name:$value;";
        }
        elsif ($offset) {

            #mixed units; punt and give it as-is
            $ie5 = "$dimension_name:$value;";
        }
        else {

            #no hack needed
            $ie5 = "$dimension_name:$value;";
        }
    }
    return ( $compliant, $ie5 );
}

sub _build_hack_declarations {
    my ( $selector, $compliant, $ie5 ) = @_;
    if ( $ie5 && $compliant ) {

        #generate hack html

        # previously generated the "be nice to opera" rule but it causes some
        # hassles in the css editor and general confusion for folks who try
        # to override the rule without the html>body selector. since it was
        # basically an old version of opera that couldn't follow along, and
        # since that browser version has vanishingly small marketshare, just
        # drop the opera rule and have the straight-up ie 5 voice-family hack.
        #        my $opera_select =
        #          $selector eq "body"
        #          ? "html>body"
        #          : "html>body $selector";
        return $ie5
          . 'voice-family:"\"}\"";'
          . qq~voice-family:inherit;$compliant~;

        #          . qq~$opera_select { $compliant~;
    }
    elsif ($ie5) {

        #no compliant but ie5 indicates that we punted on height or width
        return $ie5;
    }
    else {

        #no hack needed
        return $compliant;
    }
}

###########################################################
# INTERNAL ROUTINES: REGISTER DEFAULTS
###########################################################

sub _register_default_selectors {
    my $class = shift;

    # default styles --------------------------------
    $class->_register_selector(
        {   name     => 'body',
            priority => 1000,
            group    => 'default'
        }
    );

    $class->_register_selector(
        {   name     => 'a',
            priority => 980,
            group    => 'default'
        }
    );

    $class->_register_selector(
        {   name     => 'a:visited',
            priority => 970,
            group    => 'default'
        }
    );

    $class->_register_selector(
        {   name     => 'a:hover',
            priority => 960,
            group    => 'default'
        }
    );

    # detail styles --------------------------------
    $class->_register_selector(
        {   name     => 'h2.bmw_headline',
            priority => 990,
            group    => 'detail'
        }
    );

    $class->_register_selector(
        {   name     => 'h3.bmc_subhead',
            priority => 980,
            group    => 'detail'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_pageContent',
            priority => 970,
            group    => 'detail'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_byline',
            priority => 960,
            group    => 'detail'
        }
    );

    $class->_register_selector(
        {   name     => 'span.bmw_pubdate',
            priority => 950,
            group    => 'detail'
        }
    );

    $class->_register_selector(
        {   name     => 'span.bmw_modified',
            priority => 940,
            group    => 'detail'
        }
    );

    $class->_register_selector(
        {   name     => 'blockquote.bmc_bigPullquote',
            priority => 930,
            group    => 'detail'
        }
    );

    $class->_register_selector(
        {   name     => 'blockquote.bmc_smallPullquote',
            priority => 920,
            group    => 'detail'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmc_document',
            priority => 910,
            group    => 'detail'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmc_image',
            priority => 900,
            group    => 'detail'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_gallery',
            priority => 890,
            group    => 'detail'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_gallery h3',
            priority => 880,
            group    => 'detail'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_gallery div.bmc_image',
            priority => 870,
            group    => 'detail'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmc_caption',
            priority => 860,
            group    => 'detail'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmc_blurb',
            priority => 850,
            group    => 'detail'
        }
    );

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

    $class->_register_selector(
        {   name     => 'div.bmw_footer',
            priority => 990,
            group    => 'footer'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_footer a',
            priority => 990,
            group    => 'footer'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_footer a:visited',
            priority => 990,
            group    => 'footer'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_footer a:hover',
            priority => 990,
            group    => 'footer'
        }
    );

    # navigation --------------------------------

    $class->_register_selector(
        {   name     => 'div.bmw_navigation ul',
            priority => 990,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_navigation ul ul',
            priority => 980,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_navigation li',
            priority => 970,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name =>
              'div.bmw_navigation li.bmn_hover, div.bmw_navigation li:hover',
            priority => 960,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_navigation li.bmn_active',
            priority => 950,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_navigation li ul li',
            priority => 940,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_navigation a',
            priority => 930,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_navigation a:visited',
            priority => 920,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_navigation a:hover',
            priority => 920,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'h1.bmw_sitelogo',
            priority => 910,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'h1.bmw_sitelogo a',
            priority => 905,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'h1.bmw_sitelogo a:visited',
            priority => 900,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'h1.bmw_sitelogo a:hover',
            priority => 895,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_breadcrumbs',
            priority => 890,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_breadcrumbs a',
            priority => 880,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_breadcrumbs a:visited',
            priority => 870,
            group    => 'navigation'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_breadcrumbs a:hover',
            priority => 860,
            group    => 'navigation'
        }
    );

    # link blocks --------------------------------

    $class->_register_selector(
        {   name     => 'div.bmw_link',
            priority => 990,
            group    => 'links'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_link a',
            priority => 980,
            group    => 'links'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_link a:visited',
            priority => 970,
            group    => 'links'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_link a:hover',
            priority => 960,
            group    => 'links'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_link a.bma_head',
            priority => 950,
            group    => 'links'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_link a.bma_section',
            priority => 940,
            group    => 'links'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_link a.bma_commentcount',
            priority => 935,
            group    => 'links'
        }
    );

    $class->_register_selector(
        {   name     => 'span.bma_byline',
            priority => 930,
            group    => 'links'
        }
    );

    # spotlight blocks --------------------------------

    $class->_register_selector(
        {   name     => 'div.bmw_spotlightLinks div.bmw_link',
            priority => 920,
            group    => 'spotlight'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_spotlightLinks div.bmw_link a',
            priority => 910,
            group    => 'spotlight'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_spotlightLinks div.bmw_link a:visited',
            priority => 900,
            group    => 'spotlight'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_spotlightLinks div.bmw_link a:hover',
            priority => 890,
            group    => 'spotlight'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_spotlightLinks div.bmw_link a.bma_head',
            priority => 880,
            group    => 'spotlight'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_spotlightLinks div.bmw_link a.bma_section',
            priority => 870,
            group    => 'spotlight'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_spotlightLinks a.bma_commentcount',
            priority => 865,
            group    => 'spotlight'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_spotlightLinks span.bma_byline',
            priority => 860,
            group    => 'spotlight'
        }
    );

    # morelinks blocks --------------------------------

    $class->_register_selector(
        {   name     => 'div.bmw_moreLinks div.bmw_link',
            priority => 850,
            group    => 'morelinks'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_moreLinks div.bmw_link a',
            priority => 840,
            group    => 'morelinks'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_moreLinks div.bmw_link a:visited',
            priority => 830,
            group    => 'morelinks'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_moreLinks div.bmw_link a:hover',
            priority => 830,
            group    => 'morelinks'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_moreLinks div.bmw_link a.bma_head',
            priority => 820,
            group    => 'morelinks'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_moreLinks div.bmw_link a.bma_section',
            priority => 810,
            group    => 'morelinks'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_moreLinks a.bma_commentcount',
            priority => 805,
            group    => 'morelinks'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_moreLinks span.bma_byline',
            priority => 800,
            group    => 'morelinks'
        }
    );

    # latest blocks --------------------------------

    $class->_register_selector(
        {   name     => 'div.bmw_latestLinks div.bmw_link',
            priority => 790,
            group    => 'latest'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_latestLinks h3.bma_heading',
            priority => 785,
            group    => 'latest'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_latestLinks div.bmw_link a',
            priority => 780,
            group    => 'latest'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_latestLinks div.bmw_link a:visited',
            priority => 770,
            group    => 'latest'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_latestLinks div.bmw_link a:hover',
            priority => 760,
            group    => 'latest'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_latestLinks div.bmw_link a.bma_head',
            priority => 750,
            group    => 'latest'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_latestLinks div.bmw_link a.bma_section',
            priority => 740,
            group    => 'latest'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_latestLinks a.bma_commentcount',
            priority => 735,
            group    => 'latest'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_latestLinks span.bma_byline',
            priority => 730,
            group    => 'latest'
        }
    );

    # quicktease blocks --------------------------------

    $class->_register_selector(
        {   name     => 'div.bmw_quickteaseLinks div.bmw_link',
            priority => 720,
            group    => 'quicktease'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_quickteaseLinks h3.bma_heading',
            priority => 715,
            group    => 'quicktease'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_quickteaseLinks div.bmw_link a',
            priority => 710,
            group    => 'quicktease'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_quickteaseLinks div.bmw_link a:visited',
            priority => 700,
            group    => 'quicktease'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_quickteaseLinks div.bmw_link a:hover',
            priority => 690,
            group    => 'quicktease'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_quickteaseLinks div.bmw_link a.bma_head',
            priority => 680,
            group    => 'quicktease'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_quickteaseLinks div.bmw_link a.bma_section',
            priority => 670,
            group    => 'quicktease'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_quickteaseLinks a.bma_commentcount',
            priority => 665,
            group    => 'quicktease'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_quickteaseLinks span.bma_byline',
            priority => 660,
            group    => 'quicktease'
        }
    );

    # news blocks --------------------------------

    $class->_register_selector(
        {   name     => 'div.bmw_newsLinks div.bmw_link',
            priority => 650,
            group    => 'news'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_newsLinks div.bmw_link a',
            priority => 640,
            group    => 'news'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_newsLinks div.bmw_link a:visited',
            priority => 630,
            group    => 'news'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_newsLinks div.bmw_link a:hover',
            priority => 620,
            group    => 'news'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_newsLinks div.bmw_link a.bma_head',
            priority => 610,
            group    => 'news'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_newsLinks div.bmw_link a.bma_section',
            priority => 600,
            group    => 'news'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_newsLinks a.bma_commentcount',
            priority => 595,
            group    => 'news'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_newsLinks span.bma_byline',
            priority => 590,
            group    => 'news'
        }
    );

    # announcements blocks --------------------------------

    $class->_register_selector(
        {   name     => 'div.bmw_announcements',
            priority => 580,
            group    => 'announcements'
        }
    );

    $class->_register_selector(
        {   name     => 'h3.bmw_announce',
            priority => 570,
            group    => 'announcements'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_announce',
            priority => 560,
            group    => 'announcements'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_announce a',
            priority => 550,
            group    => 'announcements'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_announce a:visited',
            priority => 540,
            group    => 'announcements'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_announce a:hover',
            priority => 530,
            group    => 'announcements'
        }
    );

    # tips blocks --------------------------------

    $class->_register_selector(
        {   name     => 'div.bmw_tips',
            priority => 520,
            group    => 'tips'
        }
    );

    $class->_register_selector(
        {   name     => 'h3.bmw_tips',
            priority => 510,
            group    => 'tips'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_tips_tip',
            priority => 500,
            group    => 'tips'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_tips a',
            priority => 490,
            group    => 'tips'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_tips a:visited',
            priority => 480,
            group    => 'tips'
        }
    );

    $class->_register_selector(
        {   name     => 'div.bmw_tips a:hover',
            priority => 470,
            group    => 'tips'
        }
    );

    # search blocks --------------------------------

    $class->_register_selector(
        {   name     => 'form.bmw_search input',
            priority => 460,
            group    => 'search'
        }
    );
    $class->_register_selector(
        {   name     => 'form.bmw_search button',
            priority => 450,
            group    => 'search'
        }
    );


}

sub _register_default_attributes {
    my $class = shift;

    #### text attributes

    $class->_register_attribute(
        {   name     => 'font-family',
            type     => 'family',
            group    => 'text',
            priority => 99,
            edit     => {
                prompt_as   => 'value_several',
                css_class   => 'bmcp_css_fontfamily',
                numfields   => 3,
                description => 'CSSATTR_DESC_Font Family',
                optgroups   => [
                    {   name    => 'CSSLABEL_Cross-platform fonts',
                        options => [
                            q{---},         'Arial',
                            'Arial Black',  'Comic Sans MS',
                            'Courier New',  'Georgia',
                            'Impact',       'Times New Roman',
                            'Trebuchet MS', 'Verdana',
                            'sans-serif',   'serif',
                            'monospace',
                        ],
                    },
                    {   name    => 'CSSLABEL_Windows defaults',
                        options => [
                            'Calibri',              'Cambria',
                            'Candara',              'Consolas',
                            'Constantia',           'Corbel',
                            'Lucida Console',       'Lucida Sans Unicode',
                            'Microsoft Sans Serif', 'MS Mincho',
                            'Palatino Linotype',    'Symbol',
                            'Tahoma',
                        ],
                    },
                    {   name    => 'CSSLABEL_Mac defaults',
                        options => [
                            'American Typewriter', 'Andale Mono',
                            'Brush Script MT',     'Capitals',
                            'Apple Chancery',      'Baskerville',
                            'Big Caslon',          'Copperplate',
                            'Didot',               'Gadget',
                            'Geneva',              'Gill Sans',
                            'Futura',              'Helvetica',
                            'Herculanum',          'Hoefler Text',
                            'Lucida Grande',       'Marker Felt',
                            'Monaco',              'Optima',
                            'Papyrus',             'Sand',
                            'Skia',                'Techno',
                            'Textile',             'Zapfino',
                        ],
                    },
                ],
            },
        }
    );


    $class->_register_attribute(
        {   name     => 'font-size',
            type     => 'size',
            group    => 'text',
            priority => 98,
            edit     => { prompt_as => 'css_font_size', }
        }
    );

    $class->_register_attribute(
        {   name     => 'color',
            type     => 'color',
            group    => 'text',
            priority => 97,
            edit     => {
                css_class   => 'inline',
                description => 'CSSATTR_DESC_color',
            },
        }
    );

    $class->_register_attribute(
        {   name     => 'font-weight',
            type     => 'weight',
            group    => 'text',
            priority => 96,
            edit     => {
                prompt_as => 'tritoggle',
                states    => { on => 'bold', off => 'normal' },
            },
        }
    );

    $class->_register_attribute(
        {   name     => 'font-style',
            type     => 'style',
            group    => 'text',
            priority => 95,
            edit     => {
                prompt_as => 'tritoggle',
                states    => { on => 'italic', off => 'normal' },
            },
        }
    );

    $class->_register_attribute(
        {   name     => 'text-decoration',
            type     => 'decoration',
            group    => 'text',
            priority => 94,
            edit     => {
                prompt_as => 'tritoggle',
                states    => { on => 'underline', off => 'none' },
            },
        }
    );
    $class->_register_attribute(
        {   name     => 'text-align',
            type     => 'align',
            group    => 'text',
            priority => 93,
            edit     => {
                prompt_as => 'value_buttons',
                options   => [
                    {   value     => '',
                        css_class => 'bmcp_list_button_default',
                        label     => 'inherit'
                    },
                    {   value     => 'left',
                        css_class => 'bmcp_list_button_textleft'
                    },
                    {   value     => 'center',
                        css_class => 'bmcp_list_button_textcenter',
                    },
                    {   value     => 'right',
                        css_class => 'bmcp_list_button_textright'
                    },
                ],
            },
        }
    );

    $class->_register_attribute(
        {   name     => 'line-height',
            type     => 'length',
            group    => 'text',
            priority => 92,
            edit     => { prompt_as => 'css_length', }
        }
    );

    #### background attributes

    $class->_register_attribute(
        {   name     => 'background-color',
            type     => 'color',
            group    => 'background',
            priority => 87,
            edit     => { css_class => 'inline' },
        }
    );

    $class->_register_attribute(
        {   name     => 'background-image',
            type     => 'url',
            group    => 'background',
            priority => 85,
            edit     => { description => 'CSSATTR_DESC_background-image', },
        }
    );

    $class->_register_attribute(
        {   name     => 'background-repeat',
            type     => 'repeat',
            group    => 'background',
            priority => 83,
            edit     => {
                prompt_as => 'value_list',
                options   =>
                  ['', 'no-repeat', 'repeat', 'repeat-x', 'repeat-y'],
                labels => {
                    ''          => 'CSSLABEL_Inherit',
                    'no-repeat' => 'CSSLABEL_None',
                    'repeat'    => 'CSSLABEL_Tiled',
                    'repeat-x'  => 'CSSLABEL_Horizontal',
                    'repeat-y'  => 'CSSLABEL_Vertical',
                },
                css_class => 'inline',
            }
        }
    );

    #### box attributes

    $class->_register_attribute(
        {   name     => 'width',
            type     => 'length',
            group    => 'box',
            priority => 79,
            edit     => { prompt_as => 'css_length', }
        }
    );

    $class->_register_attribute(
        {   name     => 'height',
            type     => 'length',
            group    => 'box',
            priority => 78,
            edit     => { prompt_as => 'css_length', },
        }
    );

    $class->_register_attribute(
        {   name     => 'border-color',
            type     => 'color',
            group    => 'box',
            priority => 77,
            edit     => { css_class => 'inline' },
        }
    );

    $class->_register_attribute(
        {   name     => 'border-style',
            type     => 'border_style',
            group    => 'box',
            priority => 76,
            edit     => {
                prompt_as => 'value_list',
                css_class => 'inline',
                options   => [
                    q{},      'none',   'solid',  'dashed',
                    'dotted', 'double', 'groove', 'inset',
                    'outset', 'ridge'
                ],
                labels => {
                    q{}      => 'CSSLABEL_Inherit',
                    'none'   => 'CSSLABEL_None',
                    'solid'  => 'CSSLABEL_Solid',
                    'dashed' => 'CSSLABEL_Dashed',
                    'dotted' => 'CSSLABEL_Dotted',
                    'double' => 'CSSLABEL_Double',
                    'groove' => 'CSSLABEL_Groove',
                    'inset'  => 'CSSLABEL_Inset',
                    'outset' => 'CSSLABEL_Outset',
                    'ridge'  => 'CSSLABEL_Ridge',
                },
            },
        }
    );

    $class->_register_attribute(
        {   name     => 'border-width',
            type     => 'length',
            group    => 'box',
            priority => 75,
            edit     => { prompt_as => 'css_length', },
        }
    );
    $class->_register_attribute(
        {   name     => 'padding-top',
            type     => 'length',
            group    => 'box',
            priority => 74,
        }
    );

    $class->_register_attribute(
        {   name     => 'padding-right',
            type     => 'length',
            group    => 'box',
            priority => 73,
        }
    );

    $class->_register_attribute(
        {   name     => 'padding-bottom',
            type     => 'length',
            group    => 'box',
            priority => 72,
        }
    );

    $class->_register_attribute(
        {   name     => 'padding-left',
            type     => 'length',
            group    => 'box',
            priority => 71,
        }
    );
    $class->_register_attribute(
        {   name     => 'margin-top',
            type     => 'length',
            group    => 'box',
            priority => 70,
        }
    );

    $class->_register_attribute(
        {   name     => 'margin-right',
            type     => 'length',
            group    => 'box',
            priority => 69,
        }
    );

    $class->_register_attribute(
        {   name     => 'margin-bottom',
            type     => 'length',
            group    => 'box',
            priority => 68,
        }
    );

    $class->_register_attribute(
        {   name     => 'margin-left',
            type     => 'length',
            group    => 'box',
            priority => 67,
        }
    );
}

sub _register_generators_parsers {
    $css_data{generator}->{length}       = \&generate_css_length;
    $css_data{generator}->{family}       = \&generate_css_family;
    $css_data{generator}->{size}         = \&generate_css_size;
    $css_data{generator}->{color}        = \&generate_css_color;
    $css_data{generator}->{weight}       = \&generate_css_weight;
    $css_data{generator}->{style}        = \&generate_css_style;
    $css_data{generator}->{decoration}   = \&generate_css_decoration;
    $css_data{generator}->{align}        = \&generate_css_align;
    $css_data{generator}->{url}          = \&generate_css_url;
    $css_data{generator}->{repeat}       = \&generate_css_repeat;
    $css_data{generator}->{border_style} = \&generate_css_border_style;

}

sub _register_group_display {
    $groups{default}->{display}        = [qw(text background)];
    $groups{text}->{display}           = [qw(text)];
    $groups{link}->{display}           = [qw(text background)];
    $groups{'link-element'}->{display} = [qw(text)];
    $groups{'layout'}->{display}       = [qw(text background box layout)];
    $groups{'image'}->{display}        = [qw(box layout)];
}

###########################################################
# INTERNAL ROUTINES: REGISTER EXTERNAL/PLUGIN ADDITIONS
###########################################################

sub _register_ext_selectors {
    while (@selectors) { $_[0]->_register_selector( shift @selectors ) }
}

sub _register_ext_attributes {
    while (@attributes) { $_[0]->_register_attribute( shift @attributes ) }
}

###########################################################
# INTERNAL ROUTINES: ACTUAL REGISTRATION
###########################################################

sub _register_selector {
    $_[1] && ref $_[1] eq "HASH"
      or croak "Must provide a hash reference to register a css selector.";
    my $name = $_[1]->{name}
      or croak "Must provide a name when adding a selector.";
    my $group = $_[1]->{group}
      or croak "$name: Must provide a group when adding a selector.";
    $allowed_sel_groups{$group}
      or croak "$name: Must register selector with one of the approved "
      . "groups: "
      . join( ", ", @selector_groups );
    my $priority = $_[1]->{priority} + 0
      or croak "Must provide a numeric priority when adding a selector.";

    #dereference default values
    my %default = ref $_[1]->{default} eq "HASH" ? %{ $_[1]->{default} } : ();
    my %attributes = (
        name     => $name,
        default  => \%default,
        priority => $priority,
    );

    push( @{ $groups{$group}->{priority}->{$priority} }, $name );
    $css_data{selector}->{$name} = \%attributes;
    1;
}

sub _register_attribute {
    $_[1] && ref $_[1] eq "HASH"
      or croak "Must provide a hash reference to register a css attribute.";

    my %attributes;
    foreach my $req (qw(name type priority group)) {
        $attributes{$req} = $_[1]->{$req}
          or croak "Must provide a $req when adding attribute";
    }
    $allowed_att_groups{ $attributes{group} }
      or croak "$attributes{name}: Must register attribute with one of the "
      . "approved groups: "
      . join( ", ", @attribute_groups );

    $attributes{edit} = $_[1]->{edit};
    $css_data{attribute}->{ $_[1]->{name} } = \%attributes;
    1;
}

###########################################################
# INTERNAL: CSS PARSING ROUTINES
###########################################################

## these aren't intended to do particularly sophisticated
## parsing. they expect to be able to read/write only
## Big Medium-provided formats; not external, custom formats

sub generate_css_length {
    my $value = shift;
    $value =~ s/^\s+//;
    $value =~ s/\s+$//;
    $value = lc($value);
    return $value if $value eq "auto";
    $value =~ /^(\-?)\s*([\d+\.]+)\s*(\D*)$/;
    my ( $negative, $length, $unit ) = ( $1, $2, $3 );
    $negative = "" unless $negative;
    $unit = "px"
      unless $unit
      && ( $unit eq "%"
        || $unit eq "em"
        || $unit eq "pt"
        || $unit eq "ex"
        || $unit eq "in"
        || $unit eq "cm"
        || $unit eq "mm"
        || $unit eq "pi" );
    $length = 0 unless $length;
    "$negative$length$unit";
}

sub generate_css_family {
    my $value = shift;
    my @family;
    my @entries = split( /,/, $value );
    foreach (@entries) {
        $_ = lc($_);
        $_ =~ s/^(\s|")+//;
        $_ =~ s/("|\s+)$//;
        $_ = "\"$_\"" if $_ =~ /\s/;
        push( @family, $_ ) if $_;
    }
    return @family ? join( ", ", @family ) : "";
}

sub generate_css_size {
    my $value = lc( shift @_ );
    $value =~ s/\s+//g;
    $value = &generate_css_length($value)
      unless $value eq "small"
      || $value     eq "x-small"
      || $value     eq "medium"
      || $value     eq "large"
      || $value     eq "x-large"
      || $value     eq "xx-large"
      || $value     eq "xx-small";
    $value;
}

sub generate_css_color {
    my $value = lc( shift @_ );
    $value =~ s/^\s+//g;
    $value =~ s/\s+$//g;

    #plain-language color
    return $value if $colors{$value};

    if ( $value =~ /^#?\s*([a-fA-F0-9])\s*([a-fA-F0-9])\s*([a-fA-F0-9])$/ ) {
        return "#$1$2$3";
    }
    elsif ( $value
        =~ /^#?\s*([a-fA-F0-9])\s*([a-fA-F0-9])\s*([a-fA-F0-9])\s*([a-fA-F0-9])\s*([a-fA-F0-9])\s*([a-fA-F0-9])$/
      )
    {
        return "#$1$2$3$4$5$6";
    }
    elsif ( $value =~ /^rgb\s*\(\s*([^\)]+?)\)/ ) {
        my @rgb = split( /,/, $1 );
        @rgb = split( /\s+/, $rgb[0] ) if @rgb == 1;
        my @all;
        foreach my $this (@rgb) {
            $this =~ s/[^0-9\.%]//g;
            $this = "0" unless $this;
            push @all, $this;
        }
        if ( @all > 3 ) {
            @all = @all[0 .. 2];
        }
        else {
            while ( @all < 3 ) {
                push @all, "0";
            }
        }
        return "rgb(" . join( ", ", @all ) . ")";
    }
    return "";
}

sub generate_css_weight {
    my $value = lc( shift @_ );
    $value =~ s/\s+//g;
    $value = ""
      unless $value eq "normal"
      || $value     eq "bold"
      || $value     eq "bolder"
      || $value     eq "lighter"
      || $value     eq "100"
      || $value     eq "200"
      || $value     eq "300"
      || $value     eq "400"
      || $value     eq "500"
      || $value     eq "600"
      || $value     eq "700"
      || $value     eq "800"
      || $value     eq "900";
    $value;
}

sub generate_css_style {
    my $value = lc( shift @_ );
    $value =~ s/\s+//g;
    $value = "" unless $value eq "normal" || $value eq "italic";
    $value;
}

sub generate_css_decoration {
    my $value = lc( shift @_ );
    $value =~ s/\s+//g;
    $value = ""
      unless $value eq "none"
      || $value     eq "underline"
      || $value     eq "line-through"
      || $value     eq "overline";
    $value;
}

sub generate_css_align {
    my $value = lc( shift @_ );
    $value =~ s/\s+//g;
    $value = ""
      unless $value eq "left"
      || $value     eq "center"
      || $value     eq "justify"
      || $value     eq "right";
    $value;
}

sub generate_css_url {
    my $value = shift || return '';
    $value =~ s/^\s+//g;
    $value =~ s/\s+$//g;
    if ( lc substr( $value, 0, 3 ) eq "url" ) {
        $value =~ s/\s*(\(|\))\s*/$1/g;
        return $value;
    }
    $value = "url($value)" if $value;
    $value;
}

sub generate_css_repeat {
    my $value = lc( shift @_ );
    $value =~ s/\s+//g;
    $value = ""
      unless $value eq "repeat"
      || $value     eq "no-repeat"
      || $value     eq "repeat-x"
      || $value     eq "repeat-y";
    $value;
}

sub generate_css_border_style {
    my $value = lc( shift @_ );
    $value =~ s/\s+//g;
    $value = ""
      unless $value eq "none"
      || $value     eq "solid"
      || $value     eq "dashed"
      || $value     eq "dotted"
      || $value     eq "double"
      || $value     eq "groove"
      || $value     eq "inset"
      || $value     eq "outset"
      || $value     eq "ridge";
    $value;
}

1;
__END__


=head1 NAME

BigMed::CSS - Big Medium CSS record

=head1 DESCRIPTION

A BigMed::CSS object represents the style rule for a single (X)HTML element
for a website (or section of a website) in the Big Medium system. BigMed::CSS
also includes methods for building and parsing CSS declaration blocks, and
generating entire style sheets.

=head1 USAGE

BigMed::CSS is a subclass of BigMed::Data. In addition to the methods 
documented below, please see the BigMed::Data documentation for details about
topics including:

=over 4

=item Creating a new data object

=item Saving a data object

=item Finding and sorting saved data objects

=item Data access methods

=item Error handling

=back

=head1 METHODS

=head2 Data Access Methods

The BigMed::CSS object holds the following pieces of data. They can be
accessed and set using the standard data access methods described in the
BigMed::Data documentation. See the L<"Searching and Sorting"> section below
for details on the data columns available to search and sort BigMed::CSS
objects.

=over 4

=item * id

The numeric ID of the BigMed::CSS object

=item * site

The numeric ID of the BigMed::Site object with which the CSS object is
associated

=item * section

The numeric ID of the BigMed::Section object with which the CSS object is
associated

=item * selector

CSS selector string indicating the element or type of element to which this
object's style rule applies

=item * attributes

Hash of attribute values with attribute names as keys and values as pairs.
Each key/value pair composes a declaration for this selector's style rule.
All values are represented as strings. For example:

    $css->set_attributes(
      {
        'font-family' => 'helvetica, arial, sans-serif',
        'font-size' => '10px',
        'background-color' => '#f00',        
      }
    );

Attributes may include any or all of the following:

B<Text attributes>

=over 4

=item * font-family

Comma-separated font names. Names with multiple words should be
wrapped in quotes:

    font-family => '"lucida sans unicode", lucida'
    font-family => 'helvetica, arial, sans-serif'

=item * font-size

A relative size keyword (xx-small, x-small, small, medium, large, x-large,
or xx-large), a numeric value in one of eight units (px, em, pt, ex, in, cm,
mm, pi), or a percentage:

    font-size => 'xx-small'
    font-size => '12px'
    font-size => '1.4em'
    font-size => '110%'
    
=item * color

Accepted values: a plain-language color keyword (see L<Color Keywords> below),
a six-digit hexadecimal number, a three-digit hexadecimal shortcut, or a rgb()
value. For example, all of these result in the same magenta color:

    color => 'magenta',
    color => '#ff00ff',
    color => '#f0f',
    color => 'rgb(255,0,255)'
    color => 'rgb(100%,0%,100%)'

=item * font-weight

Accepted values: font weight keyword (normal, bold, bolder, lighter) or a
numeric value (100, 200, 300, 400, 500, 600, 700, 800, 900).

=item * font-style

Accepted values: italic or normal.

=item * text-decoration

Accepted values: none, underline, line-through, or overline.

=item * text-align

Accepted values: left, center, justify, right

=item * line-height

Accepted values: a numeric value in one of eight units (px, em, pt, ex, in, cm,
mm, pi), or a percentage.

=back

B<Background attributes>

=over 4

=item * background-color

Accepted values: See C<color> above.

=item * background-image

Format: URL or 'url(http://www.example.com/url/to/image.gif)'

=item * background-repeat

Accepted values: repat, no-repeat, repeat-x or repeat-y

=back

B<Box attributes>

=over 4

=item * width

Accepted values: A numeric value in one of eight units (px, em, pt, ex,
in, cm, mm, pi), or a percentage.

=item * height

Accepted values: A numeric value in one of eight units (px, em, pt, ex,
in, cm, mm, pi), or a percentage.

=item * padding-top

Accepted values: A numeric value in one of eight units (px, em, pt, ex,
in, cm, mm, pi), or a percentage.

=item * padding-right

Accepted values: A numeric value in one of eight units (px, em, pt, ex,
in, cm, mm, pi), or a percentage.

=item * padding-bottom

Accepted values: A numeric value in one of eight units (px, em, pt, ex,
in, cm, mm, pi), or a percentage.

=item * padding-left

Accepted values: A numeric value in one of eight units (px, em, pt, ex,
in, cm, mm, pi), or a percentage.

=item * border-width

Accepted values: A numeric value in one of eight units (px, em, pt, ex,
in, cm, mm, pi), or a percentage.

=item * border-color

Accepted values: See the C<color> entry above.

=item * border-style

Accepted values: none, solid, dashed, dotted, double, groove, inset,
outset, ridge.

=item * margin-top

Accepted values: A numeric value in one of eight units (px, em, pt, ex,
in, cm, mm, pi), or a percentage.

=item * margin-right

Accepted values: A numeric value in one of eight units (px, em, pt, ex,
in, cm, mm, pi), or a percentage.

=item * margin-bottom

Accepted values: A numeric value in one of eight units (px, em, pt, ex,
in, cm, mm, pi), or a percentage.

=item * margin-left

Accepted values: A numeric value in one of eight units (px, em, pt, ex,
in, cm, mm, pi), or a percentage.

=back

=item * custom

A string containing a custom-crafted rule. If defined, the C<custom> value will
be used as-is for the style rule. It should be a fully formed rule, including
the selector, brackets and declaration block. For example:

    custom => 'h1.bmHeadline { font-weight: bold; color: red; }'

=back

=head3 Searching and Sorting

You can look up and sort records by any combination of the following fields.
See the C<fetch> and C<select> documentation in BigMed::Data for more info.

=over 4

=item * id

=item * mod_time

=item * site

=item * section

=item * selector

=back

=head3 Color Keywords

Color attributes may use one of the following plain-language keywords.
Their corresponding hex values are shown here, too:

=over 4

=item * aliceblue (#f0f8ff)

=item * antiquewhite (#faebd7)

=item * cyan (#00ffff)

=item * aquamarine (#7fffd4)

=item * azure (#f0ffff)

=item * beige (#f5f5dc)

=item * bisque (#ffe4c4)

=item * black (#000000)

=item * blanchedalmond (#ffebcd)

=item * blue (#0000ff)

=item * blueviolet (#8a2be2)

=item * brass (#b5a642)

=item * brown (#a52a2a)

=item * burlywood (#deb887)

=item * cadetblue (#5f9ea0)

=item * chartreuse (#7fff00)

=item * chocolate (#d2691a)

=item * coolcopper (#d98719)

=item * copper (#b87333)

=item * coral (#ff7f50)

=item * cornflowerblue (#6495ed)

=item * cornsilk (#fff8dc)

=item * crimson (#dc143c)

=item * aqua (#00ffff)

=item * darkblue (#00008b)

=item * darkbrown (#5c4033)

=item * darkcyan (#008b8b)

=item * darkgoldenrod (#b8860b)

=item * darkgray (#a9a9a9)

=item * darkgreen (#006400)

=item * darkkhaki (#b7b76b)

=item * darkmagenta (#8b008b)

=item * darkolivegreen (#556b2f)

=item * darkorange (#ff8c00)

=item * darkorchid (#9932cc)

=item * darkred (#8b0000)

=item * darksalmon (#e9967a)

=item * darkseagreen (#8fbc8f)

=item * darkslateblue (#483d8b)

=item * darkslatagray (#2f4f4f)

=item * darkturquoise (#00ced1)

=item * darkviolet (#9400d3)

=item * deeppink (#ff1493)

=item * deepskyblue (#00bfff)

=item * dimgray (#696969)

=item * dodgerblue (#1e90ff)

=item * feldspar (#d19275)

=item * firebrick (#b22222)

=item * floralwhite (#fffaf0)

=item * forestgreen (#228b22)

=item * fuchsia (#ffooff)

=item * gainsboro (#dcdcdc)

=item * ghostwhite (#f8f8f8)

=item * gold (#ffd700)

=item * goldenrod (#daa520)

=item * gray (#808080)

=item * green (#008000)

=item * greenyellow (#adff2f)

=item * horneydew (#f0fff0)

=item * honeydewtab (#00de2b)

=item * hotpink (#ff69b4)

=item * indianred (#cd5c5c)

=item * indigo (#4b0082)

=item * ivory (#fffff0)

=item * khaki (#f0e68c)

=item * lavender (#e6e6fa)

=item * lavenderblush (#fff0f5)

=item * lawngreen (#7cfc00)

=item * lemonchiffon (#fffacd)

=item * lightblue (#add8e6)

=item * lightcoral (#f08080)

=item * lightcyan (#e0ffff)

=item * lightgoldenrodyellow (#fafad2)

=item * lightgreen (#90ee90)

=item * lightgrey (#d3d3d3)

=item * lightpink (#ffb6c1)

=item * lightsalmon (#ffa07a)

=item * lightseagreen (#20b2aa)

=item * lightskyblue (#87cefa)

=item * lightslategray (#778899)

=item * lightsteelblue (#b0c4de)

=item * lightyellow (#ffffe0)

=item * lime (#00f000)

=item * limegreen (#32cd32)

=item * linen (#fae0e6)

=item * magenta (#ffooff)

=item * maroon (#800000)

=item * mediumaquamarine (#66cdaa)

=item * mediumblue (#0000cd)

=item * mediumorchid (#ba55d3)

=item * mediumpurple (#9370db)

=item * mediumseagreen (#3cb371)

=item * mediumslateblue (#7b68ee)

=item * mediumspringgreen (#00fa9a)

=item * mediumturquoise (#48d1cc)

=item * mediumvioletred (#c71585)

=item * midnightblue (#191970)

=item * mintcream (#f5fffa)

=item * mistyrose (#ffe4e1)

=item * moccasin (#ffe4b5)

=item * navajowhite (#ffdead)

=item * navy (#000080)

=item * oldlace (#fdf5e6)

=item * olive (#808000)

=item * olivedrab (#6b8e23)

=item * orange (#ffa500)

=item * orangered (#ff4500)

=item * orchid (#da70d6)

=item * palegoldenrod (#eee8aa)

=item * palegreen (#98fb98)

=item * paleturquoise (#afeeee)

=item * palevioletred (#db7093)

=item * papayawhip (#ffefd5)

=item * peachpuff (#ffdab7)

=item * peru (#cd853f)

=item * pink (#ffc0cb)

=item * plum (#dda0dd)

=item * powderblue (#b0e0e6)

=item * purple (#800080)

=item * red (#ff0000)

=item * rosybrown (#bc8f8f)

=item * royalblue (#4169e1)

=item * sadllebrown (#8b4513)

=item * salmon (#fa8072)

=item * sandybrown (#f4a460)

=item * seagreen (#2e8b57)

=item * seashell (#fff5ee)

=item * sienna (#a0522d)

=item * silver (#c0c0c0)

=item * skyblue (#87ceeb)

=item * slateblue (#6a5acd)

=item * slategray (#708090)

=item * snow (#fffafa)

=item * springgreen (#00ff7f)

=item * steelblue (#4682b4)

=item * tan (#d2b48c)

=item * teal (#008080)

=item * thistle (#d8bfd8)

=item * transparent (transparent)

=item * tomato (#ff6347)

=item * turquoise (#40e0d0)

=item * violet (#ee82ee)

=item * wheat (#f5deb3)

=item * white (#ffffff)

=item * whitesmoke (#f5f5f5)

=item * yellow (#ffff00)

=item * yellowgreen (#9acd32)

=back

=head2 CSS-Info Accessors

=over 4

=item * C<<BigMed::CSS->selector_Groups>>

Returns the list of selector groups.

=item * C<<BigMed::CSS->selectors($group)>>

Returns the list of registered selectors, ordered by priority. If the optional
group argument is specified, then only that group's selectors are returned.

=item * C<<BigMed:CSS->attribute_groups>>

Returns the list of attribute groups.

=item * C<<BigMed::CSS->attribute_names($group)>>

Returns the list of attribute names for the group in the first argument,
ordered by priority.

=item * C<<BigMed::CSS->attribute_prompt($attribute)>>

Returns a hash of any parameters to pass to BigMed::App's prompt method
when displaying the attribute in an edit form.

=item * C<<BigMed::CSS->parse_attribute_value($attribute, $value)>>

Reads and cleans up the value for the specified attribute, returning
a clean value appropriate for inclusion in a CSS style sheet (or an
empty string if it doesn't make sense).

=back

=head2 CSS-Generating Methods

=over 4

=item * $css->build_rule

Returns a string containing the CSS rule for the object.

The object's C<custom> column takes precedence here, and if defined,
the string entered there will be returned.

Otherwise, the CSS rule will be generated from the attributes in the object's
C<attributes> column.

=item * BigMed::CSS->build_sheet($site_obj[, $section_obj])

Generates and saves the style sheets for the site object in the first argument
(the files are saved in the site's HTML directory).

The first style sheet, bm.styles.css, contains the base.css and theme css
styles. The second style sheet contains the styles governed by BigMed::CSS
objects.

=back

=head2 Selector and Attribute Registration Methods

Plugins and other external modules can use the following methods to add or
override selectors and attributes to the BigMed::CSS lineup.

=over 4

=item * BigMed::CSS->add_selector(%attributes)

The following parameters may be specified in the argument hash:

=over 4

=item * name

B<Required.> The CSS selector string.

=item * group

B<Required.> The CSS group with which to associate the selector. This affects
where the selector will be made available for editing in the control panel,
as well as where it is placed within the style sheet.

Accepted values: default, text, link, image, layout, link-element, custom

=item * priority

B<Required.> A numeric value indicating where the selector should be
listed in the control panel and the style sheet relative to other selectors
in its group. The higher the number, the higher the selector appears in the list.

=back

=item * BigMed::CSS->add_attribute(%parameters)

The following parameters may be specified in the argument hash:

=over 4

=item * name

B<Required.> The name of the CSS attribute (e.g., font-style, text-decoration,
float, etc).

=item * group

B<Required.> The attribute group with which to associate this attribute in the
control panel.

Accepted values: text, background, box, layout

=item * priority

B<Required.> A numeric value indicating where the selector should be
listed in the control panel relative to other selectors in its group. The
higher the number, the higher the selector appears in the list.

=item * type

B<Required.> The type of value this attribute accepts. This affects how the value
is parsed and displayed within the style sheet.

Accepted values: length, family, size, color, weight, style, decoration, align,
whitespace, transform, url, repeat, back_position, border_style, float, clear,
display.

=item * edit

An optional hash reference of parameters to pass to BigMed::App's prompt
method when displaying the style attribute in an edit form.

=back

=cut

