# HG changeset patch # User cmlenz # Date 1188409905 0 # Node ID 16b1be35c2654b09c8a7cda994fde543d50a46ea # Parent 6a37018199fd5779d4c239710c40ce0bfd42cddf Add current code for GenshiTutorial to the `examples` directory. diff --git a/examples/tutorial/geddit/__init__.py b/examples/tutorial/geddit/__init__.py new file mode 100644 diff --git a/examples/tutorial/geddit/controller.py b/examples/tutorial/geddit/controller.py new file mode 100755 --- /dev/null +++ b/examples/tutorial/geddit/controller.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python + +import os +import pickle +import sys + +import cherrypy +from formencode import Invalid +from genshi.filters import HTMLFormFiller +from paste.evalexception.middleware import EvalException + +from geddit.form import SubmissionForm, CommentForm +from geddit.lib import template +from geddit.model import Submission, Comment + + +class Root(object): + + def __init__(self, data): + self.data = data + self.submission_lookup = {} + self.comment_lookup = {} + for submission in self.data: + self.submission_lookup[submission.code] = submission + for comment in submission.comments: + self.comment_lookup[comment.code] = comment + def _add_replies(comment): + for reply in comment.replies: + self.comment_lookup[reply.code] = reply + _add_replies(comment) + + @cherrypy.expose + @template.output('index.html', method='html', doctype='html') + def index(self): + return template.render(submissions=self.data) + + @cherrypy.expose + @template.output('info.html', method='html', doctype='html') + def info(self, code): + submission = self.submission_lookup.get(code) + if not submission: + raise cherrypy.NotFound() + return template.render(submission=submission) + + @cherrypy.expose + @template.output('submit.html', method='html', doctype='html') + def submit(self, cancel=False, **data): + if cherrypy.request.method == 'POST': + if cancel: + raise cherrypy.HTTPRedirect('/') + form = SubmissionForm() + try: + data = form.to_python(data) + submission = Submission(**data) + self.data.append(submission) + raise cherrypy.HTTPRedirect('/') + except Invalid, e: + errors = e.unpack_errors() + else: + errors = {} + + return template.render(errors=errors) | HTMLFormFiller(data=data) + + @cherrypy.expose + @template.output('comment.html', method='html', doctype='html') + def comment(self, code, cancel=False, **data): + submission = self.submission_lookup.get(code) + if not submission: + raise cherrypy.NotFound() + if cherrypy.request.method == 'POST': + if cancel: + raise cherrypy.HTTPRedirect('/info/%s' % submission.code) + form = CommentForm() + try: + data = form.to_python(data) + comment = submission.add_comment(**data) + self.comment_lookup[comment.code] = comment + raise cherrypy.HTTPRedirect('/') + except Invalid, e: + errors = e.unpack_errors() + else: + errors = {} + + return template.render(submission=submission, comment=None, + errors=errors) + + @cherrypy.expose + @template.output('comment.html', method='html', doctype='html') + def reply(self, code, cancel=False, **data): + comment = self.comment_lookup.get(code) + submission = comment.submission + if not comment: + raise cherrypy.NotFound() + if cherrypy.request.method == 'POST': + if cancel: + raise cherrypy.HTTPRedirect('/info/%s' % submission.code) + form = CommentForm() + try: + data = form.to_python(data) + comment = comment.add_reply(**data) + self.comment_lookup[comment.code] = comment + raise cherrypy.HTTPRedirect('/') + except Invalid, e: + errors = e.unpack_errors() + else: + errors = {} + + return template.render(submission=submission, comment=comment, + errors=errors) + + +def main(filename): + # load data from the pickle file, or initialize it to an empty list + if os.path.exists(filename): + fileobj = open(filename, 'rb') + try: + data = pickle.load(fileobj) + finally: + fileobj.close() + else: + data = [] + + def _save_data(): + # save data back to the pickle file + fileobj = open(filename, 'wb') + try: + pickle.dump(data, fileobj) + finally: + fileobj.close() + cherrypy.engine.on_stop_engine_list.append(_save_data) + + # Some global configuration; note that this could be moved into a configuration file + cherrypy.config.update({ + 'request.throw_errors': True, + 'tools.encode.on': True, 'tools.encode.encoding': 'utf-8', + 'tools.decode.on': True, + 'tools.trailing_slash.on': True, + 'tools.staticdir.root': os.path.abspath(os.path.dirname(__file__)), + }) + + # Initialize the application, and add EvalException for more helpful error messages + app = cherrypy.Application(Root(data)) + app.wsgiapp.pipeline.append(('paste_exc', EvalException)) + cherrypy.quickstart(app, '/', { + '/media': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static' + } + }) + +if __name__ == '__main__': + import formencode + formencode.api.set_stdtranslation(languages=['en']) + main(sys.argv[1]) diff --git a/examples/tutorial/geddit/form.py b/examples/tutorial/geddit/form.py new file mode 100644 --- /dev/null +++ b/examples/tutorial/geddit/form.py @@ -0,0 +1,12 @@ +from formencode import Schema, validators + + +class SubmissionForm(Schema): + username = validators.UnicodeString(not_empty=True) + url = validators.URL(not_empty=True, add_http=True, check_exists=False) + title = validators.UnicodeString(not_empty=True) + + +class CommentForm(Schema): + username = validators.UnicodeString(not_empty=True) + content = validators.UnicodeString(not_empty=True) diff --git a/examples/tutorial/geddit/lib/__init__.py b/examples/tutorial/geddit/lib/__init__.py new file mode 100644 diff --git a/examples/tutorial/geddit/lib/template.py b/examples/tutorial/geddit/lib/template.py new file mode 100644 --- /dev/null +++ b/examples/tutorial/geddit/lib/template.py @@ -0,0 +1,40 @@ +import os + +import cherrypy +from genshi.core import Stream +from genshi.output import encode, get_serializer +from genshi.template import TemplateLoader + +loader = TemplateLoader( + os.path.join(os.path.dirname(__file__), '..', 'templates'), + auto_reload=True +) + +def output(filename, method=None, encoding='utf-8', **options): + """Decorator for exposed methods to specify what template the should use + for rendering, and which serialization method and options should be + applied. + """ + def decorate(func): + def wrapper(*args, **kwargs): + cherrypy.thread_data.template = loader.load(filename) + serializer = get_serializer(method, **options) + stream = func(*args, **kwargs) + if not isinstance(stream, Stream): + return stream + return encode(serializer(stream), method=serializer, + encoding=encoding) + return wrapper + return decorate + +def render(*args, **kwargs): + """Function to render the given data to the template specified via the + ``@output`` decorator. + """ + if args: + assert len(args) == 1, \ + 'Expected exactly one argument, but got %r' % (args,) + template = loader.load(args[0]) + else: + template = cherrypy.thread_data.template + return template.generate(**kwargs) diff --git a/examples/tutorial/geddit/model.py b/examples/tutorial/geddit/model.py new file mode 100644 --- /dev/null +++ b/examples/tutorial/geddit/model.py @@ -0,0 +1,71 @@ +from datetime import datetime + + +class Submission(object): + + def __init__(self, username, url, title): + self.username = username + self.url = url + self.title = title + self.time = datetime.utcnow() + self.comments = [] + + def __repr__(self): + return '<%s %r>' % (type(self).__name__, self.title) + + def add_comment(self, username, content): + comment = Comment(username, content, in_reply_to=self) + self.comments.append(comment) + return comment + + @property + def code(self): + uid = tuple([self.username, self.url, self.title, self.time]) + return hex(hash(uid))[2:] + + @property + def total_comments(self): + retval = [] + for comment in self.comments: + retval.append(comment) + retval.extend(comment.total_replies) + return retval + + +class Comment(object): + + def __init__(self, username, content, in_reply_to=None): + self.username = username + self.content = content + self.in_reply_to = in_reply_to + self.time = datetime.utcnow() + self.replies = [] + + def __repr__(self): + return '<%s>' % (type(self).__name__) + + def add_reply(self, username, content): + reply = Comment(username, content, in_reply_to=self) + self.replies.append(reply) + return reply + + @property + def code(self): + uid = tuple([self.in_reply_to.code, self.username, self.time]) + return hex(hash(uid))[2:] + + @property + def submission(self): + ref = self.in_reply_to + while ref: + if isinstance(ref, Submission): + return ref + ref = ref.in_reply_to + + @property + def total_replies(self): + retval = [] + for reply in self.replies: + retval.append(reply) + retval.extend(reply.total_replies) + return retval diff --git a/examples/tutorial/geddit/static/jquery.js b/examples/tutorial/geddit/static/jquery.js new file mode 100644 --- /dev/null +++ b/examples/tutorial/geddit/static/jquery.js @@ -0,0 +1,2508 @@ +(function(){ +/* + * jQuery 1.1.4 - New Wave Javascript + * + * Copyright (c) 2007 John Resig (jquery.com) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * $Date: 2007-08-23 21:49:27 -0400 (Thu, 23 Aug 2007) $ + * $Rev: 2862 $ + */ +// Map over jQuery in case of overwrite +if ( typeof jQuery != "undefined" ) + var _jQuery = jQuery; + +var jQuery = window.jQuery = function(a,c) { + // If the context is global, return a new object + if ( window == this || !this.init ) + return new jQuery(a,c); + + return this.init(a,c); +}; + +// Map over the $ in case of overwrite +if ( typeof $ != "undefined" ) + var _$ = $; + +// Map the jQuery namespace to the '$' one +window.$ = jQuery; + +var quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/; + +jQuery.fn = jQuery.prototype = { + init: function(a,c) { + // Make sure that a selection was provided + a = a || document; + + // Handle HTML strings + if ( typeof a == "string" ) { + var m = quickExpr.exec(a); + if ( m && (m[1] || !c) ) { + // HANDLE: $(html) -> $(array) + if ( m[1] ) + a = jQuery.clean( [ m[1] ] ); + + // HANDLE: $("#id") + else { + var tmp = document.getElementById( m[3] ); + if ( tmp ) + // Handle the case where IE and Opera return items + // by name instead of ID + if ( tmp.id != m[3] ) + return jQuery().find( a ); + else { + this[0] = tmp; + this.length = 1; + return this; + } + else + a = []; + } + + // HANDLE: $(expr) + } else + return new jQuery( c ).find( a ); + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction(a) ) + return new jQuery(document)[ jQuery.fn.ready ? "ready" : "load" ]( a ); + + return this.setArray( + // HANDLE: $(array) + a.constructor == Array && a || + + // HANDLE: $(arraylike) + // Watch for when an array-like object is passed as the selector + (a.jquery || a.length && a != window && !a.nodeType && a[0] != undefined && a[0].nodeType) && jQuery.makeArray( a ) || + + // HANDLE: $(*) + [ a ] ); + }, + jquery: "1.1.4", + + size: function() { + return this.length; + }, + + length: 0, + + get: function( num ) { + return num == undefined ? + + // Return a 'clean' array + jQuery.makeArray( this ) : + + // Return just the object + this[num]; + }, + pushStack: function( a ) { + var ret = jQuery(a); + ret.prevObject = this; + return ret; + }, + setArray: function( a ) { + this.length = 0; + Array.prototype.push.apply( this, a ); + return this; + }, + each: function( fn, args ) { + return jQuery.each( this, fn, args ); + }, + index: function( obj ) { + var pos = -1; + this.each(function(i){ + if ( this == obj ) pos = i; + }); + return pos; + }, + + attr: function( key, value, type ) { + var obj = key; + + // Look for the case where we're accessing a style value + if ( key.constructor == String ) + if ( value == undefined ) + return this.length && jQuery[ type || "attr" ]( this[0], key ) || undefined; + else { + obj = {}; + obj[ key ] = value; + } + + // Check to see if we're setting style values + return this.each(function(index){ + // Set all the styles + for ( var prop in obj ) + jQuery.attr( + type ? this.style : this, + prop, jQuery.prop(this, obj[prop], type, index, prop) + ); + }); + }, + + css: function( key, value ) { + return this.attr( key, value, "curCSS" ); + }, + + text: function(e) { + if ( typeof e != "object" && e != null ) + return this.empty().append( document.createTextNode( e ) ); + + var t = ""; + jQuery.each( e || this, function(){ + jQuery.each( this.childNodes, function(){ + if ( this.nodeType != 8 ) + t += this.nodeType != 1 ? + this.nodeValue : jQuery.fn.text([ this ]); + }); + }); + return t; + }, + + wrap: function() { + // The elements to wrap the target around + var a, args = arguments; + + // Wrap each of the matched elements individually + return this.each(function(){ + if ( !a ) + a = jQuery.clean(args, this.ownerDocument); + + // Clone the structure that we're using to wrap + var b = a[0].cloneNode(true); + + // Insert it before the element to be wrapped + this.parentNode.insertBefore( b, this ); + + // Find the deepest point in the wrap structure + while ( b.firstChild ) + b = b.firstChild; + + // Move the matched element to within the wrap structure + b.appendChild( this ); + }); + }, + append: function() { + return this.domManip(arguments, true, 1, function(a){ + this.appendChild( a ); + }); + }, + prepend: function() { + return this.domManip(arguments, true, -1, function(a){ + this.insertBefore( a, this.firstChild ); + }); + }, + before: function() { + return this.domManip(arguments, false, 1, function(a){ + this.parentNode.insertBefore( a, this ); + }); + }, + after: function() { + return this.domManip(arguments, false, -1, function(a){ + this.parentNode.insertBefore( a, this.nextSibling ); + }); + }, + end: function() { + return this.prevObject || jQuery([]); + }, + find: function(t) { + var data = jQuery.map(this, function(a){ return jQuery.find(t,a); }); + return this.pushStack( /[^+>] [^+>]/.test( t ) || t.indexOf("..") > -1 ? + jQuery.unique( data ) : data ); + }, + clone: function(deep) { + deep = deep != undefined ? deep : true; + var $this = this.add(this.find("*")); + if (jQuery.browser.msie) { + // Need to remove events on the element and its descendants + $this.each(function() { + this._$events = {}; + for (var type in this.$events) + this._$events[type] = jQuery.extend({},this.$events[type]); + }).unbind(); + } + + // Do the clone + var r = this.pushStack( jQuery.map( this, function(a){ + return a.cloneNode( deep ); + }) ); + + if (jQuery.browser.msie) { + $this.each(function() { + // Add the events back to the original and its descendants + var events = this._$events; + for (var type in events) + for (var handler in events[type]) + jQuery.event.add(this, type, events[type][handler], events[type][handler].data); + this._$events = null; + }); + } + + // copy form values over + if (deep) { + var inputs = r.add(r.find('*')).filter('select,input[@type=checkbox]'); + $this.filter('select,input[@type=checkbox]').each(function(i) { + if (this.selectedIndex) + inputs[i].selectedIndex = this.selectedIndex; + if (this.checked) + inputs[i].checked = true; + }); + } + + // Return the cloned set + return r; + }, + + filter: function(t) { + return this.pushStack( + jQuery.isFunction( t ) && + jQuery.grep(this, function(el, index){ + return t.apply(el, [index]); + }) || + + jQuery.multiFilter(t,this) ); + }, + + not: function(t) { + return this.pushStack( + t.constructor == String && + jQuery.multiFilter(t, this, true) || + + jQuery.grep(this, function(a) { + return ( t.constructor == Array || t.jquery ) + ? jQuery.inArray( a, t ) < 0 + : a != t; + }) + ); + }, + + add: function(t) { + return this.pushStack( jQuery.merge( + this.get(), + t.constructor == String ? + jQuery(t).get() : + t.length != undefined && (!t.nodeName || t.nodeName == "FORM") ? + t : [t] ) + ); + }, + is: function(expr) { + return expr ? jQuery.multiFilter(expr,this).length > 0 : false; + }, + + val: function( val ) { + return val == undefined ? + ( this.length ? this[0].value : null ) : + this.attr( "value", val ); + }, + + html: function( val ) { + return val == undefined ? + ( this.length ? this[0].innerHTML : null ) : + this.empty().append( val ); + }, + + slice: function() { + return this.pushStack( Array.prototype.slice.apply( this, arguments ) ); + }, + domManip: function(args, table, dir, fn){ + var clone = this.length > 1, a; + + return this.each(function(){ + if ( !a ) { + a = jQuery.clean(args, this.ownerDocument); + if ( dir < 0 ) + a.reverse(); + } + + var obj = this; + + if ( table && jQuery.nodeName(this, "table") && jQuery.nodeName(a[0], "tr") ) + obj = this.getElementsByTagName("tbody")[0] || this.appendChild(document.createElement("tbody")); + + jQuery.each( a, function(){ + if ( jQuery.nodeName(this, "script") ) { + if ( this.src ) + jQuery.ajax({ url: this.src, async: false, dataType: "script" }); + else + jQuery.globalEval( this.text || this.textContent || this.innerHTML || "" ); + } else + fn.apply( obj, [ clone ? this.cloneNode(true) : this ] ); + }); + }); + } +}; + +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, a = 1, al = arguments.length, deep = false; + + // Handle a deep copy situation + if ( target.constructor == Boolean ) { + deep = target; + target = arguments[1] || {}; + } + + // extend jQuery itself if only one argument is passed + if ( al == 1 ) { + target = this; + a = 0; + } + + var prop; + + for ( ; a < al; a++ ) + // Only deal with non-null/undefined values + if ( (prop = arguments[a]) != null ) + // Extend the base object + for ( var i in prop ) { + // Prevent never-ending loop + if ( target == prop[i] ) + continue; + + // Recurse if we're merging object values + if ( deep && typeof prop[i] == 'object' && target[i] ) + jQuery.extend( target[i], prop[i] ); + + // Don't bring in undefined values + else if ( prop[i] != undefined ) + target[i] = prop[i]; + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function(deep) { + window.$ = _$; + if ( deep ) + window.jQuery = _jQuery; + return jQuery; + }, + + // This may seem like some crazy code, but trust me when I say that this + // is the only cross-browser way to do this. --John + isFunction: function( fn ) { + return !!fn && typeof fn != "string" && !fn.nodeName && + fn.constructor != Array && /function/i.test( fn + "" ); + }, + + // check if an element is in a XML document + isXMLDoc: function(elem) { + return elem.documentElement && !elem.body || + elem.tagName && elem.ownerDocument && !elem.ownerDocument.body; + }, + + // Evalulates a script in a global context + // Evaluates Async. in Safari 2 :-( + globalEval: function( data ) { + data = jQuery.trim( data ); + if ( data ) { + if ( window.execScript ) + window.execScript( data ); + else if ( jQuery.browser.safari ) + // safari doesn't provide a synchronous global eval + window.setTimeout( data, 0 ); + else + eval.call( window, data ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); + }, + // args is for internal usage only + each: function( obj, fn, args ) { + if ( args ) { + if ( obj.length == undefined ) + for ( var i in obj ) + fn.apply( obj[i], args ); + else + for ( var i = 0, ol = obj.length; i < ol; i++ ) + if ( fn.apply( obj[i], args ) === false ) break; + + // A special, fast, case for the most common use of each + } else { + if ( obj.length == undefined ) + for ( var i in obj ) + fn.call( obj[i], i, obj[i] ); + else + for ( var i = 0, ol = obj.length, val = obj[0]; + i < ol && fn.call(val,i,val) !== false; val = obj[++i] ){} + } + + return obj; + }, + + prop: function(elem, value, type, index, prop){ + // Handle executable functions + if ( jQuery.isFunction( value ) ) + value = value.call( elem, [index] ); + + // exclude the following css properties to add px + var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i; + + // Handle passing in a number to a CSS property + return value && value.constructor == Number && type == "curCSS" && !exclude.test(prop) ? + value + "px" : + value; + }, + + className: { + // internal only, use addClass("class") + add: function( elem, c ){ + jQuery.each( (c || "").split(/\s+/), function(i, cur){ + if ( !jQuery.className.has( elem.className, cur ) ) + elem.className += ( elem.className ? " " : "" ) + cur; + }); + }, + + // internal only, use removeClass("class") + remove: function( elem, c ){ + elem.className = c != undefined ? + jQuery.grep( elem.className.split(/\s+/), function(cur){ + return !jQuery.className.has( c, cur ); + }).join(" ") : ""; + }, + + // internal only, use is(".class") + has: function( t, c ) { + return jQuery.inArray( c, (t.className || t).toString().split(/\s+/) ) > -1; + } + }, + swap: function(e,o,f) { + for ( var i in o ) { + e.style["old"+i] = e.style[i]; + e.style[i] = o[i]; + } + f.apply( e, [] ); + for ( var i in o ) + e.style[i] = e.style["old"+i]; + }, + + css: function(e,p) { + if ( p == "height" || p == "width" ) { + var old = {}, oHeight, oWidth, d = ["Top","Bottom","Right","Left"]; + + jQuery.each( d, function(){ + old["padding" + this] = 0; + old["border" + this + "Width"] = 0; + }); + + jQuery.swap( e, old, function() { + if ( jQuery(e).is(':visible') ) { + oHeight = e.offsetHeight; + oWidth = e.offsetWidth; + } else { + e = jQuery(e.cloneNode(true)) + .find(":radio").removeAttr("checked").end() + .css({ + visibility: "hidden", position: "absolute", display: "block", right: "0", left: "0" + }).appendTo(e.parentNode)[0]; + + var parPos = jQuery.css(e.parentNode,"position") || "static"; + if ( parPos == "static" ) + e.parentNode.style.position = "relative"; + + oHeight = e.clientHeight; + oWidth = e.clientWidth; + + if ( parPos == "static" ) + e.parentNode.style.position = "static"; + + e.parentNode.removeChild(e); + } + }); + + return p == "height" ? oHeight : oWidth; + } + + return jQuery.curCSS( e, p ); + }, + + curCSS: function(elem, prop, force) { + var ret, stack = [], swap = []; + + // A helper method for determining if an element's values are broken + function color(a){ + if ( !jQuery.browser.safari ) + return false; + + var ret = document.defaultView.getComputedStyle(a,null); + return !ret || ret.getPropertyValue("color") == ""; + } + + if (prop == "opacity" && jQuery.browser.msie) { + ret = jQuery.attr(elem.style, "opacity"); + return ret == "" ? "1" : ret; + } + + if (prop.match(/float/i)) + prop = styleFloat; + + if (!force && elem.style[prop]) + ret = elem.style[prop]; + + else if (document.defaultView && document.defaultView.getComputedStyle) { + + if (prop.match(/float/i)) + prop = "float"; + + prop = prop.replace(/([A-Z])/g,"-$1").toLowerCase(); + var cur = document.defaultView.getComputedStyle(elem, null); + + if ( cur && !color(elem) ) + ret = cur.getPropertyValue(prop); + + // If the element isn't reporting its values properly in Safari + // then some display: none elements are involved + else { + // Locate all of the parent display: none elements + for ( var a = elem; a && color(a); a = a.parentNode ) + stack.unshift(a); + + // Go through and make them visible, but in reverse + // (It would be better if we knew the exact display type that they had) + for ( a = 0; a < stack.length; a++ ) + if ( color(stack[a]) ) { + swap[a] = stack[a].style.display; + stack[a].style.display = "block"; + } + + // Since we flip the display style, we have to handle that + // one special, otherwise get the value + ret = prop == "display" && swap[stack.length-1] != null ? + "none" : + document.defaultView.getComputedStyle(elem,null).getPropertyValue(prop) || ""; + + // Finally, revert the display styles back + for ( a = 0; a < swap.length; a++ ) + if ( swap[a] != null ) + stack[a].style.display = swap[a]; + } + + if ( prop == "opacity" && ret == "" ) + ret = "1"; + + } else if (elem.currentStyle) { + var newProp = prop.replace(/\-(\w)/g,function(m,c){return c.toUpperCase();}); + ret = elem.currentStyle[prop] || elem.currentStyle[newProp]; + } + + return ret; + }, + + clean: function(a, doc) { + var r = []; + doc = doc || document; + + jQuery.each( a, function(i,arg){ + if ( !arg ) return; + + if ( arg.constructor == Number ) + arg = arg.toString(); + + // Convert html string into DOM nodes + if ( typeof arg == "string" ) { + // Trim whitespace, otherwise indexOf won't work as expected + var s = jQuery.trim(arg).toLowerCase(), div = doc.createElement("div"), tb = []; + + var wrap = + // option or optgroup + !s.indexOf("", ""] || + + !s.indexOf("", ""] || + + s.match(/^<(thead|tbody|tfoot|colg|cap)/) && + [1, "", "
"] || + + !s.indexOf("", ""] || + + // matched above + (!s.indexOf("", ""] || + + !s.indexOf("", ""] || + + // IE can't serialize and + ${select('*[local-name()!="title"]')} + + + + +
+ +
+ ${select('*|text()')} +
+ +
+
+ + diff --git a/examples/tutorial/geddit/templates/submit.html b/examples/tutorial/geddit/templates/submit.html new file mode 100644 --- /dev/null +++ b/examples/tutorial/geddit/templates/submit.html @@ -0,0 +1,38 @@ + + + + + Submit new link + + +

Submit new link

+
+ + + + + + + + + + +
+ + ${errors.username} +
+ + ${errors.url} +
+ + ${errors.title} +
+
+ + +
+
+ +