]> Dogcows Code - chaz/p5-CGI-Ex/commitdiff
CGI::Ex 2.22 v2.22
authorPaul Seamons <perl@seamons.com>
Fri, 14 Dec 2007 00:00:00 +0000 (00:00 +0000)
committerCharles McGarvey <chazmcgarvey@brokenzipper.com>
Fri, 9 May 2014 23:46:43 +0000 (17:46 -0600)
22 files changed:
Changes
MANIFEST
META.yml
lib/CGI/Ex.pm
lib/CGI/Ex/App.pm
lib/CGI/Ex/Auth.pm
lib/CGI/Ex/Conf.pm
lib/CGI/Ex/Die.pm
lib/CGI/Ex/Dump.pm
lib/CGI/Ex/Fill.pm
lib/CGI/Ex/JSONDump.pm
lib/CGI/Ex/Template.pm
lib/CGI/Ex/Validate.pm
lib/CGI/Ex/validate.js
samples/app/cgi_ex_2.cgi
samples/devel/memory_app.pl [new file with mode: 0644]
samples/validate_js_0_tests.html [new file with mode: 0644]
samples/validate_js_1_onsubmit.html [new file with mode: 0644]
samples/validate_js_2_onchange.html [new file with mode: 0644]
samples/validate_js_yaml_1.html [new file with mode: 0644]
samples/validate_js_yaml_2.html [new file with mode: 0644]
samples/validate_js_yaml_3.html [new file with mode: 0644]

diff --git a/Changes b/Changes
index 72d28751f7a77b1ec44a26b3a4b673df5aad439c..d051859ae9f8885007e239fa989c243f12c06bca 100644 (file)
--- a/Changes
+++ b/Changes
@@ -1,3 +1,14 @@
+2.22
+     2007-12-14
+        * Allow for no errors with a username of "0"
+        * Run hash_form hook before others so dump_history shows it in order of use
+        * Fix Validate error with passing in the field name
+        * Simplify and modernize validate.js
+        * Add onevent: change, blur and submit types.
+        * Add hooks and better overriding to valiate.js
+        * Cleanup Validate.pm pod
+        * Be sure Conf json read requires JSON
+
 2.21
      2007-10-18
         * Add logout_hook to Auth
index c800d2fd75bfd3c9135c03cafcbf9a4c78d34ca7..a9184f990e29a24836cc8681f672b82fb9c85f98 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -29,11 +29,14 @@ samples/benchmark/bench_jsondump.pl
 samples/benchmark/bench_validation.pl
 samples/devel/dprof_conf.d
 samples/devel/dprof_validation.d
-samples/generate_js.pl
+samples/devel/memory_app.pl
 samples/index.cgi
-samples/js_validate_1.html
-samples/js_validate_2.html
-samples/js_validate_3.html
+samples/validate_js_0_tests.html
+samples/validate_js_1_onsubmit.html
+samples/validate_js_2_onchange.html
+samples/validate_js_yaml_1.html
+samples/validate_js_yaml_2.html
+samples/validate_js_yaml_3.html
 samples/yaml_js_1.html
 samples/yaml_js_2.html
 samples/yaml_js_3.html
index 1d67b9189c433e25a87327d87ec5df14e58b1cfd..91afcc02bee7a818a8b76a9dfed4795ad8973354 100644 (file)
--- a/META.yml
+++ b/META.yml
@@ -1,14 +1,11 @@
---- #YAML:1.0
-name:                CGI-Ex
-version:             2.21
-abstract:            CGI utility suite - makes powerful application writing fun and easy
-license:             ~
-generated_by:        ExtUtils::MakeMaker version 6.36
-distribution_type:   module
-requires:     
+# http://module-build.sourceforge.net/META-spec.html
+#XXXXXXX This is a prototype!!!  It will change in the future!!! XXXXX#
+name:         CGI-Ex
+version:      2.22
+version_from: lib/CGI/Ex.pm
+installdirs:  site
+requires:
     Template::Alloy:               1.004
-meta-spec:
-    url:     http://module-build.sourceforge.net/META-spec-v1.2.html
-    version: 1.2
-author:
-    - Paul Seamons
+
+distribution_type: module
+generated_by: ExtUtils::MakeMaker version 6.30_01
index bf4bc0217b1805671b366ffbbadbfefb3b583f6a..f8b666cdf06b42db89cb0a355b25ab88353cf6b5 100644 (file)
@@ -24,7 +24,7 @@ use vars qw($VERSION
 use base qw(Exporter);
 
 BEGIN {
-    $VERSION               = '2.21';
+    $VERSION               = '2.22';
     $PREFERRED_CGI_MODULE  ||= 'CGI';
     @EXPORT = ();
     @EXPORT_OK = qw(get_form
index 4c93364aceb76c22d38742b43acf1dcf8504db5b..d03f90a79c62cdb7d7a14589fad93668bb7e488d 100644 (file)
@@ -13,7 +13,7 @@ BEGIN {
     eval { use Scalar::Util };
 }
 
-our $VERSION = '2.21';
+our $VERSION = '2.22';
 
 sub new {
     my $class = shift || croak "Usage: ".__PACKAGE__."->new";
@@ -247,9 +247,9 @@ sub prepared_print {
     my $self = shift;
     my $step = shift;
 
+    my $hash_form = $self->run_hook('hash_form',   $step) || {};
     my $hash_base = $self->run_hook('hash_base',   $step) || {};
     my $hash_comm = $self->run_hook('hash_common', $step) || {};
-    my $hash_form = $self->run_hook('hash_form',   $step) || {};
     my $hash_fill = $self->run_hook('hash_fill',   $step) || {};
     my $hash_swap = $self->run_hook('hash_swap',   $step) || {};
     my $hash_errs = $self->run_hook('hash_errors', $step) || {};
@@ -650,7 +650,6 @@ sub step_by_path_index {
     my $i    = shift || 0;
     my $ref  = $self->path;
     return '' if $i < 0;
-#    return $self->default_step if $i > $#$ref;
     return $ref->[$i];
 }
 
index 2dd895debd86b62410bf600898e4792b1fa41ebd..33e2a30e03454a1ad97df80b45f0237fd054481e 100644 (file)
@@ -18,7 +18,7 @@ use MIME::Base64 qw(encode_base64 decode_base64);
 use Digest::MD5 qw(md5_hex);
 use CGI::Ex;
 
-$VERSION = '2.21';
+$VERSION = '2.22';
 
 ###----------------------------------------------------------------###
 
@@ -74,6 +74,7 @@ sub get_valid_auth {
         next if ! defined $hash->{$key};
         last if ! $is_form && $had_form_data;  # if form info was passed in - we must use it only
         $had_form_data = 1 if $is_form;
+        next if ! length $hash->{$key};
 
         ### if it looks like a bare username (as in they didn't have javascript) - add in other items
         my $data;
@@ -403,7 +404,7 @@ sub login_hash_common {
 sub verify_token {
     my $self  = shift;
     my $args  = shift;
-    my $token = delete $args->{'token'} || die "Missing token";
+    my $token = delete $args->{'token'}; die "Missing token" if ! length $token;
     my $data  = $self->new_auth_data({token => $token, %$args});
     my $meth;
 
index f635f201612f8e7babf9f5edf2b11705441e6c49..365ece2d057b1fbd4640364a1502aea478e5e6d0 100644 (file)
@@ -29,7 +29,7 @@ use vars qw($VERSION
             );
 @EXPORT_OK = qw(conf_read conf_write in_cache);
 
-$VERSION = '2.21';
+$VERSION = '2.22';
 
 $DEFAULT_EXT = 'conf';
 
@@ -262,6 +262,7 @@ sub read_handler_json {
   open (IN, $file) || die "Couldn't open $file: $!";
   CORE::read(IN, my $text, -s $file);
   close IN;
+  require JSON;
   return scalar JSON::jsonToObj($text);
 }
 
index 4f3aa7295796187311919300f964832f30107846..afeab9b7af58d976c6304b3c63b03bd75dacb8bc 100644 (file)
@@ -23,7 +23,7 @@ use CGI::Ex;
 use CGI::Ex::Dump qw(debug ctrace dex_html);
 
 BEGIN {
-  $VERSION = '2.21';
+  $VERSION = '2.22';
   $SHOW_TRACE = 0      if ! defined $SHOW_TRACE;
   $IGNORE_EVAL = 0     if ! defined $IGNORE_EVAL;
   $EXTENDED_ERRORS = 1 if ! defined $EXTENDED_ERRORS;
index 796b83eb68cb8ddc16b35021bf669884ff1f46c2..60652725f79678723e160a2a73c1c1dc25ca93b2 100644 (file)
@@ -17,7 +17,7 @@ use vars qw(@ISA @EXPORT @EXPORT_OK $VERSION
 use strict;
 use Exporter;
 
-$VERSION   = '2.21';
+$VERSION   = '2.22';
 @ISA       = qw(Exporter);
 @EXPORT    = qw(dex dex_warn dex_text dex_html ctrace dex_trace);
 @EXPORT_OK = qw(dex dex_warn dex_text dex_html ctrace dex_trace debug);
index cbf38de1947807e378e3ad25e39a127f86c28e48..1bc90f31d5b55a0a08c7b514c71e24162c39d0d2 100644 (file)
@@ -24,7 +24,7 @@ use vars qw($VERSION
 use base qw(Exporter);
 
 BEGIN {
-    $VERSION   = '2.21';
+    $VERSION   = '2.22';
     @EXPORT    = qw(form_fill);
     @EXPORT_OK = qw(fill form_fill html_escape get_tagval_by_key swap_tagval_by_key);
 };
index 0c2852c63f81a61ac6b1d2cd3adaa9883c41366c..6fdd2d015be65e1eaf4ede87edba8da57420c8df 100644 (file)
@@ -17,7 +17,7 @@ use strict;
 use base qw(Exporter);
 
 BEGIN {
-    $VERSION  = '2.21';
+    $VERSION  = '2.22';
 
     @EXPORT = qw(JSONDump);
     @EXPORT_OK = @EXPORT;
index 57ffa0e14cddba3341179358cf53b7b7e69f2f0e..c3acbbc7f9485bd7bb23d14c0dfab8e4b3a7833e 100644 (file)
@@ -25,7 +25,7 @@ use vars qw($VERSION
             $VOBJS
             );
 
-$VERSION = '2.21';
+$VERSION = '2.22';
 
 ### install true symbol table aliases that can be localized
 *QR_PRIVATE        = *Template::Alloy::QR_PRIVATE;
index 19488728a5993b38b6b8e04718006571e5790336..37201955b59166afc36f67fd0f73b5556eed579c 100644 (file)
@@ -2,7 +2,7 @@ package CGI::Ex::Validate;
 
 =head1 NAME
 
-CGI::Ex::Validate - another form validator - but it does javascript in parallel
+CGI::Ex::Validate - The "Just Right" form validator with javascript in parallel
 
 =cut
 
@@ -22,7 +22,7 @@ use vars qw($VERSION
             @UNSUPPORTED_BROWSERS
             );
 
-$VERSION = '2.21';
+$VERSION = '2.22';
 
 $DEFAULT_EXT   = 'val';
 $QR_EXTRA      = qr/^(\w+_error|as_(array|string|hash)_\w+|no_\w+)/;
@@ -115,7 +115,7 @@ sub validate {
     foreach my $field (@field_keys) {
         die "Found nonhashref value for field $field" if ref($val_hash->{$field}) ne 'HASH';
         if (defined $val_hash->{$field}->{'field'}) {
-            push @$fields, $val_hash->{$field}->{'field'};
+            push @$fields, $val_hash->{$field};
         } else {
             push @$fields, { %{$val_hash->{$field}}, field => $field };
         }
@@ -1102,14 +1102,14 @@ __END__
         email    => {
             required => 1,
             max_len  => 100,
+            type     => 'email',
         },
         email2   => {
-            validate_if => 'email',
-            equals      => 'email',
+            equals   => 'email',
         },
     };
 
-    ### ordered
+    ### ordered (only onevent submit needs order)
     my $val_hash = {
         'group order' => [qw(username email email2)],
         username => {required => 1, max_len => 30},
@@ -1128,48 +1128,56 @@ __END__
             required => 1,
             max_len  => 100,
         }, {
-            field       => 'email2',
-            validate_if => 'email',
-            equals      => 'email',
+            field    => 'email2',
+            equals   => 'email',
         }],
     };
 
 
     my $vob    = CGI::Ex::Validate->new;
     my $errobj = $vob->validate($form, $val_hash);
-    # OR #
-    my $errobj = $vob->validate($form, "/somefile/somewhere.val"); # import config using yaml file
-    # OR #
-    my $errobj = $vob->validate($form, "/somefile/somewhere.pl");  # import config using perl file
-    # OR #
-    my $errobj = $vob->validate($form, "--- # a yaml document\n"); # import config using yaml str
 
+    # OR #
+    # import config using any type CGI::Ex::Conf supports
+    my $errobj = $vob->validate($form, "/somefile/somewhere.val");
 
     if ($errobj) {
         my $error_heading = $errobj->as_string; # OR "$errobj";
         my $error_list    = $errobj->as_array;  # ordered list of what when wrong
         my $error_hash    = $errobj->as_hash;   # hash of arrayrefs of errors
     } else {
-        # form passed validation
+        # the form passed validation
     }
 
     ### will add an error for any form key not found in $val_hash
     my $vob = CGI::Ex::Validate->new({no_extra_keys => 1});
     my $errobj = $vob->validate($form, $val_hash);
 
+
+    my $js_uri_path = '/js/';     # static or dynamic URI path to find CGI/Ex/validate.js
+    my $form_name   = "the_form"; # name of the form to attach javascript to
+    my $javascript  = $vob->generate_js($val_hash, $form_name, $js_uri_path);
+
+
 =head1 DESCRIPTION
 
 CGI::Ex::Validate is one of many validation modules.  It aims to have
 all of the basic data validation functions, avoid adding all of the
 millions of possible types, while still giving the capability for the
-developer to add their own types.
+developer to add their own types for the rare cases that the basic
+ones don't suffice.  Generally anything more than basic validation
+probably needs programmatic or data based validation.
 
-It also has full support for providing the same validation in javascript.
-It provides methods for attaching the javascript to existing forms.
+CGI::Ex::Validate also has full support for providing the same
+validation in javascript.  It provides methods for attaching the
+javascript to existing forms.  This ability is tightly integrated into
+CGI::Ex::App, but it should be easy to add validation just about
+anywhere using any type of controller.
 
 As opposed to other kitchen sync validation modules, CGI::Ex::Validate
 offers the simple types of validation, and makes it easy to add your
-own custom types.
+own custom types.  Asside from custom and custom_js, all validation
+markup is declarative.
 
 =head1 METHODS
 
@@ -1275,7 +1283,7 @@ The $JS_URI_PATH of "/cgi-bin/js" could contain the following:
     use strict;
     use CGI::Ex;
 
-    ### path_info should contain something like /CGI/Ex/yaml_load.js
+    ### path_info should contain something like /CGI/Ex/validate.js
     my $info = $ENV{PATH_INFO} || '';
     die "Invalid path" if $info !~ m|^(/\w+)+.js$|;
     $info =~ s|^/+||;
@@ -1284,29 +1292,31 @@ The $JS_URI_PATH of "/cgi-bin/js" could contain the following:
     exit;
 
 The print_js method in CGI::Ex is designed to cache the javascript in
-the browser (caching is suggested as they are medium sized files).
+the browser.
 
 =item C<-E<gt>cgix>
 
-Returns a CGI::Ex object.  Used internally.
+Returns a CGI::Ex object.  Used internally if a CGI object is
+passed to validate rather than a straight form hash.
 
 =back
 
 =head1 VALIDATION HASH
 
-The validation hash may be passed as a hashref or as a
-filename, or as a YAML document string.  If it is a filename, it will
-be translated into a hash using the %EXT_HANDLER for the extension on
-the file.  If there is no extension, it will use $DEFAULT_EXT as a
-default.  CGI::Ex::Conf is used for the reading of files.
+The validation hash may be passed as a hashref or as a filename, or as
+a YAML document string.  Experience has shown it to be better
+programming to pass in a hashref.  If the validation "hash" is a
+filename or a YAML string, it will be translated into a hash using
+CGI::Ex::Conf.
 
-Keys matching the regex m/^(general|group)\s+(\w+)$/ are reserved and
-are counted as GROUP OPTIONS.  Other keys (if any, should be field names
-that need validation).
+Keys matching the regex m/^(general|group)\s+(\w+)$/ such as "group
+onevent" are reserved and are counted as GROUP OPTIONS.  Other keys
+(if any, should be field names that need validation).
 
-If the GROUP OPTION 'group validate_if' is set, the validation will only
-be validated if the conditions are met.  If 'group validate_if' is not
-specified, then the validation will proceed.
+If the GROUP OPTION 'group validate_if' is set, the validation will
+only be validated if the conditions of the validate_if are met.  If
+'group validate_if' is not specified, then the validation will
+proceed.  See the validate_if VALIDATION type for more information.
 
 Each of the items listed in the validation will be validated.  The
 validation order is determined in one of three ways:
@@ -1359,6 +1369,16 @@ tested.
 
     'group order' => [qw(zip OR postalcode state OR region)],
 
+At this time, only "group onevent" submit works with this option.  Using
+OR is deprecated.  Instead you should use min_in_set or max_in_set.
+
+    'zip' => {
+         max_in_set: '1 of zip, postalcode',
+    },
+    'state' => {
+         max_in_set: '1 of state, region',
+    },
+
 Each individual field validation hashref will operate on the field contained
 in the 'field' key.  This key may also be a regular expression in the
 form of 'm/somepattern/'.  If a regular expression is used, all keys
@@ -1380,103 +1400,109 @@ types listed in VALIDATION TYPES.
 
 This section lists the available validation types.  Multiple instances
 of the same type may be used for some validation types by adding a
-number to the type (ie match, match2, match232, match_94).  Multiple
-instances are validated in sorted order.  Types that allow multiple
-values are:
-
-    compare
-    custom
-    equals
-    match
-    max_in_set
-    min_in_set
-    replace
-    required_if
-    sql
-    type
-    validate_if
+number to the type (ie match, match2, match232).  Multiple instances
+are validated in sorted order.  Types that allow multiple values are:
+compare, custom, custom_js, equals, enum, match, required_if, sql,
+type, validate_if, and replace (replace is a MODIFICATION TYPE).
 
 =over 4
 
-=item C<validate_if>
-
-If validate_if is specified, the field will only be validated
-if the conditions are met.  Works in JS.
-
-    validate_if => {field => 'name', required => 1, max_len => 30}
-    # Will only validate if the field "name" is present and is less than 30 chars.
-
-    validate_if => 'name',
-    # SAME as
-    validate_if => {field => 'name', required => 1},
+=item C<compare>
 
-    validate_if => '! name',
-    # SAME as
-    validate_if => {field => 'name', max_in_set => '0 of name'},
+Allows for custom comparisons.  Available types are
+>, <, >=, <=, !=, ==, gt, lt, ge, le, ne, and eq.  Comparisons
+also work in the JS.
 
-    validate_if => {field => 'country', compare => "eq US"},
-    # only if country's value is equal to US
+    {
+      field    => 'my_number',
+      match    => 'm/^\d+$/',
+      compare1 => '> 100',
+      compare2 => '< 255',
+      compare3 => '!= 150',
+    }
 
-    validate_if => {field => 'country', compare => "ne US"},
-    # if country doesn't equal US
+=item C<custom>
 
-    validate_if => {field => 'password', match => 'm/^md5\([a-z0-9]{20}\)$/'},
-    # if password looks like md5(12345678901234567890)
+Custom value - not available in JS.  Allows for extra programming types.
+May be either a boolean value predetermined before calling validate, or may be
+a coderef that will be called during validation.  If coderef is called, it will
+be passed the field name, the form value for that name, and a reference to the
+field validation hash.  If the custom type returns false the element fails
+validation and an error is added.
 
     {
-      field       => 'm/^(\w+)_pass/',
-      validate_if => '$1_user',
-      required    => 1,
+      field => 'username',
+      custom => sub {
+        my ($key, $val, $type, $field_val_hash) = @_;
+        # do something here
+        return 0;
+      },
     }
-    # will validate foo_pass only if foo_user was present.
 
-The validate_if may also contain an arrayref of validation items.  So that
-multiple checks can be run.  They will be run in order.  validate_if will
-return true only if all options returned true.
-
-    validate_if => ['email', 'phone', 'fax']
+=item C<custom_js>
 
-Optionally, if validate_if is an arrayref, it may contain the word
-'OR' as a special keyword.  If the item preceding 'OR' fails validation
-the item after 'OR' will be tested instead.  If the item preceding 'OR'
-passes validation the item after 'OR' will not be tested.
+Custom value - only available in JS.  Allows for extra programming types.
+May be a javascript function (if fully declared in javascript), a string containing
+a javascript function (that will be eval'ed into a real function),
+a boolean value pre-determined before calling validate, or may be
+section of javascript that will be eval'ed (the last value of
+the eval'ed javascript will determine if validation passed).  A false response indicates
+the value did not pass validation.  A true response indicates that it did.  See
+the samples/validate_js_0_tests.html page for a sample of usages.
 
-    validate_if => [qw(zip OR postalcode)],
+    {
+      field => 'date',
+      required => 1,
+      match    => 'm|^\d\d\d\d/\d\d/\d\d$|',
+      match_error => 'Please enter date in YYYY/MM/DD format',
+      custom_js => "function (args) {
+        var t=new Date();
+        var y=t.getYear()+1900;
+        var m=t.getMonth() + 1;
+        var d=t.getDate();
+        if (m<10) m = '0'+m;
+        if (d<10) d = '0'+d;
+        (args.value > ''+y+'/'+m+'/'+d) ? 1 : 0;
+      }",
+      custom_js_error => 'The date was not greater than today.',
+    }
 
-=item C<required_if>
+=item C<enum>
 
-Requires the form field if the condition is satisfied.  The conditions
-available are the same as for validate_if.  This is somewhat the same
-as saying:
+Allows for checking whether an item matches a set of options.  In perl
+the value may be passed as an arrayref.  In the conf or in perl the
+value may be passed of the options joined with ||.
 
-    validate_if => 'some_condition',
-    required    => 1
+    {
+      field => 'password_type',
+      enum  => 'plaintext||crypt||md5', # OR enum => [qw(plaintext crypt md5)],
+    }
 
-    required_if => 'some_condition',
+=item C<equals>
 
-If a regex is used for the field name, the required_if
-field will have any match patterns swapped in.
+Allows for comparison of two form elements.  Can have an optional !.
 
     {
-      field       => 'm/^(\w+)_pass/',
-      required_if => '$1_user',
+      field  => 'password',
+      equals => 'password_verify',
+    },
+    {
+      field  => 'domain1',
+      equals => '!domain2', # make sure the fields are not the same
     }
 
-This example would require the "foobar_pass" field to be set
-if the "foobar_user" field was passed.
-
-=item C<required>
-
-Requires the form field to have some value.  If the field is not present,
-no other checks will be run.
+=item C<match>
 
-=item C<min_values> and C<max_values>
+Allows for regular expression comparison.  Multiple matches may
+be concatenated with ||.  Available in JS.
 
-Allows for specifying the maximum number of form elements passed.
-max_values defaults to 1 (You must explicitly set it higher
-to allow more than one item by any given name).
+    {
+      field   => 'my_ip',
+      match   => 'm/^\d{1,3}(\.\d{1,3})3$/',
+      match_2 => '!/^0\./ || !/^192\./',
+    }
 
-=item C<min_in_set> and C<max_in_set>
+=item C<max_in_set> and C<min_in_set>
 
 Somewhat like min_values and max_values except that you specify the
 fields that participate in the count.  Also - entries that are not
@@ -1493,31 +1519,7 @@ be placed after the number for human readability.
     validate_if => {field => 'whatever', max_in_set => '0 of whatever'},
       # only run validation if there were zero occurrences of whatever
 
-=item C<enum>
-
-Allows for checking whether an item matches a set of options.  In perl
-the value may be passed as an arrayref.  In the conf or in perl the
-value may be passed of the options joined with ||.
-
-    {
-      field => 'password_type',
-      enum  => 'plaintext||crypt||md5', # OR enum => [qw(plaintext crypt md5)],
-    }
-
-=item C<equals>
-
-Allows for comparison of two form elements.  Can have an optional !.
-
-    {
-      field  => 'password',
-      equals => 'password_verify',
-    },
-    {
-      field  => 'domain1',
-      equals => '!domain2', # make sure the fields are not the same
-    }
-
-=item C<min_len and max_len>
+=item C<max_len and min_len>
 
 Allows for check on the length of fields
 
@@ -1527,31 +1529,39 @@ Allows for check on the length of fields
       max_len => 100,
     }
 
-=item C<match>
+=item C<max_values> and C<min_values>
 
-Allows for regular expression comparison.  Multiple matches may
-be concatenated with ||.  Available in JS.
+Allows for specifying the maximum number of form elements passed.
+max_values defaults to 1 (You must explicitly set it higher
+to allow more than one item by any given name).
 
-    {
-      field   => 'my_ip',
-      match   => 'm/^\d{1,3}(\.\d{1,3})3$/',
-      match_2 => '!/^0\./ || !/^192\./',
-    }
+=item C<required>
 
-=item C<compare>
+Requires the form field to have some value.  If the field is not present,
+no other checks will be run.
 
-Allows for custom comparisons.  Available types are
->, <, >=, <=, !=, ==, gt, lt, ge, le, ne, and eq.  Comparisons
-also work in the JS.
+=item C<required_if>
+
+Requires the form field if the condition is satisfied.  The conditions
+available are the same as for validate_if.  This is somewhat the same
+as saying:
+
+    validate_if => 'some_condition',
+    required    => 1
+
+    required_if => 'some_condition',
+
+If a regex is used for the field name, the required_if
+field will have any match patterns swapped in.
 
     {
-      field    => 'my_number',
-      match    => 'm/^\d+$/',
-      compare1 => '> 100',
-      compare2 => '< 255',
-      compare3 => '!= 150',
+      field       => 'm/^(\w+)_pass/',
+      required_if => '$1_user',
     }
 
+This example would require the "foobar_pass" field to be set
+if the "foobar_user" field was passed.
+
 =item C<sql>
 
 SQL query based - not available in JS.  The database handle will be looked
@@ -1566,60 +1576,62 @@ $self->{dbh} is a coderef - they will be called and should return a dbh.
       # sql_db_type  => 'foo', # will look for a dbh under $self->{dbhs}->{foo}
     }
 
-=item C<custom>
+=item C<type>
 
-Custom value - not available in JS.  Allows for extra programming types.
-May be either a boolean value predetermined before calling validate, or may be
-a coderef that will be called during validation.  If coderef is called, it will
-be passed the field name, the form value for that name, and a reference to the
-field validation hash.  If the custom type returns false the element fails
-validation and an error is added.
+Allows for more strict type checking.  Currently supported types
+include CC (credit card), EMAIL, DOMAIN, IP, URL.  Other types will be
+added upon request provided we can add a perl and a javascript
+version.
 
     {
-      field => 'username',
-      custom => sub {
-        my ($key, $val, $type, $field_val_hash) = @_;
-        # do something here
-        return 0;
-      },
+      field => 'credit_card',
+      type  => 'CC',
     }
 
-=item C<custom_js>
+=item C<validate_if>
 
-Custom value - only available in JS.  Allows for extra programming types.
-May be either a boolean value pre-determined before calling validate, or may be
-section of javascript that will be eval'ed.  The last value (return value) of
-the eval'ed javascript will determine if validation passed.  A false value indicates
-the value did not pass validation.  A true value indicates that it did.  See
-the t/samples/js_validate_3.html page for a sample of usage.
+If validate_if is specified, the field will only be validated
+if the conditions are met.  Works in JS.
 
-    {
-      field => 'date',
-      required => 1,
-      match    => 'm|^\d\d\d\d/\d\d/\d\d$|',
-      match_error => 'Please enter date in YYYY/MM/DD format',
-      custom_js => "
-        var t=new Date();
-        var y=t.getYear()+1900;
-        var m=t.getMonth() + 1;
-        var d=t.getDate();
-        if (m<10) m = '0'+m;
-        if (d<10) d = '0'+d;
-        (value > ''+y+'/'+m+'/'+d) ? 1 : 0;
-      ",
-      custom_js_error => 'The date was not greater than today.',
-    }
+    validate_if => {field => 'name', required => 1, max_len => 30}
+    # Will only validate if the field "name" is present and is less than 30 chars.
 
-=item C<type>
+    validate_if => 'name',
+    # SAME as
+    validate_if => {field => 'name', required => 1},
 
-Allows for more strict type checking.  Currently supported types
-include CC (credit card).  Other types will be added upon request provided
-we can add a perl and a javascript version.
+    validate_if => '! name',
+    # SAME as
+    validate_if => {field => 'name', max_in_set => '0 of name'},
+
+    validate_if => {field => 'country', compare => "eq US"},
+    # only if country's value is equal to US
+
+    validate_if => {field => 'country', compare => "ne US"},
+    # if country doesn't equal US
+
+    validate_if => {field => 'password', match => 'm/^md5\([a-z0-9]{20}\)$/'},
+    # if password looks like md5(12345678901234567890)
 
     {
-      field => 'credit_card',
-      type  => 'CC',
+      field       => 'm/^(\w+)_pass/',
+      validate_if => '$1_user',
+      required    => 1,
     }
+    # will validate foo_pass only if foo_user was present.
+
+The validate_if may also contain an arrayref of validation items.  So that
+multiple checks can be run.  They will be run in order.  validate_if will
+return true only if all options returned true.
+
+    validate_if => ['email', 'phone', 'fax']
+
+Optionally, if validate_if is an arrayref, it may contain the word
+'OR' as a special keyword.  If the item preceding 'OR' fails validation
+the item after 'OR' will be tested instead.  If the item preceding 'OR'
+passes validation the item after 'OR' will not be tested.
+
+    validate_if => [qw(zip OR postalcode)],
 
 =back
 
@@ -1847,11 +1859,11 @@ used to have the concept of validation groups - these were not
 commonly used so support has been deprecated as of the 2.10 release).
 Group options will also be looked for in the Validate object ($self)
 and can be set when instantiating the object ($self->{raise_error} is
-equivalent to $valhash->{'group raise_error'}).  The current know
-options are:
+equivalent to $valhash->{'group raise_error'}).
 
 Options may also be set globally before calling validate by
-populating the %DEFAULT_OPTIONS global hash.
+populating the %DEFAULT_OPTIONS global hash.  However, only the options
+set properly in the $valhash will be passed to the javascript.
 
 =over 4
 
@@ -1963,32 +1975,79 @@ a string that will be pre-pended on to the error string.
 If as_hash_join has been set to a true value, as_hash_footer may be set to
 a string that will be postpended on to the error string.
 
+=item C<onevent>
+
+Defaults to {submit => 1}.  This controls when the javascript validation
+will take place.  May be passed any or all or submit, change, or blur.
+Multiple events may be passed in the hash.
+
+    'group onevent' => {submit => 1, change => 1}',
+
+A comma separated string of types may also be passed:
+
+    'group onevent' => 'submit,change,blur',
+
+Currently, change and blur will not work for dynamically matched
+field names such as 'm/\w+/'.  Support will be added.
+
+=item C<set_hook>
+
+Defaults document.validate_set_hook which defaults to nothing.  If
+"group set_hook" or document.validate_set_hook are set to a function,
+they will be passed the key name of a form element that had a
+validation error and the error that will be set.  If a true value is
+returned, then validate will not also the inline error.  If no value
+or false is returned (default) the validate will continue setting the
+inline error.  This gives full control over setting inline
+errors. samples/validate_js_2_onchange.html has a good example of
+using these hooks.
+
+    'group set_hook' => "function (key, val, val_hash, form) {
+      alert("Setting error to field "+key);
+    }",
+
+The document.validate_set_hook option is probably the better option to use,
+as it helps to separate display functionality out into your html templates
+rather than storing too much html logic in your CGI.
+
+=item C<clear_hook>
+
+Similar to set_hook, but called when inline error is cleared.  Its
+corresponding default is document.validate_clear_hook.  The clear hook
+is also sampled in samples/validate_js_2_onchange.html
+
+    'group clear_hook' => "function (key, val_hash, form) {
+      alert("Clear error on field "+key);
+    }",
+
 =item C<no_inline>
 
-If set to true, the javascript validation will not attempt to generate inline
-errors.  Default is true.  Inline errors are independent of confirm and alert
-errors.
+If set to true, the javascript validation will not attempt to generate
+inline errors when the only "group onevent" type is "submit".  Default
+is true.  Inline errors are independent of confirm and alert errors.
 
-    'general no_inline' => 1,
+    'group no_inline' => 1,
 
 =item C<no_confirm>
 
-If set to true, the javascript validation will try to use an alert instead
-of a confirm to inform the user of errors.  Alert and confirm are independent
+If set to true, the javascript validation will try to use an alert
+instead of a confirm to inform the user of errors when one of the
+"group onevent" types is "submit".  Alert and confirm are independent
 or inline errors.  Default is false.
 
-    'general no_confirm' => 1,
+    'group no_confirm' => 1,
 
 =item C<no_alert>
 
 If set to true, the javascript validation will not show an alert box
 when errors occur.  Default is false.  This option only comes into
-play if no_confirm is also set.  This option is independent of inline
-errors.  Although it is possible to turn off all errors by setting
-no_inline, no_confirm, and no_alert all to 1, it is suggested that at
-least one of the error reporting facilities is left on.
+play if no_confirm is also set.  This option is only in effect if
+"group onevent" includes "submit".  This option is independent of
+inline errors.  Although it is possible to turn off all errors by
+setting no_inline, no_confirm, and no_alert all to 1, it is suggested
+that at least one of the error reporting facilities is left on.
 
-    'general no_alert' => 1,
+    'group no_alert' => 1,
 
 =back
 
@@ -2009,9 +2068,31 @@ validation will be read in using CGI::Ex::Conf::read_handler_html.
 
 All inline html validation must be written in yaml.
 
-It is anticipated that the html will contain something like either of the
+It is anticipated that the html will contain something like one of the
 following examples:
 
+  <script src="/cgi-bin/js/CGI/Ex/validate.js"></script>
+  <script>
+  document.validation = {
+    'group no_confirm': 1,
+    'group no_alert':   1,
+    'group onevent':    'change,blur,submit',
+    'group order': ['username', 'password'],
+    username: {
+      required: 1,
+      max_len: 20
+    },
+    password: {
+      required: 1,
+      max_len: 30
+    }
+  };
+  if (document.check_form) document.check_form('my_form_name');
+  </script>
+
+Prior to the realization of JSON, YAML was part of the method
+for introducing validation into the script.
+
   <script src="/cgi-bin/js/CGI/Ex/yaml_load.js"></script>
   <script src="/cgi-bin/js/CGI/Ex/validate.js"></script>
   <script>
@@ -2030,7 +2111,8 @@ following examples:
   if (document.check_form) document.check_form('my_form_name');
   </script>
 
-Alternately we can use element attributes:
+Alternately, CGI/Ex/validate.js can parse the YAML from html
+form element attributes:
 
   <form name="my_form_name">
 
@@ -2056,10 +2138,11 @@ Alternately we can use element attributes:
   if (document.check_form) document.check_form('my_form_name');
   </script>
 
-The read_handler_html from CGI::Ex::Conf will find either of these
-types of validation.
+The read_handler_html from CGI::Ex::Conf will find the YAML types
+of validation.  The JSON type is what would be generated by default
+when the validation is specified in Perl.
 
-If inline errors are asked for, each error that occurs will attempt
+If inline errors are enabled (default), each error that occurs will attempt
 to find an html element with its name as the id.  For example, if
 the field "username" failed validation and created a "username_error",
 the javascript would set the html of <span id="username_error"></span>
@@ -2074,11 +2157,34 @@ from the server side as well.
 If the javascript fails for some reason, the form should still be able
 to submit as normal (fail gracefully).
 
-If the confirm option is used, the errors will be displayed to the user.
-If they choose OK they will be able to try and fix the errors.  If they
-choose cancel, the form will submit anyway and will rely on the server
-to do the validation.  This is for fail safety to make sure that if the
-javascript didn't validate correctly, the user can still submit the data.
+Additionally, there are two hooks that are called when ever an inline
+error is set or cleared.  The following hooks are used in
+samples/validate_js_2_onchange.html.
+
+    document.validate_set_hook = function (key, val, val_hash, form) {
+      document.getElementById(key+'_img').innerHTML
+        = '<span style="font-weight:bold;color:red">!</span>';
+      document.getElementById(key+'_row').style.background
+        = '#ffdddd';
+    };
+
+    document.validate_clear_hook = function (key, val_hash, form) {
+      document.getElementById(key+'_img').innerHTML
+        = '<span style="font-weight:bold;color:green">+</span>';
+      document.getElementById(key+'_row').style.background
+        = '#ddffdd';
+    };
+
+These hooks can also be set as "group clear_hook" and "group set_hook"
+which are defined further above.
+
+If the confirm option is used ("group onevent" includes submit and
+"group no_confirm" is false), the errors will be displayed to the
+user.  If they choose OK they will be able to try and fix the errors.
+If they choose cancel, the form will submit anyway and will rely on
+the server to do the validation.  This is for fail safety to make sure
+that if the javascript didn't validate correctly, the user can still
+submit the data.
 
 =head1 THANKS
 
@@ -2091,6 +2197,6 @@ This module may be distributed under the same terms as Perl itself.
 
 =head1 AUTHOR
 
-Paul Seamons <perl at seamons dot com>
+Paul Seamons <paul at seamons dot com>
 
 =cut
index 40b4c04d6d7f92d615df5c69fe65c9e5e7c9849e..72cca57da30c5634834bb5c5f64cf90731a366e3 100644 (file)
-/**----------------------------------------------------------------***
-*  Copyright 2007 - Paul Seamons                                     *
-*  Distributed under the Perl Artistic License without warranty      *
-*  Based upon CGI/Ex/Validate.pm v1.14 from Perl                     *
-*  For instructions on usage, see perldoc of CGI::Ex::Validate       *
-***----------------------------------------------------------------**/
-// $Revision: 1.42 $
-
-function Validate () {
- this.error             = vob_error;
- this.validate          = vob_validate;
- this.check_conditional = vob_check_conditional;
- this.filter_types      = vob_filter_types;
- this.add_error         = vob_add_error;
- this.validate_buddy    = vob_validate_buddy;
- this.check_type        = vob_check_type;
- this.get_form_value    = vob_get_form_value;
-}
+// Copyright 2007 - Paul Seamons - $Revision: 1.62 $
+// Distributed under the Perl Artistic License without warranty
+// See perldoc CGI::Ex::Validate for usage
+
+var v_did_inline  = {};
 
 function ValidateError (errors, extra) {
  this.errors = errors;
  this.extra  = extra;
-
  this.as_string = eob_as_string;
  this.as_array  = eob_as_array;
  this.as_hash   = eob_as_hash;
- this.get_error_text = eob_get_error_text;
- this.first_field    = eob_first_field;
+ this.first_field = eob_first_field;
 }
 
-///----------------------------------------------------------------///
+//
 
-function vob_error (err) {
- alert (err);
-}
-
-function vob_validate (form, val_hash) {
- if (typeof(val_hash) == 'string') {
-   if (! document.yaml_load)
-     return this.error("Cannot parse yaml string - document.yaml_load is not loaded");
-   val_hash = document.yaml_load(val_hash);
- }
+function v_error (err) { alert (err); return 1 }
 
- var ERRORS = new Array ();
- var EXTRA  = new Array ();
- //  var USED_GROUPS = new Array();
-
- // distinguishing between associative and index based arrays is harder than in perl
- if (! val_hash.length) val_hash = new Array(val_hash);
- for (var i = 0; i < val_hash.length; i ++) {
-   var group_val = val_hash[i];
-   if (typeof(group_val) != 'object' || group_val.length) return this.error("Validation groups must be a hash");
-   var title       = group_val['group title'];
-   var validate_if = group_val['group validate_if'];
-
-   if (validate_if && ! this.check_conditional(form, validate_if)) continue;
-   //    USED_GROUPS.push(group_val);
-
-   /// if the validation items were not passed as an arrayref
-   /// look for a group order and then fail back to the keys of the group
-   var fields = group_val['group fields'];
-   var order  = new Array();
-   for (var key in group_val) {
-     if (key == 'extend') continue; // Protoype Array() fix
-     order[order.length] = key;
-   }
-   order = order.sort();
-   if (fields) {
-     if (typeof(fields) != 'object' || ! fields.length)
-       return this.error("'group fields' must be a non-empty array");
-   } else {
-     fields = new Array();
-     var _order = (group_val['group order']) ? group_val['group order'] : order;
-     if (typeof(_order) != 'object' || ! _order.length)
-       return this.error("'group order' must be a non-empty array");
-     for (var j = 0; j < _order.length; j ++) {
-       var field = _order[j];
-       if (field.match('^(group|general)\\s')) continue;
-       var field_val = group_val[field];
-       if (! field_val) {
-         if (field == 'OR') field_val = 'OR';
-         else return this.error('No element found in group for '+field);
-       }
-       if (typeof(field_val) == 'object' && ! field_val['field']) field_val['field'] = field;
-       fields[fields.length] = field_val;
-     }
-   }
+function v_clean_val_hash (val_hash) {
+ if (typeof(val_hash) != 'object') return {error: v_error("Validation must be an associative array (hash)")};
 
-   /// check which fields have been used
-   var found = new Array();
-   for (var j = 0; j < fields.length; j ++) {
-     var field_val = fields[j];
-     var field = field_val['field'];
-     if (! field) return this.error("Missing field key in validation");
-     // if (found[field]) return this.error('Duplicate order found for '+field+' in group order or fields');
-     found[field] = 1;
-   }
+ var order  = [];
+ for (var key in val_hash) {
+  if (key == 'extend') continue; // Protoype Array()
+  if (key.match(/^general\s/)) {
+    var new_key = key.replace(/^general\s+/, 'group ');
+    val_hash[new_key] = val_hash[key];
+    delete(val_hash[key]);
+    key = new_key;
+  }
+  order.push(key);
+ }
+ order = order.sort();
 
-   /// add any remaining fields from the order
-   for (var j = 0; j < order.length; j ++) {
-     var field = order[j];
-     if (found[field] || field.match('^(group|general)\\s')) continue;
-     var field_val = group_val[field];
-     if (typeof(field_val) != 'object' || field_val.length) return this.error('Found a non-hash value on field '+field);
-     if (! field_val['field']) field_val['field'] = field;
-     fields[fields.length] = field_val;
-   }
+ var f = val_hash['group set_hook'];
+ if (f && typeof(f) == 'string') val_hash['group set_hook'] = eval(f);
+ f = val_hash['group clear_hook'];
+ if (f && typeof(f) == 'string') val_hash['group clear_hook'] = eval(f);
 
-   /// now lets do the validation
-   var is_found  = 1;
-   var errors = new Array();
-   var hold_error;
-
-   for (var j = 0; j < fields.length; j ++) {
-     var ref = fields[j];
-     if (typeof(ref) != 'object' && ref == 'OR') {
-       if (is_found) j ++;
-       is_found = 1;
-       continue;
-     }
-     is_found = 1;
-     if (! ref['field']) return this.error("Missing field key during normal validation");
-     var err = this.validate_buddy(form, ref['field'], ref);
-
-     /// test the error - if errors occur allow for OR - if OR fails use errors from first fail
-     if (err.length) {
-       if (j <= fields.length && typeof(fields[j + 1] != 'object') && fields[j + 1] == 'OR') {
-         hold_error = err;
-       } else {
-         if (hold_error) err = hold_error;
-         for (var k = 0; k < err.length; k ++) errors[errors.length] = err[k];
-         hold_error = '';
-       }
-     } else {
-       hold_error = '';
-     }
-   }
+ var fields = val_hash['group fields'];
+ if (fields) {
+  if (typeof(fields) != 'object' || ! fields.length)
+   return {error:v_error("'group fields' must be a non-empty array")};
+ } else {
+  fields = [];
+  var _order = (val_hash['group order']) ? val_hash['group order'] : order;
+  if (typeof(_order) != 'object' || ! _order.length)
+   return {error:v_error("'group order' must be a non-empty array")};
+  for (var i = 0; i < _order.length; i++) {
+   var field = _order[i];
+   if (field.match(/^group\s/)) continue;
+   var field_val = val_hash[field];
+   if (! field_val) {
+    if (field == 'OR') field_val = 'OR';
+    else return {error:v_error('No element found in group for '+field)};
+   }
+   if (typeof(field_val) == 'object' && ! field_val['field']) field_val['field'] = field;
+   fields.push(field_val);
+  }
+ }
+
+ var found = {};
+ for (var i = 0; i < fields.length; i++) {
+  var field_val = fields[i];
+  var field = field_val.field;
+  if (! field) return {error:v_error("Missing field key in validation")};
+  found[field] = 1;
+ }
+
+ for (var i = 0; i < order.length; i++) {
+  var field = order[i];
+  if (found[field] || field.match(/^group\s/)) continue;
+  var field_val = val_hash[field];
+  if (typeof(field_val) != 'object' || field_val.length) {debug(val_hash);alert(field);return {error:v_error('Found a non-hash value on field '+field)};}
+  if (! field_val.field) field_val.field = field;
+  fields.push(field_val);
+ }
+
+ for (var i = 0; i < fields.length; i++) v_clean_field_val(fields[i]);
+
+ return {'fields':fields, 'order':order};
+}
 
-   /// add on errors as requested
-   if (errors.length) {
-     if (title) ERRORS[ERRORS.length] = title;
-     for (var j = 0; j < errors.length; j ++) ERRORS[ERRORS.length] = errors[j];
-   }
+function v_clean_field_val (field_val) {
+ if (! field_val.order) field_val.order = v_field_order(field_val);
+ if (! field_val.deps) field_val.deps = {};
+ for (var i = 0; i < field_val.order.length; i++) {
+  var k = field_val.order[i];
+  var v = field_val[k];
+  if (typeof(v) == 'undefined') return {error:v_error('No matching validation found on field '+field+' for type '+k)};
+  if (k.match(/^(min|max)_in_set(\d*)$/)) {
+   if (typeof(v) == 'string') {
+    if (! (m = v.match(/^\s*(\d+)(?:\s*[oO][fF])?\s+(.+)\s*$/))) return {error:v_error("Invalid "+k+" check "+v)};
+    field_val[k] = m[2].split(/[\s,]+/);
+    field_val[k].unshift(m[1]);
+   }
+   for (var j = 1; j < field_val[k].length; j++) if (field_val[k][j] != field_val.field) field_val.deps[field_val[k][j]] = 1;
+  } else if (k.match(/^(enum|compare)\d*$/)) {
+   if (typeof(v) == 'string') field_val[k] = v.split(/\s*\|\|\s*/);
+  } else if (k.match(/^match\d*$/)) {
+   if (typeof(v) == 'string') v = field_val[k] = v.split(/\s*\|\|\s*/);
+   for (var j = 0; j < v.length; j++) {
+    if (typeof(v[j]) != 'string' || v[j] == '!') continue;
+    var m = v[j].match(/^\s*(!\s*|)m([^\s\w])(.*)\2([eigsmx]*)\s*$/);
+    if (! m) return {error:v_error("Not sure how to parse that match ("+v[j]+")")};
+    var not = m[1];
+    var pat = m[3];
+    var opt = m[4];
+    if (opt.indexOf('e') != -1) return {error:v_error("The e option cannot be used on field "+field_val.field+", test "+k)};
+    opt = opt.replace(/[sg]/g,'');
+    v[j] = new RegExp(pat, opt);
+    if (not) v.splice(j, 0, '!');
+   }
+  } else if (k.match(/^custom_js\d*$/)) {
+   if (typeof(v) == 'string' && v.match(/^\s*function\s*/)) field_val[k] = eval(v);
+  }
+ }
+}
 
-   /// add on general options, and group options if errors in group occurred
-   var m;
-   for (var j = 0; j < order.length; j ++) {
-     var field = order[j];
-     if (! (m = field.match('^(general|group)\\s+(\\w+)$'))) continue;
-     if (m[1] == 'group' && (errors.length == 0 || m[2].match('^(field|order|title)$'))) continue;
-       EXTRA[m[2]] = group_val[field];
+function v_validate (form, val_hash) {
+ var clean  = v_clean_val_hash(val_hash);
+ if (clean.error) return;
+ var order  = clean.order;
+ var fields = clean.fields;
+
+ var ERRORS = [];
+ var EXTRA  = [];
+ var title       = val_hash['group title'];
+ var validate_if = val_hash['group validate_if'];
+ if (validate_if && ! v_check_conditional(form, validate_if)) return;
+
+ var is_found  = 1;
+ var errors = [];
+ var hold_error;
+
+ for (var j = 0; j < fields.length; j++) {
+  var ref = fields[j];
+  if (typeof(ref) != 'object' && ref == 'OR') {
+   if (is_found) j++;
+   is_found = 1;
+   continue;
+  }
+  is_found = 1;
+  if (! ref.field) return v_error("Missing field key during normal validation");
+  var err = v_validate_buddy(form, ref.field, ref);
+
+  if (err.length) {
+   if (j <= fields.length && typeof(fields[j + 1] != 'object') && fields[j + 1] == 'OR') {
+    hold_error = err;
+   } else {
+    if (hold_error) err = hold_error;
+    for (var k = 0; k < err.length; k++) errors.push(err[k]);
+    hold_error = '';
    }
+  } else {
+   hold_error = '';
+  }
  }
 
- /// store any extra items from self
- for (var key in this) {
-   if (key == 'extend') continue; // Protoype Array() fix
-   if (! key.match('_error$')
-       && ! key.match('^(raise_error|as_hash_\\w+|as_array_\\w+|as_string_\\w+)$')) continue;
-   EXTRA[key] = this[key];
+ if (errors.length) {
+  if (title) ERRORS.push(title);
+  for (var j = 0; j < errors.length; j++) ERRORS.push(errors[j]);
  }
 
- /// allow for checking for unused keys
- // if (EXTRA['no_extra_fields'])
- // won't do anything about this for now - let the server handle it
+ var m;
+ for (var j = 0; j < order.length; j++) {
+  var field = order[j];
+  if (! (m = field.match(/^group\s+(\w+)$/))) continue;
+  if (errors.length == 0 || m[1].match(/^(field|order|title|validate_if)$/)) continue;
+   EXTRA[m[1]] = val_hash[field];
+ }
 
- /// return what they want
  if (ERRORS.length) return new ValidateError(ERRORS, EXTRA);
  return;
 }
 
-
-/// allow for optional validation on groups and on individual items
-function vob_check_conditional (form, ifs, N_level, ifs_match) {
-
+function v_check_conditional (form, ifs, N_level, ifs_match) {
  if (! N_level) N_level = 0;
- N_level ++;
-
- /// can pass a single hash - or an array ref of hashes
- if (! ifs) {
-   return this.error("Need reference passed to check_conditional");
- } else if (typeof(ifs) != 'object') {
-   ifs = new Array(ifs);
- } else if (! ifs.length) { // turn hash into array of hash
-   ifs = new Array(ifs);
- }
+ N_level++;
+
+ if (! ifs) return v_error("Need reference passed to check_conditional");
+ if (typeof(ifs) != 'object' || ! ifs.length) ifs = [ifs];
 
- /// run the if options here
- /// multiple items can be passed - all are required unless OR is used to separate
  var is_found = 1;
  var m;
- for (var i = 0; i < ifs.length; i ++) {
-   var ref = ifs[i];
-   if (typeof(ref) != 'object') {
-     if (ref == 'OR') {
-       if (is_found) i++;
-       is_found = 1;
-       continue;
-     } else {
-       var field = ref;
-       ref = new Array();
-       if (m = field.match('^(\\s*!\\s*)')) {
-         field = field.substring(m[1].length);
-         ref['max_in_set'] = '0 of ' + field;
-       } else {
-         ref['required'] = 1;
-       }
-       ref['field'] = field;
-     }
-   }
-   if (! is_found) break;
-
-   /// get the field - allow for custom variables based upon a match
-   var field = ref['field'];
-   if (! field) return this.error("Missing field key during validate_if");
-   field = field.replace(new RegExp('\\$(\\d+)','g'), function (all, N) {
-     if (typeof(ifs_match) != 'object'
-         || typeof(ifs_match[N]) == 'undefined') return ''
-     return ifs_match[N];
-   });
-
-   var err = this.validate_buddy(form, field, ref, N_level);
-   if (err.length) is_found = 0;
+ for (var i = 0; i < ifs.length; i++) {
+  var ref = ifs[i];
+  if (typeof(ref) != 'object') {
+   if (ref == 'OR') {
+    if (is_found) i++;
+    is_found = 1;
+    continue;
+   } else {
+    var field = ref;
+    ref = {};
+    if (m = field.match(/^(\s*!\s*)/)) {
+     field = field.substring(m[1].length);
+     ref.max_in_set = [0, field];
+    } else {
+     ref.required = 1;
+    }
+    ref.field = field;
+   }
+  }
+  if (! is_found) break;
+
+  var field = ref.field;
+  if (! field) return v_error("Missing field key during validate_if");
+  field = field.replace(/\$(\d+)/g, function (all, N) {
+   return (typeof(ifs_match) != 'object' || typeof(ifs_match[N]) == 'undefined') ? '' : ifs_match[N];
+  });
+
+  v_clean_field_val(ref);
+  var err = v_validate_buddy(form, field, ref, N_level);
+  if (err.length) is_found = 0;
  }
  return is_found;
 }
 
-function vob_filter_types (type, types) {
- var values = new Array();
+function v_filter_types (type, types) {
+ var values = [];
  var regexp = new RegExp('^'+type+'_?\\d*$');
  for (var i = 0; i < types.length; i++)
-   if (types[i].match(regexp)) values[values.length] = types[i];
+  if (types[i].match(regexp)) values.push(types[i]);
  return values;
 }
 
-function vob_add_error (errors,field,type,field_val,ifs_match,form) {
- errors[errors.length] = new Array(field, type, field_val, ifs_match);
- if (field_val['clear_on_error']) {
-   var el = form[field];
-   if (el) {
-     var type = el.type;
-     if (type && (type == 'hidden' || type == 'password' || type == 'text' || type == 'textarea' || type == 'submit'))
-       el.value = '';
-   }
+function v_add_error (errors,field,type,field_val,ifs_match,form) {
+ errors.push([field, type, field_val, ifs_match]);
+ if (field_val.clear_on_error) {
+  var el = form[field];
+  if (el && el.type && el.type.match(/(hidden|password|text|textarea|submit)/)) el.value = '';
  }
+ return errors;
 }
 
-/// this is where the main checking goes on
-function vob_validate_buddy (form, field, field_val, N_level, ifs_match) {
- if (! N_level) N_level = 0;
- if (++ N_level > 10) return this.error("Max dependency level reached " + N_level);
- if (! form.elements) return;
-
- var errors = new Array();
- var types  = new Array();
- for (var key in field_val) {
-   if (key == 'extend') continue; // Protoype Array() fix
-   types[types.length] = key;
- }
- types = types.sort();
+function v_field_order (field_val) {
+ var o = [];
+ for (var k in field_val) if (! k.match(/^(extend|field|name)$/) && ! k.match(/_error$/)) o.push(k);
+ return o.sort();
+}
 
- /// allow for not running some tests in the cgi
- if (this.filter_types('exclude_js', types).length) return errors;
+function v_validate_buddy (form, field, field_val, N_level, ifs_match) {
+ var errors = [];
+ if (! N_level) N_level = 0;
+ if (++N_level > 10) { v_error("Max dependency level reached " + N_level); return errors }
+ if (! form.elements || field_val.exclude_js) return errors;
+ var types = field_val.order || v_field_order(field_val);
 
- /// allow for field names that contain regular expressions
  var m;
- if (m = field.match('^(!\\s*|)m([^\\s\\w])(.*)\\2([eigsmx]*)$')) {
-   var not = m[1];
-   var pat = m[3];
-   var opt = m[4];
-   if (opt.indexOf('e') != -1) return this.error("The e option cannot be used on field "+field);
-   opt = opt.replace(new RegExp('[sg]','g'),'');
-   var reg = new RegExp(pat, opt);
-
-   var keys = new Array();
-   for (var i = 0; i < form.elements.length; i ++) {
-     var _field = form.elements[i].name;
-     if (! _field) continue;
-     if ( (not && ! (m = _field.match(reg))) || (m = _field.match(reg))) {
-       var err = this.validate_buddy(form, _field, field_val, N_level, m);
-       for (var j = 0; j < err.length; j ++) errors[errors.length] = err[j];
-     }
-   }
-   return errors;
- }
+ if (m = field.match(/^(!\s*|)m([^\s\w])(.*)\2([eigsmx]*)$/)) {
+  var not = m[1];
+  var pat = m[3];
+  var opt = m[4];
+  if (opt.indexOf('e') != -1) { v_error("The e option cannot be used on field "+field); return errors }
+  opt = opt.replace(/[sg]/g,'');
+  var reg = new RegExp(pat, opt);
+
+  for (var i = 0; i < form.elements.length; i++) {
+   var _field = form.elements[i].name;
+   if (! _field) continue;
+   if ( (not && ! (m = _field.match(reg))) || (m = _field.match(reg))) {
+    var err = v_validate_buddy(form, _field, field_val, N_level, m);
+    for (var j = 0; j < err.length; j++) errors.push(err[j]);
+   }
+  }
+  return errors;
+ }
+
+ var _value   = v_get_form_value(form[field]);
+ var modified = 0;
 
- var _value = this.get_form_value(form[field]);
- var values;
- if (typeof(_value) == 'object') {
-   values = _value;
- } else {
-   values = new Array();
-   values[values.length] = _value;
+ if (typeof(field_val['default']) != 'undefined'
+     && (typeof(_value) == 'undefined'
+         || (typeof(_value) == 'object' && _value.length == 0)
+         || ! _value.length)) {
+  _value = field_val['default'];
+  modified = 1;
  }
- var n_values = (typeof(_value) == 'undefined') ? 0 : values.length;
 
- /// allow for default value
- var tests = this.filter_types('default', types);
- if (n_values == 0 || (n_values == 1 && values[0].length == 0)) {
-   for (var i = 0; i < tests.length; i ++) {
-     var el = form[field];
-     if (! el) continue;
-     var type = el.type;
-     if (type && (type == 'hidden' || type == 'password' || type == 'text' || type == 'textarea' || type == 'submit'))
-       el.value = values[0] = '' + field_val[tests[i]];
-   }
- }
+ var values   = (typeof(_value) == 'object') ? _value : [_value];
+ var n_values = (typeof(_value) == 'undefined') ? 0 : values.length;
 
- /// allow for a few form modifiers
- var modified = 0;
- for (var i = 0; i < values.length; i ++) {
-   if (typeof(values[i]) == 'undefined') continue;
-   if (! this.filter_types('do_not_trim',types).length)
-     values[i] = values[i].replace('^\\s+','').replace(new RegExp('\\s+$',''),'');
-   if (this.filter_types('trim_control_chars',types).length)
-     values[i] = values[i].replace(new RegExp('\t', 'g'),' ').replace(new RegExp('[\\x00-\\x1F]+','g'),'');
-   if (this.filter_types('to_upper_case',types).length) {
-     values[i] = values[i].toUpperCase();
-   } else if (this.filter_types('to_lower_case',types).length) {
-     values[i] = values[i].toLowerCase();
-   }
- }
- var tests = this.filter_types('replace', types);
- for (var i = 0; i < tests.length; i ++) {
-   var ref = field_val[tests[i]];
-   ref = (typeof(ref) == 'object') ? ref : ref.split(new RegExp('\\s*\\|\\|\\s*'));
-   for (var j = 0; j < ref.length; j ++) {
-     if (! (m = ref[j].match('^\\s*s([^\\s\\w])(.+)\\1(.*)\\1([eigmx]*)$')))
-       return this.error("Not sure how to parse that replace "+ref[j]);
-     var pat  = m[2];
-     var swap = m[3];
-     var opt  = m[4];
-     if (opt.indexOf('e') != -1)
-       return this.error("The e option cannot be used on field "+field+", replace "+tests[i]);
-     var regexp = new RegExp(pat, opt);
-     for (var k = 0; k < values.length; k ++) {
-       if (values[k].match(regexp)) modified = 1;
-       values[k] = values[k].replace(regexp,swap);
-     }
-   }
+ for (var i = 0; i < values.length; i++) {
+  if (typeof(values[i]) == 'undefined') continue;
+  var orig = values[i];
+  if (! field_val.do_not_trim)      values[i] = values[i].replace(/^\s+/,'').replace(/\s+$/,'');
+  if (field_val.trim_control_chars) values[i] = values[i].replace(/\t/g,' ').replace(/[\x00-\x1F]/g,'');
+  if (field_val.to_upper_case) values[i] = values[i].toUpperCase();
+  if (field_val.to_lower_case) values[i] = values[i].toLowerCase();
+
+  var tests = v_filter_types('replace', types);
+  for (var k = 0; k < tests.length; k++) {
+   var ref = field_val[tests[k]];
+   ref = (typeof(ref) == 'object') ? ref : ref.split(/\s*\|\|\s*/);
+   for (var j = 0; j < ref.length; j++) {
+    if (! (m = ref[j].match(/^\s*s([^\s\w])(.+)\1(.*)\1([eigmx]*)$/)))
+     return v_error("Not sure how to parse that replace "+ref[j]);
+    var pat  = m[2];
+    var swap = m[3];
+    var opt  = m[4];
+    if (opt.indexOf('e') != -1) { v_error("The e option cannot be used on field "+field+", replace "+tests[i]); return errors }
+    var regexp = new RegExp(pat, opt);
+    values[i] = values[i].replace(regexp, swap);
+   }
+  }
+
+  if (orig != values[i]) modified = 1;
  }
  if (modified && n_values == 1) {
-   var el = form[field];
-   var type = el.type;
-   if (! type) return '';
-   if (type == 'hidden' || type == 'password' || type == 'text' || type == 'textarea' || type == 'submit')
-     el.value = values[0];
+  var el = form[field];
+  if (el && el.type && el.type.match(/(hidden|password|text|textarea|submit)/)) el.value = values[0];
  }
 
- /// only continue if a validate_if is not present or passes test
  var needs_val = 0;
  var n_vif = 0;
- var tests = this.filter_types('validate_if', types);
- for (var i = 0; i < tests.length; i ++) {
-   n_vif ++;
-   var ifs = field_val[tests[i]];
-   var ret = this.check_conditional(form, ifs, N_level, ifs_match);
-   if (ret) needs_val ++;
+ var tests = v_filter_types('validate_if', types);
+ for (var i = 0; i < tests.length; i++) {
+  n_vif++;
+  var ifs = field_val[tests[i]];
+  var ret = v_check_conditional(form, ifs, N_level, ifs_match);
+  if (ret) needs_val++;
  }
  if (! needs_val && n_vif) return errors;
 
- /// check for simple existence
- /// optionally check only if another condition is met
  var is_required = '';
- var tests = this.filter_types('required', types);
- for (var i = 0; i < tests.length; i ++) {
-   if (! field_val[tests[i]] || field_val[tests[i]] == 0) continue;
+ var tests = v_filter_types('required', types);
+ for (var i = 0; i < tests.length; i++) {
+  if (! field_val[tests[i]] || field_val[tests[i]] == 0) continue;
+  is_required = tests[i];
+  break;
+ }
+ if (! is_required) {
+  var tests = v_filter_types('required_if', types);
+  for (var i = 0; i < tests.length; i++) {
+   var ifs = field_val[tests[i]];
+   if (! v_check_conditional(form, ifs, N_level, ifs_match)) continue;
    is_required = tests[i];
    break;
+  }
  }
- if (! is_required) {
-   var tests = this.filter_types('required_if', types);
-   for (var i = 0; i < tests.length; i ++) {
-     var ifs = field_val[tests[i]];
-     if (! this.check_conditional(form, ifs, N_level, ifs_match)) continue;
-     is_required = tests[i];
-     break;
+ if (is_required) {
+  var found;
+  for (var i = 0; i < values.length; i++) {
+   if (values[i].length) {
+    found = 1;
+    break;
    }
- }
- if (is_required && (typeof(_value) == 'undefined'
-                     || ((typeof(_value) == 'object' && _value.length == 0)
-                         || ! _value.length))) {
-   this.add_error(errors, field, is_required, field_val, ifs_match, form);
-   return errors;
+  }
+  if (! found) return v_add_error(errors, field, is_required, field_val, ifs_match, form);
  }
 
- /// min values check
- var tests = this.filter_types('min_values', types);
- for (var i = 0; i < tests.length; i ++) {
-   var n = field_val[tests[i]];
-   if (n_values < n) {
-     this.add_error(errors, field, tests[i], field_val, ifs_match, form);
-     return errors;
-   }
- }
+ if (field_val.min_values && n_values < field_val.min_values)
+  return v_add_error(errors, field, 'min_values', field_val, ifs_match, form);
 
- /// max values check
- var tests = this.filter_types('max_values', types);
- if (! tests.length) {
-   tests[tests.length] = 'max_values';
-   field_val['max_values'] = 1;
- }
- for (var i = 0; i < tests.length; i ++) {
-   var n = field_val[tests[i]];
-   if (n_values > n) {
-     this.add_error(errors, field, tests[i], field_val, ifs_match, form);
-     return errors;
-   }
- }
+ if (typeof(field_val.max_values) == 'undefined') field_val.max_values = 1;
+ if (field_val.max_values && n_values > field_val.max_values)
+  return v_add_error(errors, field, 'max_values', field_val, ifs_match, form);
 
- /// min_in_set and max_in_set check
  for (var h = 0; h < 2 ; h++) {
-   var minmax = (h == 0) ? 'min' : 'max';
-   var tests = this.filter_types(minmax+'_in_set', types);
-   for (var i = 0; i < tests.length; i ++) {
-     if (! (m = field_val[tests[i]].match('^\\s*(\\d+)(?:\\s*[oO][fF])?\\s+(.+)\\s*$')))
-       return this.error("Invalid in_set check "+field_val[tests[i]]);
-     var n       = m[1];
-     var _fields = m[2].split(new RegExp('[\\s,]+'));
-     for (var k = 0; k < _fields.length; k ++) {
-       var _value = this.get_form_value(form[_fields[k]]);
-       var _values;
-       if (typeof(_value) == 'undefined') continue;
-       if (typeof(_value) == 'object') {
-         _values = _value;
-       } else {
-         _values = new Array();
-         _values[_values.length] = _value;
-       }
-       for (var l = 0; l < _values.length; l ++) {
-         var _value = _values[l];
-         if (typeof(_value) != 'undefined' && _value.length) n --;
-       }
-     }
-     if (   (minmax == 'min' && n > 0)
-         || (minmax == 'max' && n < 0)) {
-       this.add_error(errors, field, tests[i], field_val, ifs_match, form);
-       return errors;
-     }
-   }
- }
-
- // the remaining tests operate on each value of a field
- for (var n = 0; n < values.length; n ++) {
+  var minmax = (h == 0) ? 'min' : 'max';
+  var tests = v_filter_types(minmax+'_in_set', types);
+  for (var i = 0; i < tests.length; i++) {
+   var a = field_val[tests[i]];
+   var n = a[0];
+   for (var k = 1; k < a.length; k++) {
+    var _value = v_get_form_value(form[a[k]]);
+    var _values;
+    if (typeof(_value) == 'undefined') continue;
+    _values = (typeof(_value) == 'object') ? _value : [_value];
+    for (var l = 0; l < _values.length; l++) {
+     var _value = _values[l];
+     if (typeof(_value) != 'undefined' && _value.length) n--;
+    }
+   }
+   if (   (minmax == 'min' && n > 0)
+     || (minmax == 'max' && n < 0)) {
+    v_add_error(errors, field, tests[i], field_val, ifs_match, form);
+    return errors;
+   }
+  }
+ }
+
+ for (var i = 0; i < types.length; i++) {
+  var type = types[i];
+  var _fv  = field_val[type];
+  for (var n = 0; n < values.length; n++) {
    var value = values[n];
 
-   /// allow for enum types
-   var tests = this.filter_types('enum', types);
-   for (var i = 0; i < tests.length; i ++) {
-     var hold  = field_val[tests[i]];
-     var _enum = (typeof(hold) == 'object') ? hold : hold.split(new RegExp('\\s*\\|\\|\\s*'));
-     var is_found = 0;
-     for (var j = 0; j < _enum.length; j ++) {
-       if (value != _enum[j]) continue;
-       is_found = 1;
-       break;
-     }
-     if (! is_found) this.add_error(errors, field, tests[i], field_val, ifs_match, form);
-   }
-
-   /// field equality test
-   var tests = this.filter_types('equals', types);
-   for (var i = 0; i < tests.length; i ++) {
-     var field2  = field_val[tests[i]];
-     var not = field2.match('^!\\s*');
-     if (not) field2 = field2.substring(not[0].length);
-     var success = 0;
-     if (m = field2.match('^(["\'])(.*)\\1$')) {
-       if (value == m[2]) success = 1;
+   if (type.match(/^enum\d*$/)) {
+    var is_found = 0;
+    for (var j = 0; j < _fv.length; j++) if (value == _fv[j]) { is_found = 1; break }
+    if (! is_found) v_add_error(errors, field, type, field_val, ifs_match, form);
+   }
+
+   if (type.match(/^equals\d*$/)) {
+    var not = _fv.match(/^!\s*/);
+    if (not) _fv = _fv.substring(not[0].length);
+    var success = 0;
+    if (m = _fv.match(/^([\"\'])(.*)\1$/)) {
+     if (value == m[2]) success = 1;
+    } else {
+     var value2 = v_get_form_value(form[_fv]);
+     if (typeof(value2) == 'undefined') value2 = '';
+     if (value == value2) success = 1;
+    }
+    if (not && success || ! not && ! success)
+     v_add_error(errors, field, type, field_val, ifs_match, form);
+   }
+
+   if (type == 'min_len' && value.length < _fv) v_add_error(errors, field, 'min_len', field_val, ifs_match, form);
+   if (type == 'max_len' && value.length > _fv) v_add_error(errors, field, 'max_len', field_val, ifs_match, form);
+
+   if (type.match(/^match\d*$/)) {
+    for (var j = 0; j < _fv.length; j++) {
+     if (typeof(_fv[j]) == 'string') continue;
+     var not = (j > 0 && typeof(_fv[j-1]) == 'string' && _fv[j-1] == '!') ? 1 : 0;
+     if (   (  not &&   value.match(_fv[j]))
+         || (! not && ! value.match(_fv[j]))) v_add_error(errors, field, type, field_val, ifs_match, form);
+    }
+   }
+
+   if (type.match(/^compare\d*$/)) {
+    for (var j = 0; j < _fv.length; j++) {
+     var comp = _fv[j];
+     if (! comp) continue;
+     var hold = false;
+     var copy = value;
+     if (m = comp.match(/^\s*(>|<|[><!=]=)\s*([\d\.\-]+)\s*$/)) {
+      if (! copy) copy = 0;
+      copy *= 1;
+      if      (m[1] == '>' ) hold = (copy >  m[2])
+      else if (m[1] == '<' ) hold = (copy <  m[2])
+      else if (m[1] == '>=') hold = (copy >= m[2])
+      else if (m[1] == '<=') hold = (copy <= m[2])
+      else if (m[1] == '!=') hold = (copy != m[2])
+      else if (m[1] == '==') hold = (copy == m[2])
+     } else if (m = comp.match(/^\s*(eq|ne|gt|ge|lt|le)\s+(.+?)\s*$/)) {
+      if (     m[2].match(/^\"/)) m[2] = m[2].replace(/^"(.*)"$/,'$1');
+      else if (m[2].match(/^\'/)) m[2] = m[2].replace(/^'(.*)'$/,'$1');
+      if      (m[1] == 'gt') hold = (copy >  m[2])
+      else if (m[1] == 'lt') hold = (copy <  m[2])
+      else if (m[1] == 'ge') hold = (copy >= m[2])
+      else if (m[1] == 'le') hold = (copy <= m[2])
+      else if (m[1] == 'ne') hold = (copy != m[2])
+      else if (m[1] == 'eq') hold = (copy == m[2])
      } else {
-       var value2 = this.get_form_value(form[field2]);
-       if (typeof(value2) == 'undefined') value2 = '';
-       if (value == value2) success = 1;
-     }
-     if (not && success || ! not && ! success)
-       this.add_error(errors, field, tests[i], field_val, ifs_match, form);
-   }
-
-   /// length min check
-   var tests = this.filter_types('min_len', types);
-   for (var i = 0; i < tests.length; i ++) {
-     var n = field_val[tests[i]];
-     if (value.length < n) this.add_error(errors, field, tests[i], field_val, ifs_match, form);
-   }
-
-   /// length max check
-   var tests = this.filter_types('max_len', types);
-   for (var i = 0; i < tests.length; i ++) {
-     var n = field_val[tests[i]];
-     if (value.length > n) this.add_error(errors, field, tests[i], field_val, ifs_match, form);
-   }
-
-   /// now do match types
-   var tests = this.filter_types('match', types);
-   for (var i = 0; i < tests.length; i ++) {
-     var ref = field_val[tests[i]];
-     ref = (typeof(ref) == 'object') ? ref
-       : (typeof(ref) == 'function') ? new Array(ref)
-       : ref.split(new RegExp('\\s*\\|\\|\\s*'));
-     for (var j = 0; j < ref.length; j ++) {
-       if (typeof(ref[j]) == 'function') {
-         if (! value.match(ref[j])) this.add_error(errors, field, tests[i], field_val, ifs_match, form);
-       } else {
-         if (! (m = ref[j].match('^\\s*(!\\s*|)m([^\\s\\w])(.*)\\2([eigsmx]*)\\s*$')))
-           return this.error("Not sure how to parse that match ("+ref[j]+")");
-         var not = m[1];
-         var pat = m[3];
-         var opt = m[4];
-         if (opt.indexOf('e') != -1)
-           return this.error("The e option cannot be used on field "+field+", test "+tests[i]);
-         opt = opt.replace(new RegExp('[sg]','g'),'');
-         var regexp = new RegExp(pat, opt);
-         if (   (  not &&   value.match(regexp))
-             || (! not && ! value.match(regexp))) {
-           this.add_error(errors, field, tests[i], field_val, ifs_match, form);
-         }
-       }
+      v_error("Not sure how to compare \""+comp+"\"");
+      return errors;
      }
+     if (! hold) v_add_error(errors, field, type, field_val, ifs_match, form);
+    }
    }
 
-   /// allow for comparison checks
-   var tests = this.filter_types('compare', types);
-   for (var i = 0; i < tests.length; i ++) {
-     var ref = field_val[tests[i]];
-     ref = (typeof(ref) == 'object') ? ref : ref.split(new RegExp('\\s*\\|\\|\\s*'));
-     for (var j = 0; j < ref.length; j ++) {
-       var comp = ref[j];
-       if (! comp) continue;
-       var hold = false;
-       var copy = value;
-       if (m = comp.match('^\\s*(>|<|[><!=]=)\\s*([\\d\.\-]+)\\s*$')) {
-         if (! copy) copy = 0;
-         copy *= 1;
-         if      (m[1] == '>' ) hold = (copy >  m[2])
-         else if (m[1] == '<' ) hold = (copy <  m[2])
-         else if (m[1] == '>=') hold = (copy >= m[2])
-         else if (m[1] == '<=') hold = (copy <= m[2])
-         else if (m[1] == '!=') hold = (copy != m[2])
-         else if (m[1] == '==') hold = (copy == m[2])
-       } else if (m = comp.match('^\\s*(eq|ne|gt|ge|lt|le)\\s+(.+?)\\s*$')) {
-         m[2] = m[2].replace('^(["\'])(.*)\\1$','$1');
-         if      (m[1] == 'gt') hold = (copy >  m[2])
-         else if (m[1] == 'lt') hold = (copy <  m[2])
-         else if (m[1] == 'ge') hold = (copy >= m[2])
-         else if (m[1] == 'le') hold = (copy <= m[2])
-         else if (m[1] == 'ne') hold = (copy != m[2])
-         else if (m[1] == 'eq') hold = (copy == m[2])
-       } else {
-         return this.error("Not sure how to compare \""+comp+"\"");
-       }
-       if (! hold) this.add_error(errors, field, tests[i], field_val, ifs_match, form);
-     }
-   }
+   if (type.match(/^type\d*$/))
+    if (! v_check_type(value, _fv, field, form))
+     v_add_error(errors, field, type, field_val, ifs_match, form);
+  }
 
-   /// do specific type checks
-   var tests = this.filter_types('type',types);
-   for (var i = 0; i < tests.length; i ++)
-     if (! this.check_type(value, field_val[tests[i]], field, form))
-       this.add_error(errors, field, tests[i], field_val, ifs_match, form);
-
-   /// do custom_js type checks
-   // this will allow for a custom piece of javascript
-   // the js is evaluated and should return 1 for success
-   // or 0 for failure - the variables field, value, and field_val (the hash) are available
-   var tests = this.filter_types('custom_js',types);
-   for (var i = 0; i < tests.length; i ++)
-     if (! eval(field_val[tests[i]]))
-       this.add_error(errors, field, tests[i], field_val, ifs_match, form);
+  // the js is evaluated and should return 1 for success
+  // or 0 for failure - the variables field, value, and field_val (the hash) are available
+  if (type.match(/^custom_js\d*$/)) {
+   var value = values.length == 1 ? values[0] : values;
+   if (typeof(_fv) == 'function'
+       ? ! _fv({'value':value, 'field_val':field_val, 'form':form, 'key':field_val.field})
+       : ! eval(_fv)) v_add_error(errors, field, type, field_val, ifs_match, form);
+  }
  }
 
- /// all done - time to return
  return errors;
 }
 
-/// used to validate specific types
-function vob_check_type (value, type, field, form) {
+function v_check_type (value, type, field, form) {
  var m;
+ type = type.toUpperCase();
 
- /// do valid email address for our system
  if (type == 'EMAIL') {
-   if (! value) return 0;
-   if (! (m = value.match('^(.+)\@(.+?)$'))) return 0;
-   if (m[1].length > 60)  return 0;
-   if (m[2].length > 100) return 0;
-   if (! this.check_type(m[2],'DOMAIN') && ! this.check_type(m[2],'IP')) return 0;
-   if (! this.check_type(m[1],'LOCAL_PART')) return 0;
-
- /// the "username" portion of an email address
+  if (! value) return 0;
+  if (! (m = value.match(/^(.+)@(.+?)$/))) return 0;
+  if (m[1].length > 60)  return 0;
+  if (m[2].length > 100) return 0;
+  if (! v_check_type(m[2],'DOMAIN') && ! v_check_type(m[2],'IP')) return 0;
+  if (! v_check_type(m[1],'LOCAL_PART')) return 0;
+
  } else if (type == 'LOCAL_PART') {
-   if (typeof(value) == 'undefined' || ! value.length) return 0;
-   if (value.match('[^a-z0-9.\\-!&+]'))   return 0;
-   if (value.match('^[.\\-]'))            return 0;
-   if (value.match('[.\\-&]$'))           return 0;
-   if (value.match('(\\.-|-\\.|\\.\\.)')) return 0;
+  if (typeof(value) == 'undefined' || ! value.length) return 0;
+  if (typeof(v_local_part) != 'undefined') return (value.match(v_local_part) ? 1 : 0);
+  if (value.match(/[^a-z0-9.\-!&+]/)) return 0;
+  if (value.match(/^[.\-]/))          return 0;
+  if (value.match(/[.\-&]$/))         return 0;
+  if (value.match(/(\.-|-\.|\.\.)/))  return 0;
 
- /// standard IP address
  } else if (type == 'IP') {
-   if (! value) return 0;
-   var dig = value.split(new RegExp('\\.'));
-   if (dig.length != 4) return 0;
-   for (var i = 0; i < 4; i ++)
-     if (typeof(dig[i]) == 'undefined' || dig[i].match('\\D') || dig[i] > 255) return 0;
+  if (! value) return 0;
+  var dig = value.split(/\./);
+  if (dig.length != 4) return 0;
+  for (var i = 0; i < 4; i++)
+   if (typeof(dig[i]) == 'undefined' || dig[i].match(/\D/) || dig[i] > 255) return 0;
 
- /// domain name - including tld and subdomains (which are all domains)
  } else if (type == 'DOMAIN') {
-   if (! value) return 0;
-   if (! value.match('^[a-z0-9.-]{4,255}$')) return 0;
-   if (value.match('^[.\\-]'))             return 0;
-   if (value.match('(\\.-|-\\.|\\.\\.)'))  return 0;
-   if (! (m = value.match('\.([a-z]+)$'))) return 0;
-   value = value.substring(0,value.lastIndexOf('.'));
-
-   if (m[1] == 'name') {
-     if (! value.match('^[a-z0-9][a-z0-9\\-]{0,62}\\.[a-z0-9][a-z0-9\\-]{0,62}$')) return 0;
-   } else
-     if (! value.match('^([a-z0-9][a-z0-9\\-]{0,62}\\.)*[a-z0-9][a-z0-9\\-]{0,62}$')) return 0;
-
- /// validate a url
+  if (! value) return 0;
+  if (! value.match(/^[a-z0-9.-]{4,255}$/)) return 0;
+  if (value.match(/^[.\-]/))             return 0;
+  if (value.match(/(\.-|-\.|\.\.)/))  return 0;
+  if (! (m = value.match(/\.([a-z]+)$/))) return 0;
+  value = value.substring(0,value.lastIndexOf('.'));
+  if (m[1] == 'name') {
+   if (! value.match(/^[a-z0-9][a-z0-9\-]{0,62}\.[a-z0-9][a-z0-9\-]{0,62}$/)) return 0;
+  } else
+   if (! value.match(/^([a-z0-9][a-z0-9\-]{0,62}\.)*[a-z0-9][a-z0-9\-]{0,62}$/)) return 0;
+
  } else if (type == 'URL') {
-   if (! value) return 0;
-   if (! (m = value.match(new RegExp('^https?://([^/]+)','i'),''))) return 0;
-   value = value.substring(m[0].length);
-   if (! this.check_type(m[1],'DOMAIN') && ! this.check_type(m[1],'IP')) return 0;
-   if (value && ! this.check_type(value,'URI')) return 0;
+  if (! value) return 0;
+  if (! (m = value.match(/^https?:\/\/([^\/]+)/i))) return 0;
+  value = value.substring(m[0].length);
+  var dom = m[1].replace(/:\d+$/).replace(/\.$/);
+  if (! v_check_type(dom,'DOMAIN') && ! v_check_type(m[1],'IP')) return 0;
+  if (value && ! v_check_type(value,'URI')) return 0;
 
- /// validate a uri - the path portion of a request
  } else if (type == 'URI') {
-   if (! value) return 0;
-   if (value.match('\\s')) return 0;
+  if (! value) return 0;
+  if (value.match(/\s/)) return 0;
 
  } else if (type == 'CC') {
-   if (! value) return 0;
-   if (value.match('[^\\d\\- ]') || value.length > 16 || value.length < 13) return;
-   /// simple mod10 check
-   value = value.replace(new RegExp('[\\- ]','g'), '');
-   var sum = 0;
-   var swc = 0;
-
-   for (var i = value.length - 1; i >= 0; i --) {
-     if (++ swc > 2) swc = 1;
-     var y = value.charAt(i) * swc;
-     if (y > 9) y -= 9;
-     sum += y;
-   }
-   if (sum % 10) return 0;
-
+  if (! value) return 0;
+  if (value.match(/[^\d\- ]/)) return 0;
+  value = value.replace(/[\- ]/g, '');
+  if (value.length > 16 || value.length < 13) return 0;
+  // mod10
+  var sum = 0;
+  var swc = 0;
+  for (var i = value.length - 1; i >= 0; i--) {
+   if (++swc > 2) swc = 1;
+   var y = value.charAt(i) * swc;
+   if (y > 9) y -= 9;
+   sum += y;
+  }
+  if (sum % 10) return 0;
  }
 
  return 1;
 }
 
-// little routine that will get the values from the form
-// it will return multiple values as an array
-function vob_get_form_value (el) {
+function v_get_form_value (el) {
  if (! el) return '';
  if (el.disabled) return '';
  var type = el.type ? el.type.toLowerCase() : '';
  if (el.length && type != 'select-one') {
-   var a = new Array();
-   for (var j=0;j<el.length;j++) {
-     if (type.indexOf('multiple') != -1) {
-       if (el[j].selected) a[a.length] = el[j].value;
-     } else {
-       if (el[j].checked)  a[a.length] = vob_get_form_value(el[j]);
-     }
+  var a = [];
+  for (var j=0;j<el.length;j++) {
+   if (type.indexOf('multiple') != -1) {
+    if (el[j].selected) a.push(el[j].value);
+   } else {
+    if (el[j].checked)  a.push(v_get_form_value(el[j]));
    }
-   if (a.length == 0) return '';
-   if (a.length == 1) return a[0];
-   return a;
+  }
+  if (a.length == 0) return '';
+  if (a.length == 1) return a[0];
+  return a;
  }
  if (! type) return '';
- if (type == 'hidden' || type == 'password' || type == 'text' || type == 'textarea' || type == 'submit')
-   return el.value;
+ if (type.match(/(hidden|password|text|textarea|submit)/)) return el.value;
  if (type.indexOf('select') != -1) {
-   if (! el.length) return '';
-   return el[el.selectedIndex].value;
- }
- if (type == 'checkbox' || type == 'radio') {
-   return el.checked ? el.value : '';
- }
- if (type == 'file') {
-   return el.value; // hope this works
+  if (! el.length) return '';
+  return el[el.selectedIndex].value;
  }
+ if (type == 'checkbox' || type == 'radio') return el.checked ? el.value : '';
+ if (type == 'file') return el.value;
+
  alert('Unknown form type for '+el.name+': '+type);
  return '';
 }
 
-///----------------------------------------------------------------///
-
-function eob_get_val (key, extra2, extra1, _default) {
- if (typeof(extra2[key]) != 'undefined') return extra2[key];
- if (typeof(extra1[key]) != 'undefined') return extra1[key];
- return _default;
-}
-
-function eob_as_string (extra2) {
- var extra1 = this.extra;
- if (! extra2) extra2 = new Array();
-
- var joiner = eob_get_val('as_string_join',   extra2, extra1, '\n');
- var header = eob_get_val('as_string_header', extra2, extra1, '');
- var footer = eob_get_val('as_string_footer', extra2, extra1, '');
-
- return header + this.as_array(extra2).join(joiner) + footer;
-}
-
-/// return an array of applicable errors
-function eob_as_array (extra2) {
- var errors = this.errors;
- var extra1 = this.extra;
- if (! extra2) extra2 = new Array();
-
- var title = eob_get_val('as_array_title', extra2, extra1, 'Please correct the following items:');
-
- /// if there are heading items then we may end up needing a prefix
- var has_headings;
- if (title) has_headings = 1;
- else {
-   for (var i = 0; i < errors.length; i ++) {
-     if (typeof(errors[i]) != 'string') continue;
-     has_headings = 1;
-     break;
-   }
- }
-
- var prefix = eob_get_val('as_array_prefix', extra2, extra1, has_headings ? '  ' : '');
-
- /// get the array ready
- var arr = new Array();
- if (title && title.length) arr[arr.length] = title;
- /// add the errors
- var found = new Array();
- for (var i = 0; i < errors.length; i ++) {
-   if (typeof(errors[i]) == 'string') {
-     arr[arr.length] = errors[i];
-     found = new Array();
-   } else {
-     var text = this.get_error_text(errors[i]);
-     if (found[text]) continue;
-     found[text] = 1;
-     arr[arr.length] = prefix + text;
-   }
- }
-
- return arr;
-}
-
-/// return a hash of applicable errors
-function eob_as_hash (extra2) {
- var errors = this.errors;
- var extra1 = this.extra;
- if (! extra2) extra2 = new Array();
- var suffix = eob_get_val('as_hash_suffix', extra2, extra1, '_error');
- var joiner = eob_get_val('as_hash_join',   extra2, extra1, '<br />');
-
- /// now add to the hash
- var found = new Array();
- var ret   = new Array();
- for (var i = 0; i < errors.length; i ++) {
-   if (typeof(errors[i]) == 'string') continue;
-   if (! errors[i].length) continue;
-
-   var field     = errors[i][0];
-   var type      = errors[i][1];
-   var field_val = errors[i][2];
-   var ifs_match = errors[i][3];
-
-   if (! field) return alert("Missing field name");
-   if (field_val['delegate_error']) {
-     field = field_val['delegate_error'];
-     field = field.replace(new RegExp('\\$(\\d+)','g'), function (all, N) {
-       if (typeof(ifs_match) != 'object'
-           || typeof(ifs_match[N]) == 'undefined') return ''
-       return ifs_match[N];
-     });
-   }
-
-   var text = this.get_error_text(errors[i]);
-   if (! found[field]) found[field] = new Array();
-   if (found[field][text]) continue;
-   found[field][text] = 1;
-
-   field += suffix;
-   if (! ret[field]) ret[field] = new Array();
-   ret[field].push(text);
- }
-
- /// allow for elements returned as
- if (joiner) {
-   var header = eob_get_val('as_hash_header', extra2, extra1, '');
-   var footer = eob_get_val('as_hash_footer', extra2, extra1, '');
-   for (var key in ret) {
-     if (key == 'extend') continue; // Protoype Array() fix
-     ret[key] = header + ret[key].join(joiner) + footer;
-   }
+function v_find_val () {
+ var key = arguments[0];
+ for (var i = 1; i < arguments.length; i++) {
+  if (typeof(arguments[i]) == 'string') return arguments[i];
+  if (typeof(arguments[i]) == 'undefined') continue;
+  if (typeof(arguments[i][key]) != 'undefined') return arguments[i][key];
  }
-
- return ret;
+ return '';
 }
 
-/// return a user friendly error message
-function eob_get_error_text (err) {
- var extra     = this.extra;
+function v_get_error_text (err, extra1, extra2) {
  var field     = err[0];
  var type      = err[1];
  var field_val = err[2];
  var ifs_match = err[3];
  var m;
 
- var dig = (m = type.match('(_?\\d+)$')) ? m[1] : '';
+ var dig = (m = type.match(/(_?\d+)$/)) ? m[1] : '';
  var type_lc = type.toLowerCase();
+ var v = field_val[type + dig];
 
- /// allow for delegated field names - only used for defaults
- if (field_val['delegate_error']) {
-   field = field_val['delegate_error'];
-   field = field.replace(new RegExp('\\$(\\d+)','g'), function (all, N) {
-     if (typeof(ifs_match) != 'object'
-         || typeof(ifs_match[N]) == 'undefined') return ''
-     return ifs_match[N];
-   });
- }
-
- /// the the name of this thing
- var name = (field_val['name']) ? field_val['name'] : "The field " +field;
- name = name.replace(new RegExp('\\$(\\d+)','g'), function (all, N) {
+ if (field_val.delegate_error) {
+  field = field_val.delegate_error;
+  field = field.replace(/\$(\d+)/g, function (all, N) {
    if (typeof(ifs_match) != 'object'
-       || typeof(ifs_match[N]) == 'undefined') return ''
+     || typeof(ifs_match[N]) == 'undefined') return ''
    return ifs_match[N];
+  });
+ }
+
+ var name = field_val.name || "The field " +field;
+ name = name.replace(/\$(\d+)/g, function (all, N) {
+  if (typeof(ifs_match) != 'object'
+    || typeof(ifs_match[N]) == 'undefined') return ''
+  return ifs_match[N];
  });
 
+ var msg = v_find_val(type + '_error', extra1, extra2);
+ if (! msg) {
+   if (dig.length) msg = field_val[type + dig + '_error'];
+   if (! msg)      msg = field_val[type +       '_error'];
+ }
+ if (msg) {
+  msg = msg.replace(/\$(\d+)/g, function (all, N) {
+   if (typeof(ifs_match) != 'object' || typeof(ifs_match[N]) == 'undefined') return '';
+   return ifs_match[N];
+  });
+  msg = msg.replace(/\$field/g, field);
+  msg = msg.replace(/\$name/g, name);
+  if (v && typeof(v) == 'string') msg = msg.replace(/\$value/g, v);
+  return msg;
+ }
+
+ if (type == 'equals') {
+  var field2 = field_val["equals" + dig];
+  var name2  = field_val["equals" +dig+ "_name"];
+  if (! name2) name2 = "the field " +field2;
+  name2 = name2.replace(/\$(\d+)/g, function (all, N) {
+   return (typeof(ifs_match) != 'object' || typeof(ifs_match[N]) == 'undefined') ? '' : ifs_match[N];
+  });
+  return name + " did not equal " + name2 +".";
+ }
+ if (type == 'min_in_set') return "Not enough fields were chosen from the set ("+v[0]+' of '+v.join(", ").replace(/^\d+,\s*/,'')+")";
+ if (type == 'max_in_set') return "Too many fields were chosen from the set ("  +v[0]+' of '+v.join(", ").replace(/^\d+,\s*/,'')+")";
+
+ return name + (
+  (type == 'required' || type == 'required_if') ? " is required."
+  : type == 'match'      ? " contains invalid characters."
+  : type == 'compare'    ? " did not fit comparison."
+  : type == 'custom_js'  ? " did not match custom_js"+dig+" check."
+  : type == 'enum'       ? " is not in the given list."
+  : type == 'min_values' ? " had less than "+v+" value"+(v == 1 ? '' : 's')+"."
+  : type == 'max_values' ? " had more than "+v+" value"+(v == 1 ? '' : 's')+"."
+  : type == 'min_len'    ? " was less than "+v+" character"+(v == 1 ? '' : 's')+"."
+  : type == 'max_len'    ? " was more than "+v+" character"+(v == 1 ? '' : 's')+"."
+  : type == 'type'       ? " did not match type "+v+"."
+  : alert("Missing error on field "+field+" for type "+type+dig));
+}
 
- /// type can look like "required" or "required2" or "required100023"
- /// allow for fallback from required100023_error through required_error
- var possible_keys = new Array(type + '_error');
- if (dig.length) possible_keys.unshift(type + dig + '_error');
+//
 
- /// look in the passed hash or self first
- for (var i = 0; i < possible_keys.length; i ++) {
-   var key = possible_keys[i];
-   var ret = field_val[key];
-   if (! ret) {
-     if (extra[key]) ret = extra[key];
-     else continue;
-   }
-   ret = ret.replace(new RegExp('\\$(\\d+)','g'), function (all, N) {
-     if (typeof(ifs_match) != 'object'
-         || typeof(ifs_match[N]) == 'undefined') return ''
-     return ifs_match[N];
-   });
-   ret = ret.replace(new RegExp('\\$field','g'), field);
-   ret = ret.replace(new RegExp('\\$name' ,'g'), name);
-   if (field_val[type + dig] && typeof(field_val[type + dig]) == 'string')
-     ret = ret.replace(new RegExp('\\$value' ,'g'), field_val[type + dig]);
-   return ret;
- }
+function eob_as_string (extra) {
+ var joiner = v_find_val('as_string_join',   extra, this.extra, '\n');
+ var header = v_find_val('as_string_header', extra, this.extra, '');
+ var footer = v_find_val('as_string_footer', extra, this.extra, '');
+ return header + this.as_array(extra).join(joiner) + footer;
+}
 
- /// set default messages
- if (type == 'required' || type == 'required_if') {
-   return name + " is required.";
-
- } else if (type == 'min_values') {
-   var n = field_val["min_values" + dig];
-   var values = (n == 1) ? 'value' : 'values';
-   return name + " had less than "+n+" "+values+".";
-
- } else if (type == 'max_values') {
-   var n = field_val["max_values" + dig];
-   var values = (n == 1) ? 'value' : 'values';
-   return name + " had more than "+n+" "+values+".";
-
- } else if (type == 'min_in_set') {
-   var set = field_val["min_in_set" + dig];
-   return "Not enough fields were chosen from the set ("+set+")";
-   return "Too many fields were chosen from the set ("+set+")";
-
- } else if (type == 'max_in_set') {
-   var set = field_val["max_in_set" + dig];
-   return "Too many fields were chosen from the set ("+set+")";
-
- } else if (type == 'enum') {
-   return name + " is not in the given list.";
-
- } else if (type == 'equals') {
-   var field2 = field_val["equals" + dig];
-   var name2  = field_val["equals" +dig+ "_name"];
-   if (! name2) name2 = "the field " +field2;
-   name2 = name2.replace(new RegExp('\\$(\\d+)','g'), function (all, N) {
-     if (typeof(ifs_match) != 'object'
-         || typeof(ifs_match[N]) == 'undefined') return ''
-     return ifs_match[N];
-   });
-   return name + " did not equal " + name2 +".";
+function eob_as_array (extra) {
+ var errors = this.errors;
+ var title  = v_find_val('as_array_title', extra, this.extra, 'Please correct the following items:');
+
+ var has_headings;
+ if (title) has_headings = 1;
+ else for (var i = 0; i < errors.length; i++) if (typeof(errors[i]) == 'string') has_headings = 1;
 
- } else if (type == 'min_len') {
-   var n = field_val["min_len"+dig];
-   var chars = (n == 1) ? 'character' : 'characters';
-   return name + " was less than "+n+" "+chars+".";
+ var prefix = v_find_val('as_array_prefix', extra, this.extra, (has_headings ? '  ' : ''));
 
- } else if (type == 'max_len') {
-   var n = field_val["max_len"+dig];
-   var chars = (n == 1) ? 'character' : 'characters';
-   return name + " was more than "+n+" "+chars+".";
+ var arr = [];
+ if (title && title.length) arr.push(title);
 
- } else if (type == 'match') {
-   return name + " contains invalid characters.";
+ var found = {};
+ for (var i = 0; i < errors.length; i++) {
+  if (typeof(errors[i]) == 'string') {
+   arr.push(errors[i]);
+   found = {};
+  } else {
+   var text = v_get_error_text(errors[i], extra, this.extra);
+   if (found[text]) continue;
+   found[text] = 1;
+   arr.push(prefix + text);
+  }
+ }
 
- } else if (type == 'compare') {
-   return name + " did not fit comparison.";
+ return arr;
+}
 
- } else if (type == 'type') {
-   var _type = field_val["type"+dig];
-   return name + " did not match type "+_type+".";
+function eob_as_hash (extra) {
+ var errors = this.errors;
+ var suffix = v_find_val('as_hash_suffix', extra, this.extra, '_error');
+ var joiner = v_find_val('as_hash_join',   extra, this.extra, '<br/>');
+
+ var found = {};
+ var ret   = {};
+ for (var i = 0; i < errors.length; i++) {
+  if (typeof(errors[i]) == 'string') continue;
+  if (! errors[i].length) continue;
+
+  var field     = errors[i][0];
+  var type      = errors[i][1];
+  var field_val = errors[i][2];
+  var ifs_match = errors[i][3];
+
+  if (! field) return alert("Missing field name");
+  if (field_val['delegate_error']) {
+   field = field_val['delegate_error'];
+   field = field.replace(/\$(\d+)/g, function (all, N) {
+    if (typeof(ifs_match) != 'object'
+        || typeof(ifs_match[N]) == 'undefined') return ''
+    return ifs_match[N];
+   });
+  }
 
- } else if (type == 'custom_js') {
-   return name + " did not match custom_js"+dig+" check.";
+  var text = v_get_error_text(errors[i], extra, this.extra);
+  if (! found[field]) found[field] = {};
+  if (found[field][text]) continue;
+  found[field][text] = 1;
 
+  field += suffix;
+  if (! ret[field]) ret[field] = [];
+  ret[field].push(text);
  }
 
- return alert("Missing error on field "+field+" for type "+type+dig);
+ if (joiner) {
+  var header = v_find_val('as_hash_header', extra, this.extra, '');
+  var footer = v_find_val('as_hash_footer', extra, this.extra, '');
+  for (var key in ret) {
+   if (key == 'extend') continue; // Protoype Array()
+   ret[key] = header + ret[key].join(joiner) + footer;
+  }
+ }
+
+ return ret;
 }
 
 function eob_first_field () {
  for (var i = 0; i < this.errors.length; i++) {
-   if (typeof(this.errors[i]) != 'object') continue;
-   if (! this.errors[i][0]) continue;
-   return this.errors[i][0];
+  if (typeof(this.errors[i]) != 'object') continue;
+  if (! this.errors[i][0]) continue;
+  return this.errors[i][0];
  }
  return;
 }
 
-///----------------------------------------------------------------///
+//
 
 document.validate = function (form, val_hash) {
- // undo previous inline
- if (document.did_inline) {
-   for (var key in document.did_inline) {
-     if (key == 'extend') continue; // Protoype Array() fix
-     var el = document.getElementById(key);
-     if (el) el.innerHTML = '';
-   }
-   document.did_inline = undefined;
- }
-
- // do the validate
  val_hash = document.load_val_hash(form, val_hash);
  if (typeof(val_hash) == 'undefined') return true;
- if (! document.val_obj) document.val_obj = new Validate();
- var err_obj = document.val_obj.validate(form, val_hash);
 
- // return success
+ for (var key in v_did_inline) {
+  if (key == 'extend') continue; // Protoype Array()
+  v_inline_error_clear(key, val_hash, form);
+ }
+
+ var err_obj = v_validate(form, val_hash);
  if (! err_obj) return true;
 
- // focus
  var field = err_obj.first_field();
- if (field && form[field] && form[field].focus) form[field].focus();
-
- // inline
- if (! err_obj.extra.no_inline) {
-   var d = document.did_inline = new Array();
-   var hash = err_obj.as_hash();
-   for (var key in hash) {
-     if (key == 'extend') continue; // Protoype Array() fix
-     var el = document.getElementById(key);
-     if (el) el.innerHTML = hash[key];
-     d[key] = 1;
-   }
- }
-
- // alert
- if (! err_obj.extra.no_confirm) {
-   return confirm(err_obj.as_string()) ? false : true;
- } else if (! err_obj.extra.no_alert) {
-   alert(err_obj.as_string());
-   return false;
- } else if (! err_obj.extra.no_inline) {
-   return false;
+ if (field && form[field]) {
+   if (form[field].focus) form[field].focus();
+   else if (form[field].length && form[field][0].focus) form[field][0].focus();
+ }
+
+ if (! val_hash['group no_inline']) {
+  var hash = err_obj.as_hash({as_hash_suffix:""});
+  for (var key in hash) {
+   if (key == 'extend') continue; // Protoype Array()
+   v_inline_error_set(key, hash[key], val_hash, form);
+  }
+ }
+
+ if (! val_hash['group no_confirm']) {
+  return confirm(err_obj.as_string()) ? false : true;
+ } else if (! val_hash['group no_alert']) {
+  alert(err_obj.as_string());
+  return false;
+ } else if (! val_hash['group no_inline']) {
+  return false;
  } else {
-   return true;
+  return true;
  }
 }
 
 document.load_val_hash = function (form, val_hash) {
- // check the form we are using
  if (! form) return alert('Missing form or form name');
  if (typeof(form) == 'string') {
-   if (! document[form]) return alert('No form by name '+form);
-   form = document[form];
+  if (! document[form]) return alert('No form by name '+form);
+  form = document[form];
  }
 
- // if we already have validation - use it
  if (form.val_hash) return form.val_hash;
 
- // load in the validation and save it for future use
  if (typeof(val_hash) != 'object') {
-   // get the hash from a javascript function
-   if (typeof(val_hash) == 'function') {
-     val_hash = val_hash(formname);
-   } else if (typeof(val_hash) == 'undefined') {
-     var el;
-     // get hash from a global js variable
-     if (typeof(document.validation) != 'undefined') {
-       val_hash = document.validation;
-     // get hash from a element by if of validation
-     } else if (el = document.getElementById('validation')) {
-       val_hash = el.innerHTML;
-       val_hash = val_hash.replace(new RegExp('&lt;', 'ig'),'<');
-       val_hash = val_hash.replace(new RegExp('&gt;', 'ig'),'>');
-       val_hash = val_hash.replace(new RegExp('&amp;','ig'),'&');
-     // read hash from <input name=foo validation="">
-     } else {
-       var order = new Array();
-       var str   = '';
-       var yaml  = form.getAttribute('validation');
-       if (yaml) {
-         if (m = yaml.match('^( +)')) yaml = yaml.replace(new RegExp('^'+m[1], 'g'), ''); //unindent
-         yaml = yaml.replace(new RegExp('\\s*$',''),'\n'); // add trailing
-         str += yaml;
-       }
-       var m;
-       for (var i = 0; i < form.elements.length; i ++) {
-         var name = form.elements[i].name;
-         var yaml = form.elements[i].getAttribute('validation');
-         if (! name || ! yaml) continue;
-         yaml = yaml.replace(new RegExp('\\s*$',''),'\n'); // add trailing
-         yaml = yaml.replace(new RegExp('^(.)','mg'),' $1');    // indent all
-         yaml = yaml.replace(new RegExp('^( *[^\\s&*\\[\\{])',''),'\n$1'); // add newline
-         str += name +':' + yaml;
-         order[order.length] = name;
-       }
-       if (str) val_hash = str + "group order: [" + order.join(', ') + "]\n";
-     }
-   }
-   if (typeof(val_hash) == 'string') {
-     if (! document.yaml_load) return;
-     document.hide_yaml_errors = (! document.show_yaml_errors);
-     if (location.search && location.search.indexOf('show_yaml_errors') != -1)
-       document.hide_yaml_errors = 0;
-     val_hash = document.yaml_load(val_hash);
-     if (document.yaml_error_occured) return;
-   }
+  if (typeof(val_hash) == 'function') {
+   val_hash = val_hash(formname);
+  } else if (typeof(val_hash) == 'undefined') {
+   var el;
+   if (typeof(document.validation) != 'undefined') {
+    val_hash = document.validation;
+   } else if (el = document.getElementById('validation')) {
+    val_hash = el.innerHTML.replace(/&lt;/ig,'<').replace(/&gt;/ig,'>').replace(/&amp;/ig,'&');
+   } else {
+    var order = [];
+    var str   = '';
+    var yaml  = form.getAttribute('validation');
+    if (yaml) {
+     if (m = yaml.match(/^( +)/)) yaml = yaml.replace(new RegExp('^'+m[1], 'g'), '');
+     yaml = yaml.replace(/\s*$/,'\n');
+     str += yaml;
+    }
+    var m;
+    for (var i = 0; i < form.elements.length; i++) {
+     var name = form.elements[i].name;
+     var yaml = form.elements[i].getAttribute('validation');
+     if (! name || ! yaml) continue;
+     yaml = yaml.replace(/\s*$/,'\n').replace(/^(.)/mg,' $1').replace(/^( *[^\s&*\[\{])/,'\n$1');
+     str += name +':' + yaml;
+     order.push(name);
+    }
+    if (str) val_hash = str + "group order: [" + order.join(', ') + "]\n";
+   }
+  }
+  if (typeof(val_hash) == 'string') {
+   if (! document.yaml_load) return;
+   document.hide_yaml_errors = (! document.show_yaml_errors);
+   if (location.search && location.search.indexOf('show_yaml_errors') != -1)
+    document.hide_yaml_errors = 0;
+   val_hash = document.yaml_load(val_hash);
+   if (document.yaml_error_occured) return;
+   val_hash = val_hash[0];
+  }
  }
 
- // attach to the form
  form.val_hash = val_hash;
  return form.val_hash;
 }
 
-
 document.check_form = function (form, val_hash) {
- // check the form we are using
  if (! form) return alert('Missing form or form name');
  if (typeof(form) == 'string') {
-   if (! document[form]) return alert('No form by name '+form);
-   form = document[form];
+  if (! document[form]) return alert('No form by name '+form);
+  form = document[form];
  }
 
- // void call - allow for getting it at run time rather than later
- document.load_val_hash(form, val_hash);
+ val_hash = document.load_val_hash(form, val_hash);
+ if (! val_hash) return;
+
+ var types = val_hash['group onevent'] || {submit:1};
+ if (typeof(types) == 'string') types = types.split(/\s*,\s*/);
+ if (typeof(types.length) != 'undefined') {
+  var t = {};
+  for (var i = 0; i < types.length; i++) t[types[i]] = 1;
+  types = t;
+ }
+ val_hash['group onevent'] = types;
+
+ if (types.change || types.blur) {
+  var clean = v_clean_val_hash(val_hash);
+  if (clean.error) return clean.error;
+  var h = {};
+  _add = function (k, v) { if (! h[k]) h[k] = []; h[k].push(v) };
+  for (var i = 0; i < clean.fields.length; i++) {
+   _add(clean.fields[i].field, clean.fields[i]);
+   for (var j in clean.fields[i].deps) if (j != clean.fields[i].field) _add(j, clean.fields[i]);
+  }
+  for (var k in h) {
+   if (k == 'extend') continue; // Protoype Array()
+   var el = form[k];
+   if (! el) return v_error("No form element by the name "+k);
+   v_el_attach(el, h[k], form, val_hash);
+  }
+ }
+
+ if (types.submit) {
+  var orig_submit = form.onsubmit || function () { return true };
+  form.onsubmit = function (e) { return document.validate(this) && orig_submit(e, this) };
+ }
+}
 
- // attach handler
- var orig_submit = form.onsubmit || function () { return true };
- form.onsubmit = function (e) { return document.validate(this) && orig_submit(e, this) };
+function v_el_attach (el, fvs, form, val_hash) {
+ if (! el.type) {
+  if (el.length) for (var i = 0; i < el.length; i++) v_el_attach(el[i], fvs, form, val_hash);
+  return;
+ }
+ var types = val_hash['group onevent'];
+ var func = function () {
+  var e = [];
+  var f = {};
+  for (var i = 0; i < fvs.length; i++) {
+   var field_val = fvs[i];
+   var _e = v_validate_buddy(form, field_val.field, field_val);
+   for (var j = 0; j < _e.length; j++) e.push(_e[j]);
+   f[field_val.delegate_error || field_val.field] = 1;
+  }
+  if (! e.length) {
+    for (var k in f) v_inline_error_clear(k, val_hash, form);
+    return;
+  }
+  e = new ValidateError(e, {});
+  e = e.as_hash({as_hash_suffix:"", first_only:(val_hash['group first_only']?1:0)});
+  for (var k in e) {
+   if (k == 'extend') continue; // Protoype Array()
+   v_inline_error_set(k, e[k], val_hash, form);
+  }
+ };
+ if (types.blur) el.onblur = func;
+ if (types.change) {
+  if (el.type.match(/(password|text|textarea)/)) el.onkeyup = func;
+  else if (el.type.match(/(checkbox|radio)/)) el.onclick = func;
+  else if (el.type.match(/(select)/)) el.onchange = func;
+ }
 }
 
-// the end //
+function v_inline_error_clear (key, val_hash, form) {
+   delete(v_did_inline[key]);
+   var f = val_hash['group clear_hook'] || document.validate_clear_hook;
+   if (typeof(f) == 'function') if (f(key, val_hash, form)) return 1;
+   var el = document.getElementById(key + v_find_val('as_hash_suffix', val_hash, '_error'));
+   if (el) el.innerHTML = '';
+}
+
+function v_inline_error_set (key, val, val_hash, form) {
+   v_did_inline[key] = 1;
+   var f = val_hash['group set_hook'] || document.validate_set_hook;
+   if (typeof(f) == 'function') if(f(key, val, val_hash, form)) return 1;
+   var el = document.getElementById(key + v_find_val('as_hash_suffix', val_hash, '_error'));
+   if (el) el.innerHTML = val;
+}
index 73f6e812ccc0432265905bf209c6f8520fd56a9a..1729e52276807057e33ad9a82d1bf2652a2f6fbe 100755 (executable)
@@ -118,7 +118,7 @@ sub _success_file_print {
     <body>
     <h1 style='color:green'>Success</h1>
     <br>
-    print "I can now continue on with the rest of my script!";
+    I can now continue on with the rest of my script!
     </body>
     </html>
 };
diff --git a/samples/devel/memory_app.pl b/samples/devel/memory_app.pl
new file mode 100644 (file)
index 0000000..372b634
--- /dev/null
@@ -0,0 +1,391 @@
+#!/usr/bin/perl -w
+
+=head1 NAME
+
+memory_app.pl - Test memory usage and benchmark speed comparison with CGI::Application
+
+=cut
+
+use Benchmark qw(cmpthese timethese);
+use strict;
+
+my $swap = {
+    one   => "ONE",
+    two   => "TWO",
+    three => "THREE",
+    a_var => "a",
+    hash  => {a => 1, b => 2},
+    code  => sub {"($_[0])"},
+};
+
+my $form = q{([% has_errors %])(<TMPL_VAR has_errors>)<form name=foo><input type=text name="bar" value=""><input type=text name="baz"></form>};
+my $str_ht = $form . (q{Well hello there (<TMPL_VAR script_name>)} x 20) ."\n";
+my $str_tt = $form . (q{Well hello there ([% script_name %])}      x 20) ."\n";
+
+my $template_ht = \$str_ht;
+my $template_tt = \$str_tt;
+
+###----------------------------------------------------------------###
+use Scalar::Util;
+use Time::HiRes;
+use CGI;
+use CGI::Ex::Dump qw(debug);
+use Template::Alloy load => 'Parse', 'Play', 'HTML::Template', 'Template';
+$Template::VERSION = 2.18;
+#use HTML::Template;
+
+my $tests = {
+    'C::A - bare' => sub {
+        package FooBare;
+        require CGI::Application;
+        @FooBare::ISA = qw(CGI::Application);
+
+        sub setup {
+            my $self = shift;
+            $self->start_mode('main');
+            $self->mode_param(path_info => 1);
+            $self->run_modes(main => sub { "Simple test" });
+        }
+
+        FooBare->new->run;
+    },
+    'C::E::A - bare' => sub {
+        package FooBare;
+        require CGI::Ex::App;
+        @FooBare::ISA = qw(CGI::Ex::App);
+
+        sub main_run_step {
+            my $self = shift;
+            print "Content-Type: text/html\r\n\r\n";
+            #$self->cgix->print_content_type;
+            print "Simple test";
+            1;
+        }
+
+        FooBare->navigate({form => {}});
+    },
+    'Handwritten - bare' => sub {
+        package FooBare2;
+
+        sub new { bless {}, __PACKAGE__ }
+
+        sub main {
+            my $self = shift;
+            print "Content-Type: text/html\r\n\r\n";
+            print "Simple test";
+        }
+
+        FooBare2->new->main;
+    },
+    #'CGI::Prototype - bare' => sub {
+    #    package FooBare;
+    #    require CGI::Prototype;
+    #},
+
+    ###----------------------------------------------------------------###
+
+    #'C::A - simple htonly' => sub {
+    #    require CGI::Application;
+    #    my $t = CGI::Application->new->load_tmpl($template_ht, die_on_bad_params => 0);
+    #    $t->param(script_name => 2);
+    #    print $t->output;
+    #},
+    #'C::E::A - simple htonly' => sub {
+    #    require CGI::Ex::App;
+    #    my $out = '';
+    #    CGI::Ex::App->new->template_obj({SYNTAX => 'hte'})->process($template_ht, {script_name=>2}, \$out);
+    #    print $out;
+    #},
+
+    'C::A - simple ht' => sub {
+        package FooHT;
+        require CGI::Application;
+        @FooHT::ISA = qw(CGI::Application);
+
+        sub setup {
+            my $self = shift;
+            $self->start_mode('main');
+            $self->mode_param(path_info => 1);
+            $self->run_modes(main => sub {
+                my $self = shift;
+                my $t = $self->load_tmpl($template_ht, die_on_bad_params => 0);
+                $t->param('script_name', $0);
+                return $t->output();
+            });
+        }
+
+        FooHT->new->run;
+    },
+    'C::E::A - simple ht' => sub {
+        package FooHT;
+        require CGI::Ex::App;
+        @FooHT::ISA = qw(CGI::Ex::App);
+
+        sub main_file_print { $template_ht }
+        sub template_args { {SYNTAX => 'hte'} } # , GLOBAL_CACHE => 1, COMPILE_PERL => 2} }
+        sub fill_template {}
+        sub print_out { my ($self, $step, $out) = @_; print "Content-Type: text/html\r\n\r\n$$out" }
+
+        FooHT->navigate({no_history => 1, form => {}});
+    },
+    'C::A - simple tt' => sub {
+        package FooTT;
+        require CGI::Application;
+        @FooTT::ISA = qw(CGI::Application);
+        require CGI::Application::Plugin::TT;
+        CGI::Application::Plugin::TT->import;
+
+        sub setup {
+            my $self = shift;
+            $self->start_mode('main');
+
+            $self->run_modes(main => sub {
+                my $self = shift;
+                return $self->tt_process($template_tt, {script_name => $0});
+            });
+        }
+
+        FooTT->new->run;
+    },
+    'C::E::A - simple tt' => sub {
+        package FooTT;
+        require CGI::Ex::App;
+        @FooTT::ISA = qw(CGI::Ex::App);
+        sub main_file_print { $template_tt }
+        sub fill_template {}
+        sub print_out { my ($self, $step, $out) = @_; print "Content-Type: text/html\r\n\r\n$$out" }
+        FooTT->navigate({no_history => 1, form => {}});
+    },
+
+    ###----------------------------------------------------------------###
+
+    'C::A - complex ht' => sub {
+        package FooComplexHT;
+        require CGI::Application;
+        @FooComplexHT::ISA = qw(CGI::Application);
+        require CGI::Application::Plugin::ValidateRM;
+        CGI::Application::Plugin::ValidateRM->import('check_rm');
+        require CGI::Application::Plugin::FillInForm;
+        CGI::Application::Plugin::FillInForm->import('fill_form');
+
+        sub setup {
+            my $self = shift;
+            $self->start_mode('main');
+            $self->mode_param(path_info => 1);
+            $self->run_modes(main => sub {
+                my $self = shift;
+                my ($results, $err_page) = $self->check_rm('error_page','_profile');
+                return $err_page if $err_page;
+                die "Got here";
+            });
+        }
+
+        sub error_page {
+            my $self = shift;
+            my $errs = shift;
+            my $t = $self->load_tmpl($template_ht, die_on_bad_params => 0);
+            $t->param('script_name', $0);
+            $t->param($errs) if $errs;
+            $t->param(has_errors => 1) if $errs;
+            my $q = $self->query;
+            $q->param(bar => 'BAROOSELVELT');
+            return $self->fill_form(\$t->output, $q);
+        }
+
+        sub _profile { return {required => [qw(bar baz)], msgs => {prefix => 'err_'}} };
+
+        FooComplexHT->new->run;
+    },
+    'C::E::A - complex ht' => sub {
+        package FooComplexHT;
+        require CGI::Ex::App;
+        @FooComplexHT::ISA = qw(CGI::Ex::App);
+
+        sub main_file_print { $template_ht }
+        sub main_hash_fill  { {bar => 'BAROOSELVELT'} }
+        sub main_hash_validation { {bar => {required => 1}, baz => {required => 1}} }
+        sub main_finalize { die "Got here" }
+        sub template_args { {SYNTAX => 'hte'} } # , GLOBAL_CACHE => 1, COMPILE_PERL => 2} }
+        sub print_out { my ($self, $step, $out) = @_; print "Content-Type: text/html\r\n\r\n$$out" }
+
+        local $ENV{'REQUEST_METHOD'} = 'POST';
+        FooComplexHT->navigate({no_history => 1, form => {}});
+    },
+    'C::A - complex tt' => sub {
+        package FooComplexTT;
+        require CGI::Application;
+        @FooComplexTT::ISA = qw(CGI::Application);
+        require CGI::Application::Plugin::TT;
+        CGI::Application::Plugin::TT->import;
+        require CGI::Application::Plugin::ValidateRM;
+        CGI::Application::Plugin::ValidateRM->import('check_rm');
+        require CGI::Application::Plugin::FillInForm;
+        CGI::Application::Plugin::FillInForm->import('fill_form');
+
+        sub setup {
+            my $self = shift;
+            $self->start_mode('main');
+
+            $self->run_modes(main => sub {
+                my $self = shift;
+                my ($results, $err_page) = $self->check_rm('error_page','_profile');
+                return $err_page if $err_page;
+                die "Got here";
+            });
+        }
+
+        sub error_page {
+            my $self = shift;
+            my $errs = shift;
+            my $out = $self->tt_process($template_tt, {script_name => $0, %{$errs || {}}, has_errors => ($errs ? 1 : 0)});
+            my $q = $self->query;
+            $q->param(bar => 'BAROOSELVELT');
+            return $self->fill_form(\$out, $q);
+        }
+
+        sub _profile { return {required => [qw(bar baz)], msgs => {prefix => 'err_'}} };
+
+        FooComplexTT->new->run;
+    },
+    'C::E::A - complex tt' => sub {
+        package FooComplexTT;
+        require CGI::Ex::App;
+        @FooComplexTT::ISA = qw(CGI::Ex::App);
+        sub main_file_print { $template_tt }
+        sub main_hash_fill  { {bar => 'BAROOSELVELT'} }
+        sub main_hash_validation { {bar => {required => 1}, baz => {required => 1}} }
+        sub main_finalize { die "Got here" }
+        sub print_out { my ($self, $step, $out) = @_; print "Content-Type: text/html\r\n\r\n$$out" }
+
+        local $ENV{'REQUEST_METHOD'} = 'POST';
+        FooComplexTT->navigate({no_history => 1, form => {}});
+    },
+
+    #'Template::Alloy - bare ht' => sub { require Template::Alloy; Template::Alloy->import('HTE') },
+    #'Template::Alloy - bare tt' => sub { require Template::Alloy; Template::Alloy->import('TT') },
+};
+
+#perl -d:DProf samples/devel/memory_app.pl ; dprofpp tmon.out
+#select($_) if open($_, ">>/dev/null");
+$tests->{'C::E::A - complex tt'}->()
+#    for 1 .. 1000
+    ;
+#exit;
+
+###----------------------------------------------------------------###
+
+my %_INC = %INC;
+my @pids;
+foreach my $name (sort keys %$tests) {
+    my $pid = fork;
+    if (! $pid) {
+        $0 = "$0 - $name";
+        my $fh;
+        select($fh) if open($fh, ">>/dev/null");
+        $tests->{$name}->() for 1 .. 1;
+        sleep 1;
+        select STDOUT;
+        print "$name times: (@{[times]})\n";
+        print "$name $_\n" foreach sort grep {! $_INC{$_}} keys %INC;
+        sleep 15;
+        exit;
+    }
+    push @pids, $pid;
+}
+
+sleep 2;
+#    print "Parent - $_\n" foreach sort keys %INC;
+print grep {/\Q$0\E/} `ps fauwx`;
+kill 15, @pids;
+
+###----------------------------------------------------------------###
+
+exit if grep {/no_?bench/i} @ARGV;
+
+
+foreach my $type (qw(bare simple complex)) {
+    my $hash = {};
+    open(my $fh, ">>/dev/null") || die "Can't access /dev/null: $!";
+    foreach my $name (keys %$tests) {
+        next if $name !~ /\b$type\b/;
+        (my $copy = $name) =~ s/\s*\b$type\b//;
+        $hash->{$copy} = sub {
+            select $fh;
+            $tests->{$name}->();
+            select STDOUT;
+        };
+    }
+    print "-------------------------------------------------\n";
+    print "--- Testing $type\n";
+    cmpthese timethese -2, $hash;
+}
+
+=head1 NOTES
+
+Abbreviations:
+
+  C::E::A - CGI::Ex::App
+  C::A    - CGI::Application
+
+The tests are currently run with the following code:
+
+  use Template::Alloy load => 'Parse', 'Play', 'HTML::Template', 'Template';
+
+This assures that CGI::Application will use the same templating system
+as CGI::Ex::App so that template system issues don't affect overall
+performance.  With the line commented out and CGI::Application using
+HTML::Template (ht), C::A has a slight speed benefit, though it still
+uses more memory.  With the line commented out and CGI::Application
+using Template (tt), C::E::A is 2 to 3 times faster and uses a lot
+less memory.
+
+=head1 SAMPLE OUTPUT
+
+  paul     23927  4.3  0.5   8536  6016 pts/1    S+   11:36   0:00  |       \_ perl samples/devel/memory_app.pl
+  paul     23928  1.0  0.5   8988  5992 pts/1    S+   11:36   0:00  |           \_ samples/devel/memory_app.pl - C::A - bare
+  paul     23929  2.0  0.6   9988  7152 pts/1    S+   11:36   0:00  |           \_ samples/devel/memory_app.pl - C::A - complex ht
+  paul     23930  2.5  0.7  10172  7336 pts/1    S+   11:36   0:00  |           \_ samples/devel/memory_app.pl - C::A - complex tt
+  paul     23931  1.0  0.5   8988  6024 pts/1    S+   11:36   0:00  |           \_ samples/devel/memory_app.pl - C::A - simple ht
+  paul     23932  1.5  0.6   9308  6276 pts/1    S+   11:36   0:00  |           \_ samples/devel/memory_app.pl - C::A - simple tt
+  paul     23933  0.0  0.5   8536  5200 pts/1    S+   11:36   0:00  |           \_ samples/devel/memory_app.pl - C::E::A - bare
+  paul     23934  1.0  0.6   9328  6384 pts/1    S+   11:36   0:00  |           \_ samples/devel/memory_app.pl - C::E::A - complex ht
+  paul     23935  1.0  0.6   9328  6392 pts/1    S+   11:36   0:00  |           \_ samples/devel/memory_app.pl - C::E::A - complex tt
+  paul     23936  0.0  0.5   8536  5272 pts/1    S+   11:36   0:00  |           \_ samples/devel/memory_app.pl - C::E::A - simple ht
+  paul     23937  0.0  0.5   8668  5344 pts/1    S+   11:36   0:00  |           \_ samples/devel/memory_app.pl - C::E::A - simple tt
+  paul     23938  0.0  0.4   8536  5076 pts/1    S+   11:36   0:00  |           \_ samples/devel/memory_app.pl - Handwritten - bare
+  -------------------------------------------------
+  --- Testing bare
+  Benchmark: running C::A -, C::E::A -, Handwritten - for at least 2 CPU seconds...
+      C::A -:  3 wallclock secs ( 2.08 usr +  0.01 sys =  2.09 CPU) @ 3196.17/s (n=6680)
+   C::E::A -:  3 wallclock secs ( 1.99 usr +  0.19 sys =  2.18 CPU) @ 6164.68/s (n=13439)
+  Handwritten -:  1 wallclock secs ( 2.15 usr +  0.00 sys =  2.15 CPU) @ 266711.16/s (n=573429)
+                    Rate        C::A -     C::E::A - Handwritten -
+  C::A -          3196/s            --          -48%          -99%
+  C::E::A -       6165/s           93%            --          -98%
+  Handwritten - 266711/s         8245%         4226%            --
+  -------------------------------------------------
+  --- Testing simple
+  Benchmark: running C::A - ht, C::A - tt, C::E::A - ht, C::E::A - tt for at least 2 CPU seconds...
+   C::A - ht:  2 wallclock secs ( 2.04 usr +  0.00 sys =  2.04 CPU) @ 709.80/s (n=1448)
+   C::A - tt:  2 wallclock secs ( 2.12 usr +  0.01 sys =  2.13 CPU) @ 600.47/s (n=1279)
+  C::E::A - ht:  2 wallclock secs ( 2.14 usr +  0.01 sys =  2.15 CPU) @ 663.26/s (n=1426)
+  C::E::A - tt:  3 wallclock secs ( 2.16 usr +  0.01 sys =  2.17 CPU) @ 589.40/s (n=1279)
+                Rate C::E::A - tt    C::A - tt C::E::A - ht    C::A - ht
+  C::E::A - tt 589/s           --          -2%         -11%         -17%
+  C::A - tt    600/s           2%           --          -9%         -15%
+  C::E::A - ht 663/s          13%          10%           --          -7%
+  C::A - ht    710/s          20%          18%           7%           --
+  -------------------------------------------------
+  --- Testing complex
+  Benchmark: running C::A - ht, C::A - tt, C::E::A - ht, C::E::A - tt for at least 2 CPU seconds...
+   C::A - ht:  2 wallclock secs ( 2.00 usr +  0.00 sys =  2.00 CPU) @ 438.50/s (n=877)
+   C::A - tt:  3 wallclock secs ( 2.16 usr +  0.00 sys =  2.16 CPU) @ 383.80/s (n=829)
+  C::E::A - ht:  2 wallclock secs ( 2.14 usr +  0.01 sys =  2.15 CPU) @ 457.21/s (n=983)
+  C::E::A - tt:  2 wallclock secs ( 2.13 usr +  0.00 sys =  2.13 CPU) @ 417.37/s (n=889)
+                Rate    C::A - tt C::E::A - tt    C::A - ht C::E::A - ht
+  C::A - tt    384/s           --          -8%         -12%         -16%
+  C::E::A - tt 417/s           9%           --          -5%          -9%
+  C::A - ht    438/s          14%           5%           --          -4%
+  C::E::A - ht 457/s          19%          10%           4%           --
+
+=cut
diff --git a/samples/validate_js_0_tests.html b/samples/validate_js_0_tests.html
new file mode 100644 (file)
index 0000000..e9c6604
--- /dev/null
@@ -0,0 +1,430 @@
+<html>
+<head>
+<title>CGI/Ex/validate.js tests</title>
+<script src="../lib/CGI/Ex/validate.js"></script>
+</style>
+</head>
+<body>
+<h1>CGI/Ex/validate.js tests</h1>
+<div style="background:#eee">
+Test Form
+<form name=the_form>
+<input type=text     size=3 name=text1>
+<input type=text     size=3 name=text2>
+<input type=text     size=3 name=text3>
+<input type=text     size=3 name=text3>
+<input type=password size=3 name=password1>
+<input type=hidden   name=hidden1>
+<select name=select1><option>foo</option></select>
+<select name=select2><option value="foo">foo</option></select>
+<select name=select3><option value="foo">foo</option><option value="bar">bar</option></select>
+<select name=select4 multiple=multiple><option value="foo">foo</option><option value="bar">bar</option></select>
+<textarea name=textarea1 rows=1 cols=3></textarea>
+<input type=checkbox name=checkbox1 value=1>
+<input type=checkbox name=checkbox2 value=1><input type=checkbox name=checkbox2 value=2>
+<input type=radio name=radio1 value=1>
+<input type=radio name=radio2 value=1><input type=radio name=radio2 value=2>
+</form>
+</div>
+<div id="output"></div>
+</body>
+</html>
+<script>
+var ok_i = 0;
+var nok  = 0;
+function ok (is_ok, str) {
+  if (! is_ok) nok++;
+  str = ""+ (is_ok ? "ok" : "not ok") +" "+ (++ok_i) +" - "+ str;
+  str = "<span style=color:"+(is_ok ? "green" : "red")+">"+ str +"</span><br>\n";
+  var el = document.getElementById("output")
+  el.innerHTML = str + el.innerHTML;
+}
+
+function validate (vars, vals) {
+  var f = document.the_form;
+  f.text1.value     = typeof(vars.text1    ) != 'undefined' ? vars.text1     : '';
+  f.text2.value     = typeof(vars.text2    ) != 'undefined' ? vars.text2     : '';
+  f.textarea1.value = typeof(vars.textarea1) != 'undefined' ? vars.textarea1 : '';
+  f.password1.value = typeof(vars.password1) != 'undefined' ? vars.password1 : '';
+
+  for (var j = 0; j < f.checkbox2.length; j++)
+     f.checkbox2[j].checked = false;
+  if (vars.checkbox2)
+    for (var i = 0; i < vars.checkbox2.length; i++)
+      for (var j = 0; j < f.checkbox2.length; j++)
+        if (vars.checkbox2[i] == f.checkbox2[j].value) f.checkbox2[j].checked = true;
+
+  var err_obj = v_validate(f, vals);
+  if (err_obj) return err_obj.as_hash();
+  return false;
+}
+
+function run_tests () {
+  var begin = new Date();
+  begin = begin.getTime();
+
+  ok(document.validate, "Found document.validate function");
+  ok(v_get_form_value, "Found v_get_form_value function ");
+
+  var form = document.the_form;
+  ok(form, "Got the form");
+
+  // required
+  var e = validate({}, {text1:{required:1}});
+  ok(e, "Got required error");
+  ok(e.text1_error == "The field text1 is required.", "Got the right required error");
+
+  e = validate({text1:1}, {text1:{required:1}});
+  ok(! e, "Got no required error");
+
+  // validate_if
+
+  e = validate({}, {text1:{required:1, validate_if:'text2'}});
+  ok(! e, "Got no error on validate_if");
+  e = validate({text2:1}, {text1:{required:1, validate_if:'text2'}});
+  ok(e, "Got validate_if error");
+  ok(e.text1_error == "The field text1 is required.", "Got the right required error");
+
+  // required_if
+  e = validate({}, {text1:{required_if:'text2'}});
+  ok(! e, "No required_if error");
+  e = validate({text2:1}, {text1:{required_if:'text2'}});
+  ok(e, "Required_if error");
+  ok(e.text1_error == "The field text1 is required.", "Got the right required error");
+
+  // max_values
+  e = validate({checkbox2:[1,2]}, {checkbox2:{required:1}});
+  ok(e, "Got max_values error "+e.checkbox2_error);
+  ok(e.checkbox2_error == "The field checkbox2 had more than 1 value.", "Got the right max_values error");
+  e = validate({checkbox2:[1]}, {checkbox2:{max_values:2}});
+  ok(! e, "No max_values error");
+  e = validate({checkbox2:[1,2]}, {checkbox2:{max_values:2}});
+  ok(! e, "No max_values error");
+
+  // min_values
+  e = validate({checkbox2:[1]}, {checkbox2:{min_values:2}});
+  ok(e, "Got min_values error");
+  ok(e.checkbox2_error == "The field checkbox2 had less than 2 values.", "Got the right min_values error");
+  e = validate({checkbox2:[1,2]}, {checkbox2:{min_values:2, max_values:10}});
+  ok(! e, "No min_values error");
+
+  // enum
+  var v = {text1:{'enum':[1, 2, 3]}};
+  e = validate({}, v);
+  ok(e, "Got enum error");
+  ok(e.text1_error == "The field text1 is not in the given list.", "Got the right enum error");
+  e = validate({text1:1}, v);
+  ok(! e, "No enum error");
+  e = validate({text1:4}, v);
+  ok(e, "Got enum error");
+
+  v = {text2:{'enum':"1 || 2||3"}};
+  e = validate({text2:1}, v);
+  ok(! e, "No enum error");
+  e = validate({text2:4}, v);
+  ok(e, "Got enum error");
+
+  // equals
+  v = {text1:{equals:'text2'}};
+  e = validate({}, v);
+  ok(! e, 'No equals error');
+
+  e = validate({text1:1}, v);
+  ok(e, "Got an equals error");
+  ok(e.text1_error == "The field text1 did not equal the field text2.", "Got the right equals error");
+  e = validate({text1:1, text2:2}, v);
+  ok(e, "Got equals error");
+  e = validate({text1:1, text2:1}, v);
+  ok(! e, "No equals error");
+  v = {text1:{equals:'"text2"'}};
+  e = validate({text1:1, text2:1}, v);
+  ok(e, "Got equals error");
+  e = validate({text1:'text2', text2:1}, v);
+  ok(! e, "No equals error");
+
+  //min_len
+  v = {text1:{min_len:1}};
+  e = validate({}, v);
+  ok(e, "Got min_len error");
+  ok(e.text1_error == "The field text1 was less than 1 character.", "Got the right min_len error");
+  v = {text1:{min_len:10}};
+  e = validate({}, v);
+  ok(e, "Got min_len error");
+  ok(e.text1_error == "The field text1 was less than 10 characters.", "Got the right min_len error");
+  e = validate({text1:"123456789"}, v);
+  ok(e, "Got a min_len error");
+  e = validate({text1:"1234567890"}, v);
+  ok(! e, "No min_len error");
+
+  // max_len
+  v = {text1:{max_len:10}};
+  e = validate({}, v);
+  ok(! e, "No max_len error");
+  e = validate({text1:""}, v);
+  ok(! e, "No max_len error");
+  e = validate({text1:"1234567890"}, v);
+  ok(! e, "No max_len error");
+  e = validate({text1:"12345678901"}, v);
+  ok(e, "Got a max_len error");
+  ok(e.text1_error == "The field text1 was more than 10 characters.", "Got the right max_len error");
+  v = {text1:{max_len:1}};
+  e = validate({text1:"12345678901"}, v);
+  ok(e, "Got a max_len error");
+  ok(e.text1_error == "The field text1 was more than 1 character.", "Got the right max_len error");
+
+  // match
+  v = {text1:{match:'m/^\\w+$/'}};
+  e = validate({text1:"abc"}, v);
+  ok(! e, "No match error");
+  e = validate({text1:"abc."}, v);
+  ok(e, "Got a match error");
+  ok(e.text1_error == "The field text1 contains invalid characters.", "Got the right match error");
+
+  v = {text1:{match:['m/^\\w+$/', 'm/^[a-z]+$/']}};
+  e = validate({text1:"abc"}, v);
+  ok(! e, "No match error with multiple");
+  e = validate({text1:"abc1"}, v);
+  ok(e, "Got a match error with multiple");
+
+  v = {text1:{match:'m/^\\w+$/ || m/^[a-z]+$/'}};
+  e = validate({text1:"abc"}, v);
+  ok(! e, "No match error with multiple match string");
+  e = validate({text1:"abc1"}, v);
+  ok(e, "Got a match error with multiple match string");
+
+  v = {text1:{match:'! m/^\\w+$/'}};
+  e = validate({text1:"abc"}, v);
+  ok(e, "Got a match error with not match string");
+  e = validate({text1:"abc."}, v);
+  ok(! e, "No match error with not match string");
+
+  v = {text1:{match:'m/^\\w+$/'}};
+  e = validate({}, v);
+  ok(e, "Got an error with non-existing field");
+  v = {text1:{match:'! m/^\w+$/'}};
+  e = validate({}, v);
+  ok(! e, "No match error with non-existing field and not match string");
+
+  // compare
+  v = {text1:{compare:'> 0'}};
+  e = validate({}, v);
+  ok(e, "Got an error");
+  ok(e.text1_error == "The field text1 did not fit comparison.", "Got the right compare error");
+  v = {text1:{compare:'== 0'}};
+  e = validate({}, v);
+  ok(! e, "No compare error == 0");
+  v = {text1:{compare:'== 1'}};
+  e = validate({}, v);
+  ok(e, "Got compare error == 1");
+  v = {text1:{compare:'< 0'}};
+  e = validate({}, v);
+  ok(e, "Got compare error < 0");
+  v = {text1:{compare:'> 10'}};
+  e = validate({text1:11}, v);
+  ok(! e, "No compare error > 10");
+  e = validate({text1:10}, v);
+  ok(e, "Got compare error > 10");
+  v = {text1:{compare:'== 10'}};
+  e = validate({text1:11}, v);
+  ok(e, "Got compare error == 10");
+  e = validate({text1:10}, v);
+  ok(! e, "No compare error == 10");
+  v = {text1:{compare:'< 10'}};
+  e = validate({text1:9}, v);
+  ok(! e, "No compare error < 10");
+  e = validate({text1:10}, v);
+  ok(e, "Got compare error < 10");
+  v = {text1:{compare:'>= 10'}};
+  e = validate({text1:10}, v);
+  ok(! e, "No compare error >= 10");
+  e = validate({text1:9}, v);
+  ok(e, "Got compare error >= 10");
+  v = {text1:{compare:'!= 10'}};
+  e = validate({text1:10}, v);
+  ok(e, "Got compare error >= 10");
+  e = validate({text1:9}, v);
+  ok(! e, "No compare error >= 10");
+  v = {text1:{compare:'<= 10'}};
+  e = validate({text1:11}, v);
+  ok(e, "Got compare error <= 10");
+  e = validate({text1:10}, v);
+  ok(! e, "No compare error <= 10");
+  v = {text1:{compare:'gt ""'}};
+  e = validate({}, v);
+  ok(e, "Got compare error gt ''");
+  v = {text1:{compare:'eq ""'}};
+  e = validate({}, v);
+  ok(! e, "No compare error eq ''");
+  v = {text1:{compare:'lt ""'}};
+  e = validate({}, v);
+  ok(e, "Got compare error lt ''");
+  v = {text1:{compare:'gt "c"'}};
+  e = validate({text1:'d'}, v);
+  ok(! e, "No compare error gt 'c'");
+  e = validate({text1:'c'}, v);
+  ok(e, "Got compare error gt 'c'");
+  v = {text1:{compare:'eq c'}};
+  e = validate({text1:'d'}, v);
+  ok(e, "Got compare error eq c");
+  e = validate({text1:'c'}, v);
+  ok(! e, "No compare error lt c");
+  v = {text1:{compare:'lt c'}};
+  e = validate({text1:'b'}, v);
+  ok(! e, "No compare error lt c");
+  e = validate({text1:'c'}, v);
+  ok(e, "Got compare error lt c");
+  v = {text1:{compare:'ge c'}};
+  e = validate({text1:'c'}, v);
+  ok(! e, "No compare error ge c");
+  e = validate({text1:'b'}, v);
+  ok(e, "Got compare error ge c");
+  v = {text1:{compare:'ne c'}};
+  e = validate({text1:'c'}, v);
+  ok(e, "Got compare error ne c");
+  e = validate({text1:'b'}, v);
+  ok(! e, "No compare error ne c");
+  v = {text1:{compare:'le c'}};
+  e = validate({text1:'d'}, v);
+  ok(e, "Got compare error le c");
+  e = validate({text1:'c'}, v);
+  ok(! e, "No compare error le c");
+
+  // custom_js
+  v = {text1:{custom_js:" value ? true : false "}};
+  e = validate({}, v);
+  ok(e, "Got a custom_js error for eval type");
+  ok(e.text1_error == "The field text1 did not match custom_js check.", "Got the right custom_js error for eval type");
+  e = validate({text1:"str"}, v);
+  ok(! e, "No custom_js error for eval type");
+  v = {text1:{custom_js:function (args) { return args.value ? true : false }}};
+  e = validate({}, v);
+  ok(e, "Got a custom_js error for function type");
+  ok(e.text1_error == "The field text1 did not match custom_js check.", "Got the right custom_js error for function type");
+  e = validate({text1:"str"}, v);
+  ok(! e, "No custom_js error for function type");
+
+  // type checks
+  v = {text1:{type:'ip'}};
+  e = validate({text1:'209.108.25'}, v);
+  ok(e, "Got type ip error");
+  ok(e.text1_error == "The field text1 did not match type ip.", "Got the right type ip error");
+  e = validate({text1:'209.108.25.111'}, v);
+  ok(! e, "No type ip error");
+  v = {text1:{type:'email'}};
+  e = validate({text1:'foo.bar.com'}, v)
+  ok(e, "Got an email error");
+  ok(e.text1_error == "The field text1 did not match type email.", "Got the right type email error");
+  e = validate({text1:'f oo@bar.com'}, v)
+  ok(e, "Got an email error");
+  ok(e.text1_error == "The field text1 did not match type email.", "Got the right type email error");
+  e = validate({text1:'foo@bar.com'}, v)
+  ok(!e, "No email error");
+  e = validate({text1:'+foo@bar.com'}, v)
+  ok(!e, "No email error");
+  e = validate({text1:'+bar.com'}, {text1:{type:'domain'}});
+  ok(e, "Got a domain error");
+  e = validate({text1:'foo-.bar.com'}, {text1:{type:'domain'}});
+  ok(e, "Got a domain error");
+  e = validate({text1:'foo..bar.com'}, {text1:{type:'domain'}});
+  ok(e, "Got a domain error");
+  e = validate({text1:'.foo.bar.com'}, {text1:{type:'domain'}});
+  ok(e, "Got a domain error");
+  e = validate({text1:'foo.bar.com'}, {text1:{type:'domain'}});
+  ok(!e, "No domain error");
+  e = validate({text1:'http://bar.com/'}, {text1:{type:'url'}});
+  ok(!e, "No url error");
+  e = validate({text1:'https://bar.com/foo.html'}, {text1:{type:'url'}});
+  ok(!e, "No url error");
+  e = validate({text1:'http://bar.com.:8080/'}, {text1:{type:'url'}});
+  ok(!e, "No url error");
+  e = validate({text1:'http://bar.com/fo sdf'}, {text1:{type:'url'}});
+  ok(e, "Got url error");
+  e = validate({text1:'234234234'}, {text1:{type:'cc'}});
+  ok(e, "Got cc error");
+  e = validate({text1:'4242-4242-4242-4242'}, {text1:{type:'cc'}});
+  ok(!e, "No cc error");
+  e = validate({text1:'4242 4242 4242 4242'}, {text1:{type:'cc'}});
+  ok(!e, "No cc error");
+  e = validate({text1:'4241-4242-4242-4242'}, {text1:{type:'cc'}});
+  ok(e, "Got cc error");
+  e = validate({text1:'4241-4242-4242'}, {text1:{type:'cc'}});
+  ok(e, "Got cc error");
+
+  // min_in_set checks
+  v = {text1:{min_in_set:'2 of text1 text2 password1', max_values:5}};
+  e = validate({text1:1}, v);
+  ok(e, "Had a min_in_set error");
+  ok(e.text1_error == "Not enough fields were chosen from the set (2 of text1, text2, password1)", "Got the right error");
+  e = validate({text1:1, text2:1}, v);
+  ok(! e, "No min_in_set error");
+  e = validate({text1:1, text2:0}, v);
+  ok(! e, "No min_in_set error");
+
+  // max_in_set checks
+  v = {text1:{max_in_set:'2 of text1 text2 password1'}};
+  e = validate({text1:1}, v);
+  ok(! e, "No max_in_set error");
+  e = validate({text1:1, text2:1}, v);
+  ok(! e, "No max_in_set error");
+  e = validate({text1:1, text2:1, password1:1}, v);
+  ok(e, "Got a max_in_set error");
+
+  // validate_if revisited (but negated - uses max_in_set)
+  v = {text1:{required:1, validate_if:'! text2'}};
+  e = validate({}, v);
+  ok(e, "Got an error because validate_if failed");
+  e = validate({text2:1}, v);
+  ok(! e, "Didn't run required because of validate_if");
+
+  // default value
+  v = {text1:{required:1, 'default':'hmmmm'}};
+  e = validate({}, v);
+  ok(! e, "Didn't get an error because default value was found");
+  ok(document.the_form.text1.value == "hmmmm", "Default value got set in form");
+
+  // do_not_trim / case / replace
+  e = validate({text1:" hey "}, {text1:{required:1}});
+  ok(! e, "No error - just trimmed");
+  ok(document.the_form.text1.value == "hey", "Got right trimmed value ("+document.the_form.text1.value+")");
+  e = validate({text1:"hey\n"}, {text1:{required:1}});
+  ok(! e, "No error - just trimmed");
+  ok(document.the_form.text1.value == "hey", "Got right trimmed value ("+document.the_form.text1.value+")");
+  e = validate({text1:" "}, {text1:{required:1}});
+  ok(e, "Got a required error because chars where trimmed");
+  e = validate({text1:" "}, {text1:{required:1,do_not_trim:1}});
+  ok(! e, "No error because we didn't trim");
+  e = validate({textarea1:"hey\n"}, {textarea1:{required:1,do_not_trim:1}});
+  ok(! e, "No error - just trimmed");
+  ok(document.the_form.textarea1.value.match(/hey\r?\n/), "Got right trimmed value ("+document.the_form.textarea1.value+")");
+  e = validate({textarea1:"\they\n"}, {textarea1:{required:1,do_not_trim:1,trim_control_chars:1}});
+  ok(! e, "No error - just trimmed");
+  ok(document.the_form.textarea1.value == " hey", "Got right trimmed value ("+document.the_form.textarea1.value+")");
+  e = validate({textarea1:"hey"}, {textarea1:{to_upper_case:1}});
+  ok(! e, "No error - just upper cased");
+  ok(document.the_form.textarea1.value == "HEY", "Got right uppercase value");
+  e = validate({textarea1:"HEY"}, {textarea1:{to_lower_case:1}});
+  ok(! e, "No error - just lower cased");
+  ok(document.the_form.textarea1.value == "hey", "Got right lowercase value");
+
+  e = validate({textarea1:"wow"}, {textarea1:{replace:"s/w/W/g", replace2:"s/O/0/i"}});
+  ok(! e, "No error - just replaced");
+  ok(document.the_form.textarea1.value == "W0W", "Got right value ("+document.the_form.textarea1.value+")");
+
+
+  //alert(_form_value(form.text2));
+  //form.text2[0].value = "text1";
+  //form.text2[1].value = "text2";
+  //ok(1, "We can set and get values for duplicate named items");
+
+  var end = new Date();
+  end = end.getTime();
+  var sec = (end - begin)/1000;
+
+  var str = nok ? "<span style=color:red>Failed tests: "+nok+"</span><br>\n" : "<span style=color:green>All tests passed</span><br>\n";
+  str += "(Seconds: "+sec+")<br>\n";
+  var el = document.getElementById("output")
+  el.innerHTML = str + el.innerHTML;
+}
+
+window.onload = run_tests;
+</script>
diff --git a/samples/validate_js_1_onsubmit.html b/samples/validate_js_1_onsubmit.html
new file mode 100644 (file)
index 0000000..f420527
--- /dev/null
@@ -0,0 +1,207 @@
+<html>
+<style>
+.error {
+  color: red;
+  font-size: 75%;
+}
+</style>
+
+<script src="../lib/CGI/Ex/validate.js"></script>
+<script>
+if (location.search) {
+  document.writeln('<span style="color:green"><h1>Form Information submitted</h1></span>');
+}
+if (! document.validate) {
+  document.writeln('<span style="color:red"><h1>Missing document.validate</h1>Path to ../lib/CGI/Ex/validate.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.validate</h1></span>');
+}
+
+</script>
+
+
+<form name=a>
+<table>
+<tr>
+  <td valign=top>Username:</td>
+  <td>
+    <table border=0 cellspacing=0 cellpadding=0><tr><td><input type=text size=20 name=username></td><td> Try hitting enter rather than tab.</td></tr></table>
+    <span id=username_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Password:</td>
+  <td>
+    <input type=password size=20 name=password><br>
+    <span id=password_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Verify Password:</td>
+  <td>
+    <input type=password size=20 name=password2><br>
+    <span id=password2_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Email:</td>
+  <td>
+    <input type=text size=40 name=email><br>
+    <span id=email_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Verify Email:</td>
+  <td>
+    <input type=text size=40 name=email2><br>
+    <span id=email2_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>State/Region:</td>
+  <td>
+    Specify State <input type=text size=2 name=state><br>
+    OR Region <input type=text size=20 name=region>
+    <span id=state_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Enum Check:</td>
+  <td>
+    <input type=text size=10 name=enum><br>
+    <span id=enum_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Compare Check:</td>
+  <td>
+    <input type=text size=10 name=compare><br>
+    <span id=compare_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Check one:</td>
+  <td>
+    <input type=checkbox name=checkone value=1> Foo<br>
+    <input type=checkbox name=checkone value=2> Bar<br>
+    <input type=checkbox name=checkone value=3> Baz<br>
+    <span id=checkone_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Check two:</td>
+  <td>
+    <input type=checkbox name=checktwo value=1> Foo<br>
+    <input type=checkbox name=checktwo value=2> Bar<br>
+    <input type=checkbox name=checktwo value=3> Baz<br>
+    <span id=checktwo_error class=error></span>
+  </td>
+</tr>
+<tr><td colspan=2><hr></td></tr>
+<tr>
+  <td valign=top>Fill In two:</td>
+  <td>
+    <span id=foo_error class=error></span><br>
+    <input type=text name=foo value="" size=30> Foo<br>
+    <input type=text name=bar value="" size=30> Bar<br>
+    <input type=text name=baz value="" size=30> Baz<br>
+  </td>
+</tr>
+<tr>
+  <td colspan=2 align=right>
+    <input type=submit value=Submit>
+  </td>
+</tr>
+</table>
+</form>
+
+<script src="../lib/CGI/Ex/yaml_load.js"></script>
+<script src="../lib/CGI/Ex/validate.js"></script>
+<script>
+document.validation = {
+  "group no_confirm": 1,
+  "group no_alert": 1,
+  "group order": ["username", "password", "password2", "email", "email2", "state", "region", "s_r_combo", "enum", "compare", "checkone", "checktwo", "foo"],
+  username: {
+    name: "Username",
+    required: 1,
+    min_len: 3,
+    max_len: 30
+  },
+  password: {
+    name: "Password",
+    required: 1,
+    min_len: 6,
+    max_len: 30,
+    match: ["m/\\d/", "m/[a-z]/"],
+    match_error: "$name must contain both a letter and a number."
+  },
+  password2: {
+    name: "Verify password",
+    equals: "password",
+    equals_name: "password"
+  },
+  email: {
+    name: "Email",
+    required: 1,
+    max_len: 100,
+    type: 'email',
+    type_error: "$name must be a valid email address."
+  },
+  email2: {
+    name: "Verify email",
+    equals: "email",
+    equals_name: "email"
+  },
+  state: {
+    validate_if: ["state", "! region"],
+    match: "m/^\\w{2}$/",
+    match_error: "Please type a two letter state code."
+  },
+  region: {
+    validate_if: ["region", "! state"],
+    delegate_error: "state",
+    compare: "eq Manitoba",
+    compare_error: "For this test - the region should be Manitoba."
+  },
+  s_r_combo: {
+    field: "state",
+    delegate_error:   "state",
+    max_in_set:       "1 of state region",
+    max_in_set_error: "Specify only one of state and region.",
+    min_in_set:       "1 of state region",
+    min_in_set_error: "Specify one of state and region."
+  },
+  'enum': {
+    name: "Enum check",
+    'enum': ["one", "two", "three", "four"],
+    enum_error: "$name must be one of one, two, three, or four."
+  },
+  compare: {
+    required: 1,
+    required_error: "Please type a number",
+    replace: "s/\\D//g",
+    name: "Compare check",
+    compare: ['> 99', '< 1000'],
+    compare_error: '$name must be greater than 99 and less than 1000.'
+  },
+  checkone: {
+    name: "Check one",
+    required: 1,
+    max_values: 1
+  },
+  checktwo: {
+    name: "Check two",
+    min_values: 2,
+    max_values: 2
+  },
+  foo: {
+    min_in_set: "2 of foo bar baz",
+    max_in_set: "2 of foo bar baz"
+  }
+};
+if (document.check_form) document.check_form('a');
+</script>
+
+</html>
+<script>window.onload = function () { document.a.username.focus() }</script>
diff --git a/samples/validate_js_2_onchange.html b/samples/validate_js_2_onchange.html
new file mode 100644 (file)
index 0000000..68b8fc1
--- /dev/null
@@ -0,0 +1,232 @@
+<html>
+<style>
+.error {
+  color: red;
+  font-size: 75%;
+}
+</style>
+
+<script src="../lib/CGI/Ex/validate.js"></script>
+<script>
+if (location.search) {
+  document.writeln('<span style="color:green"><h1>Form Information submitted</h1></span>');
+}
+if (! document.validate) {
+  document.writeln('<span style="color:red"><h1>Missing document.validate</h1>Path to ../lib/CGI/Ex/validate.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.validate</h1></span>');
+}
+
+</script>
+
+
+<form name=a>
+<table cellspacing=0 cellpadding=3>
+<tr id=username_row>
+  <td valign=top>Username:</td>
+  <td><input type=text size=30 name=username></td>
+  <td id=username_img></td>
+  <td id=username_error class=error></td>
+</tr>
+<tr id=password_row>
+  <td valign=top>Password:</td>
+  <td><input type=password size=30 name=password><span id=password_strength style="font-size:smaller;color:blue"></span></td>
+  <td id=password_img></td>
+  <td id=password_error class=error></td>
+</tr>
+<tr id=password2_row>
+  <td valign=top>Verify Password:</td>
+  <td><input type=password size=30 name=password2></td>
+  <td id=password2_img></td>
+  <td id=password2_error class=error></td>
+</tr>
+<tr id=email_row>
+  <td valign=top>Email:</td>
+  <td><input type=text size=40 name=email></td>
+  <td id=email_img></td>
+  <td id=email_error class=error></td>
+  </td>
+</tr>
+<tr id=email2_row>
+  <td valign=top>Verify Email:</td>
+  <td><input type=text size=40 name=email2></td>
+  <td id=email2_img></td>
+  <td id=email2_error class=error></td>
+</tr>
+<tr id=state_row>
+  <td valign=top>State/Region:</td>
+  <td>
+    Specify State <input type=text size=2 name=state><br>
+    OR Region <input type=text size=20 name=region>
+  </td>
+  <td id=state_img></td>
+  <td id=state_error class=error></td>
+</tr>
+<tr id=enum_row>
+  <td valign=top>Enum Check:</td>
+  <td><input type=text size=10 name=enum></td>
+  <td id=enum_img></td>
+  <td id=enum_error class=error></td>
+</tr>
+<tr id=compare_row>
+  <td valign=top>Compare Check:</td>
+  <td><input type=text size=10 name=compare></td>
+  <td id=compare_img></td>
+  <td id=compare_error class=error></td>
+</tr>
+<tr id=checkone_row>
+  <td valign=top>Check one:</td>
+  <td>
+    <input type=checkbox name=checkone value=1> Foo<br>
+    <input type=checkbox name=checkone value=2> Bar<br>
+    <input type=checkbox name=checkone value=3> Baz<br>
+  </td>
+  <td id=checkone_img></td>
+  <td id=checkone_error class=error></td>
+</tr>
+<tr id=checktwo_row>
+  <td valign=top>Check two:</td>
+  <td>
+    <input type=checkbox name=checktwo value=1> Foo<br>
+    <input type=checkbox name=checktwo value=2> Bar<br>
+    <input type=checkbox name=checktwo value=3> Baz<br>
+  </td>
+  <td id=checktwo_img></td>
+  <td id=checktwo_error class=error></td>
+</tr>
+<tr><td colspan=2><hr></td></tr>
+<tr id=foo_row>
+  <td valign=top>Fill In two:</td>
+  <td>
+    <input type=text name=foo value="" size=30> Foo<br>
+    <input type=text name=bar value="" size=30> Bar<br>
+    <input type=text name=baz value="" size=30> Baz<br>
+  </td>
+  <td id=foo_img></td>
+  <td id=foo_error class=error></td>
+</tr>
+<tr>
+  <td colspan=2 align=right>
+    <input type=submit value=Submit>
+  </td>
+</tr>
+</table>
+</form>
+
+<script src="../lib/CGI/Ex/validate.js"></script>
+<script>
+document.validate_set_hook = function (key) {
+  document.getElementById(key+'_img').innerHTML = '<span style="font-weight:bold;color:red">!</span>';
+  document.getElementById(key+'_row').style.background = '#ffdddd';
+};
+document.validate_clear_hook = function (key) {
+  document.getElementById(key+'_img').innerHTML = '<span style="font-weight:bold;color:green">+</span>';
+  document.getElementById(key+'_row').style.background = '#ddffdd';
+};
+document.validation = {
+  "group onevent": 'blur,change,submit',
+  "group no_confirm": 1,
+  "group no_alert": 1,
+  "group order": ["username", "password", "password2", "email", "email2", "state", "region", "s_r_combo", "enum", "compare", "checkone", "checktwo", "foo"],
+  username: {
+    name: "Username",
+    required: 1,
+    min_len: 3,
+    max_len: 30
+  },
+  password: {
+    name: "Password",
+    required: 1,
+    min_len: 6,
+    max_len: 30,
+    match: ["m/\\d/", "m/[a-z]/"],
+    match_error: "$name must contain both a letter and a number."
+  },
+  password2: {
+    name: "Verify password",
+    equals: "password",
+    equals_name: "password"
+  },
+  email: {
+    name: "Email",
+    required: 1,
+    max_len: 100,
+    type: 'email',
+    type_error: "$name must be a valid email address."
+  },
+  email2: {
+    name: "Verify email",
+    equals: "email",
+    equals_name: "email"
+  },
+  state: {
+    validate_if: ["state", "! region"],
+    match: "m/^\\w{2}$/",
+    match_error: "Please type a two letter state code."
+  },
+  region: {
+    validate_if: ["region", "! state"],
+    delegate_error: "state",
+    compare: "eq Manitoba",
+    compare_error: "For this test - the region should be Manitoba."
+  },
+  s_r_combo: {
+    field: "state",
+    delegate_error:   "state",
+    max_in_set:       "1 of state region",
+    max_in_set_error: "Specify only one of state and region.",
+    min_in_set:       "1 of state region",
+    min_in_set_error: "Specify one of state and region."
+  },
+  'enum': {
+    name: "Enum check",
+    'enum': ["one", "two", "three", "four"],
+    enum_error: "$name must be one of one, two, three, or four."
+  },
+  compare: {
+    required: 1,
+    required_error: "Please type a number",
+    replace: "s/\\D//g",
+    name: "Compare check",
+    compare: ['> 99', '< 1000'],
+    compare_error: '$name must be greater than 99 and less than 1000.'
+  },
+  checkone: {
+    name: "Check one",
+    required: 1,
+    max_values: 1
+  },
+  checktwo: {
+    name: "Check two",
+    min_values: 2,
+    max_values: 2
+  },
+  foo: {
+    min_in_set: "2 of foo bar baz",
+    max_in_set: "2 of foo bar baz"
+  },
+  pw_strength: { // separate from the pw validation
+    field: "password",
+    custom_js: function (args) {
+      var v = args.value;
+      var n = 0;
+      if (v.match(/[a-z]/)) n++;
+      if (v.match(/[A-Z]/)) n++;
+      if (v.match(/[0-9]/)) n++;
+      var sym = v.match(/[ ~!@#$%^&*()_,.?{}\[\]]/) ? 1 : 0;
+      var s = (! v.length)   ? ''
+            : (v.length < 3) ? 'weak'
+            : (v.length < 5) ? (sym || n == 3)  ? 'ok' : 'weak'
+            : (v.length < 9) ? (n < 3 && ! sym) ? 'ok' : 'good'
+            : sym ? 'excellent' : 'good';
+      document.getElementById('password_strength').innerHTML = s;
+      // we could return false to indicate the strength wasn't good enough
+      return 1;
+    }
+  }
+};
+if (document.check_form) document.check_form('a');
+</script>
+
+</html>
+<script>window.onload = function () { document.a.username.focus() }</script>
diff --git a/samples/validate_js_yaml_1.html b/samples/validate_js_yaml_1.html
new file mode 100644 (file)
index 0000000..6187228
--- /dev/null
@@ -0,0 +1,206 @@
+<html>
+<style>
+.error {
+  color: red;
+  font-size: 75%;
+}
+</style>
+
+<script src="../lib/CGI/Ex/yaml_load.js"></script>
+<script src="../lib/CGI/Ex/validate.js"></script>
+<script src="./yaml_load.js"></script>
+<script src="./validate.js"></script>
+<script>
+if (! document.yaml_load) {
+  document.writeln('<span style="color:red"><h1>Missing document.yaml_load</h1>Path to ../lib/CGI/Ex/yaml_load.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.yaml_load</h1></span>');
+}
+
+if (! document.validate) {
+  document.writeln('<span style="color:red"><h1>Missing document.validate</h1>Path to ../lib/CGI/Ex/validate.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.validate</h1></span>');
+}
+
+</script>
+
+
+<form name=a>
+<table>
+<tr>
+  <td valign=top>Username:</td>
+  <td>
+    <table border=0 cellspacing=0 cellpadding=0><tr><td><input type=text size=20 name=username></td><td> Try hitting enter rather than tab.</td></tr></table>
+    <span id=username_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Password:</td>
+  <td>
+    <input type=password size=20 name=password><br>
+    <span id=password_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Verify Password:</td>
+  <td>
+    <input type=password size=20 name=password2><br>
+    <span id=password2_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Email:</td>
+  <td>
+    <input type=text size=40 name=email><br>
+    <span id=email_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Verify Email:</td>
+  <td>
+    <input type=text size=40 name=email2><br>
+    <span id=email2_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>State/Region:</td>
+  <td>
+    Specify State <input type=text size=2 name=state><br>
+    OR Region <input type=text size=20 name=region>
+    <span id=state_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Enum Check:</td>
+  <td>
+    <input type=text size=10 name=enum><br>
+    <span id=enum_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Compare Check:</td>
+  <td>
+    <input type=text size=10 name=compare><br>
+    <span id=compare_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Check one:</td>
+  <td>
+    <input type=checkbox name=checkone value=1> Foo<br>
+    <input type=checkbox name=checkone value=2> Bar<br>
+    <input type=checkbox name=checkone value=3> Baz<br>
+    <span id=checkone_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Check two:</td>
+  <td>
+    <input type=checkbox name=checktwo value=1> Foo<br>
+    <input type=checkbox name=checktwo value=2> Bar<br>
+    <input type=checkbox name=checktwo value=3> Baz<br>
+    <span id=checktwo_error class=error></span>
+  </td>
+</tr>
+<tr><td colspan=2><hr></td></tr>
+<tr>
+  <td valign=top>Fill In two:</td>
+  <td>
+    <span id=foo_error class=error></span><br>
+    <input type=text name=foo value="" size=30> Foo<br>
+    <input type=text name=bar value="" size=30> Bar<br>
+    <input type=text name=baz value="" size=30> Baz<br>
+  </td>
+</tr>
+<tr>
+  <td colspan=2 align=right>
+    <input type=submit>
+  </td>
+</tr>
+</table>
+</form>
+
+<script src="../lib/CGI/Ex/yaml_load.js"></script>
+<script src="../lib/CGI/Ex/validate.js"></script>
+<script>
+document.validation = "\n\
+#general no_inline: 1\n\
+general no_confirm: 1\n\
+general no_alert: 1\n\
+general as_array_prefix: ' -- '\n\
+#general as_hash_header: '<ul><li>'\n\
+#general as_hash_join:   '</li><li>'\n\
+#general as_hash_footer: '</li></ul>'\n\
+group order: [username, password, password2, email, email2, state, region, s_r_combo, enum, compare, checkone, checktwo, foo]\n\
+username:\n\
+ name: Username\n\
+ required: 1\n\
+ min_len: 3\n\
+ max_len: 30\n\
+password: &pa\n\
+ name: Password\n\
+ required: 1\n\
+ min_len: 6\n\
+ max_len: 30\n\
+ match: [m/\\d/, 'm/[a-z]/']\n\
+ match_error: '$name must contain both a letter and a number.'\n\
+password2:\n\
+ name: Verify password\n\
+ validate_if: *pa\n\
+ equals: password\n\
+ equals_name: password\n\
+email: &em\n\
+ name: Email\n\
+ required: 1\n\
+ max_len: 100\n\
+ match: 'm/^[^@]+@([\\w-]+\.)+\\w+$/'\n\
+ match_error: '$name must be a valid email address.'\n\
+email2:\n\
+ name: Verify email\n\
+ validate_if: *em\n\
+ equals: email\n\
+ equals_name: email\n\
+state:\n\
+ validate_if: [state, '! region']\n\
+ match: 'm/^\\w{2}$/'\n\
+ match_error: Please type a two letter state code.\n\
+region:\n\
+ validate_if: [region, '! state']\n\
+ delegate_error: state\n\
+ compare: 'eq Manitoba'\n\
+ compare_error: For this test - the region should be Manitoba.\n\
+s_r_combo:\n\
+ field: state\n\
+ delegate_error: state\n\
+ max_in_set: 1 of state region\n\
+ max_in_set_error: Specify only one of state and region.\n\
+ min_in_set: 1 of state region\n\
+ min_in_set_error: Specify one of state and region.\n\
+enum:\n\
+ name: Enum check\n\
+ enum: [one, two, three, four]\n\
+ enum_error: '$name must be one of one, two, three, or four.'\n\
+compare:\n\
+ required: 1\n\
+ replace: 's/\\D//g'\n\
+ name: Compare check\n\
+ compare: ['> 99', '< 1000']\n\
+ compare_error: '$name must be greater than 99 and less than 1000.'\n\
+checkone:\n\
+ name: Check one\n\
+ required: 1\n\
+ max_values: 1\n\
+checktwo:\n\
+ name: Check two\n\
+ min_values: 2\n\
+ max_values: 2\n\
+foo:\n\
+ min_in_set: 2 of foo bar baz\n\
+ max_in_set: 2 of foo bar baz\n\
+";
+if (document.check_form) document.check_form('a');
+</script>
+
+</html>
+<script>window.onload = function () { document.a.username.focus() }</script>
diff --git a/samples/validate_js_yaml_2.html b/samples/validate_js_yaml_2.html
new file mode 100644 (file)
index 0000000..1e777f3
--- /dev/null
@@ -0,0 +1,119 @@
+<html>
+<style>
+.error {
+  color: red;
+  font-size: 75%;
+}
+</style>
+
+<script src="../lib/CGI/Ex/yaml_load.js"></script>
+<script src="../lib/CGI/Ex/validate.js"></script>
+<script src="./yaml_load.js"></script>
+<script src="./validate.js"></script>
+<script>
+if (! document.yaml_load) {
+  document.writeln('<span style="color:red"><h1>Missing document.yaml_load</h1>Path to ../lib/CGI/Ex/yaml_load.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.yaml_load</h1></span>');
+}
+
+if (! document.validate) {
+  document.writeln('<span style="color:red"><h1>Missing document.validate</h1>Path to ../lib/CGI/Ex/validate.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.validate</h1></span>');
+}
+
+</script>
+
+
+<form name=a validation="
+general no_confirm: 1
+general no_alert: 1
+general as_array_prefix: ' -- '
+">
+<table>
+<tr>
+  <td valign=top>Username:</td>
+  <td>
+    <input type=text size=20 name=username validation="
+ name: Username
+ required: 1
+ min_len: 3
+ max_len: 30
+ match: 'm/^\w/'
+ match_error: '$name may contain only letters and numbers'
+"><br>
+    <span id=username_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Password:</td>
+  <td>
+    <input type=password size=20 name=password validation=" &pa
+ name: Password
+ required: 1
+ min_len: 6
+ max_len: 30
+ match: [m/\d/, 'm/[a-z]/']
+ match_error: '$name must contain both a letter and a number.'
+"><br>
+    <span id=password_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Verify Password:</td>
+  <td>
+    <input type=password size=20 name=password2 validation="{name: Verify password, validate_if: *pa,  equals: password, equals_name: password}"><br>
+    <span id=password2_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Email:</td>
+  <td>
+    <input type=text size=40 name=email validation="&em
+name: Email
+required: 1
+min_len: 6
+max_len: 100
+"><br>
+    <span id=email_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Verify Email:</td>
+  <td>
+    <input type=text size=40 name=email2 validation="
+name: Verify email
+validate_if: *em
+equals: email
+equals_name: email
+clear_on_error: 1
+"><br>
+    <span id=email2_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td valign=top>Random Association:</td>
+  <td>
+    <input type=text size=40 name=random validation="
+name: random
+default: bull sun orange
+"><br> (type anything - will fill in default if none)<br>
+    <span id=email2_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td colspan=2 align=right>
+    <input type=submit>
+  </td>
+</tr>
+</table>
+</form>
+
+<script src="../lib/CGI/Ex/yaml_load.js"></script>
+<script src="../lib/CGI/Ex/validate.js"></script>
+<script>
+if (document.check_form) document.check_form('a');
+</script>
+
+</html>
\ No newline at end of file
diff --git a/samples/validate_js_yaml_3.html b/samples/validate_js_yaml_3.html
new file mode 100644 (file)
index 0000000..02187d1
--- /dev/null
@@ -0,0 +1,74 @@
+<html>
+<style>
+.error {
+  color: red;
+  font-size: 75%;
+}
+</style>
+
+<script src="../lib/CGI/Ex/yaml_load.js"></script>
+<script src="../lib/CGI/Ex/validate.js"></script>
+<script src="./yaml_load.js"></script>
+<script src="./validate.js"></script>
+<script>
+if (! document.yaml_load) {
+  document.writeln('<span style="color:red"><h1>Missing document.yaml_load</h1>Path to ../lib/CGI/Ex/yaml_load.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.yaml_load</h1></span>');
+}
+
+if (! document.validate) {
+  document.writeln('<span style="color:red"><h1>Missing document.validate</h1>Path to ../lib/CGI/Ex/validate.js may be invalid.</span>');
+} else {
+  document.writeln('<span style="color:green"><h1>Found document.validate</h1></span>');
+}
+
+</script>
+
+
+<form name=a validation="
+general no_confirm: 1
+general no_alert: 1
+general as_array_prefix: ' -- '
+">
+<table>
+<tr>
+  <td valign=top>Enter a date (YYYY/MM/DD) greater than today:<br>
+    (<script>var t=new Date();document.writeln(t.toGMTString())</script>)
+  </td>
+  <td>
+    <input type=text size=20 name=date validation="
+ name: Date
+ required: 1
+ match: 'm|^\d\d\d\d/\d\d/\d\d$|'
+ match_error: 'Please enter date in YYYY/MM/DD format'
+ custom_js: |
+  function (args) {
+   var t=new Date();
+   var y=t.getYear()+1900;
+   var m=t.getMonth() + 1;
+   var d=t.getDate();
+   if (m<10) m = '0'+m;
+   if (d<10) d = '0'+d;
+   (args.value > ''+y+'/'+m+'/'+d) ? 1 : 0;
+  }
+ custom_js_error: The date was not greater than today.
+"><br>
+    <span id=date_error class=error></span>
+  </td>
+</tr>
+<tr>
+  <td colspan=2 align=right>
+    <input type=submit>
+  </td>
+</tr>
+</table>
+</form>
+
+<script src="../lib/CGI/Ex/yaml_load.js"></script>
+<script src="../lib/CGI/Ex/validate.js"></script>
+<script>
+if (document.check_form) document.check_form('a');
+</script>
+
+</html>
\ No newline at end of file
This page took 0.11971 seconds and 4 git commands to generate.