[Jifty-commit] r3705 - in jifty/trunk: . examples/Yada/etc examples/Yada/lib examples/Yada/lib/Yada examples/Yada/lib/Yada/View examples/Yada/share/web/static/js examples/Yada/share/web/static/js/Asynapse lib/Jifty lib/Jifty/Plugin/REST lib/Jifty/Plugin/SkeletonApp lib/Jifty/View/Declare share/web/static/js share/web/templates/__jifty/webservices t/clientside

jifty-commit at lists.jifty.org jifty-commit at lists.jifty.org
Fri Jul 20 10:00:25 EDT 2007


Author: clkao
Date: Fri Jul 20 10:00:23 2007
New Revision: 3705

Added:
   jifty/trunk/examples/Yada/lib/Yada.pm
   jifty/trunk/examples/Yada/share/web/static/js/
   jifty/trunk/examples/Yada/share/web/static/js/Asynapse/
   jifty/trunk/examples/Yada/share/web/static/js/Asynapse/REST.js
   jifty/trunk/lib/Jifty/View/Declare/Compile.pm
   jifty/trunk/share/web/static/js/template_declare.js
   jifty/trunk/t/clientside/
   jifty/trunk/t/clientside/td.t
Modified:
   jifty/trunk/   (props changed)
   jifty/trunk/Makefile.PL
   jifty/trunk/examples/Yada/etc/config.yml
   jifty/trunk/examples/Yada/lib/Yada/View.pm
   jifty/trunk/examples/Yada/lib/Yada/View/Todo.pm
   jifty/trunk/lib/Jifty/Plugin/REST/Dispatcher.pm
   jifty/trunk/lib/Jifty/Plugin/SkeletonApp/View.pm
   jifty/trunk/lib/Jifty/View/Declare/BaseClass.pm
   jifty/trunk/lib/Jifty/View/Declare/CRUD.pm
   jifty/trunk/lib/Jifty/Web.pm
   jifty/trunk/lib/Jifty/Web/PageRegion.pm
   jifty/trunk/share/web/static/js/jifty.js
   jifty/trunk/share/web/templates/__jifty/webservices/xml

Log:
Push milestone 1 of trimclient to trunk.


Modified: jifty/trunk/Makefile.PL
==============================================================================
--- jifty/trunk/Makefile.PL	(original)
+++ jifty/trunk/Makefile.PL	Fri Jul 20 10:00:23 2007
@@ -57,6 +57,7 @@
 requires('Module::Refresh');
 requires('Module::ScanDeps');
 requires('Object::Declare' => '0.13');
+requires('PadWalker');
 requires('Params::Validate');
 requires('Scalar::Defer' => '0.10');
 requires('Shell::Command');

Modified: jifty/trunk/examples/Yada/etc/config.yml
==============================================================================
--- jifty/trunk/examples/Yada/etc/config.yml	(original)
+++ jifty/trunk/examples/Yada/etc/config.yml	Fri Jul 20 10:00:23 2007
@@ -2,6 +2,7 @@
 application:
   OpenIDSecret: sekrit13
 framework: 
+  ConfigFileVersion: 2
   AdminMode: 1
   SkipAccessControl: 1
   ApplicationClass: Yada
@@ -16,7 +17,7 @@
     RecordBaseClass: Jifty::DBI::Record::Cachable
     User: ''
     Version: 0.0.1
-  DevelMode: 0
+  DevelMode: 1
   L10N: 
     PoDir: share/po
   LogLevel: INFO
@@ -34,11 +35,13 @@
     - User: {}
     - Authentication::Password: {}
     - OpenID: {}
+    - SinglePage: {}
 
   PubSub: 
     Backend: Memcached
     Enable: ~
   TemplateClass: Yada::View
+  ClientTemplate: 1
   Web: 
     BaseURL: http://localhost
     DataDir: var/mason

Added: jifty/trunk/examples/Yada/lib/Yada.pm
==============================================================================
--- (empty file)
+++ jifty/trunk/examples/Yada/lib/Yada.pm	Fri Jul 20 10:00:23 2007
@@ -0,0 +1,6 @@
+package Yada;
+
+
+Jifty->web->add_javascript(qw( Asynapse/REST.js trimpath-template.js ) );
+
+1;

Modified: jifty/trunk/examples/Yada/lib/Yada/View.pm
==============================================================================
--- jifty/trunk/examples/Yada/lib/Yada/View.pm	(original)
+++ jifty/trunk/examples/Yada/lib/Yada/View.pm	Fri Jul 20 10:00:23 2007
@@ -1,18 +1,63 @@
 package Yada::View;
 use Jifty::View::Declare -base;
+use strict;
+
+use Jifty::View::Declare::CRUD;
+for (qw/todo/) {
+    Jifty::View::Declare::CRUD->mount_view($_);
+}
 
 template 'index.html' => page {
     my $self = shift;
     title { _('Yada!') };
 
+    render_region('test_region');
+
+    hyperlink(label => 'FAQ',
+	      onclick => [{region => 'test_region',
+			   replace_with => '_faq',
+			  }]);
+
     form {
+	set(item_path => '/todo/view_brief');
 	render_region(name => 'list', path => '/todo/list');
     }
 };
 
-use Jifty::View::Declare::CRUD;
-for (qw/todo/) {
-    Jifty::View::Declare::CRUD->mount_view($_);
-}
+template '_faq' => sub :Static {
+    hyperlink(label => 'close', onclick => [{replace_with => '/__jifty/empty'}]);
+
+    div {
+        attr { id => "faq" };
+        h2 { _('Using Yada') }
+        dl {
+            dt { 'Yada Yada Yada!'}
+            dd {
+                span {
+                    'are we nearly there yet?'
+                }
+	    }
+	};
+    }
+};
+
+template 'signup' => page {
+    title is _('Sign up');
+    render_region(name => 'signup_widget', path => '_signup');
+};
+
+template '_signup' => sub :Action {
+    my $action = Jifty->web->new_action( class => 'Signup', moniker => 'signupnow');
+    my $next = undef;
+#    with ( call => $next ),
+    form {
+	render_param( $action => 'name' , focus => 1);
+	render_param( $action => $_ ) for ( grep {$_ ne 'name'} $action->argument_names );
+
+	form_return( label => _('Sign up'), submit => $action );
+    }
+
+};
+
 
 1;

Modified: jifty/trunk/examples/Yada/lib/Yada/View/Todo.pm
==============================================================================
--- jifty/trunk/examples/Yada/lib/Yada/View/Todo.pm	(original)
+++ jifty/trunk/examples/Yada/lib/Yada/View/Todo.pm	Fri Jul 20 10:00:23 2007
@@ -1,6 +1,21 @@
 package Yada::View::Todo;
 use strict;
-use Jifty::View::Declare -base;
 use base 'Jifty::View::Declare::CRUD';
+use Jifty::View::Declare -base;
+
+template 'view_brief' => sub {
+    my $self = shift;
+    my ( $object_type, $id ) = ( $self->object_type, get('id') );
+    my $record = $self->_get_record($id);
+
+    div { {class is "description" };
+	  outs($record->description);
+	  hyperlink(label => 'details',
+		    onclick => [{region => 'test_region',
+				 replace_with => $self->fragment_for('view'),
+				 args         => { id => $id },
+				}]);
+      };
+};
 
 1;

Added: jifty/trunk/examples/Yada/share/web/static/js/Asynapse/REST.js
==============================================================================
--- (empty file)
+++ jifty/trunk/examples/Yada/share/web/static/js/Asynapse/REST.js	Fri Jul 20 10:00:23 2007
@@ -0,0 +1,285 @@
+if ( typeof Asynapse == 'undefined' ) {
+    Asynapse = {}
+}
+
+if ( typeof Asynapse.REST == 'undefined' ) {
+    Asynapse.REST = {}
+}
+
+Asynapse.REST.VERSION = "0.10"
+
+Asynapse.REST.Model = function(model) {
+    this._model = model
+    return this;
+}
+
+Asynapse.REST.Model.prototype = {
+    /* Corresponds Jifty's REST Pluing API */
+    show_item_field: function(column, key, field) {
+        var url = "/=/model/*/*/*/*.js"
+            .replace("*", this._model)
+            .replace("*", column)
+            .replace("*", key)
+            .replace("*", field)
+
+        return this.eval_ajax_get(url);        
+    },
+    
+    show_item: function(column, key) {
+        var url = "/=/model/*/*/*.js"
+            .replace("*", this._model)
+            .replace("*", column)
+            .replace("*", key)
+
+        return this.eval_ajax_get(url);
+    },
+
+    list_model_items: function(column) {
+        var url = "/=/model/*/*.js"
+            .replace("*", this._model)
+            .replace("*", column)
+
+        return this.eval_ajax_get(url);
+    },
+
+    list_model_columns: function() {
+        var url = "/=/model/*.js"
+            .replace("*", this._model)
+
+        return this.eval_ajax_get(url);
+    },
+
+    list_models: function() {
+        var url = "/=/model.js"
+
+        return this.eval_ajax_get(url);
+    },
+
+    create_item: function(item) {
+        var url ="/=/model/*.js"
+            .replace("*", this._model)
+
+        var req = new Ajax.Request(url, {
+            method: 'post',
+            asynchronous: false,
+            postBody: $H(item).toQueryString()
+        });
+        if ( req.responseIsSuccess() ) {
+            eval(req.transport.responseText);
+            return $H($_)
+        } else {
+            return null;
+        }
+    },
+    
+    replace_item: function(item) {
+        var url = "/=/action/update" + this._model + ".js"
+        new Ajax.Request(url, {
+            method: 'post',
+            contentType: 'application/x-www-form-urlencoded',
+            postBody: $H(item).toQueryString()            
+        });
+    },
+
+    delete_item: function(column, key) {
+        var url = "/=/model/*/*/*"
+            .replace("*", this._model)
+            .replace("*", column)
+            .replace("*", key)
+        
+        new Ajax.Request(url, {
+            method: 'DELETE',
+            contentType: 'application/x-www-form-urlencoded'
+        });
+        return null;
+    },
+    
+    /* Internal Helpers */
+    eval_ajax_get: function(url) {
+        eval(this.ajax_get(url));
+        return $_ ? Object.extend({},$_) : null;
+    },
+    ajax_get: function(url) {
+        var req = new Ajax.Request(url, {
+            method: 'GET',
+            asynchronous: false
+        })
+        if ( req.responseIsSuccess() ) {
+            return req.transport.responseText;
+        }
+        else {
+            return "var $_ = null";
+        }
+    }
+}
+
+Asynapse.REST.Model.ActiveRecord = function(model) {
+    Object.extend(this, new Asynapse.REST.Model(model));
+    this._attributes = {}
+    return this;
+}
+
+Asynapse.REST.Model.ActiveRecord.prototype = {
+    new: function() {
+        return this;
+    },
+    
+    find: function(param) {
+        if ( typeof param == 'number' ) {
+            return this.show_item("id", param)
+        }
+    },
+
+    find_by_id: function(id) {
+        return this.show_item("id", id)
+    },
+
+    create: function(attributes) {
+        var r = this.create_item(attributes);
+
+        if (r.success) {
+            return this.show_item("id", Number(r.content.id) )
+        }
+        return null;
+    },
+
+    delete: function(id) {
+        this.delete_item("id", id)
+        return null
+    },
+
+    update: function(id, attributes) {
+        var obj = this.find(id)
+        obj = Object.extend(obj, attributes)
+        return this.replace_item( obj )
+    },
+
+    write_attribute: function(attr, value) {
+    }
+}
+
+/* Great Aliases */
+Asynapse.Model = Asynapse.REST.Model
+Asynapse.ActiveRecord = Asynapse.REST.Model.ActiveRecord
+AsynapseRecord = Asynapse.REST.Model.ActiveRecord
+
+/**
+=head1 NAME
+
+Asynapse.REST - Asynapse REST Client
+
+=head1 VERSION
+
+This document describes Asynapse.REST version 0.10
+
+=head1 SYNOPSIS
+
+    # Define Your own AsynapseRecord Class.
+    Person = new AsynapseRecord('person')
+
+    # Use it
+    var p = Person.find(1)
+
+=head1 DESCRIPTION
+
+Asynapse.REST is the namespace for being a general REST client in
+Asynapse framework. Under this namespace, so far we arrange
+C<Asynapse.REST.Model> for Asynapse Model Classes. It means to
+provide an abstration layer for data existing at given REST server(s).
+
+With many flavours of data abstration layer in the world, we choose
+to emulate ActiveRecord as our first target, which has a plain
+simple object semantics, and very compatible to javascript.
+The implementation of it called C<AsynapseRecord>.
+
+To use it, you must first create your own record classes, like this:
+
+    Person = new AsynapseRecord('person')
+
+After this, Person becomes your Person model class, and then you
+can do:
+
+    var p = Person.find(1)
+
+To find a person by its id. Besides C<find>, C<create>, C<update>,
+and C<delete> are also implemented.
+
+Here's more detail about how to use these interfaces. They are all
+"class methods".
+
+=over
+
+=item find( id )
+
+Retrieve a record from this model with given id.
+
+=item create( attr )
+
+Create a new with attributes specified in attr hash.
+
+=item update( id, attr )
+
+Update the record with primary key id with new sets of
+attributes specified in attr hash.
+
+=item delete( id )
+
+Remove the record with given id.
+
+=back
+
+=head1 CONFIGURATION AND ENVIRONMENT
+
+AsynapseRecord requires no configuration files or environment
+variables.  However, you need a Jifty instance with REST plugin
+(which is given by default now.)
+
+Since JavaScript cannot do XSS, it assumed the your Jifty instance's
+URL resides at C</>, and entry points of REST servces starts from
+C</=/>. It should be made possible to change this assumption in the
+future to match more presets in different frameworks.
+
+=head1 BUGS AND LIMITATIONS
+
+The asynapse project is hosted at L<http://code.google.com/p/asynapse/>.
+You may contact the authors or submit issues using the web interface.
+
+=head1 AUTHOR
+
+Kang-min Liu  C<< <gugod at gugod.org> >>
+
+=head1 LICENCE AND COPYRIGHT
+
+Copyright (c) 2007, Kang-min Liu C<< <gugod at gugod.org> >>. All rights reserved.
+
+This module is free software; you can redistribute it and/or
+modify it under the same terms as Perl itself. See L<perlartistic>.
+
+
+=head1 DISCLAIMER OF WARRANTY
+
+BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
+ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
+YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
+NECESSARY SERVICING, REPAIR, OR CORRECTION.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
+LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
+OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
+THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+=cut
+
+*/ 
+

Modified: jifty/trunk/lib/Jifty/Plugin/REST/Dispatcher.pm
==============================================================================
--- jifty/trunk/lib/Jifty/Plugin/REST/Dispatcher.pm	(original)
+++ jifty/trunk/lib/Jifty/Plugin/REST/Dispatcher.pm	Fri Jul 20 10:00:23 2007
@@ -215,6 +215,11 @@
     elsif ($accept =~ /j(?:ava)?s|ecmascript/i) {
         $apache->header_out('Content-Type' => 'application/javascript; charset=UTF-8');
         $apache->send_http_header;
+	# XXX: temporary hack to fix _() that aren't respected by json dumper
+	for (values %{$_[0]}) {
+	    $_->{label} = "$_->{label}" if exists $_->{label} && defined ref $_->{label};
+	    $_->{hints} = "$_->{hints}" if exists $_->{hints} && defined ref $_->{hints};
+	}
         print 'var $_ = ', Jifty::JSON::objToJson( @_, { singlequote => 1 } );
     }
     elsif ($accept =~ qr{^(?:application/x-)?(?:perl|pl)$}i) {
@@ -592,6 +597,7 @@
     label
     hints
     mandatory
+    ajax_validates
     length
 );
 

Modified: jifty/trunk/lib/Jifty/Plugin/SkeletonApp/View.pm
==============================================================================
--- jifty/trunk/lib/Jifty/Plugin/SkeletonApp/View.pm	(original)
+++ jifty/trunk/lib/Jifty/Plugin/SkeletonApp/View.pm	Fri Jul 20 10:00:23 2007
@@ -42,7 +42,7 @@
         Jifty->web->navigation->render_as_menu; };
 };
 
-template '__jifty/empty' => sub {
+template '__jifty/empty' => sub :Static {
         '';
 };
 

Modified: jifty/trunk/lib/Jifty/View/Declare/BaseClass.pm
==============================================================================
--- jifty/trunk/lib/Jifty/View/Declare/BaseClass.pm	(original)
+++ jifty/trunk/lib/Jifty/View/Declare/BaseClass.pm	Fri Jul 20 10:00:23 2007
@@ -5,7 +5,7 @@
 use base qw/Exporter Jifty::View::Declare::Helpers/;
 use Scalar::Defer;
 use Template::Declare::Tags;
-
+use PadWalker;
 
 use Jifty::View::Declare::Helpers;
 
@@ -54,6 +54,40 @@
     }
 }
 
+sub _actual_td_code {
+    my $class = shift;
+    my $path = shift;
+    my $code = Template::Declare->resolve_template($path) or return;
+    my $closed_over = PadWalker::closed_over($code)->{'$coderef'};
+    return $closed_over ? $$closed_over : $code;
+}
+
+use Attribute::Handlers;
+my (%Static, %Action);
+sub Static :ATTR(CODE,BEGIN) {
+    $Static{$_[2]}++;
+}
+
+sub Action :ATTR(CODE,BEGIN) {
+    $Action{$_[2]}++;
+}
+
+=head2 client_cacheable
+
+Returns the type of cacheable object for client
+
+=cut
+
+sub client_cacheable {
+    my $self = shift;
+    my $path = shift;
+    my $code = $self->_actual_td_code($path) or return;
+
+    return 'static' if $Static{$code};
+    return 'action' if $Action{$code};
+    return;
+}
+
 
 =head2 show templatename arguments
 

Modified: jifty/trunk/lib/Jifty/View/Declare/CRUD.pm
==============================================================================
--- jifty/trunk/lib/Jifty/View/Declare/CRUD.pm	(original)
+++ jifty/trunk/lib/Jifty/View/Declare/CRUD.pm	Fri Jul 20 10:00:23 2007
@@ -4,6 +4,14 @@
 package Jifty::View::Declare::CRUD;
 use Jifty::View::Declare -base;
 
+# XXX: should register 'template type' handler, so the
+# client_cache_content & the TD sub here agrees with the arguments.
+use Attribute::Handlers;
+my %VIEW;
+sub CRUDView :ATTR(CODE,BEGIN) {
+    $VIEW{$_[2]}++;
+}
+
 
 =head1 NAME
 
@@ -38,6 +46,19 @@
     *{$vclass."::object_type"} = sub { $model };
 }
 
+sub _dispatch_template {
+    my $class = shift;
+    my $code  = shift;
+    if ($VIEW{$code} && !UNIVERSAL::isa($_[0], 'Evil')) {
+	my ( $object_type, $id ) = ( $class->object_type, get('id') );
+	@_ = ($class, $class->_get_record($id), @_);
+    }
+    else {
+	unshift @_, $class;
+    }
+    goto $code;
+}
+
 
 =head2 object_type
 
@@ -163,26 +184,23 @@
         }
 };
 
-
 =head2 view
 
 This template displays the data held by a single model record.
 
 =cut
 
-template 'view' => sub {
-    my $self = shift;
-    my ( $object_type, $id ) = ( $self->object_type, get('id') );
-      my $record =   $self->_get_record($id);
+template 'view' => sub :CRUDView {
+    my ($self, $record) = @_;
     my $update = new_action(
-        class   => 'Update' . $object_type,
+        class   => 'Update' . $self->object_type,
         moniker => "update-" . Jifty->web->serial,
-        record  => $record 
+        record  => $record,
     );
 
     div {
         { class is 'crud read item inline' };
-        my @fields =$self->display_columns($update);
+        my @fields = $self->display_columns($update);
         render_action( $update, \@fields, { render_mode => 'read' } );
 
         show ('./view_item_controls', $record, $update); 
@@ -202,7 +220,7 @@
             class   => "editlink",
             onclick => {
                 replace_with => $self->fragment_for('update'),
-                args         => { object_type => $self->object_type, id => $record->id }
+                args         => { id => $record->id }
             },
         );
     }

Added: jifty/trunk/lib/Jifty/View/Declare/Compile.pm
==============================================================================
--- (empty file)
+++ jifty/trunk/lib/Jifty/View/Declare/Compile.pm	Fri Jul 20 10:00:23 2007
@@ -0,0 +1,210 @@
+package Jifty::View::Declare::Compile;
+use strict;
+use base 'B::Deparse';
+use B qw(class main_root main_start main_cv svref_2object opnumber perlstring
+	 OPf_WANT OPf_WANT_VOID OPf_WANT_SCALAR OPf_WANT_LIST
+	 OPf_KIDS OPf_REF OPf_STACKED OPf_SPECIAL OPf_MOD
+	 OPpLVAL_INTRO OPpOUR_INTRO OPpENTERSUB_AMPER OPpSLICE OPpCONST_BARE
+	 OPpTRANS_SQUASH OPpTRANS_DELETE OPpTRANS_COMPLEMENT OPpTARGET_MY
+	 OPpCONST_ARYBASE OPpEXISTS_SUB OPpSORT_NUMERIC OPpSORT_INTEGER
+	 OPpSORT_REVERSE OPpSORT_INPLACE OPpSORT_DESCEND OPpITER_REVERSED
+	 SVf_IOK SVf_NOK SVf_ROK SVf_POK SVpad_OUR SVf_FAKE SVs_RMG SVs_SMG
+         CVf_METHOD CVf_LOCKED CVf_LVALUE CVf_ASSERTION
+	 PMf_KEEP PMf_GLOBAL PMf_CONTINUE PMf_EVAL PMf_ONCE PMf_SKIPWHITE
+	 PMf_MULTILINE PMf_SINGLELINE PMf_FOLD PMf_EXTENDED);
+BEGIN {
+    die "You need a custom version of B::Deparse from http://svn.jifty.org/svn/jifty.org/B/"
+        unless B::Deparse->can('e_method')
+}
+sub is_scope { goto \&B::Deparse::is_scope }
+sub is_state { goto \&B::Deparse::is_state }
+sub null { goto \&B::Deparse::null }
+
+sub padname {
+    my $self = shift;
+    my $targ = shift;
+    return substr($self->padname_sv($targ)->PVX, 1);
+}
+
+require CGI;
+our %TAGS = (
+    map { $_ => +{} }
+        map {@{$_||[]}} @CGI::EXPORT_TAGS{qw/:html2 :html3 :html4 :netscape :form/}
+);
+
+sub deparse {
+    my $self = shift;
+    my $ret = $self->SUPER::deparse(@_);
+    return '' if $ret =~ m/use (strict|warnings)/;
+    return $ret;
+}
+
+sub loop_common {
+    my $self = shift;
+    my($op, $cx, $init) = @_;
+    my $enter = $op->first;
+    my $kid = $enter->sibling;
+    if ($enter->name eq "enteriter") { # foreach
+	my $ary = $enter->first->sibling; # first was pushmark
+	my $var = $ary->sibling;
+
+	if ($ary->name eq 'null' and $enter->private & OPpITER_REVERSED) {
+	    # "reverse" was optimised away
+	    return $self->SUPER::loop_common(@_);
+	} elsif ($enter->flags & OPf_STACKED
+	    and not null $ary->first->sibling->sibling)
+	{
+	    return $self->SUPER::loop_common(@_);
+	} else {
+	    $ary = $self->deparse($ary, 1);
+	}
+
+	if (null $var) {
+	    if ($enter->flags & OPf_SPECIAL) { # thread special var
+		$var = $self->pp_threadsv($enter, 1);
+	    } else { # regular my() variable
+		$var = $self->padname($enter->targ);
+	    }
+	} elsif ($var->name eq "rv2gv") {
+	    $var = $self->pp_rv2sv($var, 1);
+	    if ($enter->private & OPpOUR_INTRO) {
+		# our declarations don't have package names
+		$var =~ s/^(.).*::/$1/;
+		$var = "our $var";
+	    }
+	} elsif ($var->name eq "gv") {
+	    $var = $self->deparse($var, 1);
+	    $var = '$' . $var if $var eq '_';
+	}
+	else {
+	    return $self->SUPER::loop_common(@_);
+	}
+
+
+	my $body = $kid->first->first->sibling; # skip OP_AND and OP_ITER
+	# statement() foreach (@foo);
+	if (!is_state $body->first and $body->first->name ne "stub") {
+	    Carp::confess unless $var eq '$_';
+	    $body = $body->first;
+	    return "$ary.each(function (\$_) {".$self->deparse($body, 2)."} )";
+	}
+	# XXX not handling cont block here yet
+	return "$ary.each(function ($var) {".$self->deparse($body, 0)."} )";
+    }
+    return $self->SUPER::loop_common(@_);
+}
+
+sub maybe_my {
+    my $self = shift;
+    my($op, $cx, $text) = @_;
+    if ($op->private & OPpLVAL_INTRO and not $self->{'avoid_local'}{$$op}) {
+	if (B::Deparse::want_scalar($op)) {
+	    return "var $text";
+	} else {
+	    return $self->maybe_parens_func("my", $text, $cx, 16);
+	}
+    } else {
+	return $text;
+    }
+}
+
+sub maybe_parens_func {
+    my $self = shift;
+    my($func, $text, $cx, $prec) = @_;
+    return "$func($text)";
+
+}
+
+sub const {
+    my $self = shift;
+    my($sv, $cx) = @_;
+    if (class($sv) eq "NULL") {
+       return 'null';
+    }
+    return $self->SUPER::const(@_);
+}
+
+sub pp_undef { 'null' }
+sub pp_sne { shift->binop(@_, "!=", 14) }
+sub pp_grepwhile { shift->mapop(@_, "grep") }
+
+sub mapop {
+    my $self = shift;
+    my($op, $cx, $name) = @_;
+    return $self->SUPER::mapop(@_) unless $name eq 'grep';
+    my($expr, @exprs);
+    my $kid = $op->first; # this is the (map|grep)start
+    $kid = $kid->first->sibling; # skip a pushmark
+    my $code = $kid->first; # skip a null
+    if (is_scope $code) {
+	$code = "{" . $self->deparse($code, 0) . "} ";
+    } else {
+	$code = $self->deparse($code, 24) . ", ";
+    }
+    $kid = $kid->sibling;
+    for (; !null($kid); $kid = $kid->sibling) {
+	$expr = $self->deparse($kid, 6);
+	push @exprs, $expr if defined $expr;
+    }
+    return "(".join(", ", @exprs).").select(function (\$_) $code)";
+}
+
+sub e_anoncode {
+    my ($self, $info) = @_;
+    my $text = $self->deparse_sub($info->{code});
+    return "function () " . $text;
+}
+
+sub e_anonhash {
+    my ($self, $info) = @_;
+    my @exprs = @{$info->{exprs}};
+    my @pairs;
+    while (my @p = splice(@exprs, 0, 2)) {
+	push @pairs, join(': ', map { $self->deparse($_, 6) } @p);
+    }
+    return '{' . join(", ", @pairs) . '}';
+}
+
+sub pp_entersub {
+    my $self = shift;
+    my $ret = $self->SUPER::pp_entersub(@_);
+    $ret =~ s/return\s*\((.*)\)/return [$1]/ if $ret =~ m/^attr/;
+
+    return $ret;
+}
+
+sub e_method {
+    my ($self, $info) = @_;
+    my $obj = $info->{object};
+    if ($obj->name eq 'const') {
+        $obj = $self->const_sv($obj)->PV;
+    }
+    else {
+        $obj = $self->deparse($obj, 24);
+    }
+
+    my $meth = $info->{method};
+    $meth = $self->deparse($meth, 1) if $info->{variable_method};
+    my $args = join(", ", map { $self->deparse($_, 6) } @{$info->{args}} );
+    my $kid = $obj . "." . $meth;
+    return $kid . "(" . $args . ")"; # parens mandatory
+}
+
+sub walk_lineseq {
+    my ($self, $op, $kids, $callback) = @_;
+    my $xcallback = $callback;
+    if ((!$op || $op->next->name eq 'grepwhile') && $kids->[-1]->name ne 'return') {
+	$callback = sub { my ($expr, $index) = @_;
+			  $expr = "return ($expr)" if $index == $#{$kids};
+			  $xcallback->($expr, $index) };
+    }
+    $self->SUPER::walk_lineseq($op, $kids, $callback);
+}
+
+sub compile_to_js {
+    my $class = shift;
+    my $code = shift;
+    return 'function() '.$class->new->coderef2text($code);
+}
+
+1;

Modified: jifty/trunk/lib/Jifty/Web.pm
==============================================================================
--- jifty/trunk/lib/Jifty/Web.pm	(original)
+++ jifty/trunk/lib/Jifty/Web.pm	Fri Jul 20 10:00:23 2007
@@ -50,6 +50,7 @@
     scriptaculous/effects.js
     scriptaculous/controls.js
     formatDate.js
+    template_declare.js
     jifty.js
     jifty_utils.js
     jifty_subs.js

Modified: jifty/trunk/lib/Jifty/Web/PageRegion.pm
==============================================================================
--- jifty/trunk/lib/Jifty/Web/PageRegion.pm	(original)
+++ jifty/trunk/lib/Jifty/Web/PageRegion.pm	Fri Jul 20 10:00:23 2007
@@ -384,4 +384,30 @@
     return "#region-" . $self->qualified_name . ' ' . join(' ', @_);
 }
 
+my $can_compile = eval 'use Jifty::View::Declare::Compile; 1' ? 1 : 0;
+
+=head2 client_cacheable
+
+=cut
+
+sub client_cacheable {
+    my $self = shift;
+    return unless $can_compile;
+
+    return Jifty::View::Declare::BaseClass->client_cacheable($self->path);
+}
+
+=head2 client_cacheable
+
+=cut
+
+sub client_cache_content {
+    my $self = shift;
+    return unless $can_compile;
+
+    return Jifty::View::Declare::Compile->compile_to_js(
+        Jifty::View::Declare::BaseClass->_actual_td_code($self->path)
+    );
+}
+
 1;

Modified: jifty/trunk/share/web/static/js/jifty.js
==============================================================================
--- jifty/trunk/share/web/static/js/jifty.js	(original)
+++ jifty/trunk/share/web/static/js/jifty.js	Fri Jul 20 10:00:23 2007
@@ -1,6 +1,97 @@
 /* An empty class so we can create things inside it */
 var Jifty = Class.create();
 
+Jifty.Web = Class.create();
+Jifty.Web.current_actions = new Array;
+Jifty.Web.new_action = function() {
+    var args = _get_named_args(arguments);
+    var a;
+    Jifty.Web.current_actions.each(function(x) { if (x.moniker == args.moniker) a = x });
+    if (!a) throw "hate";
+    
+    return a;
+};
+
+Jifty.web = function() { return Jifty.Web };
+
+function _get_named_args(args) {
+    var result = {};
+    for (var i = 0; i < args.length; i+=2) {
+	result[args[i]] = args[i+1];
+    }
+    return result;
+
+}
+
+function _get_onclick(action_hash, name, args, path) {
+    var onclick = 'if(event.ctrlKey||event.metaKey||event.altKey||event.shiftKey) return true; return update('
+    + JSON.stringify({'continuation': {},
+		      'actions': action_hash,
+		      'fragments': [{'mode': 'Replace', 'args': args, 'region': name, 'path': path}]})
+    +', this)';
+    onclick = onclick.replace(/"/g, "'"); //"' )# grr emacs!
+	return onclick;
+}
+// XXX
+var hyperlink  = function() {
+    var args = _get_named_args(arguments);
+    var current_region = Jifty.Web.current_region;
+    var onclick = _get_onclick({}, current_region.name, current_region.args, args.onclick[0].replace_with);
+    outs( a(function() { attr(function()
+			      {return ['onclick', onclick, 'href', '#']});
+	    return args.label
+		}));
+}
+
+var render_param = function(a, field) { outs(a.render_param(field)) };
+var form_return  = function() {
+    var args = _get_named_args(arguments);
+    var action_hash = {};
+    action_hash[args.submit.moniker] = 1;
+    // XXX: fix the fabricated refresh-self
+    // XXX: implicit onclick only for now
+
+    // $self->_push_onclick($args, { refresh_self => 1, submit => $args->{submit} });
+    // @args{qw/mode path region/} = ('Replace', Jifty->web->current_region->path, Jifty->web->current_region);
+
+    var current_region = Jifty.Web.current_region;
+    var onclick = _get_onclick(action_hash, current_region.name, current_region.args, current_region.path);
+    outs(
+	 div(function() {
+		 attr(function() { return ['class', 'submit_button'] });
+		 return input(function() { attr(function()
+						{return ['type', 'submit',
+							 'onclick', onclick,
+							 'class', 'widget button',
+							 'id', 'S' + (++SERIAL + SERIAL_postfix),
+							 'value', args.label,
+							 'name', 'J:V-region-__page-signup_widget=_signup|J:ACTIONS=signupnow'] })});
+		     }));
+
+};
+
+function register_action(a) {
+    outs(div(function() {
+		attr(function() { return ['class', 'hidden'] });
+		return input(function() { attr(function() {
+				return ['type', 'hidden',
+					'name', a.register_name(),
+					'id', a.register_name(),
+					'value', a.actionClass] }) } ) } ));
+    /* XXX: fallback values */
+}
+
+function apply_cached_for_action(code, actions) {
+    Jifty.Web.current_actions = actions;
+    this['out_buf'] = '';
+    this['outs'] = function(text) { this.out_buf += text };
+    actions.each(register_action);
+    var foo = code();
+    return foo;
+    alert(foo);
+    throw 'not yet';
+}
+
 /* Actions */
 var Action = Class.create();
 Action.prototype = {
@@ -235,10 +326,145 @@
         var enable = function() { arguments[0].disabled = false; };
         this.fields().each( enable );
         this.buttons().each( enable );
-    }
+    },
+
+
+    /* client side logic extracted from Jifty::Action */
+    _action_spec: function() {
+	if (!this.s_a) {
+	    /* XXX: make REST client accessible */
+	    var Todo = new AsynapseRecord('todo');
+	    this.s_a = $H(Todo.eval_ajax_get('/=/action/'+this.actionClass+'.js'));
+	}
+	
+	return this.s_a
+    },
+    argument_names: function() {
+	return this._action_spec().keys();
+    },
+
+    render_param: function(field) {
+	var a_s = this._action_spec();
+	var type = 'text';
+	var f = new ActionField(field, a_s[field], this);
+	return f.render();
+    },
+    register_name: function() { return this.register.id }
+
 };
 
+var SERIAL_postfix = Math.ceil(10000*Math.random());
+var SERIAL = 0;
+ActionField = Class.create();
+ActionField.prototype = {
+ initialize: function(name, args, action) {
+	this.name = name;
+	this.label = args.label;
+	this.hints = args.hints;
+	this.mandatory = args.mandatory;
+	this.ajax_validates = args.ajax_validates;
+	this.current_value = action.data_structure().fields[name].value;
+        this.error = action.result.field_error[name];
+	this.action = action;
+	if (!this.render_mode) this.render_mode = 'update';
+	this.type = 'text';
+    },
+
+ render: function() {
+	if (this.render_mode == 'read')
+	    return this.render_wrapper
+		(this.render_preamble,
+		 this.render_label,
+		 this.render_value);
+	else
+	    return this.render_wrapper
+	    (this.render_preamble,
+	     this.render_label,
+	     this.render_widget,
+	     this.render_autocomplete_div,
+	     this.render_inline_javascript,
+	     this.render_hints,
+	     this.render_errors,
+	     this.render_warnings,
+	     this.render_canonicalization_notes);
+    },
+ render_wrapper: function () {
+	var classes = ['form_field'];
+	if (this.mandatory) classes.push('mandatory');
+	if (this.name) classes.push('argument-'+this.name);
+	var args = arguments;
+	var tthis = this;
+	return div(function() {
+		attr(function(){return ['class', classes.join(' ')]});
+		var buf = new Array;
+		for (var i = 0; i < args.length; ++i) {
+		    buf.push(typeof(args[i]) == 'function' ? args[i].apply(tthis) : args[i]);
+		}
+		return buf.join('');
+	    });
+    },
+    render_preamble: function() {
+	var tthis = this;
+	return span(function(){attr(function(){return ['class', "preamble"]});
+		return tthis.preamble });
+    },
+
+    render_label: function() {
+	var tthis = this;
+	if(this.render_mode == 'update')
+	    return label(function(){attr(function(){return['class', "label", 'for', tthis.element_id()]});
+		    return tthis.label });
+	else
+	    return span(function(){attr(function(){return['class', "label" ]});
+		    return tthis.label });
+    },
+ input_name: function() {
+	return ['J:A:F', this.name, this.action.moniker].join('-');
+    },
+ render_hints: function() {
+	var tthis = this;
+	return span(function(){attr(function(){return ['class', "hints"]});
+		return tthis.hints });
+    },
+
+ render_errors: function() {
+	if (!this.action) return '';
+	var tthis = this;
+	// XXX: post-request handler needs to extract field error messages
+	return span(function(){attr(function(){return ['class', "error", 'id', 'errors-'+tthis.input_name()]});
+		return tthis.error });
+    },
+
+ render_widget: function () {
+	var tthis = this;
+	return input(function(){
+		    attr(function(){
+			    var fields = ['type', tthis.type];
+			    if (tthis.input_name) fields.push('name', tthis.input_name());
+			    fields.push('id', tthis.element_id());
+			    if (tthis.current_value) fields.push('value', tthis.current_value);
+			    fields.push('class', tthis._widget_class().join(' '));
+			    if (tthis.max_length) fields.push('size', tthis.max_length, 'maxlength', tthis.max_length);
+			    if (tthis.disable_autocomplete) fields.push('autocomplete', "off");
+			    //" " .$self->other_widget_properties;
+			    return fields;
+			})});
+    },
+ _widget_class: function() {
+	var classes = ['form_field'];
+	if (this.mandatory)      classes.push('mandatory');
+	if (this.name)           classes.push('argument-'+this.name);
+	if (this.ajax_validates) classes.push('ajaxvalidation');
+	return classes;
+    },
+
+ element_id: function() { if(!this._element_id) this._element_id = this.input_name() + '-S' + (++SERIAL + SERIAL_postfix);
+			  return this._element_id; },
+ __noSuchMethod__: function(name) {
+	return '<!-- '+name+' not implemented yet -->';
+    }
 
+};
 
 /* Forms */
 Object.extend(Form, {
@@ -249,7 +475,7 @@
 
         for (var i = 0; i < possible.length; i++) {
             if (Form.Element.getType(possible[i]) == "registration")
-                elements.push(new Action(Form.Element.getMoniker(possible[i])));
+                elements.push(Form.Element.getAction(possible[i]));
         }
         
         return elements;
@@ -264,6 +490,7 @@
 });
 
 
+var current_actions = $H();
 
 /* Fields */
 Object.extend(Form.Element, {
@@ -287,9 +514,10 @@
     // Takes an element or an element id
     getAction: function (element) {
         element = $(element);    
-
         var moniker = Form.Element.getMoniker(element);
-        return new Action(moniker);
+	if (!current_actions[moniker])
+	    current_actions[moniker] = new Action(moniker);
+	return current_actions[moniker];
     },
 
     // Returns the name of the field
@@ -652,6 +880,42 @@
 
     return f;    
 }
+
+var CACHE = {};
+
+
+var walk_node = function(node, table) {
+    for (var child = node.firstChild;
+         child != null;
+         child = child.nextSibling) {
+        var name = child.nodeName.toLowerCase();
+        if (table[name])
+	    table[name](child);
+    }
+}
+
+var extract_cacheable = function(fragment, f) {
+    walk_node(fragment,
+    { cacheable: function(fragment_bit) {
+            var c_type = fragment_bit.getAttribute("type");
+            var textContent = '';
+            if (fragment_bit.textContent) {
+                textContent = fragment_bit.textContent;
+            } else if (fragment_bit.firstChild) {
+                textContent = fragment_bit.firstChild.nodeValue;
+            } 
+	    try {
+		var cache_func = eval(textContent);
+		CACHE[f['path']] = { 'type': c_type, 'content': cache_func };
+	    }
+	    catch(e) { 
+		alert(e);
+		alert(textContent);
+	    }
+        }
+    });
+};
+
 // applying updates from a fragment
 //   - fragment: the fragment from the server
 //   - f: fragment spec
@@ -659,11 +923,10 @@
     // We found the right fragment
     var dom_fragment = fragments[f['region']];
     var new_dom_args = $H();
+
     var element = f['element'];
-    for (var fragment_bit = fragment.firstChild;
-	 fragment_bit != null;
-	 fragment_bit = fragment_bit.nextSibling) {
-	if (fragment_bit.nodeName == 'argument') {
+    walk_node(fragment,
+    { argument: function(fragment_bit) {
 	    // First, update the fragment's arguments
 	    // with what the server actually used --
 	    // this is needed in case there was
@@ -675,7 +938,8 @@
 		textContent = fragment_bit.firstChild.nodeValue;
 	    }
 	    new_dom_args[fragment_bit.getAttribute("name")] = textContent;
-	} else if (fragment_bit.nodeName.toLowerCase() == 'content') {
+	},
+      content: function(fragment_bit) {
 	    var textContent = '';
 	    if (fragment_bit.textContent) {
 		textContent = fragment_bit.textContent;
@@ -697,7 +961,7 @@
         });
         Behaviour.apply(element);
 	}
-    }
+    });
     dom_fragment.setArgs(new_dom_args);
 
     // Also, set us up the effect
@@ -733,7 +997,6 @@
         window.event.returnValue = false;
     }
 
-    show_wait_message();
     var named_args = arguments[0];
     var trigger    = arguments[1];
 
@@ -760,15 +1023,17 @@
     if (form && form['J:CALL']) 
 	optional_fragments = [ prepare_element_for_update({'mode':'Replace','args':{},'region':'__page','path': null}) ];
     // Build actions structure
+    var has_request = 0;
     request['actions'] = $H();
     for (var moniker in named_args['actions']) {
         var disable = named_args['actions'][moniker];
         var a = new Action(moniker, button_args);
+	current_actions[moniker] = a; // XXX: how do i make this bloody singleton?
         // Special case for Redirect, allow optional, implicit __page
         // from the response to be used.
         if (a.actionClass == 'Jifty::Action::Redirect')
             optional_fragments = [ prepare_element_for_update({'mode':'Replace','args':{},'region':'__page','path': a.fields().last().value}) ];
-
+        a.result = {}; a.result.field_error = {};
         if (a.register) {
             if (a.hasUpload())
                 return true;
@@ -776,15 +1041,71 @@
                 a.disable_input_fields();
             }
             request['actions'][moniker] = a.data_structure();
+            ++has_request;
         }
+
     }
 
     request['fragments'] = $H();
+    var update_from_cache = new Array;
+
     // Build fragments structure
     for (var i = 0; i < named_args['fragments'].length; i++) {
         var f = named_args['fragments'][i];
         f = prepare_element_for_update(f);
         if (!f) continue;
+
+        var cached = CACHE[f['path']];
+        if (cached && cached['type'] == 'static') {
+            var my_fragment = document.createElement('fragment');
+            var content_node = document.createElement('content');
+	    var cached_result;
+
+	    Jifty.Web.current_region = fragments[f['region']];
+	    try { cached_result = apply_cached_for_action(cached['content'], []) }
+	    catch (e) { alert(e) }
+
+            content_node.textContent = cached_result;
+            my_fragment.appendChild(content_node);
+            my_fragment.setAttribute('id', f['region']);
+
+            update_from_cache.push(function(){ apply_fragment_updates(my_fragment, f);
+ } );
+            continue;
+        }
+	else if (cached && cached['type'] == 'action') {
+            var my_fragment = document.createElement('fragment');
+            var content_node = document.createElement('content');
+
+            my_fragment.appendChild(content_node);
+            my_fragment.setAttribute('id', f['region']);
+            update_from_cache.push(function(){
+		    var cached_result;
+		    Jifty.Web.current_region = fragments[f['region']];
+		    try {
+			cached_result = apply_cached_for_action(cached['content'], Form.getActions(form));
+		    }
+		    catch (e) { alert(e); throw e }
+		    content_node.textContent = cached_result;
+		    apply_fragment_updates(my_fragment, f);
+ } );
+            continue;
+	}
+        else if (cached && cached['type'] == 'crudview') {
+	    try { // XXX: get model class etc as metadata in cache 
+		// XXX: kill dup code
+	    var Todo = new AsynapseRecord('todo');
+	    var record = Todo.find(f['args']['id']);
+            var my_fragment = document.createElement('fragment');
+            var content_node = document.createElement('content');
+            content_node.textContent = cached['content'](record);
+            my_fragment.appendChild(content_node);
+            my_fragment.setAttribute('id', f['region']);
+            update_from_cache.push(function(){ apply_fragment_updates(my_fragment, f); } );
+	    } catch (e) { alert(e) };
+	    continue;
+	}
+
         // Update with all new values
         var name = f['region'];
         var fragment_request = fragments[name].data_structure(f['path'], f['args']);
@@ -795,49 +1116,70 @@
 
         // Push it onto the request stack
         request['fragments'][name] = fragment_request;
+        ++has_request;
+    }
+
+    if (!has_request) {
+        for (var i = 0; i < update_from_cache.length; i++)
+            update_from_cache[i]();
+        return false;
     }
 
+    show_wait_message();
+
     // And when we get the result back..
     var onSuccess = function(transport, object) {
         // Grab the XML response
         var response = transport.responseXML.documentElement;
+
+	// Get action results
+        walk_node(response,
+	{ result: function(result) {
+		var moniker = result.getAttribute("moniker");
+		walk_node(result,
+			  { field: function(field) {
+				  var error = field.getElementsByTagName('error')[0];
+				  if (error) {
+				      var text = error.textContent
+					  ? error.textContent
+					  : (error.firstChild ? error.firstChild.nodeValue : '');
+				      var action = current_actions[moniker];
+				      action.result.field_error[field.getAttribute("name")] = text;
+				      }
+			      }});
+	    }});
+	// empty known action. XXX: we should only need to discard actions being submitted
+
         // Loop through the result looking for it
         var expected_fragments = optional_fragments ? optional_fragments : named_args['fragments'];
         for (var response_fragment = response.firstChild;
              response_fragment != null && response_fragment.nodeName == 'fragment';
              response_fragment = response_fragment.nextSibling) {
 
-            var f; 
-            for (var i = 0; i < expected_fragments.length; i++) {
-                f = expected_fragments[i];
-                if (response_fragment.getAttribute("id") == f['region'])
-                    break;
-            }
-            if (response_fragment.getAttribute("id") != f['region'])
+            var exp_id = response_fragment.getAttribute("id");
+            if (!expected_fragments.find(function(f) { return exp_id == f['region'] }))
                 continue;
 
-	    try {
-            apply_fragment_updates(response_fragment, f);
-	    }catch (e) { alert(e) }
+            try {
+                apply_fragment_updates(response_fragment, f);
+            }catch (e) { alert(e) }
+            extract_cacheable(response_fragment, f);
         }
-        for (var result = response.firstChild;
-             result != null;
-             result = result.nextSibling) {
-            if (result.nodeName == 'result') {
+
+	update_from_cache.each(function(x) { x() });
+
+        walk_node(response,
+	{ result: function(result) {
                 for (var key = result.firstChild;
                      key != null;
                      key = key.nextSibling) {
                     show_action_result(result.getAttribute("moniker"),key);
                 }
-            }
-        }
-        for (var redirect = response.firstChild;
-             redirect != null;
-             redirect = redirect.nextSibling) {
-            if (redirect.nodeName == 'redirect') {
+            },
+	  redirect: function(redirect) {
                 document.location =  redirect.firstChild.firstChild.nodeValue;
-            }
-        }
+	}});
+	current_actions = $H();
     };
     var onFailure = function(transport, object) {
         hide_wait_message_now();

Added: jifty/trunk/share/web/static/js/template_declare.js
==============================================================================
--- (empty file)
+++ jifty/trunk/share/web/static/js/template_declare.js	Fri Jul 20 10:00:23 2007
@@ -0,0 +1,53 @@
+// FIXME: try not to pollute the namespace!
+var tags = ['div', 'h2', 'dl', 'dt', 'dd', 'span', 'label', 'input', 'a'];
+for (var i in tags) {
+    this[tags[i]] = _mk_tag_wrapper(tags[i]);
+}
+this['form'] = _mk_tag_wrapper('form', function(attr) {
+	return '<form method="post" enctype="multipart/form-data" >'; // XXX action: & friends
+    }, null, 1);
+var _ = function(str) { return str };
+var attr = function() {};
+
+function _mk_tag_wrapper(name, pre, post, want_outbuf) {
+    return function() {
+	var buf = new Array;
+	var sp = this['attr'];
+	var attr = {};
+	this['attr'] = function(a) {
+	    var foo;
+	    a = a();
+	    while(foo = a.splice(0, 2)) {
+		if (foo.length == 0)
+		    break;
+		attr[foo[0]] = foo[1];
+	    }
+	};
+
+	var flushed = '';
+	if (this.out_buf) {
+	    flushed = this.out_buf;
+	    this.out_buf = '';
+	}
+
+	for (var i = 0; i < arguments.length; ++i) {
+	    buf.push(typeof(arguments[i]) == 'function' ? arguments[i]() : arguments[i]);
+	}
+	var _mk_attr = function() {
+	    var foo = ' ';
+	    for (var k in attr) {
+		if (k == 'extend') continue;
+		foo += k + '="' + attr[k] + '"';
+	    }
+	    return foo;
+	};
+	var first = buf.splice(0, 1);
+	var _pre = pre ? pre(attr) : '<'+name+_mk_attr(attr)+'>';
+	var _post = post ? post(attr) : '</'+name+'>';
+	if (want_outbuf && this.out_buf) {
+	    first += this.out_buf;
+	    this.out_buf = '';
+	}
+	return flushed + _pre + first + _post + buf.join('');
+    }
+};

Modified: jifty/trunk/share/web/templates/__jifty/webservices/xml
==============================================================================
--- jifty/trunk/share/web/templates/__jifty/webservices/xml	(original)
+++ jifty/trunk/share/web/templates/__jifty/webservices/xml	Fri Jul 20 10:00:23 2007
@@ -52,6 +52,9 @@
     $writer->startTag( "fragment", id => Jifty->web->current_region->qualified_name );
     my %args = %{ Jifty->web->current_region->arguments };
     $writer->dataElement( "argument", $args{$_}, name => $_) for sort keys %args;
+    if (Jifty->config->framework('ClientTemplate') && Jifty->web->current_region->client_cacheable) {
+        $writer->cdataElement( "cacheable", Jifty->web->current_region->client_cache_content, type => Jifty->web->current_region->client_cacheable );
+    }
     $writer->cdataElement( "content", Jifty->web->current_region->as_string );
     $writer->endTag();
 

Added: jifty/trunk/t/clientside/td.t
==============================================================================
--- (empty file)
+++ jifty/trunk/t/clientside/td.t	Fri Jul 20 10:00:23 2007
@@ -0,0 +1,104 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+use Jifty;
+
+package Foo;
+use Jifty::View::Declare -base;
+
+template _faq => \&_faq;
+
+sub _faq {
+    div {
+        attr { id => "faq" };
+        h2 { 'Using Yada' }
+        dl {
+            dt { 'Yada Yada Yada!'}
+            dd {
+                span {
+                    'are we nearly there yet?'
+                }
+	    }
+	};
+    }
+};
+
+template _faq2 => \&_faq2;
+
+sub _faq2 {
+    div {
+        attr { id => "faq" };
+        h2 { 'Using Yada' }
+        dl {
+            dt { 'Yada Yada Yada!'};
+            dd {
+                span {
+                    'are we nearly there yet?'
+                }
+	    }
+	};
+    }
+};
+
+package main;
+
+use Test::More;
+use IPC::Run3;
+eval 'use Jifty::View::Declare::Compile; 1'
+    or plan skip_all => "Can't load Jifty::View::Declare::Compile";
+
+my $jsbin = can_run('js')
+    or plan skip_all => "Can't find spidermonkey js binary";
+
+Template::Declare->init( roots => ['Foo']);
+
+plan tests => 2;
+
+is_compatible('_faq');
+TODO: {
+local $TODO = 'buf handling (non-katamari version) not yet';
+is_compatible('_faq2');
+
+};
+
+
+
+sub is_compatible {
+    my $template = shift;
+    my $js = js_output( js_code( Foo->can($template) ) );
+    my $td = Template::Declare->show($template);
+    $js =~ s/\s*//g;
+    $td =~ s/\s*//g;
+    unshift @_, $js, $td;
+    goto \&is;
+}
+
+sub js_code {
+    my $code = shift;
+    return '(function() '.Jifty::View::Declare::Compile->new->coderef2text($code) . ')()';
+}
+
+sub js_output {
+    my $code = shift;
+    my ($out, $err);
+    run3 [$jsbin],
+	['load("share/web/static/js/template_declare.js");', "print($code);"],
+	    \$out, \$err;
+    diag $err if $err;
+    return $out;
+
+}
+
+use File::Spec::Functions 'catfile';
+sub can_run {
+    my ($_cmd, @path) = @_;
+
+    return $_cmd if -x $_cmd;
+
+    for my $dir ((split /$Config::Config{path_sep}/, $ENV{PATH}), @path, '.') {
+        my $abs = catfile($dir, $_[0]);
+        return $abs if -x $abs;
+    }
+
+    return;
+}


More information about the Jifty-commit mailing list