[Jifty-commit] r3446 - in jifty/branches/trimclient: . 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/View/Declare lib/Jifty/Web share/web/static/js

jifty-commit at lists.jifty.org jifty-commit at lists.jifty.org
Mon Jun 11 17:57:20 EDT 2007


Author: clkao
Date: Mon Jun 11 17:57:19 2007
New Revision: 3446

Added:
   jifty/branches/trimclient/examples/Yada/lib/Yada.pm
   jifty/branches/trimclient/examples/Yada/share/web/static/js/
   jifty/branches/trimclient/examples/Yada/share/web/static/js/Asynapse/
   jifty/branches/trimclient/examples/Yada/share/web/static/js/Asynapse/REST.js
   jifty/branches/trimclient/examples/Yada/share/web/static/js/trimpath-template.js
Modified:
   jifty/branches/trimclient/   (props changed)
   jifty/branches/trimclient/examples/Yada/etc/config.yml
   jifty/branches/trimclient/examples/Yada/lib/Yada/View.pm
   jifty/branches/trimclient/examples/Yada/lib/Yada/View/Todo.pm
   jifty/branches/trimclient/lib/Jifty/View/Declare/CRUD.pm
   jifty/branches/trimclient/lib/Jifty/Web/PageRegion.pm
   jifty/branches/trimclient/share/web/static/js/jifty.js
   jifty/branches/trimclient/share/web/templates/__jifty/webservices/xml

Log:
Proof of concept.

Modified: jifty/branches/trimclient/examples/Yada/etc/config.yml
==============================================================================
--- jifty/branches/trimclient/examples/Yada/etc/config.yml	(original)
+++ jifty/branches/trimclient/examples/Yada/etc/config.yml	Mon Jun 11 17:57:19 2007
@@ -16,7 +16,7 @@
     RecordBaseClass: Jifty::DBI::Record::Cachable
     User: ''
     Version: 0.0.1
-  DevelMode: 0
+  DevelMode: 1
   L10N: 
     PoDir: share/po
   LogLevel: INFO

Added: jifty/branches/trimclient/examples/Yada/lib/Yada.pm
==============================================================================
--- (empty file)
+++ jifty/branches/trimclient/examples/Yada/lib/Yada.pm	Mon Jun 11 17:57:19 2007
@@ -0,0 +1,6 @@
+package Yada;
+
+
+Jifty->web->add_javascript(qw( Asynapse/REST.js trimpath-template.js ) );
+
+1;

Modified: jifty/branches/trimclient/examples/Yada/lib/Yada/View.pm
==============================================================================
--- jifty/branches/trimclient/examples/Yada/lib/Yada/View.pm	(original)
+++ jifty/branches/trimclient/examples/Yada/lib/Yada/View.pm	Mon Jun 11 17:57:19 2007
@@ -1,18 +1,31 @@
 package Yada::View;
 use Jifty::View::Declare -base;
 
+use Jifty::View::Declare::CRUD;
+for (qw/todo/) {
+    Jifty::View::Declare::CRUD->mount_view($_);
+}
+
 template 'index.html' => page {
     my $self = shift;
     title { _('Yada!') };
 
+    hyperlink(label => 'clear',
+	      onclick => [{region => 'test_region',
+			   replace_with => '__jifty/empty',
+			  }]);
+
+    render_region(
+        name => 'test_region',
+        path => '/__jifty/empty'
+    );
+
+
+
     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($_);
-}
-
 1;

Modified: jifty/branches/trimclient/examples/Yada/lib/Yada/View/Todo.pm
==============================================================================
--- jifty/branches/trimclient/examples/Yada/lib/Yada/View/Todo.pm	(original)
+++ jifty/branches/trimclient/examples/Yada/lib/Yada/View/Todo.pm	Mon Jun 11 17:57:19 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/branches/trimclient/examples/Yada/share/web/static/js/Asynapse/REST.js
==============================================================================
--- (empty file)
+++ jifty/branches/trimclient/examples/Yada/share/web/static/js/Asynapse/REST.js	Mon Jun 11 17:57:19 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
+
+*/ 
+

Added: jifty/branches/trimclient/examples/Yada/share/web/static/js/trimpath-template.js
==============================================================================
--- (empty file)
+++ jifty/branches/trimclient/examples/Yada/share/web/static/js/trimpath-template.js	Mon Jun 11 17:57:19 2007
@@ -0,0 +1,397 @@
+/**
+ * TrimPath Template. Release 1.0.38.
+ * Copyright (C) 2004, 2005 Metaha.
+ * 
+ * TrimPath Template is licensed under the GNU General Public License
+ * and the Apache License, Version 2.0, as follows:
+ *
+ * This program is free software; you can redistribute it and/or 
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ * 
+ * This program is distributed WITHOUT ANY WARRANTY; without even the 
+ * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
+ * See the GNU General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+var TrimPath;
+
+// TODO: Debugging mode vs stop-on-error mode - runtime flag.
+// TODO: Handle || (or) characters and backslashes.
+// TODO: Add more modifiers.
+
+(function() {               // Using a closure to keep global namespace clean.
+    if (TrimPath == null)
+        TrimPath = new Object();
+    if (TrimPath.evalEx == null)
+        TrimPath.evalEx = function(src) { return eval(src); };
+
+    var UNDEFINED;
+    if (Array.prototype.pop == null)  // IE 5.x fix from Igor Poteryaev.
+        Array.prototype.pop = function() {
+            if (this.length === 0) {return UNDEFINED;}
+            return this[--this.length];
+        };
+    if (Array.prototype.push == null) // IE 5.x fix from Igor Poteryaev.
+        Array.prototype.push = function() {
+            for (var i = 0; i < arguments.length; ++i) {this[this.length] = arguments[i];}
+            return this.length;
+        };
+
+    TrimPath.parseTemplate = function(tmplContent, optTmplName, optEtc) {
+        if (optEtc == null)
+            optEtc = TrimPath.parseTemplate_etc;
+        var funcSrc = parse(tmplContent, optTmplName, optEtc);
+        var func = TrimPath.evalEx(funcSrc, optTmplName, 1);
+        if (func != null)
+            return new optEtc.Template(optTmplName, tmplContent, funcSrc, func, optEtc);
+        return null;
+    }
+    
+    try {
+        String.prototype.process = function(context, optFlags) {
+            var template = TrimPath.parseTemplate(this, null);
+            if (template != null)
+                return template.process(context, optFlags);
+            return this;
+        }
+    } catch (e) { // Swallow exception, such as when String.prototype is sealed.
+    }
+    
+    TrimPath.parseTemplate_etc = {};            // Exposed for extensibility.
+    TrimPath.parseTemplate_etc.statementTag = "forelse|for|if|elseif|else|var|macro";
+    TrimPath.parseTemplate_etc.statementDef = { // Lookup table for statement tags.
+        "if"     : { delta:  1, prefix: "if (", suffix: ") {", paramMin: 1 },
+        "else"   : { delta:  0, prefix: "} else {" },
+        "elseif" : { delta:  0, prefix: "} else if (", suffix: ") {", paramDefault: "true" },
+        "/if"    : { delta: -1, prefix: "}" },
+        "for"    : { delta:  1, paramMin: 3, 
+                     prefixFunc : function(stmtParts, state, tmplName, etc) {
+                        if (stmtParts[2] != "in")
+                            throw new etc.ParseError(tmplName, state.line, "bad for loop statement: " + stmtParts.join(' '));
+                        var iterVar = stmtParts[1];
+                        var listVar = "__LIST__" + iterVar;
+                        return [ "var ", listVar, " = ", stmtParts[3], ";",
+                             // Fix from Ross Shaull for hash looping, make sure that we have an array of loop lengths to treat like a stack.
+                             "var __LENGTH_STACK__;",
+                             "if (typeof(__LENGTH_STACK__) == 'undefined' || !__LENGTH_STACK__.length) __LENGTH_STACK__ = new Array();", 
+                             "__LENGTH_STACK__[__LENGTH_STACK__.length] = 0;", // Push a new for-loop onto the stack of loop lengths.
+                             "if ((", listVar, ") != null) { ",
+                             "var ", iterVar, "_ct = 0;",       // iterVar_ct variable, added by B. Bittman     
+                             "for (var ", iterVar, "_index in ", listVar, ") { ",
+                             iterVar, "_ct++;",
+                             "if (typeof(", listVar, "[", iterVar, "_index]) == 'function') {continue;}", // IE 5.x fix from Igor Poteryaev.
+                             "__LENGTH_STACK__[__LENGTH_STACK__.length - 1]++;",
+                             "var ", iterVar, " = ", listVar, "[", iterVar, "_index];" ].join("");
+                     } },
+        "forelse" : { delta:  0, prefix: "} } if (__LENGTH_STACK__[__LENGTH_STACK__.length - 1] == 0) { if (", suffix: ") {", paramDefault: "true" },
+        "/for"    : { delta: -1, prefix: "} }; delete __LENGTH_STACK__[__LENGTH_STACK__.length - 1];" }, // Remove the just-finished for-loop from the stack of loop lengths.
+        "var"     : { delta:  0, prefix: "var ", suffix: ";" },
+        "macro"   : { delta:  1, 
+                      prefixFunc : function(stmtParts, state, tmplName, etc) {
+                          var macroName = stmtParts[1].split('(')[0];
+                          return [ "var ", macroName, " = function", 
+                                   stmtParts.slice(1).join(' ').substring(macroName.length),
+                                   "{ var _OUT_arr = []; var _OUT = { write: function(m) { if (m) _OUT_arr.push(m); } }; " ].join('');
+                     } }, 
+        "/macro"  : { delta: -1, prefix: " return _OUT_arr.join(''); };" }
+    }
+    TrimPath.parseTemplate_etc.modifierDef = {
+        "eat"        : function(v)    { return ""; },
+        "escape"     : function(s)    { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); },
+        "capitalize" : function(s)    { return String(s).toUpperCase(); },
+        "default"    : function(s, d) { return s != null ? s : d; }
+    }
+    TrimPath.parseTemplate_etc.modifierDef.h = TrimPath.parseTemplate_etc.modifierDef.escape;
+
+    TrimPath.parseTemplate_etc.Template = function(tmplName, tmplContent, funcSrc, func, etc) {
+        this.process = function(context, flags) {
+            if (context == null)
+                context = {};
+            if (context._MODIFIERS == null)
+                context._MODIFIERS = {};
+            if (context.defined == null)
+                context.defined = function(str) { return (context[str] != undefined); };
+            for (var k in etc.modifierDef) {
+                if (context._MODIFIERS[k] == null)
+                    context._MODIFIERS[k] = etc.modifierDef[k];
+            }
+            if (flags == null)
+                flags = {};
+            var resultArr = [];
+            var resultOut = { write: function(m) { resultArr.push(m); } };
+            try {
+                func(resultOut, context, flags);
+            } catch (e) {
+                if (flags.throwExceptions == true)
+                    throw e;
+                var result = new String(resultArr.join("") + "[ERROR: " + e.toString() + (e.message ? '; ' + e.message : '') + "]");
+                result["exception"] = e;
+                return result;
+            }
+            return resultArr.join("");
+        }
+        this.name       = tmplName;
+        this.source     = tmplContent; 
+        this.sourceFunc = funcSrc;
+        this.toString   = function() { return "TrimPath.Template [" + tmplName + "]"; }
+    }
+    TrimPath.parseTemplate_etc.ParseError = function(name, line, message) {
+        this.name    = name;
+        this.line    = line;
+        this.message = message;
+    }
+    TrimPath.parseTemplate_etc.ParseError.prototype.toString = function() { 
+        return ("TrimPath template ParseError in " + this.name + ": line " + this.line + ", " + this.message);
+    }
+    
+    var parse = function(body, tmplName, etc) {
+        body = cleanWhiteSpace(body);
+        var funcText = [ "var TrimPath_Template_TEMP = function(_OUT, _CONTEXT, _FLAGS) { with (_CONTEXT) {" ];
+        var state    = { stack: [], line: 1 };                              // TODO: Fix line number counting.
+        var endStmtPrev = -1;
+        while (endStmtPrev + 1 < body.length) {
+            var begStmt = endStmtPrev;
+            // Scan until we find some statement markup.
+            begStmt = body.indexOf("{", begStmt + 1);
+            while (begStmt >= 0) {
+                var endStmt = body.indexOf('}', begStmt + 1);
+                var stmt = body.substring(begStmt, endStmt);
+                var blockrx = stmt.match(/^\{(cdata|minify|eval)/); // From B. Bittman, minify/eval/cdata implementation.
+                if (blockrx) {
+                    var blockType = blockrx[1]; 
+                    var blockMarkerBeg = begStmt + blockType.length + 1;
+                    var blockMarkerEnd = body.indexOf('}', blockMarkerBeg);
+                    if (blockMarkerEnd >= 0) {
+                        var blockMarker;
+                        if( blockMarkerEnd - blockMarkerBeg <= 0 ) {
+                            blockMarker = "{/" + blockType + "}";
+                        } else {
+                            blockMarker = body.substring(blockMarkerBeg + 1, blockMarkerEnd);
+                        }                        
+                        
+                        var blockEnd = body.indexOf(blockMarker, blockMarkerEnd + 1);
+                        if (blockEnd >= 0) {                            
+                            emitSectionText(body.substring(endStmtPrev + 1, begStmt), funcText);
+                            
+                            var blockText = body.substring(blockMarkerEnd + 1, blockEnd);
+                            if (blockType == 'cdata') {
+                                emitText(blockText, funcText);
+                            } else if (blockType == 'minify') {
+                                emitText(scrubWhiteSpace(blockText), funcText);
+                            } else if (blockType == 'eval') {
+                                if (blockText != null && blockText.length > 0) // From B. Bittman, eval should not execute until process().
+                                    funcText.push('_OUT.write( (function() { ' + blockText + ' })() );');
+                            }
+                            begStmt = endStmtPrev = blockEnd + blockMarker.length - 1;
+                        }
+                    }                        
+                } else if (body.charAt(begStmt - 1) != '$' &&               // Not an expression or backslashed,
+                           body.charAt(begStmt - 1) != '\\') {              // so check if it is a statement tag.
+                    var offset = (body.charAt(begStmt + 1) == '/' ? 2 : 1); // Close tags offset of 2 skips '/'.
+                                                                            // 10 is larger than maximum statement tag length.
+                    if (body.substring(begStmt + offset, begStmt + 10 + offset).search(TrimPath.parseTemplate_etc.statementTag) == 0) 
+                        break;                                              // Found a match.
+                }
+                begStmt = body.indexOf("{", begStmt + 1);
+            }
+            if (begStmt < 0)                              // In "a{for}c", begStmt will be 1.
+                break;
+            var endStmt = body.indexOf("}", begStmt + 1); // In "a{for}c", endStmt will be 5.
+            if (endStmt < 0)
+                break;
+            emitSectionText(body.substring(endStmtPrev + 1, begStmt), funcText);
+            emitStatement(body.substring(begStmt, endStmt + 1), state, funcText, tmplName, etc);
+            endStmtPrev = endStmt;
+        }
+        emitSectionText(body.substring(endStmtPrev + 1), funcText);
+        if (state.stack.length != 0)
+            throw new etc.ParseError(tmplName, state.line, "unclosed, unmatched statement(s): " + state.stack.join(","));
+        funcText.push("}}; TrimPath_Template_TEMP");
+        return funcText.join("");
+    }
+    
+    var emitStatement = function(stmtStr, state, funcText, tmplName, etc) {
+        var parts = stmtStr.slice(1, -1).split(' ');
+        var stmt = etc.statementDef[parts[0]]; // Here, parts[0] == for/if/else/...
+        if (stmt == null) {                    // Not a real statement.
+            emitSectionText(stmtStr, funcText);
+            return;
+        }
+        if (stmt.delta < 0) {
+            if (state.stack.length <= 0)
+                throw new etc.ParseError(tmplName, state.line, "close tag does not match any previous statement: " + stmtStr);
+            state.stack.pop();
+        } 
+        if (stmt.delta > 0)
+            state.stack.push(stmtStr);
+
+        if (stmt.paramMin != null &&
+            stmt.paramMin >= parts.length)
+            throw new etc.ParseError(tmplName, state.line, "statement needs more parameters: " + stmtStr);
+        if (stmt.prefixFunc != null)
+            funcText.push(stmt.prefixFunc(parts, state, tmplName, etc));
+        else 
+            funcText.push(stmt.prefix);
+        if (stmt.suffix != null) {
+            if (parts.length <= 1) {
+                if (stmt.paramDefault != null)
+                    funcText.push(stmt.paramDefault);
+            } else {
+                for (var i = 1; i < parts.length; i++) {
+                    if (i > 1)
+                        funcText.push(' ');
+                    funcText.push(parts[i]);
+                }
+            }
+            funcText.push(stmt.suffix);
+        }
+    }
+
+    var emitSectionText = function(text, funcText) {
+        if (text.length <= 0)
+            return;
+        var nlPrefix = 0;               // Index to first non-newline in prefix.
+        var nlSuffix = text.length - 1; // Index to first non-space/tab in suffix.
+        while (nlPrefix < text.length && (text.charAt(nlPrefix) == '\n'))
+            nlPrefix++;
+        while (nlSuffix >= 0 && (text.charAt(nlSuffix) == ' ' || text.charAt(nlSuffix) == '\t'))
+            nlSuffix--;
+        if (nlSuffix < nlPrefix)
+            nlSuffix = nlPrefix;
+        if (nlPrefix > 0) {
+            funcText.push('if (_FLAGS.keepWhitespace == true) _OUT.write("');
+            var s = text.substring(0, nlPrefix).replace('\n', '\\n'); // A macro IE fix from BJessen.
+            if (s.charAt(s.length - 1) == '\n')
+            	s = s.substring(0, s.length - 1);
+            funcText.push(s);
+            funcText.push('");');
+        }
+        var lines = text.substring(nlPrefix, nlSuffix + 1).split('\n');
+        for (var i = 0; i < lines.length; i++) {
+            emitSectionTextLine(lines[i], funcText);
+            if (i < lines.length - 1)
+                funcText.push('_OUT.write("\\n");\n');
+        }
+        if (nlSuffix + 1 < text.length) {
+            funcText.push('if (_FLAGS.keepWhitespace == true) _OUT.write("');
+            var s = text.substring(nlSuffix + 1).replace('\n', '\\n');
+            if (s.charAt(s.length - 1) == '\n')
+            	s = s.substring(0, s.length - 1);
+            funcText.push(s);
+            funcText.push('");');
+        }
+    }
+    
+    var emitSectionTextLine = function(line, funcText) {
+        var endMarkPrev = '}';
+        var endExprPrev = -1;
+        while (endExprPrev + endMarkPrev.length < line.length) {
+            var begMark = "${", endMark = "}";
+            var begExpr = line.indexOf(begMark, endExprPrev + endMarkPrev.length); // In "a${b}c", begExpr == 1
+            if (begExpr < 0)
+                break;
+            if (line.charAt(begExpr + 2) == '%') {
+                begMark = "${%";
+                endMark = "%}";
+            }
+            var endExpr = line.indexOf(endMark, begExpr + begMark.length);         // In "a${b}c", endExpr == 4;
+            if (endExpr < 0)
+                break;
+            emitText(line.substring(endExprPrev + endMarkPrev.length, begExpr), funcText);                
+            // Example: exprs == 'firstName|default:"John Doe"|capitalize'.split('|')
+            var exprArr = line.substring(begExpr + begMark.length, endExpr).replace(/\|\|/g, "#@@#").split('|');
+            for (var k in exprArr) {
+                if (exprArr[k].replace) // IE 5.x fix from Igor Poteryaev.
+                    exprArr[k] = exprArr[k].replace(/#@@#/g, '||');
+            }
+            funcText.push('_OUT.write(');
+            emitExpression(exprArr, exprArr.length - 1, funcText); 
+            funcText.push(');');
+            endExprPrev = endExpr;
+            endMarkPrev = endMark;
+        }
+        emitText(line.substring(endExprPrev + endMarkPrev.length), funcText); 
+    }
+    
+    var emitText = function(text, funcText) {
+        if (text == null ||
+            text.length <= 0)
+            return;
+        text = text.replace(/\\/g, '\\\\');
+        text = text.replace(/\n/g, '\\n');
+        text = text.replace(/"/g,  '\\"');
+        funcText.push('_OUT.write("');
+        funcText.push(text);
+        funcText.push('");');
+    }
+    
+    var emitExpression = function(exprArr, index, funcText) {
+        // Ex: foo|a:x|b:y1,y2|c:z1,z2 is emitted as c(b(a(foo,x),y1,y2),z1,z2)
+        var expr = exprArr[index]; // Ex: exprArr == [firstName,capitalize,default:"John Doe"]
+        if (index <= 0) {          // Ex: expr    == 'default:"John Doe"'
+            funcText.push(expr);
+            return;
+        }
+        var parts = expr.split(':');
+        funcText.push('_MODIFIERS["');
+        funcText.push(parts[0]); // The parts[0] is a modifier function name, like capitalize.
+        funcText.push('"](');
+        emitExpression(exprArr, index - 1, funcText);
+        if (parts.length > 1) {
+            funcText.push(',');
+            funcText.push(parts[1]);
+        }
+        funcText.push(')');
+    }
+
+    var cleanWhiteSpace = function(result) {
+        result = result.replace(/\t/g,   "    ");
+        result = result.replace(/\r\n/g, "\n");
+        result = result.replace(/\r/g,   "\n");
+        result = result.replace(/^(\s*\S*(\s+\S+)*)\s*$/, '$1'); // Right trim by Igor Poteryaev.
+        return result;
+    }
+
+    var scrubWhiteSpace = function(result) {
+        result = result.replace(/^\s+/g,   "");
+        result = result.replace(/\s+$/g,   "");
+        result = result.replace(/\s+/g,   " ");
+        result = result.replace(/^(\s*\S*(\s+\S+)*)\s*$/, '$1'); // Right trim by Igor Poteryaev.
+        return result;
+    }
+
+    // The DOM helper functions depend on DOM/DHTML, so they only work in a browser.
+    // However, these are not considered core to the engine.
+    //
+    TrimPath.parseDOMTemplate = function(elementId, optDocument, optEtc) {
+        if (optDocument == null)
+            optDocument = document;
+        var element = optDocument.getElementById(elementId);
+        var content = element.value;     // Like textarea.value.
+        if (content == null)
+            content = element.innerHTML; // Like textarea.innerHTML.
+        content = content.replace(/&lt;/g, "<").replace(/&gt;/g, ">");
+        return TrimPath.parseTemplate(content, elementId, optEtc);
+    }
+
+    TrimPath.processDOMTemplate = function(elementId, context, optFlags, optDocument, optEtc) {
+        return TrimPath.parseDOMTemplate(elementId, optDocument, optEtc).process(context, optFlags);
+    }
+}) ();

Modified: jifty/branches/trimclient/lib/Jifty/View/Declare/CRUD.pm
==============================================================================
--- jifty/branches/trimclient/lib/Jifty/View/Declare/CRUD.pm	(original)
+++ jifty/branches/trimclient/lib/Jifty/View/Declare/CRUD.pm	Mon Jun 11 17:57:19 2007
@@ -6,6 +6,14 @@
 use base 'Exporter';
 our @EXPORT = qw(object_type fragment_for get_record current_collection);
 
+# 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]}++;
+}
+
 sub mount_view {
     my ($class, $model, $vclass, $path) = @_;
     my $caller = caller(0);
@@ -21,6 +29,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;
+}
+
 sub object_type {
     my $self = shift;
     return $self->package_variable('object_type') || get('object_type');
@@ -84,26 +105,24 @@
      return   grep { !( m/_confirm/ || lc $action->arguments->{$_}{render_as} eq 'password' ) } $action->argument_names;
 }
 
-
-template 'view' => sub {
-    my $self = shift;
-    my ( $object_type, $id ) = ( $self->object_type, get('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  => $self->get_record($id)
+        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' } );
         hyperlink(
             label   => "Edit",
             class   => "editlink",
             onclick => {
                 replace_with => $self->fragment_for('update'),
-                args         => { object_type => $object_type, id => $id }
+                args         => { id => $record->id }
             },
         );
 

Modified: jifty/branches/trimclient/lib/Jifty/Web/PageRegion.pm
==============================================================================
--- jifty/branches/trimclient/lib/Jifty/Web/PageRegion.pm	(original)
+++ jifty/branches/trimclient/lib/Jifty/Web/PageRegion.pm	Mon Jun 11 17:57:19 2007
@@ -384,4 +384,36 @@
     return "#region-" . $self->qualified_name . ' ' . join(' ', @_);
 }
 
+sub client_cacheable {
+    my $self = shift;
+    return 'static' if $self->path eq '__jifty/empty';
+    return 'crudview' if $self->path eq '//todo/view';
+}
+
+sub client_cache_content {
+    my $self = shift;
+    my $code = Template::Declare->resolve_template($self->path);
+
+    local @Evil::ISA = ('Yada::Model::Todo');
+    local $HTML::Mason::Commands::m = undef;
+    local Jifty->handler->apache->{http_header_sent} = 1;
+
+    # dup from JV::Declare::Handler
+    no warnings qw/redefine/;
+    local *Jifty::Web::out = sub {
+        shift;    # Turn the method into a function
+        goto &Template::Declare::Tags::outs_raw;
+    };
+
+    return Template::Declare::Tags::show_page( $self->path, (bless {}, 'Evil') );
+}
+
+package Evil;
+
+sub id { return '${id}' }
+
+sub _value {
+    return '${'.$_[1].'}';
+}
+
 1;

Modified: jifty/branches/trimclient/share/web/static/js/jifty.js
==============================================================================
--- jifty/branches/trimclient/share/web/static/js/jifty.js	(original)
+++ jifty/branches/trimclient/share/web/static/js/jifty.js	Mon Jun 11 17:57:19 2007
@@ -652,6 +652,26 @@
 
     return f;    
 }
+
+var CACHE = {};
+
+var extract_cacheable = function(fragment, f) {
+    for (var fragment_bit = fragment.firstChild;
+         fragment_bit != null;
+         fragment_bit = fragment_bit.nextSibling) {
+        if (fragment_bit.nodeName == 'cacheable') {
+            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;
+            } 
+            CACHE[f['path']] = { 'type': c_type, 'content': textContent };
+        }
+    }
+}
+
 // applying updates from a fragment
 //   - fragment: the fragment from the server
 //   - f: fragment spec
@@ -659,6 +679,7 @@
     // 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;
@@ -732,7 +753,6 @@
         window.event.returnValue = false;
     }
 
-    show_wait_message();
     var named_args = arguments[0];
     var trigger    = arguments[1];
 
@@ -759,6 +779,7 @@
     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];
@@ -775,15 +796,47 @@
                 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');
+            content_node.textContent = cached['content'];
+            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'] == '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'].process(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']);
@@ -794,8 +847,17 @@
 
         // 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
@@ -815,9 +877,13 @@
             if (response_fragment.getAttribute("id") != f['region'])
                 continue;
 
-	    try {
-            apply_fragment_updates(response_fragment, f);
-	    }catch (e) { alert(e) }
+            for (var i = 0; i < update_from_cache.length; i++)
+                update_from_cache[i]();
+
+            try {
+                apply_fragment_updates(response_fragment, f);
+            }catch (e) { alert(e) }
+            extract_cacheable(response_fragment, f);
         }
         for (var result = response.firstChild;
              result != null;

Modified: jifty/branches/trimclient/share/web/templates/__jifty/webservices/xml
==============================================================================
--- jifty/branches/trimclient/share/web/templates/__jifty/webservices/xml	(original)
+++ jifty/branches/trimclient/share/web/templates/__jifty/webservices/xml	Mon Jun 11 17:57:19 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->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();
 


More information about the Jifty-commit mailing list