[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