Bivio::UI::FacadeComponent::Font
# Copyright (c) 1999-2010 bivio Software, Inc. All rights reserved. # $Id$ package Bivio::UI::FacadeComponent::Font; use strict; use Bivio::Base 'UI.FacadeComponent'; # C<b_use('FacadeComponent.Font')> is a map of font names to html values. # # The configuration of a font is an array_ref. The elements are # either C<key=value>, e.g. C<face=verdana,arial> and C<size=2>, # or simple tags, e.g. C<bold> or C<smaller>. # # Here is a complete list of known tags. All other tags will # be rejected. The list is kept small to avoid specification # errors. Feel free to add to the list in internal_initialize: # # family=list # color=color-name # size=string # size% # class=string # id=string # bold # code # italic # larger # smaller # strike # underline # style=descriptor # # Do not surround values to the right of the equals (=) with quotes. # The string following size can be a number as long as a style sheet # is not used. For style sheets, should be "x-small", "large", etc. # These will be mapped into numeric sizes. # # C<default> is a special font name which must exist. It is used # to set the default font of the entire page. B<Do not set the # color with this attribute. Netscape doesn't handle color styles # correctly.> # # The C<color> attribute is looked up implicitly if there is only one name in # a group and there is a color by that name. # # Fonts behave differently depending on if the # L<Bivio::Type::UserAgent|Bivio::Type::UserAgent> is a # C<BROWSER_HTML3> or other BROWSER type, see # L<Bivio::UI::HTML::Widget::Style|Bivio::UI::HTML::Widget::Style>. # # # The implementations of style sheets is pretty horrible for the most part. # Netscape is the worst. The implementation is qualified by Netscape. # We "try" to do things as best we can. If we fail, it will look ok. # This is the key. # # The first problem is that Netscape reads the C<font-color> property # strangely, e.g. # # font-color: #FF0000; # # is green, not red as in IE. This is why we don't allow the default # font to have a color. It will be plain wrong in Netscape. Instead # we assume C<page_text> will be set by # L<Bivio::UI::HTML::Widget::Page|Bivio::UI::HTML::Widget::Page> # correctly. # # The next problem is that C<larger> and C<smaller> don't work in IE # for some reason. Worse, of course, is Netscape which only allows # you to use C<larger> and C<smaller> and NOT C<small> and C<big> # tags if you have a style sheet in the header which sets C<font-size>-- # got that? # # The solution is tightly coupled with # L<Bivio::UI::HTML::Widget::Style|Bivio::UI::HTML::Widget::Style>. # We set the C<font-family> in the Style, if there is a style. our($_TRACE); my($_A) = b_use('IO.Alert'); my($_C) = b_use('FacadeComponent.Color'); # Map style names to numeric sizes my($_SIZE_MAP) = { 'xx-small' => 1, 'x-small' => 2, 'small' => 3, 'medium' => 4, 'large' => 5, 'x-large' => 6, 'xx-large' => 7, }; # Certain attributes map one to one to tags. See refererences to # _TAG_MAP below and _initialize_html my($_TAG_MAP) = { bold => 'b', italic => 'i', code => 'tt', # No one handles +1/-1 correctly with styles. small # and big work everywhere, it seems. larger => 'big', smaller => 'small', strike => 'strike', underline => 'u', }; # CSS-only my($_CSS_MAP) = { capitalize => 'text-transform: capitalize', center => 'text-align: center', inline => 'display: inline', justify => 'text-align: justify', left => 'text-align: left', lowercase => 'text-transform: lowercase', normal => 'text-decoration:none;font-weight:normal;font-style:normal;white-space:normal;text-transform:none;font-size:100%;text-align:left', # There is no "text-align: none" # http://www.w3.org/TR/CSS21/text.html: # Initial: a nameless value that acts as 'left' if 'direction' is 'ltr', # 'right' if 'direction' is 'rtl' normal_align => 'text-align: left', normal_decoration => 'text-decoration: none', normal_size => 'font-size: 100%', normal_style => 'font-style: normal', normal_transform => 'text-transform: none', normal_weight => 'font-weight: normal', normal_wrap => 'white-space: normal', nowrap => 'white-space: nowrap', pre => 'white-space: pre', pre_line => 'white-space: pre-line', pre_wrap => 'white-space: pre-wrap', right => 'text-align: right', uppercase => 'text-transform: uppercase', }; my($_CONFIG_ATTR) = '_config'; sub REGISTER_PREREQUISITES { return ['Color']; } sub UNDEF_CONFIG { # : array_ref # Returns config for no font. return []; } sub format_css { # (proto, string, Collection.Attributes) : string # (self, string) : array my($proto, $name, $req) = @_; return '' unless $name and my $v = $proto->internal_get_value($name, $req); return $v->{css}; } sub format_html { # (proto, string, Collection.Attributes) : string # (self, string) : array # font_with_style : boolean [0] # # If set to true, the fonts will be rendered assuming the default font # was set in an inline style. my($proto, $name, $req) = @_; unless (Bivio::Agent::Request->is_blesser_of($req)) { b_warn('pass $source->req, not $source'); $req = $req->req; } return '' unless $name and my $v = $proto->internal_get_value($name, $req); $req ||= Bivio::Agent::Request->get_current; return $req->unsafe_get('font_with_style') ? @{$v->{html_with_style}} : @{$v->{html_no_style}}; } sub get_attrs { # (proto, string, Collection.Attributes) : hash_ref # (self, string) : hash_ref # Returns the font attributes. B<Do not modify.> # # May return C<undef> if no such font. my($proto, $name, $req) = @_; return undef unless $name; my($v) = $proto->internal_get_value($name, $req); return $v ? $v->{attrs} : undef; } sub initialization_complete { # (self) : undef # Verifies all standard fonts have been defined and converts # the values to html and such. my($self) = @_; my($default) = $self->internal_get_value('default'); $self->initialization_error( {names => ['default']}, ': default font not defined' ) unless $default; _initialize($self, $default, $default); $self->initialization_error( $default, 'do not set color on default, use page_text' ) if defined($default->{attrs}->{color}); foreach my $v (@{$self->internal_get_all}) { _initialize($self, $v, $default); } $self->SUPER::initialization_complete(); return; } sub internal_initialize_value { # (self, hash_ref) : undef # Initializes the internal value from the configuration. my($self, $value) = @_; my($v) = $value->{config}; unless (ref($v)) { $v = $value->{config} = [$v]; } elsif (ref($v) ne 'ARRAY') { $self->initialization_error($value, 'not an array_ref'); $value->{config} = []; } # Special case the UNDEF_CONFIG. The names list is empty in this case. return if @{$value->{names}}; $value->{attrs} = {}; $value->{html_no_style} = ['', '']; $value->{html_with_style} = ['', '']; return; } sub _initialize { my($self, $value, $default) = @_; return if $value->{html}; my($config) =[@{$value->{config}}]; #TODO: This should be lower down so that the each name is separate, i.e # [[qw(foo bar)] => []] # shares color for foo and bar unless foo and bar each have a color if (int(@{$value->{names}}) == 1 && !grep(/^color=/, @$config)) { my($name) = $value->{names}->[0]; if ($self->get_facade->get('Color')->exists($name)) { # Only set color if doesn't already exist. push(@$config, 'color='.$name); } } my($attrs) = {}; $attrs->{$_CONFIG_ATTR} = $config; foreach my $attr (@$config) { if ($_TAG_MAP->{$attr}) { $attrs->{'tag_' . $_TAG_MAP->{$attr}} = 1; } elsif ($attr =~ /^(family|weight|size|class|id|style)=(.*)/) { $attrs->{$1} = $2; } elsif ($attr =~ /^\d+(?:\%|px)$/ || $_SIZE_MAP->{$attr}) { $attrs->{size} = $attr; } elsif ($attr =~ /^color=(.+)/) { $attrs->{color} = $_C->format_html($1, '', $self->get_facade); } elsif ($_CSS_MAP->{$attr}) { push(@{$attrs->{other_styles} ||= []}, $_CSS_MAP->{$attr}); } else { $self->initialization_error($value, 'unknown attribute: ', $attr); %$attrs = (); last; } } $value->{attrs} = $attrs; _initialize_html_no_style($value, $default); _initialize_css($self, $value); _initialize_html_with_style($value, $default); return; } sub _initialize_css { # (hash) : array_ref # Returns my($self, $value) = @_; my($attr) = $value->{attrs}; $value->{css} = join(' ', map( $_ =~ /;$/ ? $_ : "$_;", map($attr->{$_} ? ($_ eq 'color' ? '' : 'font-') . "$_: $attr->{$_}" : (), qw(family weight color size)), map($attr->{"tag_$_->[0]"} ? $_->[1] : (), [b => 'font-weight: bold'], [big => 'font-size: 120%'], [i => 'font-style: italic'], [small => 'font-size: 80%'], [strike => 'text-decoration: line-through'], [tt => 'font-family: monospace'], [u => 'text-decoration: underline'], ), @{$attr->{other_styles} || []}, $attr->{style} ? $attr->{style} : (), )); return; } sub _initialize_html { # (hash) : array_ref # Returns the html (prefix, suffix) tuple for these attributes. my(%attrs) = @_; my($p, $s) = ('', ''); foreach my $k (qw(family size color class id style)) { next unless $attrs{$k}; unless ($p) { $p = '<font'; $s = '</font>'; } my($n) = $k eq 'family' ? 'face' : $k; my($v) = $attrs{$k}; # Map to numeric sizes, but only in <FONT> attributes $v = $_SIZE_MAP->{$v} if $k eq 'size' && $_SIZE_MAP->{$v}; $p .= ' '.$n.'="'.$v.'"'; } $p .= '>' if $p; foreach my $k (keys(%attrs)) { next unless $k =~ /^tag_(\w+)$/; $p .= "<$1>"; $s = "</$1>".$s; } return [$p, $s]; } sub _initialize_html_no_style { # (hash_ref, hash_ref) : undef # Sets the html_no_style attributes based on attrs of value and default. my($value, $default) = @_; $value->{html_no_style} = _initialize_html(%{$default->{attrs}}, %{$value->{attrs}}); return; } sub _initialize_html_with_style { # (hash_ref, hash_ref) : undef # Sets the html_with_style attributes based on attrs of value and default. my($value, $default) = @_; $value->{html_with_style} = _initialize_html(%{$value->{attrs}}); return; } 1;