Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSebastian Silva <sebastian@somosazucar.org>2011-07-09 00:17:44 (GMT)
committer Icarito <icarito@spock.(none)>2011-07-09 00:18:57 (GMT)
commit570a268e7562303690ef6b599ea244945a3100ce (patch)
tree1f772420739a73515671f73dfeb397870daa9fe0
parent365ef228a2a94708024030d3993bb9f0a152a038 (diff)
Still importing WebSDK.
Need to read up on GIT.
-rw-r--r--.gitignore2
-rw-r--r--AUTHORS2
-rw-r--r--HACKING1
-rw-r--r--MANIFEST58
-rw-r--r--MANTAINERS1
-rw-r--r--NEWS4
-rw-r--r--TODO14
-rw-r--r--appservice.py156
-rw-r--r--cherrypy/LICENSE.txt25
-rwxr-xr-xcherrypy/__init__.py620
-rwxr-xr-xcherrypy/_cpchecker.py327
-rwxr-xr-xcherrypy/_cpcompat.py283
-rwxr-xr-xcherrypy/_cpconfig.py295
-rwxr-xr-xcherrypy/_cpdispatch.py622
-rwxr-xr-xcherrypy/_cperror.py553
-rwxr-xr-xcherrypy/_cplogging.py393
-rwxr-xr-xcherrypy/_cpmodpy.py333
-rwxr-xr-xcherrypy/_cpnative_server.py149
-rwxr-xr-xcherrypy/_cpreqbody.py941
-rwxr-xr-xcherrypy/_cprequest.py926
-rwxr-xr-xcherrypy/_cpserver.py195
-rwxr-xr-xcherrypy/_cpthreadinglocal.py239
-rwxr-xr-xcherrypy/_cptools.py510
-rwxr-xr-xcherrypy/_cptree.py279
-rwxr-xr-xcherrypy/_cpwsgi.py347
-rwxr-xr-xcherrypy/_cpwsgi_server.py54
-rw-r--r--cherrypy/cherryd109
-rw-r--r--cherrypy/favicon.icobin0 -> 1406 bytes
-rwxr-xr-xcherrypy/lib/__init__.py45
-rwxr-xr-xcherrypy/lib/auth.py87
-rwxr-xr-xcherrypy/lib/auth_basic.py87
-rwxr-xr-xcherrypy/lib/auth_digest.py365
-rwxr-xr-xcherrypy/lib/caching.py465
-rwxr-xr-xcherrypy/lib/covercp.py365
-rwxr-xr-xcherrypy/lib/cpstats.py661
-rwxr-xr-xcherrypy/lib/cptools.py611
-rwxr-xr-xcherrypy/lib/encoding.py388
-rwxr-xr-xcherrypy/lib/http.py7
-rwxr-xr-xcherrypy/lib/httpauth.py354
-rwxr-xr-xcherrypy/lib/httputil.py469
-rwxr-xr-xcherrypy/lib/jsontools.py87
-rwxr-xr-xcherrypy/lib/profiler.py208
-rwxr-xr-xcherrypy/lib/reprconf.py351
-rwxr-xr-xcherrypy/lib/sessions.py832
-rwxr-xr-xcherrypy/lib/static.py352
-rwxr-xr-xcherrypy/lib/xmlrpc.py49
-rwxr-xr-xcherrypy/process/__init__.py14
-rwxr-xr-xcherrypy/process/plugins.py681
-rwxr-xr-xcherrypy/process/servers.py418
-rwxr-xr-xcherrypy/process/win32.py174
-rwxr-xr-xcherrypy/process/wspbus.py393
-rwxr-xr-xcherrypy/scaffold/__init__.py61
-rw-r--r--cherrypy/scaffold/example.conf3
-rw-r--r--cherrypy/scaffold/site.conf14
-rw-r--r--cherrypy/scaffold/static/made_with_cherrypy_small.pngbin0 -> 7455 bytes
-rwxr-xr-xcherrypy/test/__init__.py25
-rwxr-xr-xcherrypy/test/_test_decorators.py41
-rwxr-xr-xcherrypy/test/_test_states_demo.py66
-rwxr-xr-xcherrypy/test/benchmark.py409
-rwxr-xr-xcherrypy/test/checkerdemo.py47
-rwxr-xr-xcherrypy/test/helper.py476
-rwxr-xr-xcherrypy/test/logtest.py181
-rwxr-xr-xcherrypy/test/modfastcgi.py135
-rwxr-xr-xcherrypy/test/modfcgid.py125
-rwxr-xr-xcherrypy/test/modpy.py163
-rwxr-xr-xcherrypy/test/modwsgi.py148
-rwxr-xr-xcherrypy/test/sessiondemo.py153
-rw-r--r--cherrypy/test/static/dirback.jpgbin0 -> 18238 bytes
-rw-r--r--cherrypy/test/static/index.html1
-rw-r--r--cherrypy/test/style.css1
-rw-r--r--cherrypy/test/test.pem38
-rwxr-xr-xcherrypy/test/test_auth_basic.py79
-rwxr-xr-xcherrypy/test/test_auth_digest.py115
-rwxr-xr-xcherrypy/test/test_bus.py263
-rwxr-xr-xcherrypy/test/test_caching.py329
-rwxr-xr-xcherrypy/test/test_config.py249
-rwxr-xr-xcherrypy/test/test_config_server.py121
-rwxr-xr-xcherrypy/test/test_conn.py734
-rwxr-xr-xcherrypy/test/test_core.py617
-rwxr-xr-xcherrypy/test/test_dynamicobjectmapping.py403
-rwxr-xr-xcherrypy/test/test_encoding.py363
-rwxr-xr-xcherrypy/test/test_etags.py81
-rwxr-xr-xcherrypy/test/test_http.py168
-rwxr-xr-xcherrypy/test/test_httpauth.py151
-rwxr-xr-xcherrypy/test/test_httplib.py29
-rwxr-xr-xcherrypy/test/test_json.py79
-rwxr-xr-xcherrypy/test/test_logging.py149
-rwxr-xr-xcherrypy/test/test_mime.py128
-rwxr-xr-xcherrypy/test/test_misc_tools.py202
-rwxr-xr-xcherrypy/test/test_objectmapping.py403
-rwxr-xr-xcherrypy/test/test_proxy.py129
-rwxr-xr-xcherrypy/test/test_refleaks.py119
-rwxr-xr-xcherrypy/test/test_request_obj.py722
-rwxr-xr-xcherrypy/test/test_routes.py69
-rwxr-xr-xcherrypy/test/test_session.py464
-rwxr-xr-xcherrypy/test/test_sessionauthenticate.py62
-rwxr-xr-xcherrypy/test/test_states.py436
-rwxr-xr-xcherrypy/test/test_static.py300
-rwxr-xr-xcherrypy/test/test_tools.py393
-rwxr-xr-xcherrypy/test/test_tutorials.py201
-rwxr-xr-xcherrypy/test/test_virtualhost.py107
-rwxr-xr-xcherrypy/test/test_wsgi_ns.py80
-rwxr-xr-xcherrypy/test/test_wsgi_vhost.py36
-rwxr-xr-xcherrypy/test/test_wsgiapps.py111
-rwxr-xr-xcherrypy/test/test_xmlrpc.py172
-rwxr-xr-xcherrypy/test/webtest.py535
-rw-r--r--cherrypy/tutorial/README.txt16
-rwxr-xr-xcherrypy/tutorial/__init__.py3
-rwxr-xr-xcherrypy/tutorial/bonus-sqlobject.py168
-rw-r--r--cherrypy/tutorial/custom_error.html14
-rw-r--r--cherrypy/tutorial/pdf_file.pdfbin0 -> 85698 bytes
-rwxr-xr-xcherrypy/tutorial/tut01_helloworld.py35
-rwxr-xr-xcherrypy/tutorial/tut02_expose_methods.py32
-rwxr-xr-xcherrypy/tutorial/tut03_get_and_post.py53
-rwxr-xr-xcherrypy/tutorial/tut04_complex_site.py98
-rwxr-xr-xcherrypy/tutorial/tut05_derived_objects.py83
-rwxr-xr-xcherrypy/tutorial/tut06_default_method.py64
-rwxr-xr-xcherrypy/tutorial/tut07_sessions.py44
-rwxr-xr-xcherrypy/tutorial/tut08_generators_and_yield.py47
-rwxr-xr-xcherrypy/tutorial/tut09_files.py107
-rwxr-xr-xcherrypy/tutorial/tut10_http_errors.py81
-rw-r--r--cherrypy/tutorial/tutorial.conf4
-rwxr-xr-xcherrypy/wsgiserver/__init__.py2219
-rwxr-xr-xcherrypy/wsgiserver/ssl_builtin.py72
-rwxr-xr-xcherrypy/wsgiserver/ssl_pyopenssl.py256
-rw-r--r--genshi/__init__.py26
-rw-r--r--genshi/builder.py359
-rw-r--r--genshi/core.py727
-rw-r--r--genshi/filters/__init__.py20
-rw-r--r--genshi/filters/html.py453
-rw-r--r--genshi/filters/i18n.py1238
-rw-r--r--genshi/filters/transform.py1310
-rw-r--r--genshi/input.py443
-rw-r--r--genshi/output.py838
-rw-r--r--genshi/path.py1528
-rw-r--r--genshi/template/__init__.py23
-rw-r--r--genshi/template/_ast24.py446
-rw-r--r--genshi/template/ast24.py505
-rw-r--r--genshi/template/astutil.py784
-rw-r--r--genshi/template/base.py634
-rw-r--r--genshi/template/directives.py725
-rw-r--r--genshi/template/eval.py629
-rw-r--r--genshi/template/interpolation.py153
-rw-r--r--genshi/template/loader.py344
-rw-r--r--genshi/template/markup.py397
-rw-r--r--genshi/template/plugin.py176
-rw-r--r--genshi/template/text.py333
-rw-r--r--genshi/util.py274
-rw-r--r--icons/activity-debug.svg141
-rw-r--r--icons/document-generic.svg25
-rw-r--r--icons/folder.svg149
-rw-r--r--icons/image-x-generic.svg13
-rw-r--r--icons/text-uri-list.svg22
-rw-r--r--icons/text-x-generic.svg14
-rw-r--r--icons/text-x-python.svg91
-rw-r--r--inspector.py79
-rw-r--r--templates/editor.html62
-rw-r--r--templates/filer.html32
-rw-r--r--templates/skel.html19
-rw-r--r--templates/split-view.html10
-rw-r--r--webkit_local/__init__.py5
-rw-r--r--webkit_local/libWebKitGdk.so.1bin0 -> 11256488 bytes
-rw-r--r--webkit_local/libxslt.so.1bin0 -> 222060 bytes
-rw-r--r--webkit_local/webkit.sobin0 -> 34527 bytes
-rw-r--r--www/css/main.css102
l---------www/icons1
-rw-r--r--www/index.html49
-rw-r--r--www/js/ace/ace-uncompressed.js15284
-rw-r--r--www/js/ace/ace.js1
-rw-r--r--www/js/ace/cockpit-uncompressed.js2504
-rw-r--r--www/js/ace/cockpit.js1
-rw-r--r--www/js/ace/keybinding-emacs.js1
-rw-r--r--www/js/ace/keybinding-vim.js1
-rw-r--r--www/js/ace/mode-c_cpp.js1
-rw-r--r--www/js/ace/mode-clojure.js1
-rw-r--r--www/js/ace/mode-coffee.js1
-rw-r--r--www/js/ace/mode-csharp.js1
-rw-r--r--www/js/ace/mode-css.js1
-rw-r--r--www/js/ace/mode-html.js1
-rw-r--r--www/js/ace/mode-java.js1
-rw-r--r--www/js/ace/mode-javascript.js1
-rw-r--r--www/js/ace/mode-json.js1
-rw-r--r--www/js/ace/mode-perl.js1
-rw-r--r--www/js/ace/mode-php.js1
-rw-r--r--www/js/ace/mode-python.js1
-rw-r--r--www/js/ace/mode-ruby.js1
-rw-r--r--www/js/ace/mode-scss.js1
-rw-r--r--www/js/ace/mode-svg.js1
-rw-r--r--www/js/ace/mode-xml.js1
-rw-r--r--www/js/ace/theme-clouds.js1
-rw-r--r--www/js/ace/theme-clouds_midnight.js1
-rw-r--r--www/js/ace/theme-cobalt.js1
-rw-r--r--www/js/ace/theme-dawn.js1
-rw-r--r--www/js/ace/theme-eclipse.js1
-rw-r--r--www/js/ace/theme-idle_fingers.js1
-rw-r--r--www/js/ace/theme-kr_theme.js1
-rw-r--r--www/js/ace/theme-merbivore.js1
-rw-r--r--www/js/ace/theme-merbivore_soft.js1
-rw-r--r--www/js/ace/theme-mono_industrial.js1
-rw-r--r--www/js/ace/theme-monokai.js1
-rw-r--r--www/js/ace/theme-pastel_on_dark.js1
-rw-r--r--www/js/ace/theme-twilight.js1
-rw-r--r--www/js/ace/theme-vibrant_ink.js1
-rw-r--r--www/js/ace/worker-coffee.js1
-rw-r--r--www/js/ace/worker-css.js1
-rw-r--r--www/js/ace/worker-javascript.js1
-rw-r--r--www/js/jquery-1.6.2.js8981
-rw-r--r--www/js/jquery.corner.js249
-rw-r--r--www/js/jquery.quicksand.js307
209 files changed, 69524 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ba9db1a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.pyc
+dist/
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..e1e4d69
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,2 @@
+WebSDK by Icarito
+Hello World Activity by Erikos and Gonzalo
diff --git a/HACKING b/HACKING
new file mode 100644
index 0000000..6bca753
--- /dev/null
+++ b/HACKING
@@ -0,0 +1 @@
+Instructions to come. \ No newline at end of file
diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 0000000..10be201
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,58 @@
+README
+appservice.py
+ABOUT
+setup.py
+activity.py
+about.py
+NEWS
+AUTHORS
+HACKING
+MANTAINERS
+TODO
+COPYING
+icons/activity-about.svg
+templates/editor.html
+po/HelloWorld.pot
+www/index.html
+www/css/main.css
+www/js/ace/keybinding-vim.js
+www/js/ace/theme-kr_theme.js
+www/js/ace/mode-scss.js
+www/js/ace/theme-pastel_on_dark.js
+www/js/ace/mode-coffee.js
+www/js/ace/mode-javascript.js
+www/js/ace/mode-xml.js
+www/js/ace/theme-merbivore_soft.js
+www/js/ace/theme-merbivore.js
+www/js/ace/ace.js
+www/js/ace/theme-clouds_midnight.js
+www/js/ace/mode-python.js
+www/js/ace/theme-idle_fingers.js
+www/js/ace/ace-uncompressed.js
+www/js/ace/mode-html.js
+www/js/ace/mode-svg.js
+www/js/ace/theme-monokai.js
+www/js/ace/mode-c_cpp.js
+www/js/ace/mode-php.js
+www/js/ace/mode-java.js
+www/js/ace/cockpit-uncompressed.js
+www/js/ace/mode-json.js
+www/js/ace/theme-eclipse.js
+www/js/ace/mode-css.js
+www/js/ace/theme-mono_industrial.js
+www/js/ace/cockpit.js
+www/js/ace/theme-cobalt.js
+www/js/ace/theme-dawn.js
+www/js/ace/theme-clouds.js
+www/js/ace/keybinding-emacs.js
+www/js/ace/mode-perl.js
+www/js/ace/theme-vibrant_ink.js
+www/js/ace/worker-css.js
+www/js/ace/theme-twilight.js
+www/js/ace/worker-javascript.js
+www/js/ace/mode-clojure.js
+www/js/ace/mode-csharp.js
+www/js/ace/worker-coffee.js
+www/js/ace/mode-ruby.js
+activity/activity-helloworld.svg
+activity/activity.info
diff --git a/MANTAINERS b/MANTAINERS
new file mode 100644
index 0000000..63136f1
--- /dev/null
+++ b/MANTAINERS
@@ -0,0 +1 @@
+Sebastian Silva <sebastian@somosazucar.org> (icarito) \ No newline at end of file
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..8b74be6
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,4 @@
+20110808 - Technology Preview Release
+
+ Implemented initial Webpy IDE
+ Basic framework functionality in place
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..3f0d62a
--- /dev/null
+++ b/TODO
@@ -0,0 +1,14 @@
+= Abstract away uglinnes API =
+
+*just some ideas*
+
+in javascript api:
+ websdk.go( python controller )
+ websdk.do( python controller )
+ json = websdk.get( python controller )
+
+in python controller:
+ websdk.activity( activity callback )
+
+in activity callback
+ websdk.do( javascript ) \ No newline at end of file
diff --git a/appservice.py b/appservice.py
new file mode 100644
index 0000000..592f369
--- /dev/null
+++ b/appservice.py
@@ -0,0 +1,156 @@
+#!/bin/env python
+# -*- coding: UTF-8 -*-
+
+import os
+import os.path
+import sys
+import cherrypy
+from genshi.template import TemplateLoader
+
+loader = TemplateLoader(
+ os.path.join(os.path.dirname(__file__), 'templates'),
+ auto_reload=True)
+
+class Root(object):
+
+ def __init__(self, data):
+ self.data = data
+
+ @cherrypy.expose
+ def index(self):
+ port = cherrypy.config['server.socket_port']
+ return '''Server is running on port %s. <br/>
+ Try pointing a browser at
+ <a href="http://localhost:%s/www/index.html">http://localhost:%s/www/index.html</a>''' % (port, port, port)
+
+ @cherrypy.expose
+ def debug(self):
+ port = cherrypy.config['server.socket_port']
+ return '''Try right clicking on any element on the main canvas and choosing "Inspect Element".
+ <br /><a href="http://localhost:%s/www/index.html">Return to main</a>''' % (port)
+
+ @cherrypy.expose
+ def candy(self):
+ return '''Not yet implemented.
+ <br /><a href="http://localhost:%s/www/index.html">Return to main</a>''' % (port)
+
+ @cherrypy.expose
+ def journal(self):
+ port = cherrypy.config['server.socket_port']
+ return '''Not yet implemented.
+ <br /><a href="http://localhost:%s/www/index.html">Return to main</a>''' % (port)
+
+ @cherrypy.expose
+ def collaboration(self):
+ port = cherrypy.config['server.socket_port']
+ return '''Not yet implemented.
+ <br /><a href="http://localhost:%s/www/index.html">Return to main</a>''' % (port)
+
+ def list_files(self, directory):
+ files=os.listdir(directory)
+ print "showing %s" % directory
+ return files
+
+ @cherrypy.expose
+ def browse(self, directory="."):
+ filelist = self.list_files(directory)
+ files = []
+ if not os.path.abspath(directory)==os.path.abspath("."):
+ files.append( { 'name': '..',
+ 'icon': 'folder.svg',
+ 'href': 'browse?directory=%s' % os.path.join(directory,"..") })
+ for filename in sorted(filelist):
+ fullname = os.path.join(directory,filename)
+ icon = 'document-generic.svg'
+ href = 'edit?filename=%s&directory=%s' % (fullname,directory)
+ if filename.endswith('.py'):
+ icon = 'text-x-python.svg'
+ if filename.endswith('.html'):
+ icon = 'text-uri-list.svg'
+ if filename.endswith('.css'):
+ icon = 'text-uri-list.svg'
+ if filename.endswith('.js'):
+ icon = 'text-uri-list.svg'
+ if os.path.isdir(fullname):
+ icon = 'folder.svg'
+ href = 'browse?directory=%s' % fullname
+ if filename.endswith('.xo'):
+ href = '#'
+ if filename.startswith('.'):
+ continue
+ if filename.endswith('.pyc'):
+ continue
+ files.append( { 'name': filename,
+ 'icon': icon,
+ 'href': href } )
+
+ tmpl = loader.load('filer.html')
+ return tmpl.generate(files=files, absdir=os.path.normpath(directory)
+ ).render('html', doctype='html')
+ @cherrypy.expose
+ def vsplit(self, frame1="/browse", frame2="/browse"):
+ tmpl = loader.load('split-view.html')
+ return tmpl.generate(frame1=frame1, frame2=frame2
+ ).render('html', doctype='html')
+
+
+ @cherrypy.expose
+ def edit(self, directory=".", filename="activity.py"):
+ icon = 'document-generic.svg'
+ mode = ''
+ if filename.endswith('.py'):
+ icon = 'text-x-python.svg'
+ mode = 'python'
+ if filename.endswith('.html'):
+ icon = 'text-uri-list.svg'
+ mode = 'html'
+ if filename.endswith('.css'):
+ icon = 'text-uri-list.svg'
+ mode = 'css'
+ if filename.endswith('.js'):
+ icon = 'text-uri-list.svg'
+ mode = 'javascript'
+ content = open(filename).read().decode('utf-8')
+ tmpl = loader.load('editor.html')
+ return tmpl.generate(content=content, icon=icon,basename=os.path.basename(filename),
+ filename=filename, directory=directory, absdir=os.path.normpath(directory),
+ mode=mode).render('html', doctype='html', encoding='utf-8')
+
+ @cherrypy.expose
+ def save(self, filename, content, directory):
+ f=open(filename,"wb")
+ content = content.replace('\r\n', '\n').replace('\r', '\n') # HACK
+ f.write(content)
+ print "saving content: %s" % filename
+ f.close()
+ href = "/browse?directory=%s" % directory
+ cherrypy.tools.redirect.callable(url=href, internal=False)
+ return "content saved: %s" % content
+
+ @cherrypy.expose
+ def delete(self, filename):
+ os.unlink(filename)
+ cherrypy.tools.redirect.callable(url='/browse', internal=True)
+
+def start(port=8080):
+ data = {} # We'll replace this later
+
+ # Some global configuration; note that this could be moved into a
+ # configuration file
+ cherrypy.config.update({
+ 'server.socket_port': port,
+ '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__)),
+ })
+
+ cherrypy.quickstart(Root(data), '/', {
+ '/www': {
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': 'www'
+ }
+ })
+
+if __name__ == '__main__':
+ start(int(sys.argv[1]))
diff --git a/cherrypy/LICENSE.txt b/cherrypy/LICENSE.txt
new file mode 100644
index 0000000..8db13fb
--- /dev/null
+++ b/cherrypy/LICENSE.txt
@@ -0,0 +1,25 @@
+Copyright (c) 2004-2011, CherryPy Team (team@cherrypy.org)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of the CherryPy Team nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/cherrypy/__init__.py b/cherrypy/__init__.py
new file mode 100755
index 0000000..eb7cabf
--- /dev/null
+++ b/cherrypy/__init__.py
@@ -0,0 +1,620 @@
+"""CherryPy is a pythonic, object-oriented HTTP framework.
+
+
+CherryPy consists of not one, but four separate API layers.
+
+The APPLICATION LAYER is the simplest. CherryPy applications are written as
+a tree of classes and methods, where each branch in the tree corresponds to
+a branch in the URL path. Each method is a 'page handler', which receives
+GET and POST params as keyword arguments, and returns or yields the (HTML)
+body of the response. The special method name 'index' is used for paths
+that end in a slash, and the special method name 'default' is used to
+handle multiple paths via a single handler. This layer also includes:
+
+ * the 'exposed' attribute (and cherrypy.expose)
+ * cherrypy.quickstart()
+ * _cp_config attributes
+ * cherrypy.tools (including cherrypy.session)
+ * cherrypy.url()
+
+The ENVIRONMENT LAYER is used by developers at all levels. It provides
+information about the current request and response, plus the application
+and server environment, via a (default) set of top-level objects:
+
+ * cherrypy.request
+ * cherrypy.response
+ * cherrypy.engine
+ * cherrypy.server
+ * cherrypy.tree
+ * cherrypy.config
+ * cherrypy.thread_data
+ * cherrypy.log
+ * cherrypy.HTTPError, NotFound, and HTTPRedirect
+ * cherrypy.lib
+
+The EXTENSION LAYER allows advanced users to construct and share their own
+plugins. It consists of:
+
+ * Hook API
+ * Tool API
+ * Toolbox API
+ * Dispatch API
+ * Config Namespace API
+
+Finally, there is the CORE LAYER, which uses the core API's to construct
+the default components which are available at higher layers. You can think
+of the default components as the 'reference implementation' for CherryPy.
+Megaframeworks (and advanced users) may replace the default components
+with customized or extended components. The core API's are:
+
+ * Application API
+ * Engine API
+ * Request API
+ * Server API
+ * WSGI API
+
+These API's are described in the CherryPy specification:
+http://www.cherrypy.org/wiki/CherryPySpec
+"""
+
+__version__ = "3.2.0"
+
+from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode
+from cherrypy._cpcompat import basestring, unicodestr
+
+from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect
+from cherrypy._cperror import NotFound, CherryPyException, TimeoutError
+
+from cherrypy import _cpdispatch as dispatch
+
+from cherrypy import _cptools
+tools = _cptools.default_toolbox
+Tool = _cptools.Tool
+
+from cherrypy import _cprequest
+from cherrypy.lib import httputil as _httputil
+
+from cherrypy import _cptree
+tree = _cptree.Tree()
+from cherrypy._cptree import Application
+from cherrypy import _cpwsgi as wsgi
+
+from cherrypy import process
+try:
+ from cherrypy.process import win32
+ engine = win32.Win32Bus()
+ engine.console_control_handler = win32.ConsoleCtrlHandler(engine)
+ del win32
+except ImportError:
+ engine = process.bus
+
+
+# Timeout monitor
+class _TimeoutMonitor(process.plugins.Monitor):
+
+ def __init__(self, bus):
+ self.servings = []
+ process.plugins.Monitor.__init__(self, bus, self.run)
+
+ def acquire(self):
+ self.servings.append((serving.request, serving.response))
+
+ def release(self):
+ try:
+ self.servings.remove((serving.request, serving.response))
+ except ValueError:
+ pass
+
+ def run(self):
+ """Check timeout on all responses. (Internal)"""
+ for req, resp in self.servings:
+ resp.check_timeout()
+engine.timeout_monitor = _TimeoutMonitor(engine)
+engine.timeout_monitor.subscribe()
+
+engine.autoreload = process.plugins.Autoreloader(engine)
+engine.autoreload.subscribe()
+
+engine.thread_manager = process.plugins.ThreadManager(engine)
+engine.thread_manager.subscribe()
+
+engine.signal_handler = process.plugins.SignalHandler(engine)
+
+
+from cherrypy import _cpserver
+server = _cpserver.Server()
+server.subscribe()
+
+
+def quickstart(root=None, script_name="", config=None):
+ """Mount the given root, start the builtin server (and engine), then block.
+
+ root: an instance of a "controller class" (a collection of page handler
+ methods) which represents the root of the application.
+ script_name: a string containing the "mount point" of the application.
+ This should start with a slash, and be the path portion of the URL
+ at which to mount the given root. For example, if root.index() will
+ handle requests to "http://www.example.com:8080/dept/app1/", then
+ the script_name argument would be "/dept/app1".
+
+ It MUST NOT end in a slash. If the script_name refers to the root
+ of the URI, it MUST be an empty string (not "/").
+ config: a file or dict containing application config. If this contains
+ a [global] section, those entries will be used in the global
+ (site-wide) config.
+ """
+ if config:
+ _global_conf_alias.update(config)
+
+ tree.mount(root, script_name, config)
+
+ if hasattr(engine, "signal_handler"):
+ engine.signal_handler.subscribe()
+ if hasattr(engine, "console_control_handler"):
+ engine.console_control_handler.subscribe()
+
+ engine.start()
+ engine.block()
+
+
+from cherrypy._cpcompat import threadlocal as _local
+
+class _Serving(_local):
+ """An interface for registering request and response objects.
+
+ Rather than have a separate "thread local" object for the request and
+ the response, this class works as a single threadlocal container for
+ both objects (and any others which developers wish to define). In this
+ way, we can easily dump those objects when we stop/start a new HTTP
+ conversation, yet still refer to them as module-level globals in a
+ thread-safe way.
+ """
+
+ request = _cprequest.Request(_httputil.Host("127.0.0.1", 80),
+ _httputil.Host("127.0.0.1", 1111))
+ """
+ The request object for the current thread. In the main thread,
+ and any threads which are not receiving HTTP requests, this is None."""
+
+ response = _cprequest.Response()
+ """
+ The response object for the current thread. In the main thread,
+ and any threads which are not receiving HTTP requests, this is None."""
+
+ def load(self, request, response):
+ self.request = request
+ self.response = response
+
+ def clear(self):
+ """Remove all attributes of self."""
+ self.__dict__.clear()
+
+serving = _Serving()
+
+
+class _ThreadLocalProxy(object):
+
+ __slots__ = ['__attrname__', '__dict__']
+
+ def __init__(self, attrname):
+ self.__attrname__ = attrname
+
+ def __getattr__(self, name):
+ child = getattr(serving, self.__attrname__)
+ return getattr(child, name)
+
+ def __setattr__(self, name, value):
+ if name in ("__attrname__", ):
+ object.__setattr__(self, name, value)
+ else:
+ child = getattr(serving, self.__attrname__)
+ setattr(child, name, value)
+
+ def __delattr__(self, name):
+ child = getattr(serving, self.__attrname__)
+ delattr(child, name)
+
+ def _get_dict(self):
+ child = getattr(serving, self.__attrname__)
+ d = child.__class__.__dict__.copy()
+ d.update(child.__dict__)
+ return d
+ __dict__ = property(_get_dict)
+
+ def __getitem__(self, key):
+ child = getattr(serving, self.__attrname__)
+ return child[key]
+
+ def __setitem__(self, key, value):
+ child = getattr(serving, self.__attrname__)
+ child[key] = value
+
+ def __delitem__(self, key):
+ child = getattr(serving, self.__attrname__)
+ del child[key]
+
+ def __contains__(self, key):
+ child = getattr(serving, self.__attrname__)
+ return key in child
+
+ def __len__(self):
+ child = getattr(serving, self.__attrname__)
+ return len(child)
+
+ def __nonzero__(self):
+ child = getattr(serving, self.__attrname__)
+ return bool(child)
+ # Python 3
+ __bool__ = __nonzero__
+
+# Create request and response object (the same objects will be used
+# throughout the entire life of the webserver, but will redirect
+# to the "serving" object)
+request = _ThreadLocalProxy('request')
+response = _ThreadLocalProxy('response')
+
+# Create thread_data object as a thread-specific all-purpose storage
+class _ThreadData(_local):
+ """A container for thread-specific data."""
+thread_data = _ThreadData()
+
+
+# Monkeypatch pydoc to allow help() to go through the threadlocal proxy.
+# Jan 2007: no Googleable examples of anyone else replacing pydoc.resolve.
+# The only other way would be to change what is returned from type(request)
+# and that's not possible in pure Python (you'd have to fake ob_type).
+def _cherrypy_pydoc_resolve(thing, forceload=0):
+ """Given an object or a path to an object, get the object and its name."""
+ if isinstance(thing, _ThreadLocalProxy):
+ thing = getattr(serving, thing.__attrname__)
+ return _pydoc._builtin_resolve(thing, forceload)
+
+try:
+ import pydoc as _pydoc
+ _pydoc._builtin_resolve = _pydoc.resolve
+ _pydoc.resolve = _cherrypy_pydoc_resolve
+except ImportError:
+ pass
+
+
+from cherrypy import _cplogging
+
+class _GlobalLogManager(_cplogging.LogManager):
+ """A site-wide LogManager; routes to app.log or global log as appropriate.
+
+ This :class:`LogManager<cherrypy._cplogging.LogManager>` implements
+ cherrypy.log() and cherrypy.log.access(). If either
+ function is called during a request, the message will be sent to the
+ logger for the current Application. If they are called outside of a
+ request, the message will be sent to the site-wide logger.
+ """
+
+ def __call__(self, *args, **kwargs):
+ """Log the given message to the app.log or global log as appropriate."""
+ # Do NOT use try/except here. See http://www.cherrypy.org/ticket/945
+ if hasattr(request, 'app') and hasattr(request.app, 'log'):
+ log = request.app.log
+ else:
+ log = self
+ return log.error(*args, **kwargs)
+
+ def access(self):
+ """Log an access message to the app.log or global log as appropriate."""
+ try:
+ return request.app.log.access()
+ except AttributeError:
+ return _cplogging.LogManager.access(self)
+
+
+log = _GlobalLogManager()
+# Set a default screen handler on the global log.
+log.screen = True
+log.error_file = ''
+# Using an access file makes CP about 10% slower. Leave off by default.
+log.access_file = ''
+
+def _buslog(msg, level):
+ log.error(msg, 'ENGINE', severity=level)
+engine.subscribe('log', _buslog)
+
+# Helper functions for CP apps #
+
+
+def expose(func=None, alias=None):
+ """Expose the function, optionally providing an alias or set of aliases."""
+ def expose_(func):
+ func.exposed = True
+ if alias is not None:
+ if isinstance(alias, basestring):
+ parents[alias.replace(".", "_")] = func
+ else:
+ for a in alias:
+ parents[a.replace(".", "_")] = func
+ return func
+
+ import sys, types
+ if isinstance(func, (types.FunctionType, types.MethodType)):
+ if alias is None:
+ # @expose
+ func.exposed = True
+ return func
+ else:
+ # func = expose(func, alias)
+ parents = sys._getframe(1).f_locals
+ return expose_(func)
+ elif func is None:
+ if alias is None:
+ # @expose()
+ parents = sys._getframe(1).f_locals
+ return expose_
+ else:
+ # @expose(alias="alias") or
+ # @expose(alias=["alias1", "alias2"])
+ parents = sys._getframe(1).f_locals
+ return expose_
+ else:
+ # @expose("alias") or
+ # @expose(["alias1", "alias2"])
+ parents = sys._getframe(1).f_locals
+ alias = func
+ return expose_
+
+def popargs(*args, **kwargs):
+ """A decorator for _cp_dispatch
+ (cherrypy.dispatch.Dispatcher.dispatch_method_name).
+
+ Optional keyword argument: handler=(Object or Function)
+
+ Provides a _cp_dispatch function that pops off path segments into
+ cherrypy.request.params under the names specified. The dispatch
+ is then forwarded on to the next vpath element.
+
+ Note that any existing (and exposed) member function of the class that
+ popargs is applied to will override that value of the argument. For
+ instance, if you have a method named "list" on the class decorated with
+ popargs, then accessing "/list" will call that function instead of popping
+ it off as the requested parameter. This restriction applies to all
+ _cp_dispatch functions. The only way around this restriction is to create
+ a "blank class" whose only function is to provide _cp_dispatch.
+
+ If there are path elements after the arguments, or more arguments
+ are requested than are available in the vpath, then the 'handler'
+ keyword argument specifies the next object to handle the parameterized
+ request. If handler is not specified or is None, then self is used.
+ If handler is a function rather than an instance, then that function
+ will be called with the args specified and the return value from that
+ function used as the next object INSTEAD of adding the parameters to
+ cherrypy.request.args.
+
+ This decorator may be used in one of two ways:
+
+ As a class decorator:
+ @cherrypy.popargs('year', 'month', 'day')
+ class Blog:
+ def index(self, year=None, month=None, day=None):
+ #Process the parameters here; any url like
+ #/, /2009, /2009/12, or /2009/12/31
+ #will fill in the appropriate parameters.
+
+ def create(self):
+ #This link will still be available at /create. Defined functions
+ #take precedence over arguments.
+
+ Or as a member of a class:
+ class Blog:
+ _cp_dispatch = cherrypy.popargs('year', 'month', 'day')
+ #...
+
+ The handler argument may be used to mix arguments with built in functions.
+ For instance, the following setup allows different activities at the
+ day, month, and year level:
+
+ class DayHandler:
+ def index(self, year, month, day):
+ #Do something with this day; probably list entries
+
+ def delete(self, year, month, day):
+ #Delete all entries for this day
+
+ @cherrypy.popargs('day', handler=DayHandler())
+ class MonthHandler:
+ def index(self, year, month):
+ #Do something with this month; probably list entries
+
+ def delete(self, year, month):
+ #Delete all entries for this month
+
+ @cherrypy.popargs('month', handler=MonthHandler())
+ class YearHandler:
+ def index(self, year):
+ #Do something with this year
+
+ #...
+
+ @cherrypy.popargs('year', handler=YearHandler())
+ class Root:
+ def index(self):
+ #...
+
+ """
+
+ #Since keyword arg comes after *args, we have to process it ourselves
+ #for lower versions of python.
+
+ handler = None
+ handler_call = False
+ for k,v in kwargs.items():
+ if k == 'handler':
+ handler = v
+ else:
+ raise TypeError(
+ "cherrypy.popargs() got an unexpected keyword argument '{0}'" \
+ .format(k)
+ )
+
+ import inspect
+
+ if handler is not None \
+ and (hasattr(handler, '__call__') or inspect.isclass(handler)):
+ handler_call = True
+
+ def decorated(cls_or_self=None, vpath=None):
+ if inspect.isclass(cls_or_self):
+ #cherrypy.popargs is a class decorator
+ cls = cls_or_self
+ setattr(cls, dispatch.Dispatcher.dispatch_method_name, decorated)
+ return cls
+
+ #We're in the actual function
+ self = cls_or_self
+ parms = {}
+ for arg in args:
+ if not vpath:
+ break
+ parms[arg] = vpath.pop(0)
+
+ if handler is not None:
+ if handler_call:
+ return handler(**parms)
+ else:
+ request.params.update(parms)
+ return handler
+
+ request.params.update(parms)
+
+ #If we are the ultimate handler, then to prevent our _cp_dispatch
+ #from being called again, we will resolve remaining elements through
+ #getattr() directly.
+ if vpath:
+ return getattr(self, vpath.pop(0), None)
+ else:
+ return self
+
+ return decorated
+
+def url(path="", qs="", script_name=None, base=None, relative=None):
+ """Create an absolute URL for the given path.
+
+ If 'path' starts with a slash ('/'), this will return
+ (base + script_name + path + qs).
+ If it does not start with a slash, this returns
+ (base + script_name [+ request.path_info] + path + qs).
+
+ If script_name is None, cherrypy.request will be used
+ to find a script_name, if available.
+
+ If base is None, cherrypy.request.base will be used (if available).
+ Note that you can use cherrypy.tools.proxy to change this.
+
+ Finally, note that this function can be used to obtain an absolute URL
+ for the current request path (minus the querystring) by passing no args.
+ If you call url(qs=cherrypy.request.query_string), you should get the
+ original browser URL (assuming no internal redirections).
+
+ If relative is None or not provided, request.app.relative_urls will
+ be used (if available, else False). If False, the output will be an
+ absolute URL (including the scheme, host, vhost, and script_name).
+ If True, the output will instead be a URL that is relative to the
+ current request path, perhaps including '..' atoms. If relative is
+ the string 'server', the output will instead be a URL that is
+ relative to the server root; i.e., it will start with a slash.
+ """
+ if isinstance(qs, (tuple, list, dict)):
+ qs = _urlencode(qs)
+ if qs:
+ qs = '?' + qs
+
+ if request.app:
+ if not path.startswith("/"):
+ # Append/remove trailing slash from path_info as needed
+ # (this is to support mistyped URL's without redirecting;
+ # if you want to redirect, use tools.trailing_slash).
+ pi = request.path_info
+ if request.is_index is True:
+ if not pi.endswith('/'):
+ pi = pi + '/'
+ elif request.is_index is False:
+ if pi.endswith('/') and pi != '/':
+ pi = pi[:-1]
+
+ if path == "":
+ path = pi
+ else:
+ path = _urljoin(pi, path)
+
+ if script_name is None:
+ script_name = request.script_name
+ if base is None:
+ base = request.base
+
+ newurl = base + script_name + path + qs
+ else:
+ # No request.app (we're being called outside a request).
+ # We'll have to guess the base from server.* attributes.
+ # This will produce very different results from the above
+ # if you're using vhosts or tools.proxy.
+ if base is None:
+ base = server.base()
+
+ path = (script_name or "") + path
+ newurl = base + path + qs
+
+ if './' in newurl:
+ # Normalize the URL by removing ./ and ../
+ atoms = []
+ for atom in newurl.split('/'):
+ if atom == '.':
+ pass
+ elif atom == '..':
+ atoms.pop()
+ else:
+ atoms.append(atom)
+ newurl = '/'.join(atoms)
+
+ # At this point, we should have a fully-qualified absolute URL.
+
+ if relative is None:
+ relative = getattr(request.app, "relative_urls", False)
+
+ # See http://www.ietf.org/rfc/rfc2396.txt
+ if relative == 'server':
+ # "A relative reference beginning with a single slash character is
+ # termed an absolute-path reference, as defined by <abs_path>..."
+ # This is also sometimes called "server-relative".
+ newurl = '/' + '/'.join(newurl.split('/', 3)[3:])
+ elif relative:
+ # "A relative reference that does not begin with a scheme name
+ # or a slash character is termed a relative-path reference."
+ old = url().split('/')[:-1]
+ new = newurl.split('/')
+ while old and new:
+ a, b = old[0], new[0]
+ if a != b:
+ break
+ old.pop(0)
+ new.pop(0)
+ new = (['..'] * len(old)) + new
+ newurl = '/'.join(new)
+
+ return newurl
+
+
+# import _cpconfig last so it can reference other top-level objects
+from cherrypy import _cpconfig
+# Use _global_conf_alias so quickstart can use 'config' as an arg
+# without shadowing cherrypy.config.
+config = _global_conf_alias = _cpconfig.Config()
+config.defaults = {
+ 'tools.log_tracebacks.on': True,
+ 'tools.log_headers.on': True,
+ 'tools.trailing_slash.on': True,
+ 'tools.encode.on': True
+ }
+config.namespaces["log"] = lambda k, v: setattr(log, k, v)
+config.namespaces["checker"] = lambda k, v: setattr(checker, k, v)
+# Must reset to get our defaults applied.
+config.reset()
+
+from cherrypy import _cpchecker
+checker = _cpchecker.Checker()
+engine.subscribe('start', checker)
diff --git a/cherrypy/_cpchecker.py b/cherrypy/_cpchecker.py
new file mode 100755
index 0000000..7ccfd89
--- /dev/null
+++ b/cherrypy/_cpchecker.py
@@ -0,0 +1,327 @@
+import os
+import warnings
+
+import cherrypy
+from cherrypy._cpcompat import iteritems, copykeys, builtins
+
+
+class Checker(object):
+ """A checker for CherryPy sites and their mounted applications.
+
+ When this object is called at engine startup, it executes each
+ of its own methods whose names start with ``check_``. If you wish
+ to disable selected checks, simply add a line in your global
+ config which sets the appropriate method to False::
+
+ [global]
+ checker.check_skipped_app_config = False
+
+ You may also dynamically add or replace ``check_*`` methods in this way.
+ """
+
+ on = True
+ """If True (the default), run all checks; if False, turn off all checks."""
+
+
+ def __init__(self):
+ self._populate_known_types()
+
+ def __call__(self):
+ """Run all check_* methods."""
+ if self.on:
+ oldformatwarning = warnings.formatwarning
+ warnings.formatwarning = self.formatwarning
+ try:
+ for name in dir(self):
+ if name.startswith("check_"):
+ method = getattr(self, name)
+ if method and hasattr(method, '__call__'):
+ method()
+ finally:
+ warnings.formatwarning = oldformatwarning
+
+ def formatwarning(self, message, category, filename, lineno, line=None):
+ """Function to format a warning."""
+ return "CherryPy Checker:\n%s\n\n" % message
+
+ # This value should be set inside _cpconfig.
+ global_config_contained_paths = False
+
+ def check_app_config_entries_dont_start_with_script_name(self):
+ """Check for Application config with sections that repeat script_name."""
+ for sn, app in cherrypy.tree.apps.items():
+ if not isinstance(app, cherrypy.Application):
+ continue
+ if not app.config:
+ continue
+ if sn == '':
+ continue
+ sn_atoms = sn.strip("/").split("/")
+ for key in app.config.keys():
+ key_atoms = key.strip("/").split("/")
+ if key_atoms[:len(sn_atoms)] == sn_atoms:
+ warnings.warn(
+ "The application mounted at %r has config " \
+ "entries that start with its script name: %r" % (sn, key))
+
+ def check_site_config_entries_in_app_config(self):
+ """Check for mounted Applications that have site-scoped config."""
+ for sn, app in iteritems(cherrypy.tree.apps):
+ if not isinstance(app, cherrypy.Application):
+ continue
+
+ msg = []
+ for section, entries in iteritems(app.config):
+ if section.startswith('/'):
+ for key, value in iteritems(entries):
+ for n in ("engine.", "server.", "tree.", "checker."):
+ if key.startswith(n):
+ msg.append("[%s] %s = %s" % (section, key, value))
+ if msg:
+ msg.insert(0,
+ "The application mounted at %r contains the following "
+ "config entries, which are only allowed in site-wide "
+ "config. Move them to a [global] section and pass them "
+ "to cherrypy.config.update() instead of tree.mount()." % sn)
+ warnings.warn(os.linesep.join(msg))
+
+ def check_skipped_app_config(self):
+ """Check for mounted Applications that have no config."""
+ for sn, app in cherrypy.tree.apps.items():
+ if not isinstance(app, cherrypy.Application):
+ continue
+ if not app.config:
+ msg = "The Application mounted at %r has an empty config." % sn
+ if self.global_config_contained_paths:
+ msg += (" It looks like the config you passed to "
+ "cherrypy.config.update() contains application-"
+ "specific sections. You must explicitly pass "
+ "application config via "
+ "cherrypy.tree.mount(..., config=app_config)")
+ warnings.warn(msg)
+ return
+
+ def check_app_config_brackets(self):
+ """Check for Application config with extraneous brackets in section names."""
+ for sn, app in cherrypy.tree.apps.items():
+ if not isinstance(app, cherrypy.Application):
+ continue
+ if not app.config:
+ continue
+ for key in app.config.keys():
+ if key.startswith("[") or key.endswith("]"):
+ warnings.warn(
+ "The application mounted at %r has config " \
+ "section names with extraneous brackets: %r. "
+ "Config *files* need brackets; config *dicts* "
+ "(e.g. passed to tree.mount) do not." % (sn, key))
+
+ def check_static_paths(self):
+ """Check Application config for incorrect static paths."""
+ # Use the dummy Request object in the main thread.
+ request = cherrypy.request
+ for sn, app in cherrypy.tree.apps.items():
+ if not isinstance(app, cherrypy.Application):
+ continue
+ request.app = app
+ for section in app.config:
+ # get_resource will populate request.config
+ request.get_resource(section + "/dummy.html")
+ conf = request.config.get
+
+ if conf("tools.staticdir.on", False):
+ msg = ""
+ root = conf("tools.staticdir.root")
+ dir = conf("tools.staticdir.dir")
+ if dir is None:
+ msg = "tools.staticdir.dir is not set."
+ else:
+ fulldir = ""
+ if os.path.isabs(dir):
+ fulldir = dir
+ if root:
+ msg = ("dir is an absolute path, even "
+ "though a root is provided.")
+ testdir = os.path.join(root, dir[1:])
+ if os.path.exists(testdir):
+ msg += ("\nIf you meant to serve the "
+ "filesystem folder at %r, remove "
+ "the leading slash from dir." % testdir)
+ else:
+ if not root:
+ msg = "dir is a relative path and no root provided."
+ else:
+ fulldir = os.path.join(root, dir)
+ if not os.path.isabs(fulldir):
+ msg = "%r is not an absolute path." % fulldir
+
+ if fulldir and not os.path.exists(fulldir):
+ if msg:
+ msg += "\n"
+ msg += ("%r (root + dir) is not an existing "
+ "filesystem path." % fulldir)
+
+ if msg:
+ warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r"
+ % (msg, section, root, dir))
+
+
+ # -------------------------- Compatibility -------------------------- #
+
+ obsolete = {
+ 'server.default_content_type': 'tools.response_headers.headers',
+ 'log_access_file': 'log.access_file',
+ 'log_config_options': None,
+ 'log_file': 'log.error_file',
+ 'log_file_not_found': None,
+ 'log_request_headers': 'tools.log_headers.on',
+ 'log_to_screen': 'log.screen',
+ 'show_tracebacks': 'request.show_tracebacks',
+ 'throw_errors': 'request.throw_errors',
+ 'profiler.on': ('cherrypy.tree.mount(profiler.make_app('
+ 'cherrypy.Application(Root())))'),
+ }
+
+ deprecated = {}
+
+ def _compat(self, config):
+ """Process config and warn on each obsolete or deprecated entry."""
+ for section, conf in config.items():
+ if isinstance(conf, dict):
+ for k, v in conf.items():
+ if k in self.obsolete:
+ warnings.warn("%r is obsolete. Use %r instead.\n"
+ "section: [%s]" %
+ (k, self.obsolete[k], section))
+ elif k in self.deprecated:
+ warnings.warn("%r is deprecated. Use %r instead.\n"
+ "section: [%s]" %
+ (k, self.deprecated[k], section))
+ else:
+ if section in self.obsolete:
+ warnings.warn("%r is obsolete. Use %r instead."
+ % (section, self.obsolete[section]))
+ elif section in self.deprecated:
+ warnings.warn("%r is deprecated. Use %r instead."
+ % (section, self.deprecated[section]))
+
+ def check_compatibility(self):
+ """Process config and warn on each obsolete or deprecated entry."""
+ self._compat(cherrypy.config)
+ for sn, app in cherrypy.tree.apps.items():
+ if not isinstance(app, cherrypy.Application):
+ continue
+ self._compat(app.config)
+
+
+ # ------------------------ Known Namespaces ------------------------ #
+
+ extra_config_namespaces = []
+
+ def _known_ns(self, app):
+ ns = ["wsgi"]
+ ns.extend(copykeys(app.toolboxes))
+ ns.extend(copykeys(app.namespaces))
+ ns.extend(copykeys(app.request_class.namespaces))
+ ns.extend(copykeys(cherrypy.config.namespaces))
+ ns += self.extra_config_namespaces
+
+ for section, conf in app.config.items():
+ is_path_section = section.startswith("/")
+ if is_path_section and isinstance(conf, dict):
+ for k, v in conf.items():
+ atoms = k.split(".")
+ if len(atoms) > 1:
+ if atoms[0] not in ns:
+ # Spit out a special warning if a known
+ # namespace is preceded by "cherrypy."
+ if (atoms[0] == "cherrypy" and atoms[1] in ns):
+ msg = ("The config entry %r is invalid; "
+ "try %r instead.\nsection: [%s]"
+ % (k, ".".join(atoms[1:]), section))
+ else:
+ msg = ("The config entry %r is invalid, because "
+ "the %r config namespace is unknown.\n"
+ "section: [%s]" % (k, atoms[0], section))
+ warnings.warn(msg)
+ elif atoms[0] == "tools":
+ if atoms[1] not in dir(cherrypy.tools):
+ msg = ("The config entry %r may be invalid, "
+ "because the %r tool was not found.\n"
+ "section: [%s]" % (k, atoms[1], section))
+ warnings.warn(msg)
+
+ def check_config_namespaces(self):
+ """Process config and warn on each unknown config namespace."""
+ for sn, app in cherrypy.tree.apps.items():
+ if not isinstance(app, cherrypy.Application):
+ continue
+ self._known_ns(app)
+
+
+
+
+ # -------------------------- Config Types -------------------------- #
+
+ known_config_types = {}
+
+ def _populate_known_types(self):
+ b = [x for x in vars(builtins).values()
+ if type(x) is type(str)]
+
+ def traverse(obj, namespace):
+ for name in dir(obj):
+ # Hack for 3.2's warning about body_params
+ if name == 'body_params':
+ continue
+ vtype = type(getattr(obj, name, None))
+ if vtype in b:
+ self.known_config_types[namespace + "." + name] = vtype
+
+ traverse(cherrypy.request, "request")
+ traverse(cherrypy.response, "response")
+ traverse(cherrypy.server, "server")
+ traverse(cherrypy.engine, "engine")
+ traverse(cherrypy.log, "log")
+
+ def _known_types(self, config):
+ msg = ("The config entry %r in section %r is of type %r, "
+ "which does not match the expected type %r.")
+
+ for section, conf in config.items():
+ if isinstance(conf, dict):
+ for k, v in conf.items():
+ if v is not None:
+ expected_type = self.known_config_types.get(k, None)
+ vtype = type(v)
+ if expected_type and vtype != expected_type:
+ warnings.warn(msg % (k, section, vtype.__name__,
+ expected_type.__name__))
+ else:
+ k, v = section, conf
+ if v is not None:
+ expected_type = self.known_config_types.get(k, None)
+ vtype = type(v)
+ if expected_type and vtype != expected_type:
+ warnings.warn(msg % (k, section, vtype.__name__,
+ expected_type.__name__))
+
+ def check_config_types(self):
+ """Assert that config values are of the same type as default values."""
+ self._known_types(cherrypy.config)
+ for sn, app in cherrypy.tree.apps.items():
+ if not isinstance(app, cherrypy.Application):
+ continue
+ self._known_types(app.config)
+
+
+ # -------------------- Specific config warnings -------------------- #
+
+ def check_localhost(self):
+ """Warn if any socket_host is 'localhost'. See #711."""
+ for k, v in cherrypy.config.items():
+ if k == 'server.socket_host' and v == 'localhost':
+ warnings.warn("The use of 'localhost' as a socket host can "
+ "cause problems on newer systems, since 'localhost' can "
+ "map to either an IPv4 or an IPv6 address. You should "
+ "use '127.0.0.1' or '[::1]' instead.")
diff --git a/cherrypy/_cpcompat.py b/cherrypy/_cpcompat.py
new file mode 100755
index 0000000..216dddd
--- /dev/null
+++ b/cherrypy/_cpcompat.py
@@ -0,0 +1,283 @@
+"""Compatibility code for using CherryPy with various versions of Python.
+
+CherryPy 3.2 is compatible with Python versions 2.3+. This module provides a
+useful abstraction over the differences between Python versions, sometimes by
+preferring a newer idiom, sometimes an older one, and sometimes a custom one.
+
+In particular, Python 2 uses str and '' for byte strings, while Python 3
+uses str and '' for unicode strings. We will call each of these the 'native
+string' type for each version. Because of this major difference, this module
+provides new 'bytestr', 'unicodestr', and 'nativestr' attributes, as well as
+two functions: 'ntob', which translates native strings (of type 'str') into
+byte strings regardless of Python version, and 'ntou', which translates native
+strings to unicode strings. This also provides a 'BytesIO' name for dealing
+specifically with bytes, and a 'StringIO' name for dealing with native strings.
+It also provides a 'base64_decode' function with native strings as input and
+output.
+"""
+import os
+import sys
+
+if sys.version_info >= (3, 0):
+ bytestr = bytes
+ unicodestr = str
+ nativestr = unicodestr
+ basestring = (bytes, str)
+ def ntob(n, encoding='ISO-8859-1'):
+ """Return the given native string as a byte string in the given encoding."""
+ # In Python 3, the native string type is unicode
+ return n.encode(encoding)
+ def ntou(n, encoding='ISO-8859-1'):
+ """Return the given native string as a unicode string with the given encoding."""
+ # In Python 3, the native string type is unicode
+ return n
+ # type("")
+ from io import StringIO
+ # bytes:
+ from io import BytesIO as BytesIO
+else:
+ # Python 2
+ bytestr = str
+ unicodestr = unicode
+ nativestr = bytestr
+ basestring = basestring
+ def ntob(n, encoding='ISO-8859-1'):
+ """Return the given native string as a byte string in the given encoding."""
+ # In Python 2, the native string type is bytes. Assume it's already
+ # in the given encoding, which for ISO-8859-1 is almost always what
+ # was intended.
+ return n
+ def ntou(n, encoding='ISO-8859-1'):
+ """Return the given native string as a unicode string with the given encoding."""
+ # In Python 2, the native string type is bytes. Assume it's already
+ # in the given encoding, which for ISO-8859-1 is almost always what
+ # was intended.
+ return n.decode(encoding)
+ try:
+ # type("")
+ from cStringIO import StringIO
+ except ImportError:
+ # type("")
+ from StringIO import StringIO
+ # bytes:
+ BytesIO = StringIO
+
+try:
+ set = set
+except NameError:
+ from sets import Set as set
+
+try:
+ # Python 3.1+
+ from base64 import decodebytes as _base64_decodebytes
+except ImportError:
+ # Python 3.0-
+ # since CherryPy claims compability with Python 2.3, we must use
+ # the legacy API of base64
+ from base64 import decodestring as _base64_decodebytes
+
+def base64_decode(n, encoding='ISO-8859-1'):
+ """Return the native string base64-decoded (as a native string)."""
+ if isinstance(n, unicodestr):
+ b = n.encode(encoding)
+ else:
+ b = n
+ b = _base64_decodebytes(b)
+ if nativestr is unicodestr:
+ return b.decode(encoding)
+ else:
+ return b
+
+try:
+ # Python 2.5+
+ from hashlib import md5
+except ImportError:
+ from md5 import new as md5
+
+try:
+ # Python 2.5+
+ from hashlib import sha1 as sha
+except ImportError:
+ from sha import new as sha
+
+try:
+ sorted = sorted
+except NameError:
+ def sorted(i):
+ i = i[:]
+ i.sort()
+ return i
+
+try:
+ reversed = reversed
+except NameError:
+ def reversed(x):
+ i = len(x)
+ while i > 0:
+ i -= 1
+ yield x[i]
+
+try:
+ # Python 3
+ from urllib.parse import urljoin, urlencode
+ from urllib.parse import quote, quote_plus
+ from urllib.request import unquote, urlopen
+ from urllib.request import parse_http_list, parse_keqv_list
+except ImportError:
+ # Python 2
+ from urlparse import urljoin
+ from urllib import urlencode, urlopen
+ from urllib import quote, quote_plus
+ from urllib import unquote
+ from urllib2 import parse_http_list, parse_keqv_list
+
+try:
+ from threading import local as threadlocal
+except ImportError:
+ from cherrypy._cpthreadinglocal import local as threadlocal
+
+try:
+ dict.iteritems
+ # Python 2
+ iteritems = lambda d: d.iteritems()
+ copyitems = lambda d: d.items()
+except AttributeError:
+ # Python 3
+ iteritems = lambda d: d.items()
+ copyitems = lambda d: list(d.items())
+
+try:
+ dict.iterkeys
+ # Python 2
+ iterkeys = lambda d: d.iterkeys()
+ copykeys = lambda d: d.keys()
+except AttributeError:
+ # Python 3
+ iterkeys = lambda d: d.keys()
+ copykeys = lambda d: list(d.keys())
+
+try:
+ dict.itervalues
+ # Python 2
+ itervalues = lambda d: d.itervalues()
+ copyvalues = lambda d: d.values()
+except AttributeError:
+ # Python 3
+ itervalues = lambda d: d.values()
+ copyvalues = lambda d: list(d.values())
+
+try:
+ # Python 3
+ import builtins
+except ImportError:
+ # Python 2
+ import __builtin__ as builtins
+
+try:
+ # Python 2. We have to do it in this order so Python 2 builds
+ # don't try to import the 'http' module from cherrypy.lib
+ from Cookie import SimpleCookie, CookieError
+ from httplib import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected
+ from BaseHTTPServer import BaseHTTPRequestHandler
+except ImportError:
+ # Python 3
+ from http.cookies import SimpleCookie, CookieError
+ from http.client import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected
+ from http.server import BaseHTTPRequestHandler
+
+try:
+ # Python 2
+ xrange = xrange
+except NameError:
+ # Python 3
+ xrange = range
+
+import threading
+if hasattr(threading.Thread, "daemon"):
+ # Python 2.6+
+ def get_daemon(t):
+ return t.daemon
+ def set_daemon(t, val):
+ t.daemon = val
+else:
+ def get_daemon(t):
+ return t.isDaemon()
+ def set_daemon(t, val):
+ t.setDaemon(val)
+
+try:
+ from email.utils import formatdate
+ def HTTPDate(timeval=None):
+ return formatdate(timeval, usegmt=True)
+except ImportError:
+ from rfc822 import formatdate as HTTPDate
+
+try:
+ # Python 3
+ from urllib.parse import unquote as parse_unquote
+ def unquote_qs(atom, encoding, errors='strict'):
+ return parse_unquote(atom.replace('+', ' '), encoding=encoding, errors=errors)
+except ImportError:
+ # Python 2
+ from urllib import unquote as parse_unquote
+ def unquote_qs(atom, encoding, errors='strict'):
+ return parse_unquote(atom.replace('+', ' ')).decode(encoding, errors)
+
+try:
+ # Prefer simplejson, which is usually more advanced than the builtin module.
+ import simplejson as json
+ json_decode = json.JSONDecoder().decode
+ json_encode = json.JSONEncoder().iterencode
+except ImportError:
+ if sys.version_info >= (3, 0):
+ # Python 3.0: json is part of the standard library,
+ # but outputs unicode. We need bytes.
+ import json
+ json_decode = json.JSONDecoder().decode
+ _json_encode = json.JSONEncoder().iterencode
+ def json_encode(value):
+ for chunk in _json_encode(value):
+ yield chunk.encode('utf8')
+ elif sys.version_info >= (2, 6):
+ # Python 2.6: json is part of the standard library
+ import json
+ json_decode = json.JSONDecoder().decode
+ json_encode = json.JSONEncoder().iterencode
+ else:
+ json = None
+ def json_decode(s):
+ raise ValueError('No JSON library is available')
+ def json_encode(s):
+ raise ValueError('No JSON library is available')
+
+try:
+ import cPickle as pickle
+except ImportError:
+ # In Python 2, pickle is a Python version.
+ # In Python 3, pickle is the sped-up C version.
+ import pickle
+
+try:
+ os.urandom(20)
+ import binascii
+ def random20():
+ return binascii.hexlify(os.urandom(20)).decode('ascii')
+except (AttributeError, NotImplementedError):
+ import random
+ # os.urandom not available until Python 2.4. Fall back to random.random.
+ def random20():
+ return sha('%s' % random.random()).hexdigest()
+
+try:
+ from _thread import get_ident as get_thread_ident
+except ImportError:
+ from thread import get_ident as get_thread_ident
+
+try:
+ # Python 3
+ next = next
+except NameError:
+ # Python 2
+ def next(i):
+ return i.next()
+
diff --git a/cherrypy/_cpconfig.py b/cherrypy/_cpconfig.py
new file mode 100755
index 0000000..7b4c6a4
--- /dev/null
+++ b/cherrypy/_cpconfig.py
@@ -0,0 +1,295 @@
+"""
+Configuration system for CherryPy.
+
+Configuration in CherryPy is implemented via dictionaries. Keys are strings
+which name the mapped value, which may be of any type.
+
+
+Architecture
+------------
+
+CherryPy Requests are part of an Application, which runs in a global context,
+and configuration data may apply to any of those three scopes:
+
+Global
+ Configuration entries which apply everywhere are stored in
+ cherrypy.config.
+
+Application
+ Entries which apply to each mounted application are stored
+ on the Application object itself, as 'app.config'. This is a two-level
+ dict where each key is a path, or "relative URL" (for example, "/" or
+ "/path/to/my/page"), and each value is a config dict. Usually, this
+ data is provided in the call to tree.mount(root(), config=conf),
+ although you may also use app.merge(conf).
+
+Request
+ Each Request object possesses a single 'Request.config' dict.
+ Early in the request process, this dict is populated by merging global
+ config entries, Application entries (whose path equals or is a parent
+ of Request.path_info), and any config acquired while looking up the
+ page handler (see next).
+
+
+Declaration
+-----------
+
+Configuration data may be supplied as a Python dictionary, as a filename,
+or as an open file object. When you supply a filename or file, CherryPy
+uses Python's builtin ConfigParser; you declare Application config by
+writing each path as a section header::
+
+ [/path/to/my/page]
+ request.stream = True
+
+To declare global configuration entries, place them in a [global] section.
+
+You may also declare config entries directly on the classes and methods
+(page handlers) that make up your CherryPy application via the ``_cp_config``
+attribute. For example::
+
+ class Demo:
+ _cp_config = {'tools.gzip.on': True}
+
+ def index(self):
+ return "Hello world"
+ index.exposed = True
+ index._cp_config = {'request.show_tracebacks': False}
+
+.. note::
+
+ This behavior is only guaranteed for the default dispatcher.
+ Other dispatchers may have different restrictions on where
+ you can attach _cp_config attributes.
+
+
+Namespaces
+----------
+
+Configuration keys are separated into namespaces by the first "." in the key.
+Current namespaces:
+
+engine
+ Controls the 'application engine', including autoreload.
+ These can only be declared in the global config.
+
+tree
+ Grafts cherrypy.Application objects onto cherrypy.tree.
+ These can only be declared in the global config.
+
+hooks
+ Declares additional request-processing functions.
+
+log
+ Configures the logging for each application.
+ These can only be declared in the global or / config.
+
+request
+ Adds attributes to each Request.
+
+response
+ Adds attributes to each Response.
+
+server
+ Controls the default HTTP server via cherrypy.server.
+ These can only be declared in the global config.
+
+tools
+ Runs and configures additional request-processing packages.
+
+wsgi
+ Adds WSGI middleware to an Application's "pipeline".
+ These can only be declared in the app's root config ("/").
+
+checker
+ Controls the 'checker', which looks for common errors in
+ app state (including config) when the engine starts.
+ Global config only.
+
+The only key that does not exist in a namespace is the "environment" entry.
+This special entry 'imports' other config entries from a template stored in
+cherrypy._cpconfig.environments[environment]. It only applies to the global
+config, and only when you use cherrypy.config.update.
+
+You can define your own namespaces to be called at the Global, Application,
+or Request level, by adding a named handler to cherrypy.config.namespaces,
+app.namespaces, or app.request_class.namespaces. The name can
+be any string, and the handler must be either a callable or a (Python 2.5
+style) context manager.
+"""
+
+import cherrypy
+from cherrypy._cpcompat import set, basestring
+from cherrypy.lib import reprconf
+
+# Deprecated in CherryPy 3.2--remove in 3.3
+NamespaceSet = reprconf.NamespaceSet
+
+def merge(base, other):
+ """Merge one app config (from a dict, file, or filename) into another.
+
+ If the given config is a filename, it will be appended to
+ the list of files to monitor for "autoreload" changes.
+ """
+ if isinstance(other, basestring):
+ cherrypy.engine.autoreload.files.add(other)
+
+ # Load other into base
+ for section, value_map in reprconf.as_dict(other).items():
+ if not isinstance(value_map, dict):
+ raise ValueError(
+ "Application config must include section headers, but the "
+ "config you tried to merge doesn't have any sections. "
+ "Wrap your config in another dict with paths as section "
+ "headers, for example: {'/': config}.")
+ base.setdefault(section, {}).update(value_map)
+
+
+class Config(reprconf.Config):
+ """The 'global' configuration data for the entire CherryPy process."""
+
+ def update(self, config):
+ """Update self from a dict, file or filename."""
+ if isinstance(config, basestring):
+ # Filename
+ cherrypy.engine.autoreload.files.add(config)
+ reprconf.Config.update(self, config)
+
+ def _apply(self, config):
+ """Update self from a dict."""
+ if isinstance(config.get("global", None), dict):
+ if len(config) > 1:
+ cherrypy.checker.global_config_contained_paths = True
+ config = config["global"]
+ if 'tools.staticdir.dir' in config:
+ config['tools.staticdir.section'] = "global"
+ reprconf.Config._apply(self, config)
+
+ def __call__(self, *args, **kwargs):
+ """Decorator for page handlers to set _cp_config."""
+ if args:
+ raise TypeError(
+ "The cherrypy.config decorator does not accept positional "
+ "arguments; you must use keyword arguments.")
+ def tool_decorator(f):
+ if not hasattr(f, "_cp_config"):
+ f._cp_config = {}
+ for k, v in kwargs.items():
+ f._cp_config[k] = v
+ return f
+ return tool_decorator
+
+
+Config.environments = environments = {
+ "staging": {
+ 'engine.autoreload_on': False,
+ 'checker.on': False,
+ 'tools.log_headers.on': False,
+ 'request.show_tracebacks': False,
+ 'request.show_mismatched_params': False,
+ },
+ "production": {
+ 'engine.autoreload_on': False,
+ 'checker.on': False,
+ 'tools.log_headers.on': False,
+ 'request.show_tracebacks': False,
+ 'request.show_mismatched_params': False,
+ 'log.screen': False,
+ },
+ "embedded": {
+ # For use with CherryPy embedded in another deployment stack.
+ 'engine.autoreload_on': False,
+ 'checker.on': False,
+ 'tools.log_headers.on': False,
+ 'request.show_tracebacks': False,
+ 'request.show_mismatched_params': False,
+ 'log.screen': False,
+ 'engine.SIGHUP': None,
+ 'engine.SIGTERM': None,
+ },
+ "test_suite": {
+ 'engine.autoreload_on': False,
+ 'checker.on': False,
+ 'tools.log_headers.on': False,
+ 'request.show_tracebacks': True,
+ 'request.show_mismatched_params': True,
+ 'log.screen': False,
+ },
+ }
+
+
+def _server_namespace_handler(k, v):
+ """Config handler for the "server" namespace."""
+ atoms = k.split(".", 1)
+ if len(atoms) > 1:
+ # Special-case config keys of the form 'server.servername.socket_port'
+ # to configure additional HTTP servers.
+ if not hasattr(cherrypy, "servers"):
+ cherrypy.servers = {}
+
+ servername, k = atoms
+ if servername not in cherrypy.servers:
+ from cherrypy import _cpserver
+ cherrypy.servers[servername] = _cpserver.Server()
+ # On by default, but 'on = False' can unsubscribe it (see below).
+ cherrypy.servers[servername].subscribe()
+
+ if k == 'on':
+ if v:
+ cherrypy.servers[servername].subscribe()
+ else:
+ cherrypy.servers[servername].unsubscribe()
+ else:
+ setattr(cherrypy.servers[servername], k, v)
+ else:
+ setattr(cherrypy.server, k, v)
+Config.namespaces["server"] = _server_namespace_handler
+
+def _engine_namespace_handler(k, v):
+ """Backward compatibility handler for the "engine" namespace."""
+ engine = cherrypy.engine
+ if k == 'autoreload_on':
+ if v:
+ engine.autoreload.subscribe()
+ else:
+ engine.autoreload.unsubscribe()
+ elif k == 'autoreload_frequency':
+ engine.autoreload.frequency = v
+ elif k == 'autoreload_match':
+ engine.autoreload.match = v
+ elif k == 'reload_files':
+ engine.autoreload.files = set(v)
+ elif k == 'deadlock_poll_freq':
+ engine.timeout_monitor.frequency = v
+ elif k == 'SIGHUP':
+ engine.listeners['SIGHUP'] = set([v])
+ elif k == 'SIGTERM':
+ engine.listeners['SIGTERM'] = set([v])
+ elif "." in k:
+ plugin, attrname = k.split(".", 1)
+ plugin = getattr(engine, plugin)
+ if attrname == 'on':
+ if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'):
+ plugin.subscribe()
+ return
+ elif (not v) and hasattr(getattr(plugin, 'unsubscribe', None), '__call__'):
+ plugin.unsubscribe()
+ return
+ setattr(plugin, attrname, v)
+ else:
+ setattr(engine, k, v)
+Config.namespaces["engine"] = _engine_namespace_handler
+
+
+def _tree_namespace_handler(k, v):
+ """Namespace handler for the 'tree' config namespace."""
+ if isinstance(v, dict):
+ for script_name, app in v.items():
+ cherrypy.tree.graft(app, script_name)
+ cherrypy.engine.log("Mounted: %s on %s" % (app, script_name or "/"))
+ else:
+ cherrypy.tree.graft(v, v.script_name)
+ cherrypy.engine.log("Mounted: %s on %s" % (v, v.script_name or "/"))
+Config.namespaces["tree"] = _tree_namespace_handler
+
+
diff --git a/cherrypy/_cpdispatch.py b/cherrypy/_cpdispatch.py
new file mode 100755
index 0000000..7250ac9
--- /dev/null
+++ b/cherrypy/_cpdispatch.py
@@ -0,0 +1,622 @@
+"""CherryPy dispatchers.
+
+A 'dispatcher' is the object which looks up the 'page handler' callable
+and collects config for the current request based on the path_info, other
+request attributes, and the application architecture. The core calls the
+dispatcher as early as possible, passing it a 'path_info' argument.
+
+The default dispatcher discovers the page handler by matching path_info
+to a hierarchical arrangement of objects, starting at request.app.root.
+"""
+
+import string
+import sys
+import types
+
+import cherrypy
+
+
+class PageHandler(object):
+ """Callable which sets response.body."""
+
+ def __init__(self, callable, *args, **kwargs):
+ self.callable = callable
+ self.args = args
+ self.kwargs = kwargs
+
+ def __call__(self):
+ try:
+ return self.callable(*self.args, **self.kwargs)
+ except TypeError:
+ x = sys.exc_info()[1]
+ try:
+ test_callable_spec(self.callable, self.args, self.kwargs)
+ except cherrypy.HTTPError:
+ raise sys.exc_info()[1]
+ except:
+ raise x
+ raise
+
+
+def test_callable_spec(callable, callable_args, callable_kwargs):
+ """
+ Inspect callable and test to see if the given args are suitable for it.
+
+ When an error occurs during the handler's invoking stage there are 2
+ erroneous cases:
+ 1. Too many parameters passed to a function which doesn't define
+ one of *args or **kwargs.
+ 2. Too little parameters are passed to the function.
+
+ There are 3 sources of parameters to a cherrypy handler.
+ 1. query string parameters are passed as keyword parameters to the handler.
+ 2. body parameters are also passed as keyword parameters.
+ 3. when partial matching occurs, the final path atoms are passed as
+ positional args.
+ Both the query string and path atoms are part of the URI. If they are
+ incorrect, then a 404 Not Found should be raised. Conversely the body
+ parameters are part of the request; if they are invalid a 400 Bad Request.
+ """
+ show_mismatched_params = getattr(
+ cherrypy.serving.request, 'show_mismatched_params', False)
+ try:
+ (args, varargs, varkw, defaults) = inspect.getargspec(callable)
+ except TypeError:
+ if isinstance(callable, object) and hasattr(callable, '__call__'):
+ (args, varargs, varkw, defaults) = inspect.getargspec(callable.__call__)
+ else:
+ # If it wasn't one of our own types, re-raise
+ # the original error
+ raise
+
+ if args and args[0] == 'self':
+ args = args[1:]
+
+ arg_usage = dict([(arg, 0,) for arg in args])
+ vararg_usage = 0
+ varkw_usage = 0
+ extra_kwargs = set()
+
+ for i, value in enumerate(callable_args):
+ try:
+ arg_usage[args[i]] += 1
+ except IndexError:
+ vararg_usage += 1
+
+ for key in callable_kwargs.keys():
+ try:
+ arg_usage[key] += 1
+ except KeyError:
+ varkw_usage += 1
+ extra_kwargs.add(key)
+
+ # figure out which args have defaults.
+ args_with_defaults = args[-len(defaults or []):]
+ for i, val in enumerate(defaults or []):
+ # Defaults take effect only when the arg hasn't been used yet.
+ if arg_usage[args_with_defaults[i]] == 0:
+ arg_usage[args_with_defaults[i]] += 1
+
+ missing_args = []
+ multiple_args = []
+ for key, usage in arg_usage.items():
+ if usage == 0:
+ missing_args.append(key)
+ elif usage > 1:
+ multiple_args.append(key)
+
+ if missing_args:
+ # In the case where the method allows body arguments
+ # there are 3 potential errors:
+ # 1. not enough query string parameters -> 404
+ # 2. not enough body parameters -> 400
+ # 3. not enough path parts (partial matches) -> 404
+ #
+ # We can't actually tell which case it is,
+ # so I'm raising a 404 because that covers 2/3 of the
+ # possibilities
+ #
+ # In the case where the method does not allow body
+ # arguments it's definitely a 404.
+ message = None
+ if show_mismatched_params:
+ message="Missing parameters: %s" % ",".join(missing_args)
+ raise cherrypy.HTTPError(404, message=message)
+
+ # the extra positional arguments come from the path - 404 Not Found
+ if not varargs and vararg_usage > 0:
+ raise cherrypy.HTTPError(404)
+
+ body_params = cherrypy.serving.request.body.params or {}
+ body_params = set(body_params.keys())
+ qs_params = set(callable_kwargs.keys()) - body_params
+
+ if multiple_args:
+ if qs_params.intersection(set(multiple_args)):
+ # If any of the multiple parameters came from the query string then
+ # it's a 404 Not Found
+ error = 404
+ else:
+ # Otherwise it's a 400 Bad Request
+ error = 400
+
+ message = None
+ if show_mismatched_params:
+ message="Multiple values for parameters: "\
+ "%s" % ",".join(multiple_args)
+ raise cherrypy.HTTPError(error, message=message)
+
+ if not varkw and varkw_usage > 0:
+
+ # If there were extra query string parameters, it's a 404 Not Found
+ extra_qs_params = set(qs_params).intersection(extra_kwargs)
+ if extra_qs_params:
+ message = None
+ if show_mismatched_params:
+ message="Unexpected query string "\
+ "parameters: %s" % ", ".join(extra_qs_params)
+ raise cherrypy.HTTPError(404, message=message)
+
+ # If there were any extra body parameters, it's a 400 Not Found
+ extra_body_params = set(body_params).intersection(extra_kwargs)
+ if extra_body_params:
+ message = None
+ if show_mismatched_params:
+ message="Unexpected body parameters: "\
+ "%s" % ", ".join(extra_body_params)
+ raise cherrypy.HTTPError(400, message=message)
+
+
+try:
+ import inspect
+except ImportError:
+ test_callable_spec = lambda callable, args, kwargs: None
+
+
+
+class LateParamPageHandler(PageHandler):
+ """When passing cherrypy.request.params to the page handler, we do not
+ want to capture that dict too early; we want to give tools like the
+ decoding tool a chance to modify the params dict in-between the lookup
+ of the handler and the actual calling of the handler. This subclass
+ takes that into account, and allows request.params to be 'bound late'
+ (it's more complicated than that, but that's the effect).
+ """
+
+ def _get_kwargs(self):
+ kwargs = cherrypy.serving.request.params.copy()
+ if self._kwargs:
+ kwargs.update(self._kwargs)
+ return kwargs
+
+ def _set_kwargs(self, kwargs):
+ self._kwargs = kwargs
+
+ kwargs = property(_get_kwargs, _set_kwargs,
+ doc='page handler kwargs (with '
+ 'cherrypy.request.params copied in)')
+
+
+punctuation_to_underscores = string.maketrans(
+ string.punctuation, '_' * len(string.punctuation))
+
+class Dispatcher(object):
+ """CherryPy Dispatcher which walks a tree of objects to find a handler.
+
+ The tree is rooted at cherrypy.request.app.root, and each hierarchical
+ component in the path_info argument is matched to a corresponding nested
+ attribute of the root object. Matching handlers must have an 'exposed'
+ attribute which evaluates to True. The special method name "index"
+ matches a URI which ends in a slash ("/"). The special method name
+ "default" may match a portion of the path_info (but only when no longer
+ substring of the path_info matches some other object).
+
+ This is the default, built-in dispatcher for CherryPy.
+ """
+
+ dispatch_method_name = '_cp_dispatch'
+ """
+ The name of the dispatch method that nodes may optionally implement
+ to provide their own dynamic dispatch algorithm.
+ """
+
+ def __init__(self, dispatch_method_name=None,
+ translate=punctuation_to_underscores):
+ if not isinstance(translate, str) or len(translate) != 256:
+ raise ValueError("The translate argument must be a str of len 256.")
+ self.translate = translate
+ if dispatch_method_name:
+ self.dispatch_method_name = dispatch_method_name
+
+ def __call__(self, path_info):
+ """Set handler and config for the current request."""
+ request = cherrypy.serving.request
+ func, vpath = self.find_handler(path_info)
+
+ if func:
+ # Decode any leftover %2F in the virtual_path atoms.
+ vpath = [x.replace("%2F", "/") for x in vpath]
+ request.handler = LateParamPageHandler(func, *vpath)
+ else:
+ request.handler = cherrypy.NotFound()
+
+ def find_handler(self, path):
+ """Return the appropriate page handler, plus any virtual path.
+
+ This will return two objects. The first will be a callable,
+ which can be used to generate page output. Any parameters from
+ the query string or request body will be sent to that callable
+ as keyword arguments.
+
+ The callable is found by traversing the application's tree,
+ starting from cherrypy.request.app.root, and matching path
+ components to successive objects in the tree. For example, the
+ URL "/path/to/handler" might return root.path.to.handler.
+
+ The second object returned will be a list of names which are
+ 'virtual path' components: parts of the URL which are dynamic,
+ and were not used when looking up the handler.
+ These virtual path components are passed to the handler as
+ positional arguments.
+ """
+ request = cherrypy.serving.request
+ app = request.app
+ root = app.root
+ dispatch_name = self.dispatch_method_name
+
+ # Get config for the root object/path.
+ fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
+ fullpath_len = len(fullpath)
+ segleft = fullpath_len
+ nodeconf = {}
+ if hasattr(root, "_cp_config"):
+ nodeconf.update(root._cp_config)
+ if "/" in app.config:
+ nodeconf.update(app.config["/"])
+ object_trail = [['root', root, nodeconf, segleft]]
+
+ node = root
+ iternames = fullpath[:]
+ while iternames:
+ name = iternames[0]
+ # map to legal Python identifiers (e.g. replace '.' with '_')
+ objname = name.translate(self.translate)
+
+ nodeconf = {}
+ subnode = getattr(node, objname, None)
+ pre_len = len(iternames)
+ if subnode is None:
+ dispatch = getattr(node, dispatch_name, None)
+ if dispatch and hasattr(dispatch, '__call__') and not \
+ getattr(dispatch, 'exposed', False) and \
+ pre_len > 1:
+ #Don't expose the hidden 'index' token to _cp_dispatch
+ #We skip this if pre_len == 1 since it makes no sense
+ #to call a dispatcher when we have no tokens left.
+ index_name = iternames.pop()
+ subnode = dispatch(vpath=iternames)
+ iternames.append(index_name)
+ else:
+ #We didn't find a path, but keep processing in case there
+ #is a default() handler.
+ iternames.pop(0)
+ else:
+ #We found the path, remove the vpath entry
+ iternames.pop(0)
+ segleft = len(iternames)
+ if segleft > pre_len:
+ #No path segment was removed. Raise an error.
+ raise cherrypy.CherryPyException(
+ "A vpath segment was added. Custom dispatchers may only "
+ + "remove elements. While trying to process "
+ + "{0} in {1}".format(name, fullpath)
+ )
+ elif segleft == pre_len:
+ #Assume that the handler used the current path segment, but
+ #did not pop it. This allows things like
+ #return getattr(self, vpath[0], None)
+ iternames.pop(0)
+ segleft -= 1
+ node = subnode
+
+ if node is not None:
+ # Get _cp_config attached to this node.
+ if hasattr(node, "_cp_config"):
+ nodeconf.update(node._cp_config)
+
+ # Mix in values from app.config for this path.
+ existing_len = fullpath_len - pre_len
+ if existing_len != 0:
+ curpath = '/' + '/'.join(fullpath[0:existing_len])
+ else:
+ curpath = ''
+ new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft]
+ for seg in new_segs:
+ curpath += '/' + seg
+ if curpath in app.config:
+ nodeconf.update(app.config[curpath])
+
+ object_trail.append([name, node, nodeconf, segleft])
+
+ def set_conf():
+ """Collapse all object_trail config into cherrypy.request.config."""
+ base = cherrypy.config.copy()
+ # Note that we merge the config from each node
+ # even if that node was None.
+ for name, obj, conf, segleft in object_trail:
+ base.update(conf)
+ if 'tools.staticdir.dir' in conf:
+ base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0:fullpath_len - segleft])
+ return base
+
+ # Try successive objects (reverse order)
+ num_candidates = len(object_trail) - 1
+ for i in range(num_candidates, -1, -1):
+
+ name, candidate, nodeconf, segleft = object_trail[i]
+ if candidate is None:
+ continue
+
+ # Try a "default" method on the current leaf.
+ if hasattr(candidate, "default"):
+ defhandler = candidate.default
+ if getattr(defhandler, 'exposed', False):
+ # Insert any extra _cp_config from the default handler.
+ conf = getattr(defhandler, "_cp_config", {})
+ object_trail.insert(i+1, ["default", defhandler, conf, segleft])
+ request.config = set_conf()
+ # See http://www.cherrypy.org/ticket/613
+ request.is_index = path.endswith("/")
+ return defhandler, fullpath[fullpath_len - segleft:-1]
+
+ # Uncomment the next line to restrict positional params to "default".
+ # if i < num_candidates - 2: continue
+
+ # Try the current leaf.
+ if getattr(candidate, 'exposed', False):
+ request.config = set_conf()
+ if i == num_candidates:
+ # We found the extra ".index". Mark request so tools
+ # can redirect if path_info has no trailing slash.
+ request.is_index = True
+ else:
+ # We're not at an 'index' handler. Mark request so tools
+ # can redirect if path_info has NO trailing slash.
+ # Note that this also includes handlers which take
+ # positional parameters (virtual paths).
+ request.is_index = False
+ return candidate, fullpath[fullpath_len - segleft:-1]
+
+ # We didn't find anything
+ request.config = set_conf()
+ return None, []
+
+
+class MethodDispatcher(Dispatcher):
+ """Additional dispatch based on cherrypy.request.method.upper().
+
+ Methods named GET, POST, etc will be called on an exposed class.
+ The method names must be all caps; the appropriate Allow header
+ will be output showing all capitalized method names as allowable
+ HTTP verbs.
+
+ Note that the containing class must be exposed, not the methods.
+ """
+
+ def __call__(self, path_info):
+ """Set handler and config for the current request."""
+ request = cherrypy.serving.request
+ resource, vpath = self.find_handler(path_info)
+
+ if resource:
+ # Set Allow header
+ avail = [m for m in dir(resource) if m.isupper()]
+ if "GET" in avail and "HEAD" not in avail:
+ avail.append("HEAD")
+ avail.sort()
+ cherrypy.serving.response.headers['Allow'] = ", ".join(avail)
+
+ # Find the subhandler
+ meth = request.method.upper()
+ func = getattr(resource, meth, None)
+ if func is None and meth == "HEAD":
+ func = getattr(resource, "GET", None)
+ if func:
+ # Grab any _cp_config on the subhandler.
+ if hasattr(func, "_cp_config"):
+ request.config.update(func._cp_config)
+
+ # Decode any leftover %2F in the virtual_path atoms.
+ vpath = [x.replace("%2F", "/") for x in vpath]
+ request.handler = LateParamPageHandler(func, *vpath)
+ else:
+ request.handler = cherrypy.HTTPError(405)
+ else:
+ request.handler = cherrypy.NotFound()
+
+
+class RoutesDispatcher(object):
+ """A Routes based dispatcher for CherryPy."""
+
+ def __init__(self, full_result=False):
+ """
+ Routes dispatcher
+
+ Set full_result to True if you wish the controller
+ and the action to be passed on to the page handler
+ parameters. By default they won't be.
+ """
+ import routes
+ self.full_result = full_result
+ self.controllers = {}
+ self.mapper = routes.Mapper()
+ self.mapper.controller_scan = self.controllers.keys
+
+ def connect(self, name, route, controller, **kwargs):
+ self.controllers[name] = controller
+ self.mapper.connect(name, route, controller=name, **kwargs)
+
+ def redirect(self, url):
+ raise cherrypy.HTTPRedirect(url)
+
+ def __call__(self, path_info):
+ """Set handler and config for the current request."""
+ func = self.find_handler(path_info)
+ if func:
+ cherrypy.serving.request.handler = LateParamPageHandler(func)
+ else:
+ cherrypy.serving.request.handler = cherrypy.NotFound()
+
+ def find_handler(self, path_info):
+ """Find the right page handler, and set request.config."""
+ import routes
+
+ request = cherrypy.serving.request
+
+ config = routes.request_config()
+ config.mapper = self.mapper
+ if hasattr(request, 'wsgi_environ'):
+ config.environ = request.wsgi_environ
+ config.host = request.headers.get('Host', None)
+ config.protocol = request.scheme
+ config.redirect = self.redirect
+
+ result = self.mapper.match(path_info)
+
+ config.mapper_dict = result
+ params = {}
+ if result:
+ params = result.copy()
+ if not self.full_result:
+ params.pop('controller', None)
+ params.pop('action', None)
+ request.params.update(params)
+
+ # Get config for the root object/path.
+ request.config = base = cherrypy.config.copy()
+ curpath = ""
+
+ def merge(nodeconf):
+ if 'tools.staticdir.dir' in nodeconf:
+ nodeconf['tools.staticdir.section'] = curpath or "/"
+ base.update(nodeconf)
+
+ app = request.app
+ root = app.root
+ if hasattr(root, "_cp_config"):
+ merge(root._cp_config)
+ if "/" in app.config:
+ merge(app.config["/"])
+
+ # Mix in values from app.config.
+ atoms = [x for x in path_info.split("/") if x]
+ if atoms:
+ last = atoms.pop()
+ else:
+ last = None
+ for atom in atoms:
+ curpath = "/".join((curpath, atom))
+ if curpath in app.config:
+ merge(app.config[curpath])
+
+ handler = None
+ if result:
+ controller = result.get('controller')
+ controller = self.controllers.get(controller, controller)
+ if controller:
+ if isinstance(controller, (type, types.ClassType)):
+ controller = controller()
+ # Get config from the controller.
+ if hasattr(controller, "_cp_config"):
+ merge(controller._cp_config)
+
+ action = result.get('action')
+ if action is not None:
+ handler = getattr(controller, action, None)
+ # Get config from the handler
+ if hasattr(handler, "_cp_config"):
+ merge(handler._cp_config)
+ else:
+ handler = controller
+
+ # Do the last path atom here so it can
+ # override the controller's _cp_config.
+ if last:
+ curpath = "/".join((curpath, last))
+ if curpath in app.config:
+ merge(app.config[curpath])
+
+ return handler
+
+
+def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
+ from cherrypy.lib import xmlrpc
+ def xmlrpc_dispatch(path_info):
+ path_info = xmlrpc.patched_path(path_info)
+ return next_dispatcher(path_info)
+ return xmlrpc_dispatch
+
+
+def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains):
+ """
+ Select a different handler based on the Host header.
+
+ This can be useful when running multiple sites within one CP server.
+ It allows several domains to point to different parts of a single
+ website structure. For example::
+
+ http://www.domain.example -> root
+ http://www.domain2.example -> root/domain2/
+ http://www.domain2.example:443 -> root/secure
+
+ can be accomplished via the following config::
+
+ [/]
+ request.dispatch = cherrypy.dispatch.VirtualHost(
+ **{'www.domain2.example': '/domain2',
+ 'www.domain2.example:443': '/secure',
+ })
+
+ next_dispatcher
+ The next dispatcher object in the dispatch chain.
+ The VirtualHost dispatcher adds a prefix to the URL and calls
+ another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
+
+ use_x_forwarded_host
+ If True (the default), any "X-Forwarded-Host"
+ request header will be used instead of the "Host" header. This
+ is commonly added by HTTP servers (such as Apache) when proxying.
+
+ ``**domains``
+ A dict of {host header value: virtual prefix} pairs.
+ The incoming "Host" request header is looked up in this dict,
+ and, if a match is found, the corresponding "virtual prefix"
+ value will be prepended to the URL path before calling the
+ next dispatcher. Note that you often need separate entries
+ for "example.com" and "www.example.com". In addition, "Host"
+ headers may contain the port number.
+ """
+ from cherrypy.lib import httputil
+ def vhost_dispatch(path_info):
+ request = cherrypy.serving.request
+ header = request.headers.get
+
+ domain = header('Host', '')
+ if use_x_forwarded_host:
+ domain = header("X-Forwarded-Host", domain)
+
+ prefix = domains.get(domain, "")
+ if prefix:
+ path_info = httputil.urljoin(prefix, path_info)
+
+ result = next_dispatcher(path_info)
+
+ # Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
+ section = request.config.get('tools.staticdir.section')
+ if section:
+ section = section[len(prefix):]
+ request.config['tools.staticdir.section'] = section
+
+ return result
+ return vhost_dispatch
+
diff --git a/cherrypy/_cperror.py b/cherrypy/_cperror.py
new file mode 100755
index 0000000..00e5b53
--- /dev/null
+++ b/cherrypy/_cperror.py
@@ -0,0 +1,553 @@
+"""Exception classes for CherryPy.
+
+CherryPy provides (and uses) exceptions for declaring that the HTTP response
+should be a status other than the default "200 OK". You can ``raise`` them like
+normal Python exceptions. You can also call them and they will raise themselves;
+this means you can set an :class:`HTTPError<cherrypy._cperror.HTTPError>`
+or :class:`HTTPRedirect<cherrypy._cperror.HTTPRedirect>` as the
+:attr:`request.handler<cherrypy._cprequest.Request.handler>`.
+
+.. _redirectingpost:
+
+Redirecting POST
+================
+
+When you GET a resource and are redirected by the server to another Location,
+there's generally no problem since GET is both a "safe method" (there should
+be no side-effects) and an "idempotent method" (multiple calls are no different
+than a single call).
+
+POST, however, is neither safe nor idempotent--if you
+charge a credit card, you don't want to be charged twice by a redirect!
+
+For this reason, *none* of the 3xx responses permit a user-agent (browser) to
+resubmit a POST on redirection without first confirming the action with the user:
+
+===== ================================= ===========
+300 Multiple Choices Confirm with the user
+301 Moved Permanently Confirm with the user
+302 Found (Object moved temporarily) Confirm with the user
+303 See Other GET the new URI--no confirmation
+304 Not modified (for conditional GET only--POST should not raise this error)
+305 Use Proxy Confirm with the user
+307 Temporary Redirect Confirm with the user
+===== ================================= ===========
+
+However, browsers have historically implemented these restrictions poorly;
+in particular, many browsers do not force the user to confirm 301, 302
+or 307 when redirecting POST. For this reason, CherryPy defaults to 303,
+which most user-agents appear to have implemented correctly. Therefore, if
+you raise HTTPRedirect for a POST request, the user-agent will most likely
+attempt to GET the new URI (without asking for confirmation from the user).
+We realize this is confusing for developers, but it's the safest thing we
+could do. You are of course free to raise ``HTTPRedirect(uri, status=302)``
+or any other 3xx status if you know what you're doing, but given the
+environment, we couldn't let any of those be the default.
+
+Custom Error Handling
+=====================
+
+.. image:: /refman/cperrors.gif
+
+Anticipated HTTP responses
+--------------------------
+
+The 'error_page' config namespace can be used to provide custom HTML output for
+expected responses (like 404 Not Found). Supply a filename from which the output
+will be read. The contents will be interpolated with the values %(status)s,
+%(message)s, %(traceback)s, and %(version)s using plain old Python
+`string formatting <http://www.python.org/doc/2.6.4/library/stdtypes.html#string-formatting-operations>`_.
+
+::
+
+ _cp_config = {'error_page.404': os.path.join(localDir, "static/index.html")}
+
+
+Beginning in version 3.1, you may also provide a function or other callable as
+an error_page entry. It will be passed the same status, message, traceback and
+version arguments that are interpolated into templates::
+
+ def error_page_402(status, message, traceback, version):
+ return "Error %s - Well, I'm very sorry but you haven't paid!" % status
+ cherrypy.config.update({'error_page.402': error_page_402})
+
+Also in 3.1, in addition to the numbered error codes, you may also supply
+"error_page.default" to handle all codes which do not have their own error_page entry.
+
+
+
+Unanticipated errors
+--------------------
+
+CherryPy also has a generic error handling mechanism: whenever an unanticipated
+error occurs in your code, it will call
+:func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to set
+the response status, headers, and body. By default, this is the same output as
+:class:`HTTPError(500) <cherrypy._cperror.HTTPError>`. If you want to provide
+some other behavior, you generally replace "request.error_response".
+
+Here is some sample code that shows how to display a custom error message and
+send an e-mail containing the error::
+
+ from cherrypy import _cperror
+
+ def handle_error():
+ cherrypy.response.status = 500
+ cherrypy.response.body = ["<html><body>Sorry, an error occured</body></html>"]
+ sendMail('error@domain.com', 'Error in your web app', _cperror.format_exc())
+
+ class Root:
+ _cp_config = {'request.error_response': handle_error}
+
+
+Note that you have to explicitly set :attr:`response.body <cherrypy._cprequest.Response.body>`
+and not simply return an error message as a result.
+"""
+
+from cgi import escape as _escape
+from sys import exc_info as _exc_info
+from traceback import format_exception as _format_exception
+from cherrypy._cpcompat import basestring, iteritems, urljoin as _urljoin
+from cherrypy.lib import httputil as _httputil
+
+
+class CherryPyException(Exception):
+ """A base class for CherryPy exceptions."""
+ pass
+
+
+class TimeoutError(CherryPyException):
+ """Exception raised when Response.timed_out is detected."""
+ pass
+
+
+class InternalRedirect(CherryPyException):
+ """Exception raised to switch to the handler for a different URL.
+
+ This exception will redirect processing to another path within the site
+ (without informing the client). Provide the new path as an argument when
+ raising the exception. Provide any params in the querystring for the new URL.
+ """
+
+ def __init__(self, path, query_string=""):
+ import cherrypy
+ self.request = cherrypy.serving.request
+
+ self.query_string = query_string
+ if "?" in path:
+ # Separate any params included in the path
+ path, self.query_string = path.split("?", 1)
+
+ # Note that urljoin will "do the right thing" whether url is:
+ # 1. a URL relative to root (e.g. "/dummy")
+ # 2. a URL relative to the current path
+ # Note that any query string will be discarded.
+ path = _urljoin(self.request.path_info, path)
+
+ # Set a 'path' member attribute so that code which traps this
+ # error can have access to it.
+ self.path = path
+
+ CherryPyException.__init__(self, path, self.query_string)
+
+
+class HTTPRedirect(CherryPyException):
+ """Exception raised when the request should be redirected.
+
+ This exception will force a HTTP redirect to the URL or URL's you give it.
+ The new URL must be passed as the first argument to the Exception,
+ e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list.
+ If a URL is absolute, it will be used as-is. If it is relative, it is
+ assumed to be relative to the current cherrypy.request.path_info.
+
+ If one of the provided URL is a unicode object, it will be encoded
+ using the default encoding or the one passed in parameter.
+
+ There are multiple types of redirect, from which you can select via the
+ ``status`` argument. If you do not provide a ``status`` arg, it defaults to
+ 303 (or 302 if responding with HTTP/1.0).
+
+ Examples::
+
+ raise cherrypy.HTTPRedirect("")
+ raise cherrypy.HTTPRedirect("/abs/path", 307)
+ raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301)
+
+ See :ref:`redirectingpost` for additional caveats.
+ """
+
+ status = None
+ """The integer HTTP status code to emit."""
+
+ urls = None
+ """The list of URL's to emit."""
+
+ encoding = 'utf-8'
+ """The encoding when passed urls are unicode objects"""
+
+ def __init__(self, urls, status=None, encoding=None):
+ import cherrypy
+ request = cherrypy.serving.request
+
+ if isinstance(urls, basestring):
+ urls = [urls]
+
+ abs_urls = []
+ for url in urls:
+ if isinstance(url, unicode):
+ url = url.encode(encoding or self.encoding)
+
+ # Note that urljoin will "do the right thing" whether url is:
+ # 1. a complete URL with host (e.g. "http://www.example.com/test")
+ # 2. a URL relative to root (e.g. "/dummy")
+ # 3. a URL relative to the current path
+ # Note that any query string in cherrypy.request is discarded.
+ url = _urljoin(cherrypy.url(), url)
+ abs_urls.append(url)
+ self.urls = abs_urls
+
+ # RFC 2616 indicates a 301 response code fits our goal; however,
+ # browser support for 301 is quite messy. Do 302/303 instead. See
+ # http://www.alanflavell.org.uk/www/post-redirect.html
+ if status is None:
+ if request.protocol >= (1, 1):
+ status = 303
+ else:
+ status = 302
+ else:
+ status = int(status)
+ if status < 300 or status > 399:
+ raise ValueError("status must be between 300 and 399.")
+
+ self.status = status
+ CherryPyException.__init__(self, abs_urls, status)
+
+ def set_response(self):
+ """Modify cherrypy.response status, headers, and body to represent self.
+
+ CherryPy uses this internally, but you can also use it to create an
+ HTTPRedirect object and set its output without *raising* the exception.
+ """
+ import cherrypy
+ response = cherrypy.serving.response
+ response.status = status = self.status
+
+ if status in (300, 301, 302, 303, 307):
+ response.headers['Content-Type'] = "text/html;charset=utf-8"
+ # "The ... URI SHOULD be given by the Location field
+ # in the response."
+ response.headers['Location'] = self.urls[0]
+
+ # "Unless the request method was HEAD, the entity of the response
+ # SHOULD contain a short hypertext note with a hyperlink to the
+ # new URI(s)."
+ msg = {300: "This resource can be found at <a href='%s'>%s</a>.",
+ 301: "This resource has permanently moved to <a href='%s'>%s</a>.",
+ 302: "This resource resides temporarily at <a href='%s'>%s</a>.",
+ 303: "This resource can be found at <a href='%s'>%s</a>.",
+ 307: "This resource has moved temporarily to <a href='%s'>%s</a>.",
+ }[status]
+ msgs = [msg % (u, u) for u in self.urls]
+ response.body = "<br />\n".join(msgs)
+ # Previous code may have set C-L, so we have to reset it
+ # (allow finalize to set it).
+ response.headers.pop('Content-Length', None)
+ elif status == 304:
+ # Not Modified.
+ # "The response MUST include the following header fields:
+ # Date, unless its omission is required by section 14.18.1"
+ # The "Date" header should have been set in Response.__init__
+
+ # "...the response SHOULD NOT include other entity-headers."
+ for key in ('Allow', 'Content-Encoding', 'Content-Language',
+ 'Content-Length', 'Content-Location', 'Content-MD5',
+ 'Content-Range', 'Content-Type', 'Expires',
+ 'Last-Modified'):
+ if key in response.headers:
+ del response.headers[key]
+
+ # "The 304 response MUST NOT contain a message-body."
+ response.body = None
+ # Previous code may have set C-L, so we have to reset it.
+ response.headers.pop('Content-Length', None)
+ elif status == 305:
+ # Use Proxy.
+ # self.urls[0] should be the URI of the proxy.
+ response.headers['Location'] = self.urls[0]
+ response.body = None
+ # Previous code may have set C-L, so we have to reset it.
+ response.headers.pop('Content-Length', None)
+ else:
+ raise ValueError("The %s status code is unknown." % status)
+
+ def __call__(self):
+ """Use this exception as a request.handler (raise self)."""
+ raise self
+
+
+def clean_headers(status):
+ """Remove any headers which should not apply to an error response."""
+ import cherrypy
+
+ response = cherrypy.serving.response
+
+ # Remove headers which applied to the original content,
+ # but do not apply to the error page.
+ respheaders = response.headers
+ for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After",
+ "Vary", "Content-Encoding", "Content-Length", "Expires",
+ "Content-Location", "Content-MD5", "Last-Modified"]:
+ if key in respheaders:
+ del respheaders[key]
+
+ if status != 416:
+ # A server sending a response with status code 416 (Requested
+ # range not satisfiable) SHOULD include a Content-Range field
+ # with a byte-range-resp-spec of "*". The instance-length
+ # specifies the current length of the selected resource.
+ # A response with status code 206 (Partial Content) MUST NOT
+ # include a Content-Range field with a byte-range- resp-spec of "*".
+ if "Content-Range" in respheaders:
+ del respheaders["Content-Range"]
+
+
+class HTTPError(CherryPyException):
+ """Exception used to return an HTTP error code (4xx-5xx) to the client.
+
+ This exception can be used to automatically send a response using a http status
+ code, with an appropriate error page. It takes an optional
+ ``status`` argument (which must be between 400 and 599); it defaults to 500
+ ("Internal Server Error"). It also takes an optional ``message`` argument,
+ which will be returned in the response body. See
+ `RFC 2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_
+ for a complete list of available error codes and when to use them.
+
+ Examples::
+
+ raise cherrypy.HTTPError(403)
+ raise cherrypy.HTTPError("403 Forbidden", "You are not allowed to access this resource.")
+ """
+
+ status = None
+ """The HTTP status code. May be of type int or str (with a Reason-Phrase)."""
+
+ code = None
+ """The integer HTTP status code."""
+
+ reason = None
+ """The HTTP Reason-Phrase string."""
+
+ def __init__(self, status=500, message=None):
+ self.status = status
+ try:
+ self.code, self.reason, defaultmsg = _httputil.valid_status(status)
+ except ValueError, x:
+ raise self.__class__(500, x.args[0])
+
+ if self.code < 400 or self.code > 599:
+ raise ValueError("status must be between 400 and 599.")
+
+ # See http://www.python.org/dev/peps/pep-0352/
+ # self.message = message
+ self._message = message or defaultmsg
+ CherryPyException.__init__(self, status, message)
+
+ def set_response(self):
+ """Modify cherrypy.response status, headers, and body to represent self.
+
+ CherryPy uses this internally, but you can also use it to create an
+ HTTPError object and set its output without *raising* the exception.
+ """
+ import cherrypy
+
+ response = cherrypy.serving.response
+
+ clean_headers(self.code)
+
+ # In all cases, finalize will be called after this method,
+ # so don't bother cleaning up response values here.
+ response.status = self.status
+ tb = None
+ if cherrypy.serving.request.show_tracebacks:
+ tb = format_exc()
+ response.headers['Content-Type'] = "text/html;charset=utf-8"
+ response.headers.pop('Content-Length', None)
+
+ content = self.get_error_page(self.status, traceback=tb,
+ message=self._message)
+ response.body = content
+
+ _be_ie_unfriendly(self.code)
+
+ def get_error_page(self, *args, **kwargs):
+ return get_error_page(*args, **kwargs)
+
+ def __call__(self):
+ """Use this exception as a request.handler (raise self)."""
+ raise self
+
+
+class NotFound(HTTPError):
+ """Exception raised when a URL could not be mapped to any handler (404).
+
+ This is equivalent to raising
+ :class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`.
+ """
+
+ def __init__(self, path=None):
+ if path is None:
+ import cherrypy
+ request = cherrypy.serving.request
+ path = request.script_name + request.path_info
+ self.args = (path,)
+ HTTPError.__init__(self, 404, "The path '%s' was not found." % path)
+
+
+_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
+ <title>%(status)s</title>
+ <style type="text/css">
+ #powered_by {
+ margin-top: 20px;
+ border-top: 2px solid black;
+ font-style: italic;
+ }
+
+ #traceback {
+ color: red;
+ }
+ </style>
+</head>
+ <body>
+ <h2>%(status)s</h2>
+ <p>%(message)s</p>
+ <pre id="traceback">%(traceback)s</pre>
+ <div id="powered_by">
+ <span>Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a></span>
+ </div>
+ </body>
+</html>
+'''
+
+def get_error_page(status, **kwargs):
+ """Return an HTML page, containing a pretty error response.
+
+ status should be an int or a str.
+ kwargs will be interpolated into the page template.
+ """
+ import cherrypy
+
+ try:
+ code, reason, message = _httputil.valid_status(status)
+ except ValueError, x:
+ raise cherrypy.HTTPError(500, x.args[0])
+
+ # We can't use setdefault here, because some
+ # callers send None for kwarg values.
+ if kwargs.get('status') is None:
+ kwargs['status'] = "%s %s" % (code, reason)
+ if kwargs.get('message') is None:
+ kwargs['message'] = message
+ if kwargs.get('traceback') is None:
+ kwargs['traceback'] = ''
+ if kwargs.get('version') is None:
+ kwargs['version'] = cherrypy.__version__
+
+ for k, v in iteritems(kwargs):
+ if v is None:
+ kwargs[k] = ""
+ else:
+ kwargs[k] = _escape(kwargs[k])
+
+ # Use a custom template or callable for the error page?
+ pages = cherrypy.serving.request.error_page
+ error_page = pages.get(code) or pages.get('default')
+ if error_page:
+ try:
+ if hasattr(error_page, '__call__'):
+ return error_page(**kwargs)
+ else:
+ return open(error_page, 'rb').read() % kwargs
+ except:
+ e = _format_exception(*_exc_info())[-1]
+ m = kwargs['message']
+ if m:
+ m += "<br />"
+ m += "In addition, the custom error page failed:\n<br />%s" % e
+ kwargs['message'] = m
+
+ return _HTTPErrorTemplate % kwargs
+
+
+_ie_friendly_error_sizes = {
+ 400: 512, 403: 256, 404: 512, 405: 256,
+ 406: 512, 408: 512, 409: 512, 410: 256,
+ 500: 512, 501: 512, 505: 512,
+ }
+
+
+def _be_ie_unfriendly(status):
+ import cherrypy
+ response = cherrypy.serving.response
+
+ # For some statuses, Internet Explorer 5+ shows "friendly error
+ # messages" instead of our response.body if the body is smaller
+ # than a given size. Fix this by returning a body over that size
+ # (by adding whitespace).
+ # See http://support.microsoft.com/kb/q218155/
+ s = _ie_friendly_error_sizes.get(status, 0)
+ if s:
+ s += 1
+ # Since we are issuing an HTTP error status, we assume that
+ # the entity is short, and we should just collapse it.
+ content = response.collapse_body()
+ l = len(content)
+ if l and l < s:
+ # IN ADDITION: the response must be written to IE
+ # in one chunk or it will still get replaced! Bah.
+ content = content + (" " * (s - l))
+ response.body = content
+ response.headers['Content-Length'] = str(len(content))
+
+
+def format_exc(exc=None):
+ """Return exc (or sys.exc_info if None), formatted."""
+ if exc is None:
+ exc = _exc_info()
+ if exc == (None, None, None):
+ return ""
+ import traceback
+ return "".join(traceback.format_exception(*exc))
+
+def bare_error(extrabody=None):
+ """Produce status, headers, body for a critical error.
+
+ Returns a triple without calling any other questionable functions,
+ so it should be as error-free as possible. Call it from an HTTP server
+ if you get errors outside of the request.
+
+ If extrabody is None, a friendly but rather unhelpful error message
+ is set in the body. If extrabody is a string, it will be appended
+ as-is to the body.
+ """
+
+ # The whole point of this function is to be a last line-of-defense
+ # in handling errors. That is, it must not raise any errors itself;
+ # it cannot be allowed to fail. Therefore, don't add to it!
+ # In particular, don't call any other CP functions.
+
+ body = "Unrecoverable error in the server."
+ if extrabody is not None:
+ if not isinstance(extrabody, str):
+ extrabody = extrabody.encode('utf-8')
+ body += "\n" + extrabody
+
+ return ("500 Internal Server Error",
+ [('Content-Type', 'text/plain'),
+ ('Content-Length', str(len(body)))],
+ [body])
+
+
diff --git a/cherrypy/_cplogging.py b/cherrypy/_cplogging.py
new file mode 100755
index 0000000..d6ca979
--- /dev/null
+++ b/cherrypy/_cplogging.py
@@ -0,0 +1,393 @@
+"""
+Simple config
+=============
+
+Although CherryPy uses the :mod:`Python logging module <logging>`, it does so
+behind the scenes so that simple logging is simple, but complicated logging
+is still possible. "Simple" logging means that you can log to the screen
+(i.e. console/stdout) or to a file, and that you can easily have separate
+error and access log files.
+
+Here are the simplified logging settings. You use these by adding lines to
+your config file or dict. You should set these at either the global level or
+per application (see next), but generally not both.
+
+ * ``log.screen``: Set this to True to have both "error" and "access" messages
+ printed to stdout.
+ * ``log.access_file``: Set this to an absolute filename where you want
+ "access" messages written.
+ * ``log.error_file``: Set this to an absolute filename where you want "error"
+ messages written.
+
+Many events are automatically logged; to log your own application events, call
+:func:`cherrypy.log`.
+
+Architecture
+============
+
+Separate scopes
+---------------
+
+CherryPy provides log managers at both the global and application layers.
+This means you can have one set of logging rules for your entire site,
+and another set of rules specific to each application. The global log
+manager is found at :func:`cherrypy.log`, and the log manager for each
+application is found at :attr:`app.log<cherrypy._cptree.Application.log>`.
+If you're inside a request, the latter is reachable from
+``cherrypy.request.app.log``; if you're outside a request, you'll have to obtain
+a reference to the ``app``: either the return value of
+:func:`tree.mount()<cherrypy._cptree.Tree.mount>` or, if you used
+:func:`quickstart()<cherrypy.quickstart>` instead, via ``cherrypy.tree.apps['/']``.
+
+By default, the global logs are named "cherrypy.error" and "cherrypy.access",
+and the application logs are named "cherrypy.error.2378745" and
+"cherrypy.access.2378745" (the number is the id of the Application object).
+This means that the application logs "bubble up" to the site logs, so if your
+application has no log handlers, the site-level handlers will still log the
+messages.
+
+Errors vs. Access
+-----------------
+
+Each log manager handles both "access" messages (one per HTTP request) and
+"error" messages (everything else). Note that the "error" log is not just for
+errors! The format of access messages is highly formalized, but the error log
+isn't--it receives messages from a variety of sources (including full error
+tracebacks, if enabled).
+
+
+Custom Handlers
+===============
+
+The simple settings above work by manipulating Python's standard :mod:`logging`
+module. So when you need something more complex, the full power of the standard
+module is yours to exploit. You can borrow or create custom handlers, formats,
+filters, and much more. Here's an example that skips the standard FileHandler
+and uses a RotatingFileHandler instead:
+
+::
+
+ #python
+ log = app.log
+
+ # Remove the default FileHandlers if present.
+ log.error_file = ""
+ log.access_file = ""
+
+ maxBytes = getattr(log, "rot_maxBytes", 10000000)
+ backupCount = getattr(log, "rot_backupCount", 1000)
+
+ # Make a new RotatingFileHandler for the error log.
+ fname = getattr(log, "rot_error_file", "error.log")
+ h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
+ h.setLevel(DEBUG)
+ h.setFormatter(_cplogging.logfmt)
+ log.error_log.addHandler(h)
+
+ # Make a new RotatingFileHandler for the access log.
+ fname = getattr(log, "rot_access_file", "access.log")
+ h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
+ h.setLevel(DEBUG)
+ h.setFormatter(_cplogging.logfmt)
+ log.access_log.addHandler(h)
+
+
+The ``rot_*`` attributes are pulled straight from the application log object.
+Since "log.*" config entries simply set attributes on the log object, you can
+add custom attributes to your heart's content. Note that these handlers are
+used ''instead'' of the default, simple handlers outlined above (so don't set
+the "log.error_file" config entry, for example).
+"""
+
+import datetime
+import logging
+# Silence the no-handlers "warning" (stderr write!) in stdlib logging
+logging.Logger.manager.emittedNoHandlerWarning = 1
+logfmt = logging.Formatter("%(message)s")
+import os
+import sys
+
+import cherrypy
+from cherrypy import _cperror
+
+
+class LogManager(object):
+ """An object to assist both simple and advanced logging.
+
+ ``cherrypy.log`` is an instance of this class.
+ """
+
+ appid = None
+ """The id() of the Application object which owns this log manager. If this
+ is a global log manager, appid is None."""
+
+ error_log = None
+ """The actual :class:`logging.Logger` instance for error messages."""
+
+ access_log = None
+ """The actual :class:`logging.Logger` instance for access messages."""
+
+ access_log_format = \
+ '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
+
+ logger_root = None
+ """The "top-level" logger name.
+
+ This string will be used as the first segment in the Logger names.
+ The default is "cherrypy", for example, in which case the Logger names
+ will be of the form::
+
+ cherrypy.error.<appid>
+ cherrypy.access.<appid>
+ """
+
+ def __init__(self, appid=None, logger_root="cherrypy"):
+ self.logger_root = logger_root
+ self.appid = appid
+ if appid is None:
+ self.error_log = logging.getLogger("%s.error" % logger_root)
+ self.access_log = logging.getLogger("%s.access" % logger_root)
+ else:
+ self.error_log = logging.getLogger("%s.error.%s" % (logger_root, appid))
+ self.access_log = logging.getLogger("%s.access.%s" % (logger_root, appid))
+ self.error_log.setLevel(logging.INFO)
+ self.access_log.setLevel(logging.INFO)
+ cherrypy.engine.subscribe('graceful', self.reopen_files)
+
+ def reopen_files(self):
+ """Close and reopen all file handlers."""
+ for log in (self.error_log, self.access_log):
+ for h in log.handlers:
+ if isinstance(h, logging.FileHandler):
+ h.acquire()
+ h.stream.close()
+ h.stream = open(h.baseFilename, h.mode)
+ h.release()
+
+ def error(self, msg='', context='', severity=logging.INFO, traceback=False):
+ """Write the given ``msg`` to the error log.
+
+ This is not just for errors! Applications may call this at any time
+ to log application-specific information.
+
+ If ``traceback`` is True, the traceback of the current exception
+ (if any) will be appended to ``msg``.
+ """
+ if traceback:
+ msg += _cperror.format_exc()
+ self.error_log.log(severity, ' '.join((self.time(), context, msg)))
+
+ def __call__(self, *args, **kwargs):
+ """An alias for ``error``."""
+ return self.error(*args, **kwargs)
+
+ def access(self):
+ """Write to the access log (in Apache/NCSA Combined Log format).
+
+ See http://httpd.apache.org/docs/2.0/logs.html#combined for format
+ details.
+
+ CherryPy calls this automatically for you. Note there are no arguments;
+ it collects the data itself from
+ :class:`cherrypy.request<cherrypy._cprequest.Request>`.
+
+ Like Apache started doing in 2.0.46, non-printable and other special
+ characters in %r (and we expand that to all parts) are escaped using
+ \\xhh sequences, where hh stands for the hexadecimal representation
+ of the raw byte. Exceptions from this rule are " and \\, which are
+ escaped by prepending a backslash, and all whitespace characters,
+ which are written in their C-style notation (\\n, \\t, etc).
+ """
+ request = cherrypy.serving.request
+ remote = request.remote
+ response = cherrypy.serving.response
+ outheaders = response.headers
+ inheaders = request.headers
+ if response.output_status is None:
+ status = "-"
+ else:
+ status = response.output_status.split(" ", 1)[0]
+
+ atoms = {'h': remote.name or remote.ip,
+ 'l': '-',
+ 'u': getattr(request, "login", None) or "-",
+ 't': self.time(),
+ 'r': request.request_line,
+ 's': status,
+ 'b': dict.get(outheaders, 'Content-Length', '') or "-",
+ 'f': dict.get(inheaders, 'Referer', ''),
+ 'a': dict.get(inheaders, 'User-Agent', ''),
+ }
+ for k, v in atoms.items():
+ if isinstance(v, unicode):
+ v = v.encode('utf8')
+ elif not isinstance(v, str):
+ v = str(v)
+ # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
+ # and backslash for us. All we have to do is strip the quotes.
+ v = repr(v)[1:-1]
+ # Escape double-quote.
+ atoms[k] = v.replace('"', '\\"')
+
+ try:
+ self.access_log.log(logging.INFO, self.access_log_format % atoms)
+ except:
+ self(traceback=True)
+
+ def time(self):
+ """Return now() in Apache Common Log Format (no timezone)."""
+ now = datetime.datetime.now()
+ monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun',
+ 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
+ month = monthnames[now.month - 1].capitalize()
+ return ('[%02d/%s/%04d:%02d:%02d:%02d]' %
+ (now.day, month, now.year, now.hour, now.minute, now.second))
+
+ def _get_builtin_handler(self, log, key):
+ for h in log.handlers:
+ if getattr(h, "_cpbuiltin", None) == key:
+ return h
+
+
+ # ------------------------- Screen handlers ------------------------- #
+
+ def _set_screen_handler(self, log, enable, stream=None):
+ h = self._get_builtin_handler(log, "screen")
+ if enable:
+ if not h:
+ if stream is None:
+ stream=sys.stderr
+ h = logging.StreamHandler(stream)
+ h.setFormatter(logfmt)
+ h._cpbuiltin = "screen"
+ log.addHandler(h)
+ elif h:
+ log.handlers.remove(h)
+
+ def _get_screen(self):
+ h = self._get_builtin_handler
+ has_h = h(self.error_log, "screen") or h(self.access_log, "screen")
+ return bool(has_h)
+
+ def _set_screen(self, newvalue):
+ self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr)
+ self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout)
+ screen = property(_get_screen, _set_screen,
+ doc="""Turn stderr/stdout logging on or off.
+
+ If you set this to True, it'll add the appropriate StreamHandler for
+ you. If you set it to False, it will remove the handler.
+ """)
+
+ # -------------------------- File handlers -------------------------- #
+
+ def _add_builtin_file_handler(self, log, fname):
+ h = logging.FileHandler(fname)
+ h.setFormatter(logfmt)
+ h._cpbuiltin = "file"
+ log.addHandler(h)
+
+ def _set_file_handler(self, log, filename):
+ h = self._get_builtin_handler(log, "file")
+ if filename:
+ if h:
+ if h.baseFilename != os.path.abspath(filename):
+ h.close()
+ log.handlers.remove(h)
+ self._add_builtin_file_handler(log, filename)
+ else:
+ self._add_builtin_file_handler(log, filename)
+ else:
+ if h:
+ h.close()
+ log.handlers.remove(h)
+
+ def _get_error_file(self):
+ h = self._get_builtin_handler(self.error_log, "file")
+ if h:
+ return h.baseFilename
+ return ''
+ def _set_error_file(self, newvalue):
+ self._set_file_handler(self.error_log, newvalue)
+ error_file = property(_get_error_file, _set_error_file,
+ doc="""The filename for self.error_log.
+
+ If you set this to a string, it'll add the appropriate FileHandler for
+ you. If you set it to ``None`` or ``''``, it will remove the handler.
+ """)
+
+ def _get_access_file(self):
+ h = self._get_builtin_handler(self.access_log, "file")
+ if h:
+ return h.baseFilename
+ return ''
+ def _set_access_file(self, newvalue):
+ self._set_file_handler(self.access_log, newvalue)
+ access_file = property(_get_access_file, _set_access_file,
+ doc="""The filename for self.access_log.
+
+ If you set this to a string, it'll add the appropriate FileHandler for
+ you. If you set it to ``None`` or ``''``, it will remove the handler.
+ """)
+
+ # ------------------------- WSGI handlers ------------------------- #
+
+ def _set_wsgi_handler(self, log, enable):
+ h = self._get_builtin_handler(log, "wsgi")
+ if enable:
+ if not h:
+ h = WSGIErrorHandler()
+ h.setFormatter(logfmt)
+ h._cpbuiltin = "wsgi"
+ log.addHandler(h)
+ elif h:
+ log.handlers.remove(h)
+
+ def _get_wsgi(self):
+ return bool(self._get_builtin_handler(self.error_log, "wsgi"))
+
+ def _set_wsgi(self, newvalue):
+ self._set_wsgi_handler(self.error_log, newvalue)
+ wsgi = property(_get_wsgi, _set_wsgi,
+ doc="""Write errors to wsgi.errors.
+
+ If you set this to True, it'll add the appropriate
+ :class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you
+ (which writes errors to ``wsgi.errors``).
+ If you set it to False, it will remove the handler.
+ """)
+
+
+class WSGIErrorHandler(logging.Handler):
+ "A handler class which writes logging records to environ['wsgi.errors']."
+
+ def flush(self):
+ """Flushes the stream."""
+ try:
+ stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors')
+ except (AttributeError, KeyError):
+ pass
+ else:
+ stream.flush()
+
+ def emit(self, record):
+ """Emit a record."""
+ try:
+ stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors')
+ except (AttributeError, KeyError):
+ pass
+ else:
+ try:
+ msg = self.format(record)
+ fs = "%s\n"
+ import types
+ if not hasattr(types, "UnicodeType"): #if no unicode support...
+ stream.write(fs % msg)
+ else:
+ try:
+ stream.write(fs % msg)
+ except UnicodeError:
+ stream.write(fs % msg.encode("UTF-8"))
+ self.flush()
+ except:
+ self.handleError(record)
diff --git a/cherrypy/_cpmodpy.py b/cherrypy/_cpmodpy.py
new file mode 100755
index 0000000..ba2ab22
--- /dev/null
+++ b/cherrypy/_cpmodpy.py
@@ -0,0 +1,333 @@
+"""Native adapter for serving CherryPy via mod_python
+
+Basic usage:
+
+##########################################
+# Application in a module called myapp.py
+##########################################
+
+import cherrypy
+
+class Root:
+ @cherrypy.expose
+ def index(self):
+ return 'Hi there, Ho there, Hey there'
+
+
+# We will use this method from the mod_python configuration
+# as the entry point to our application
+def setup_server():
+ cherrypy.tree.mount(Root())
+ cherrypy.config.update({'environment': 'production',
+ 'log.screen': False,
+ 'show_tracebacks': False})
+
+##########################################
+# mod_python settings for apache2
+# This should reside in your httpd.conf
+# or a file that will be loaded at
+# apache startup
+##########################################
+
+# Start
+DocumentRoot "/"
+Listen 8080
+LoadModule python_module /usr/lib/apache2/modules/mod_python.so
+
+<Location "/">
+ PythonPath "sys.path+['/path/to/my/application']"
+ SetHandler python-program
+ PythonHandler cherrypy._cpmodpy::handler
+ PythonOption cherrypy.setup myapp::setup_server
+ PythonDebug On
+</Location>
+# End
+
+The actual path to your mod_python.so is dependent on your
+environment. In this case we suppose a global mod_python
+installation on a Linux distribution such as Ubuntu.
+
+We do set the PythonPath configuration setting so that
+your application can be found by from the user running
+the apache2 instance. Of course if your application
+resides in the global site-package this won't be needed.
+
+Then restart apache2 and access http://127.0.0.1:8080
+"""
+
+import logging
+import sys
+
+import cherrypy
+from cherrypy._cpcompat import BytesIO, copyitems, ntob
+from cherrypy._cperror import format_exc, bare_error
+from cherrypy.lib import httputil
+
+
+# ------------------------------ Request-handling
+
+
+
+def setup(req):
+ from mod_python import apache
+
+ # Run any setup functions defined by a "PythonOption cherrypy.setup" directive.
+ options = req.get_options()
+ if 'cherrypy.setup' in options:
+ for function in options['cherrypy.setup'].split():
+ atoms = function.split('::', 1)
+ if len(atoms) == 1:
+ mod = __import__(atoms[0], globals(), locals())
+ else:
+ modname, fname = atoms
+ mod = __import__(modname, globals(), locals(), [fname])
+ func = getattr(mod, fname)
+ func()
+
+ cherrypy.config.update({'log.screen': False,
+ "tools.ignore_headers.on": True,
+ "tools.ignore_headers.headers": ['Range'],
+ })
+
+ engine = cherrypy.engine
+ if hasattr(engine, "signal_handler"):
+ engine.signal_handler.unsubscribe()
+ if hasattr(engine, "console_control_handler"):
+ engine.console_control_handler.unsubscribe()
+ engine.autoreload.unsubscribe()
+ cherrypy.server.unsubscribe()
+
+ def _log(msg, level):
+ newlevel = apache.APLOG_ERR
+ if logging.DEBUG >= level:
+ newlevel = apache.APLOG_DEBUG
+ elif logging.INFO >= level:
+ newlevel = apache.APLOG_INFO
+ elif logging.WARNING >= level:
+ newlevel = apache.APLOG_WARNING
+ # On Windows, req.server is required or the msg will vanish. See
+ # http://www.modpython.org/pipermail/mod_python/2003-October/014291.html.
+ # Also, "When server is not specified...LogLevel does not apply..."
+ apache.log_error(msg, newlevel, req.server)
+ engine.subscribe('log', _log)
+
+ engine.start()
+
+ def cherrypy_cleanup(data):
+ engine.exit()
+ try:
+ # apache.register_cleanup wasn't available until 3.1.4.
+ apache.register_cleanup(cherrypy_cleanup)
+ except AttributeError:
+ req.server.register_cleanup(req, cherrypy_cleanup)
+
+
+class _ReadOnlyRequest:
+ expose = ('read', 'readline', 'readlines')
+ def __init__(self, req):
+ for method in self.expose:
+ self.__dict__[method] = getattr(req, method)
+
+
+recursive = False
+
+_isSetUp = False
+def handler(req):
+ from mod_python import apache
+ try:
+ global _isSetUp
+ if not _isSetUp:
+ setup(req)
+ _isSetUp = True
+
+ # Obtain a Request object from CherryPy
+ local = req.connection.local_addr
+ local = httputil.Host(local[0], local[1], req.connection.local_host or "")
+ remote = req.connection.remote_addr
+ remote = httputil.Host(remote[0], remote[1], req.connection.remote_host or "")
+
+ scheme = req.parsed_uri[0] or 'http'
+ req.get_basic_auth_pw()
+
+ try:
+ # apache.mpm_query only became available in mod_python 3.1
+ q = apache.mpm_query
+ threaded = q(apache.AP_MPMQ_IS_THREADED)
+ forked = q(apache.AP_MPMQ_IS_FORKED)
+ except AttributeError:
+ bad_value = ("You must provide a PythonOption '%s', "
+ "either 'on' or 'off', when running a version "
+ "of mod_python < 3.1")
+
+ threaded = options.get('multithread', '').lower()
+ if threaded == 'on':
+ threaded = True
+ elif threaded == 'off':
+ threaded = False
+ else:
+ raise ValueError(bad_value % "multithread")
+
+ forked = options.get('multiprocess', '').lower()
+ if forked == 'on':
+ forked = True
+ elif forked == 'off':
+ forked = False
+ else:
+ raise ValueError(bad_value % "multiprocess")
+
+ sn = cherrypy.tree.script_name(req.uri or "/")
+ if sn is None:
+ send_response(req, '404 Not Found', [], '')
+ else:
+ app = cherrypy.tree.apps[sn]
+ method = req.method
+ path = req.uri
+ qs = req.args or ""
+ reqproto = req.protocol
+ headers = copyitems(req.headers_in)
+ rfile = _ReadOnlyRequest(req)
+ prev = None
+
+ try:
+ redirections = []
+ while True:
+ request, response = app.get_serving(local, remote, scheme,
+ "HTTP/1.1")
+ request.login = req.user
+ request.multithread = bool(threaded)
+ request.multiprocess = bool(forked)
+ request.app = app
+ request.prev = prev
+
+ # Run the CherryPy Request object and obtain the response
+ try:
+ request.run(method, path, qs, reqproto, headers, rfile)
+ break
+ except cherrypy.InternalRedirect:
+ ir = sys.exc_info()[1]
+ app.release_serving()
+ prev = request
+
+ if not recursive:
+ if ir.path in redirections:
+ raise RuntimeError("InternalRedirector visited the "
+ "same URL twice: %r" % ir.path)
+ else:
+ # Add the *previous* path_info + qs to redirections.
+ if qs:
+ qs = "?" + qs
+ redirections.append(sn + path + qs)
+
+ # Munge environment and try again.
+ method = "GET"
+ path = ir.path
+ qs = ir.query_string
+ rfile = BytesIO()
+
+ send_response(req, response.status, response.header_list,
+ response.body, response.stream)
+ finally:
+ app.release_serving()
+ except:
+ tb = format_exc()
+ cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR)
+ s, h, b = bare_error()
+ send_response(req, s, h, b)
+ return apache.OK
+
+
+def send_response(req, status, headers, body, stream=False):
+ # Set response status
+ req.status = int(status[:3])
+
+ # Set response headers
+ req.content_type = "text/plain"
+ for header, value in headers:
+ if header.lower() == 'content-type':
+ req.content_type = value
+ continue
+ req.headers_out.add(header, value)
+
+ if stream:
+ # Flush now so the status and headers are sent immediately.
+ req.flush()
+
+ # Set response body
+ if isinstance(body, basestring):
+ req.write(body)
+ else:
+ for seg in body:
+ req.write(seg)
+
+
+
+# --------------- Startup tools for CherryPy + mod_python --------------- #
+
+
+import os
+import re
+
+
+def read_process(cmd, args=""):
+ fullcmd = "%s %s" % (cmd, args)
+ pipein, pipeout = os.popen4(fullcmd)
+ try:
+ firstline = pipeout.readline()
+ if (re.search(ntob("(not recognized|No such file|not found)"), firstline,
+ re.IGNORECASE)):
+ raise IOError('%s must be on your system path.' % cmd)
+ output = firstline + pipeout.read()
+ finally:
+ pipeout.close()
+ return output
+
+
+class ModPythonServer(object):
+
+ template = """
+# Apache2 server configuration file for running CherryPy with mod_python.
+
+DocumentRoot "/"
+Listen %(port)s
+LoadModule python_module modules/mod_python.so
+
+<Location %(loc)s>
+ SetHandler python-program
+ PythonHandler %(handler)s
+ PythonDebug On
+%(opts)s
+</Location>
+"""
+
+ def __init__(self, loc="/", port=80, opts=None, apache_path="apache",
+ handler="cherrypy._cpmodpy::handler"):
+ self.loc = loc
+ self.port = port
+ self.opts = opts
+ self.apache_path = apache_path
+ self.handler = handler
+
+ def start(self):
+ opts = "".join([" PythonOption %s %s\n" % (k, v)
+ for k, v in self.opts])
+ conf_data = self.template % {"port": self.port,
+ "loc": self.loc,
+ "opts": opts,
+ "handler": self.handler,
+ }
+
+ mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf")
+ f = open(mpconf, 'wb')
+ try:
+ f.write(conf_data)
+ finally:
+ f.close()
+
+ response = read_process(self.apache_path, "-k start -f %s" % mpconf)
+ self.ready = True
+ return response
+
+ def stop(self):
+ os.popen("apache -k stop")
+ self.ready = False
+
diff --git a/cherrypy/_cpnative_server.py b/cherrypy/_cpnative_server.py
new file mode 100755
index 0000000..57f715a
--- /dev/null
+++ b/cherrypy/_cpnative_server.py
@@ -0,0 +1,149 @@
+"""Native adapter for serving CherryPy via its builtin server."""
+
+import logging
+import sys
+
+import cherrypy
+from cherrypy._cpcompat import BytesIO
+from cherrypy._cperror import format_exc, bare_error
+from cherrypy.lib import httputil
+from cherrypy import wsgiserver
+
+
+class NativeGateway(wsgiserver.Gateway):
+
+ recursive = False
+
+ def respond(self):
+ req = self.req
+ try:
+ # Obtain a Request object from CherryPy
+ local = req.server.bind_addr
+ local = httputil.Host(local[0], local[1], "")
+ remote = req.conn.remote_addr, req.conn.remote_port
+ remote = httputil.Host(remote[0], remote[1], "")
+
+ scheme = req.scheme
+ sn = cherrypy.tree.script_name(req.uri or "/")
+ if sn is None:
+ self.send_response('404 Not Found', [], [''])
+ else:
+ app = cherrypy.tree.apps[sn]
+ method = req.method
+ path = req.path
+ qs = req.qs or ""
+ headers = req.inheaders.items()
+ rfile = req.rfile
+ prev = None
+
+ try:
+ redirections = []
+ while True:
+ request, response = app.get_serving(
+ local, remote, scheme, "HTTP/1.1")
+ request.multithread = True
+ request.multiprocess = False
+ request.app = app
+ request.prev = prev
+
+ # Run the CherryPy Request object and obtain the response
+ try:
+ request.run(method, path, qs, req.request_protocol, headers, rfile)
+ break
+ except cherrypy.InternalRedirect:
+ ir = sys.exc_info()[1]
+ app.release_serving()
+ prev = request
+
+ if not self.recursive:
+ if ir.path in redirections:
+ raise RuntimeError("InternalRedirector visited the "
+ "same URL twice: %r" % ir.path)
+ else:
+ # Add the *previous* path_info + qs to redirections.
+ if qs:
+ qs = "?" + qs
+ redirections.append(sn + path + qs)
+
+ # Munge environment and try again.
+ method = "GET"
+ path = ir.path
+ qs = ir.query_string
+ rfile = BytesIO()
+
+ self.send_response(
+ response.output_status, response.header_list,
+ response.body)
+ finally:
+ app.release_serving()
+ except:
+ tb = format_exc()
+ #print tb
+ cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR)
+ s, h, b = bare_error()
+ self.send_response(s, h, b)
+
+ def send_response(self, status, headers, body):
+ req = self.req
+
+ # Set response status
+ req.status = str(status or "500 Server Error")
+
+ # Set response headers
+ for header, value in headers:
+ req.outheaders.append((header, value))
+ if (req.ready and not req.sent_headers):
+ req.sent_headers = True
+ req.send_headers()
+
+ # Set response body
+ for seg in body:
+ req.write(seg)
+
+
+class CPHTTPServer(wsgiserver.HTTPServer):
+ """Wrapper for wsgiserver.HTTPServer.
+
+ wsgiserver has been designed to not reference CherryPy in any way,
+ so that it can be used in other frameworks and applications.
+ Therefore, we wrap it here, so we can apply some attributes
+ from config -> cherrypy.server -> HTTPServer.
+ """
+
+ def __init__(self, server_adapter=cherrypy.server):
+ self.server_adapter = server_adapter
+
+ server_name = (self.server_adapter.socket_host or
+ self.server_adapter.socket_file or
+ None)
+
+ wsgiserver.HTTPServer.__init__(
+ self, server_adapter.bind_addr, NativeGateway,
+ minthreads=server_adapter.thread_pool,
+ maxthreads=server_adapter.thread_pool_max,
+ server_name=server_name)
+
+ self.max_request_header_size = self.server_adapter.max_request_header_size or 0
+ self.max_request_body_size = self.server_adapter.max_request_body_size or 0
+ self.request_queue_size = self.server_adapter.socket_queue_size
+ self.timeout = self.server_adapter.socket_timeout
+ self.shutdown_timeout = self.server_adapter.shutdown_timeout
+ self.protocol = self.server_adapter.protocol_version
+ self.nodelay = self.server_adapter.nodelay
+
+ ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
+ if self.server_adapter.ssl_context:
+ adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
+ self.ssl_adapter = adapter_class(
+ self.server_adapter.ssl_certificate,
+ self.server_adapter.ssl_private_key,
+ self.server_adapter.ssl_certificate_chain)
+ self.ssl_adapter.context = self.server_adapter.ssl_context
+ elif self.server_adapter.ssl_certificate:
+ adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
+ self.ssl_adapter = adapter_class(
+ self.server_adapter.ssl_certificate,
+ self.server_adapter.ssl_private_key,
+ self.server_adapter.ssl_certificate_chain)
+
+
diff --git a/cherrypy/_cpreqbody.py b/cherrypy/_cpreqbody.py
new file mode 100755
index 0000000..1b0496e
--- /dev/null
+++ b/cherrypy/_cpreqbody.py
@@ -0,0 +1,941 @@
+"""Request body processing for CherryPy.
+
+.. versionadded:: 3.2
+
+Application authors have complete control over the parsing of HTTP request
+entities. In short, :attr:`cherrypy.request.body<cherrypy._cprequest.Request.body>`
+is now always set to an instance of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>`,
+and *that* class is a subclass of :class:`Entity<cherrypy._cpreqbody.Entity>`.
+
+When an HTTP request includes an entity body, it is often desirable to
+provide that information to applications in a form other than the raw bytes.
+Different content types demand different approaches. Examples:
+
+ * For a GIF file, we want the raw bytes in a stream.
+ * An HTML form is better parsed into its component fields, and each text field
+ decoded from bytes to unicode.
+ * A JSON body should be deserialized into a Python dict or list.
+
+When the request contains a Content-Type header, the media type is used as a
+key to look up a value in the
+:attr:`request.body.processors<cherrypy._cpreqbody.Entity.processors>` dict.
+If the full media
+type is not found, then the major type is tried; for example, if no processor
+is found for the 'image/jpeg' type, then we look for a processor for the 'image'
+types altogether. If neither the full type nor the major type has a matching
+processor, then a default processor is used
+(:func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>`). For most
+types, this means no processing is done, and the body is left unread as a
+raw byte stream. Processors are configurable in an 'on_start_resource' hook.
+
+Some processors, especially those for the 'text' types, attempt to decode bytes
+to unicode. If the Content-Type request header includes a 'charset' parameter,
+this is used to decode the entity. Otherwise, one or more default charsets may
+be attempted, although this decision is up to each processor. If a processor
+successfully decodes an Entity or Part, it should set the
+:attr:`charset<cherrypy._cpreqbody.Entity.charset>` attribute
+on the Entity or Part to the name of the successful charset, so that
+applications can easily re-encode or transcode the value if they wish.
+
+If the Content-Type of the request entity is of major type 'multipart', then
+the above parsing process, and possibly a decoding process, is performed for
+each part.
+
+For both the full entity and multipart parts, a Content-Disposition header may
+be used to fill :attr:`name<cherrypy._cpreqbody.Entity.name>` and
+:attr:`filename<cherrypy._cpreqbody.Entity.filename>` attributes on the
+request.body or the Part.
+
+.. _custombodyprocessors:
+
+Custom Processors
+=================
+
+You can add your own processors for any specific or major MIME type. Simply add
+it to the :attr:`processors<cherrypy._cprequest.Entity.processors>` dict in a
+hook/tool that runs at ``on_start_resource`` or ``before_request_body``.
+Here's the built-in JSON tool for an example::
+
+ def json_in(force=True, debug=False):
+ request = cherrypy.serving.request
+ def json_processor(entity):
+ \"""Read application/json data into request.json.\"""
+ if not entity.headers.get("Content-Length", ""):
+ raise cherrypy.HTTPError(411)
+
+ body = entity.fp.read()
+ try:
+ request.json = json_decode(body)
+ except ValueError:
+ raise cherrypy.HTTPError(400, 'Invalid JSON document')
+ if force:
+ request.body.processors.clear()
+ request.body.default_proc = cherrypy.HTTPError(
+ 415, 'Expected an application/json content type')
+ request.body.processors['application/json'] = json_processor
+
+We begin by defining a new ``json_processor`` function to stick in the ``processors``
+dictionary. All processor functions take a single argument, the ``Entity`` instance
+they are to process. It will be called whenever a request is received (for those
+URI's where the tool is turned on) which has a ``Content-Type`` of
+"application/json".
+
+First, it checks for a valid ``Content-Length`` (raising 411 if not valid), then
+reads the remaining bytes on the socket. The ``fp`` object knows its own length, so
+it won't hang waiting for data that never arrives. It will return when all data
+has been read. Then, we decode those bytes using Python's built-in ``json`` module,
+and stick the decoded result onto ``request.json`` . If it cannot be decoded, we
+raise 400.
+
+If the "force" argument is True (the default), the ``Tool`` clears the ``processors``
+dict so that request entities of other ``Content-Types`` aren't parsed at all. Since
+there's no entry for those invalid MIME types, the ``default_proc`` method of ``cherrypy.request.body``
+is called. But this does nothing by default (usually to provide the page handler an opportunity to handle it.)
+But in our case, we want to raise 415, so we replace ``request.body.default_proc``
+with the error (``HTTPError`` instances, when called, raise themselves).
+
+If we were defining a custom processor, we can do so without making a ``Tool``. Just add the config entry::
+
+ request.body.processors = {'application/json': json_processor}
+
+Note that you can only replace the ``processors`` dict wholesale this way, not update the existing one.
+"""
+
+import re
+import sys
+import tempfile
+from urllib import unquote_plus
+
+import cherrypy
+from cherrypy._cpcompat import basestring, ntob, ntou
+from cherrypy.lib import httputil
+
+
+# -------------------------------- Processors -------------------------------- #
+
+def process_urlencoded(entity):
+ """Read application/x-www-form-urlencoded data into entity.params."""
+ qs = entity.fp.read()
+ for charset in entity.attempt_charsets:
+ try:
+ params = {}
+ for aparam in qs.split(ntob('&')):
+ for pair in aparam.split(ntob(';')):
+ if not pair:
+ continue
+
+ atoms = pair.split(ntob('='), 1)
+ if len(atoms) == 1:
+ atoms.append(ntob(''))
+
+ key = unquote_plus(atoms[0]).decode(charset)
+ value = unquote_plus(atoms[1]).decode(charset)
+
+ if key in params:
+ if not isinstance(params[key], list):
+ params[key] = [params[key]]
+ params[key].append(value)
+ else:
+ params[key] = value
+ except UnicodeDecodeError:
+ pass
+ else:
+ entity.charset = charset
+ break
+ else:
+ raise cherrypy.HTTPError(
+ 400, "The request entity could not be decoded. The following "
+ "charsets were attempted: %s" % repr(entity.attempt_charsets))
+
+ # Now that all values have been successfully parsed and decoded,
+ # apply them to the entity.params dict.
+ for key, value in params.items():
+ if key in entity.params:
+ if not isinstance(entity.params[key], list):
+ entity.params[key] = [entity.params[key]]
+ entity.params[key].append(value)
+ else:
+ entity.params[key] = value
+
+
+def process_multipart(entity):
+ """Read all multipart parts into entity.parts."""
+ ib = ""
+ if 'boundary' in entity.content_type.params:
+ # http://tools.ietf.org/html/rfc2046#section-5.1.1
+ # "The grammar for parameters on the Content-type field is such that it
+ # is often necessary to enclose the boundary parameter values in quotes
+ # on the Content-type line"
+ ib = entity.content_type.params['boundary'].strip('"')
+
+ if not re.match("^[ -~]{0,200}[!-~]$", ib):
+ raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
+
+ ib = ('--' + ib).encode('ascii')
+
+ # Find the first marker
+ while True:
+ b = entity.readline()
+ if not b:
+ return
+
+ b = b.strip()
+ if b == ib:
+ break
+
+ # Read all parts
+ while True:
+ part = entity.part_class.from_fp(entity.fp, ib)
+ entity.parts.append(part)
+ part.process()
+ if part.fp.done:
+ break
+
+def process_multipart_form_data(entity):
+ """Read all multipart/form-data parts into entity.parts or entity.params."""
+ process_multipart(entity)
+
+ kept_parts = []
+ for part in entity.parts:
+ if part.name is None:
+ kept_parts.append(part)
+ else:
+ if part.filename is None:
+ # It's a regular field
+ value = part.fullvalue()
+ else:
+ # It's a file upload. Retain the whole part so consumer code
+ # has access to its .file and .filename attributes.
+ value = part
+
+ if part.name in entity.params:
+ if not isinstance(entity.params[part.name], list):
+ entity.params[part.name] = [entity.params[part.name]]
+ entity.params[part.name].append(value)
+ else:
+ entity.params[part.name] = value
+
+ entity.parts = kept_parts
+
+def _old_process_multipart(entity):
+ """The behavior of 3.2 and lower. Deprecated and will be changed in 3.3."""
+ process_multipart(entity)
+
+ params = entity.params
+
+ for part in entity.parts:
+ if part.name is None:
+ key = ntou('parts')
+ else:
+ key = part.name
+
+ if part.filename is None:
+ # It's a regular field
+ value = part.fullvalue()
+ else:
+ # It's a file upload. Retain the whole part so consumer code
+ # has access to its .file and .filename attributes.
+ value = part
+
+ if key in params:
+ if not isinstance(params[key], list):
+ params[key] = [params[key]]
+ params[key].append(value)
+ else:
+ params[key] = value
+
+
+
+# --------------------------------- Entities --------------------------------- #
+
+
+class Entity(object):
+ """An HTTP request body, or MIME multipart body.
+
+ This class collects information about the HTTP request entity. When a
+ given entity is of MIME type "multipart", each part is parsed into its own
+ Entity instance, and the set of parts stored in
+ :attr:`entity.parts<cherrypy._cpreqbody.Entity.parts>`.
+
+ Between the ``before_request_body`` and ``before_handler`` tools, CherryPy
+ tries to process the request body (if any) by calling
+ :func:`request.body.process<cherrypy._cpreqbody.RequestBody.process`.
+ This uses the ``content_type`` of the Entity to look up a suitable processor
+ in :attr:`Entity.processors<cherrypy._cpreqbody.Entity.processors>`, a dict.
+ If a matching processor cannot be found for the complete Content-Type,
+ it tries again using the major type. For example, if a request with an
+ entity of type "image/jpeg" arrives, but no processor can be found for
+ that complete type, then one is sought for the major type "image". If a
+ processor is still not found, then the
+ :func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>` method of the
+ Entity is called (which does nothing by default; you can override this too).
+
+ CherryPy includes processors for the "application/x-www-form-urlencoded"
+ type, the "multipart/form-data" type, and the "multipart" major type.
+ CherryPy 3.2 processes these types almost exactly as older versions.
+ Parts are passed as arguments to the page handler using their
+ ``Content-Disposition.name`` if given, otherwise in a generic "parts"
+ argument. Each such part is either a string, or the
+ :class:`Part<cherrypy._cpreqbody.Part>` itself if it's a file. (In this
+ case it will have ``file`` and ``filename`` attributes, or possibly a
+ ``value`` attribute). Each Part is itself a subclass of
+ Entity, and has its own ``process`` method and ``processors`` dict.
+
+ There is a separate processor for the "multipart" major type which is more
+ flexible, and simply stores all multipart parts in
+ :attr:`request.body.parts<cherrypy._cpreqbody.Entity.parts>`. You can
+ enable it with::
+
+ cherrypy.request.body.processors['multipart'] = _cpreqbody.process_multipart
+
+ in an ``on_start_resource`` tool.
+ """
+
+ # http://tools.ietf.org/html/rfc2046#section-4.1.2:
+ # "The default character set, which must be assumed in the
+ # absence of a charset parameter, is US-ASCII."
+ # However, many browsers send data in utf-8 with no charset.
+ attempt_charsets = ['utf-8']
+ """A list of strings, each of which should be a known encoding.
+
+ When the Content-Type of the request body warrants it, each of the given
+ encodings will be tried in order. The first one to successfully decode the
+ entity without raising an error is stored as
+ :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
+ to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
+ `HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
+ but ``['us-ascii', 'utf-8']`` for multipart parts.
+ """
+
+ charset = None
+ """The successful decoding; see "attempt_charsets" above."""
+
+ content_type = None
+ """The value of the Content-Type request header.
+
+ If the Entity is part of a multipart payload, this will be the Content-Type
+ given in the MIME headers for this part.
+ """
+
+ default_content_type = 'application/x-www-form-urlencoded'
+ """This defines a default ``Content-Type`` to use if no Content-Type header
+ is given. The empty string is used for RequestBody, which results in the
+ request body not being read or parsed at all. This is by design; a missing
+ ``Content-Type`` header in the HTTP request entity is an error at best,
+ and a security hole at worst. For multipart parts, however, the MIME spec
+ declares that a part with no Content-Type defaults to "text/plain"
+ (see :class:`Part<cherrypy._cpreqbody.Part>`).
+ """
+
+ filename = None
+ """The ``Content-Disposition.filename`` header, if available."""
+
+ fp = None
+ """The readable socket file object."""
+
+ headers = None
+ """A dict of request/multipart header names and values.
+
+ This is a copy of the ``request.headers`` for the ``request.body``;
+ for multipart parts, it is the set of headers for that part.
+ """
+
+ length = None
+ """The value of the ``Content-Length`` header, if provided."""
+
+ name = None
+ """The "name" parameter of the ``Content-Disposition`` header, if any."""
+
+ params = None
+ """
+ If the request Content-Type is 'application/x-www-form-urlencoded' or
+ multipart, this will be a dict of the params pulled from the entity
+ body; that is, it will be the portion of request.params that come
+ from the message body (sometimes called "POST params", although they
+ can be sent with various HTTP method verbs). This value is set between
+ the 'before_request_body' and 'before_handler' hooks (assuming that
+ process_request_body is True)."""
+
+ processors = {'application/x-www-form-urlencoded': process_urlencoded,
+ 'multipart/form-data': process_multipart_form_data,
+ 'multipart': process_multipart,
+ }
+ """A dict of Content-Type names to processor methods."""
+
+ parts = None
+ """A list of Part instances if ``Content-Type`` is of major type "multipart"."""
+
+ part_class = None
+ """The class used for multipart parts.
+
+ You can replace this with custom subclasses to alter the processing of
+ multipart parts.
+ """
+
+ def __init__(self, fp, headers, params=None, parts=None):
+ # Make an instance-specific copy of the class processors
+ # so Tools, etc. can replace them per-request.
+ self.processors = self.processors.copy()
+
+ self.fp = fp
+ self.headers = headers
+
+ if params is None:
+ params = {}
+ self.params = params
+
+ if parts is None:
+ parts = []
+ self.parts = parts
+
+ # Content-Type
+ self.content_type = headers.elements('Content-Type')
+ if self.content_type:
+ self.content_type = self.content_type[0]
+ else:
+ self.content_type = httputil.HeaderElement.from_str(
+ self.default_content_type)
+
+ # Copy the class 'attempt_charsets', prepending any Content-Type charset
+ dec = self.content_type.params.get("charset", None)
+ if dec:
+ #dec = dec.decode('ISO-8859-1')
+ self.attempt_charsets = [dec] + [c for c in self.attempt_charsets
+ if c != dec]
+ else:
+ self.attempt_charsets = self.attempt_charsets[:]
+
+ # Length
+ self.length = None
+ clen = headers.get('Content-Length', None)
+ # If Transfer-Encoding is 'chunked', ignore any Content-Length.
+ if clen is not None and 'chunked' not in headers.get('Transfer-Encoding', ''):
+ try:
+ self.length = int(clen)
+ except ValueError:
+ pass
+
+ # Content-Disposition
+ self.name = None
+ self.filename = None
+ disp = headers.elements('Content-Disposition')
+ if disp:
+ disp = disp[0]
+ if 'name' in disp.params:
+ self.name = disp.params['name']
+ if self.name.startswith('"') and self.name.endswith('"'):
+ self.name = self.name[1:-1]
+ if 'filename' in disp.params:
+ self.filename = disp.params['filename']
+ if self.filename.startswith('"') and self.filename.endswith('"'):
+ self.filename = self.filename[1:-1]
+
+ # The 'type' attribute is deprecated in 3.2; remove it in 3.3.
+ type = property(lambda self: self.content_type,
+ doc="""A deprecated alias for :attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`.""")
+
+ def read(self, size=None, fp_out=None):
+ return self.fp.read(size, fp_out)
+
+ def readline(self, size=None):
+ return self.fp.readline(size)
+
+ def readlines(self, sizehint=None):
+ return self.fp.readlines(sizehint)
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ line = self.readline()
+ if not line:
+ raise StopIteration
+ return line
+
+ def read_into_file(self, fp_out=None):
+ """Read the request body into fp_out (or make_file() if None). Return fp_out."""
+ if fp_out is None:
+ fp_out = self.make_file()
+ self.read(fp_out=fp_out)
+ return fp_out
+
+ def make_file(self):
+ """Return a file-like object into which the request body will be read.
+
+ By default, this will return a TemporaryFile. Override as needed.
+ See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`."""
+ return tempfile.TemporaryFile()
+
+ def fullvalue(self):
+ """Return this entity as a string, whether stored in a file or not."""
+ if self.file:
+ # It was stored in a tempfile. Read it.
+ self.file.seek(0)
+ value = self.file.read()
+ self.file.seek(0)
+ else:
+ value = self.value
+ return value
+
+ def process(self):
+ """Execute the best-match processor for the given media type."""
+ proc = None
+ ct = self.content_type.value
+ try:
+ proc = self.processors[ct]
+ except KeyError:
+ toptype = ct.split('/', 1)[0]
+ try:
+ proc = self.processors[toptype]
+ except KeyError:
+ pass
+ if proc is None:
+ self.default_proc()
+ else:
+ proc(self)
+
+ def default_proc(self):
+ """Called if a more-specific processor is not found for the ``Content-Type``."""
+ # Leave the fp alone for someone else to read. This works fine
+ # for request.body, but the Part subclasses need to override this
+ # so they can move on to the next part.
+ pass
+
+
+class Part(Entity):
+ """A MIME part entity, part of a multipart entity."""
+
+ # "The default character set, which must be assumed in the absence of a
+ # charset parameter, is US-ASCII."
+ attempt_charsets = ['us-ascii', 'utf-8']
+ """A list of strings, each of which should be a known encoding.
+
+ When the Content-Type of the request body warrants it, each of the given
+ encodings will be tried in order. The first one to successfully decode the
+ entity without raising an error is stored as
+ :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
+ to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
+ `HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
+ but ``['us-ascii', 'utf-8']`` for multipart parts.
+ """
+
+ boundary = None
+ """The MIME multipart boundary."""
+
+ default_content_type = 'text/plain'
+ """This defines a default ``Content-Type`` to use if no Content-Type header
+ is given. The empty string is used for RequestBody, which results in the
+ request body not being read or parsed at all. This is by design; a missing
+ ``Content-Type`` header in the HTTP request entity is an error at best,
+ and a security hole at worst. For multipart parts, however (this class),
+ the MIME spec declares that a part with no Content-Type defaults to
+ "text/plain".
+ """
+
+ # This is the default in stdlib cgi. We may want to increase it.
+ maxrambytes = 1000
+ """The threshold of bytes after which point the ``Part`` will store its data
+ in a file (generated by :func:`make_file<cherrypy._cprequest.Entity.make_file>`)
+ instead of a string. Defaults to 1000, just like the :mod:`cgi` module in
+ Python's standard library.
+ """
+
+ def __init__(self, fp, headers, boundary):
+ Entity.__init__(self, fp, headers)
+ self.boundary = boundary
+ self.file = None
+ self.value = None
+
+ def from_fp(cls, fp, boundary):
+ headers = cls.read_headers(fp)
+ return cls(fp, headers, boundary)
+ from_fp = classmethod(from_fp)
+
+ def read_headers(cls, fp):
+ headers = httputil.HeaderMap()
+ while True:
+ line = fp.readline()
+ if not line:
+ # No more data--illegal end of headers
+ raise EOFError("Illegal end of headers.")
+
+ if line == ntob('\r\n'):
+ # Normal end of headers
+ break
+ if not line.endswith(ntob('\r\n')):
+ raise ValueError("MIME requires CRLF terminators: %r" % line)
+
+ if line[0] in ntob(' \t'):
+ # It's a continuation line.
+ v = line.strip().decode('ISO-8859-1')
+ else:
+ k, v = line.split(ntob(":"), 1)
+ k = k.strip().decode('ISO-8859-1')
+ v = v.strip().decode('ISO-8859-1')
+
+ existing = headers.get(k)
+ if existing:
+ v = ", ".join((existing, v))
+ headers[k] = v
+
+ return headers
+ read_headers = classmethod(read_headers)
+
+ def read_lines_to_boundary(self, fp_out=None):
+ """Read bytes from self.fp and return or write them to a file.
+
+ If the 'fp_out' argument is None (the default), all bytes read are
+ returned in a single byte string.
+
+ If the 'fp_out' argument is not None, it must be a file-like object that
+ supports the 'write' method; all bytes read will be written to the fp,
+ and that fp is returned.
+ """
+ endmarker = self.boundary + ntob("--")
+ delim = ntob("")
+ prev_lf = True
+ lines = []
+ seen = 0
+ while True:
+ line = self.fp.readline(1<<16)
+ if not line:
+ raise EOFError("Illegal end of multipart body.")
+ if line.startswith(ntob("--")) and prev_lf:
+ strippedline = line.strip()
+ if strippedline == self.boundary:
+ break
+ if strippedline == endmarker:
+ self.fp.finish()
+ break
+
+ line = delim + line
+
+ if line.endswith(ntob("\r\n")):
+ delim = ntob("\r\n")
+ line = line[:-2]
+ prev_lf = True
+ elif line.endswith(ntob("\n")):
+ delim = ntob("\n")
+ line = line[:-1]
+ prev_lf = True
+ else:
+ delim = ntob("")
+ prev_lf = False
+
+ if fp_out is None:
+ lines.append(line)
+ seen += len(line)
+ if seen > self.maxrambytes:
+ fp_out = self.make_file()
+ for line in lines:
+ fp_out.write(line)
+ else:
+ fp_out.write(line)
+
+ if fp_out is None:
+ result = ntob('').join(lines)
+ for charset in self.attempt_charsets:
+ try:
+ result = result.decode(charset)
+ except UnicodeDecodeError:
+ pass
+ else:
+ self.charset = charset
+ return result
+ else:
+ raise cherrypy.HTTPError(
+ 400, "The request entity could not be decoded. The following "
+ "charsets were attempted: %s" % repr(self.attempt_charsets))
+ else:
+ fp_out.seek(0)
+ return fp_out
+
+ def default_proc(self):
+ """Called if a more-specific processor is not found for the ``Content-Type``."""
+ if self.filename:
+ # Always read into a file if a .filename was given.
+ self.file = self.read_into_file()
+ else:
+ result = self.read_lines_to_boundary()
+ if isinstance(result, basestring):
+ self.value = result
+ else:
+ self.file = result
+
+ def read_into_file(self, fp_out=None):
+ """Read the request body into fp_out (or make_file() if None). Return fp_out."""
+ if fp_out is None:
+ fp_out = self.make_file()
+ self.read_lines_to_boundary(fp_out=fp_out)
+ return fp_out
+
+Entity.part_class = Part
+
+
+class Infinity(object):
+ def __cmp__(self, other):
+ return 1
+ def __sub__(self, other):
+ return self
+inf = Infinity()
+
+
+comma_separated_headers = ['Accept', 'Accept-Charset', 'Accept-Encoding',
+ 'Accept-Language', 'Accept-Ranges', 'Allow', 'Cache-Control', 'Connection',
+ 'Content-Encoding', 'Content-Language', 'Expect', 'If-Match',
+ 'If-None-Match', 'Pragma', 'Proxy-Authenticate', 'Te', 'Trailer',
+ 'Transfer-Encoding', 'Upgrade', 'Vary', 'Via', 'Warning', 'Www-Authenticate']
+
+
+class SizedReader:
+
+ def __init__(self, fp, length, maxbytes, bufsize=8192, has_trailers=False):
+ # Wrap our fp in a buffer so peek() works
+ self.fp = fp
+ self.length = length
+ self.maxbytes = maxbytes
+ self.buffer = ntob('')
+ self.bufsize = bufsize
+ self.bytes_read = 0
+ self.done = False
+ self.has_trailers = has_trailers
+
+ def read(self, size=None, fp_out=None):
+ """Read bytes from the request body and return or write them to a file.
+
+ A number of bytes less than or equal to the 'size' argument are read
+ off the socket. The actual number of bytes read are tracked in
+ self.bytes_read. The number may be smaller than 'size' when 1) the
+ client sends fewer bytes, 2) the 'Content-Length' request header
+ specifies fewer bytes than requested, or 3) the number of bytes read
+ exceeds self.maxbytes (in which case, 413 is raised).
+
+ If the 'fp_out' argument is None (the default), all bytes read are
+ returned in a single byte string.
+
+ If the 'fp_out' argument is not None, it must be a file-like object that
+ supports the 'write' method; all bytes read will be written to the fp,
+ and None is returned.
+ """
+
+ if self.length is None:
+ if size is None:
+ remaining = inf
+ else:
+ remaining = size
+ else:
+ remaining = self.length - self.bytes_read
+ if size and size < remaining:
+ remaining = size
+ if remaining == 0:
+ self.finish()
+ if fp_out is None:
+ return ntob('')
+ else:
+ return None
+
+ chunks = []
+
+ # Read bytes from the buffer.
+ if self.buffer:
+ if remaining is inf:
+ data = self.buffer
+ self.buffer = ntob('')
+ else:
+ data = self.buffer[:remaining]
+ self.buffer = self.buffer[remaining:]
+ datalen = len(data)
+ remaining -= datalen
+
+ # Check lengths.
+ self.bytes_read += datalen
+ if self.maxbytes and self.bytes_read > self.maxbytes:
+ raise cherrypy.HTTPError(413)
+
+ # Store the data.
+ if fp_out is None:
+ chunks.append(data)
+ else:
+ fp_out.write(data)
+
+ # Read bytes from the socket.
+ while remaining > 0:
+ chunksize = min(remaining, self.bufsize)
+ try:
+ data = self.fp.read(chunksize)
+ except Exception:
+ e = sys.exc_info()[1]
+ if e.__class__.__name__ == 'MaxSizeExceeded':
+ # Post data is too big
+ raise cherrypy.HTTPError(
+ 413, "Maximum request length: %r" % e.args[1])
+ else:
+ raise
+ if not data:
+ self.finish()
+ break
+ datalen = len(data)
+ remaining -= datalen
+
+ # Check lengths.
+ self.bytes_read += datalen
+ if self.maxbytes and self.bytes_read > self.maxbytes:
+ raise cherrypy.HTTPError(413)
+
+ # Store the data.
+ if fp_out is None:
+ chunks.append(data)
+ else:
+ fp_out.write(data)
+
+ if fp_out is None:
+ return ntob('').join(chunks)
+
+ def readline(self, size=None):
+ """Read a line from the request body and return it."""
+ chunks = []
+ while size is None or size > 0:
+ chunksize = self.bufsize
+ if size is not None and size < self.bufsize:
+ chunksize = size
+ data = self.read(chunksize)
+ if not data:
+ break
+ pos = data.find(ntob('\n')) + 1
+ if pos:
+ chunks.append(data[:pos])
+ remainder = data[pos:]
+ self.buffer += remainder
+ self.bytes_read -= len(remainder)
+ break
+ else:
+ chunks.append(data)
+ return ntob('').join(chunks)
+
+ def readlines(self, sizehint=None):
+ """Read lines from the request body and return them."""
+ if self.length is not None:
+ if sizehint is None:
+ sizehint = self.length - self.bytes_read
+ else:
+ sizehint = min(sizehint, self.length - self.bytes_read)
+
+ lines = []
+ seen = 0
+ while True:
+ line = self.readline()
+ if not line:
+ break
+ lines.append(line)
+ seen += len(line)
+ if seen >= sizehint:
+ break
+ return lines
+
+ def finish(self):
+ self.done = True
+ if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'):
+ self.trailers = {}
+
+ try:
+ for line in self.fp.read_trailer_lines():
+ if line[0] in ntob(' \t'):
+ # It's a continuation line.
+ v = line.strip()
+ else:
+ try:
+ k, v = line.split(ntob(":"), 1)
+ except ValueError:
+ raise ValueError("Illegal header line.")
+ k = k.strip().title()
+ v = v.strip()
+
+ if k in comma_separated_headers:
+ existing = self.trailers.get(envname)
+ if existing:
+ v = ntob(", ").join((existing, v))
+ self.trailers[k] = v
+ except Exception:
+ e = sys.exc_info()[1]
+ if e.__class__.__name__ == 'MaxSizeExceeded':
+ # Post data is too big
+ raise cherrypy.HTTPError(
+ 413, "Maximum request length: %r" % e.args[1])
+ else:
+ raise
+
+
+class RequestBody(Entity):
+ """The entity of the HTTP request."""
+
+ bufsize = 8 * 1024
+ """The buffer size used when reading the socket."""
+
+ # Don't parse the request body at all if the client didn't provide
+ # a Content-Type header. See http://www.cherrypy.org/ticket/790
+ default_content_type = ''
+ """This defines a default ``Content-Type`` to use if no Content-Type header
+ is given. The empty string is used for RequestBody, which results in the
+ request body not being read or parsed at all. This is by design; a missing
+ ``Content-Type`` header in the HTTP request entity is an error at best,
+ and a security hole at worst. For multipart parts, however, the MIME spec
+ declares that a part with no Content-Type defaults to "text/plain"
+ (see :class:`Part<cherrypy._cpreqbody.Part>`).
+ """
+
+ maxbytes = None
+ """Raise ``MaxSizeExceeded`` if more bytes than this are read from the socket."""
+
+ def __init__(self, fp, headers, params=None, request_params=None):
+ Entity.__init__(self, fp, headers, params)
+
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
+ # When no explicit charset parameter is provided by the
+ # sender, media subtypes of the "text" type are defined
+ # to have a default charset value of "ISO-8859-1" when
+ # received via HTTP.
+ if self.content_type.value.startswith('text/'):
+ for c in ('ISO-8859-1', 'iso-8859-1', 'Latin-1', 'latin-1'):
+ if c in self.attempt_charsets:
+ break
+ else:
+ self.attempt_charsets.append('ISO-8859-1')
+
+ # Temporary fix while deprecating passing .parts as .params.
+ self.processors['multipart'] = _old_process_multipart
+
+ if request_params is None:
+ request_params = {}
+ self.request_params = request_params
+
+ def process(self):
+ """Process the request entity based on its Content-Type."""
+ # "The presence of a message-body in a request is signaled by the
+ # inclusion of a Content-Length or Transfer-Encoding header field in
+ # the request's message-headers."
+ # It is possible to send a POST request with no body, for example;
+ # however, app developers are responsible in that case to set
+ # cherrypy.request.process_body to False so this method isn't called.
+ h = cherrypy.serving.request.headers
+ if 'Content-Length' not in h and 'Transfer-Encoding' not in h:
+ raise cherrypy.HTTPError(411)
+
+ self.fp = SizedReader(self.fp, self.length,
+ self.maxbytes, bufsize=self.bufsize,
+ has_trailers='Trailer' in h)
+ super(RequestBody, self).process()
+
+ # Body params should also be a part of the request_params
+ # add them in here.
+ request_params = self.request_params
+ for key, value in self.params.items():
+ # Python 2 only: keyword arguments must be byte strings (type 'str').
+ if isinstance(key, unicode):
+ key = key.encode('ISO-8859-1')
+
+ if key in request_params:
+ if not isinstance(request_params[key], list):
+ request_params[key] = [request_params[key]]
+ request_params[key].append(value)
+ else:
+ request_params[key] = value
diff --git a/cherrypy/_cprequest.py b/cherrypy/_cprequest.py
new file mode 100755
index 0000000..ae5e897
--- /dev/null
+++ b/cherrypy/_cprequest.py
@@ -0,0 +1,926 @@
+
+import os
+import sys
+import time
+import warnings
+
+import cherrypy
+from cherrypy._cpcompat import basestring, copykeys, ntob, unicodestr
+from cherrypy._cpcompat import SimpleCookie, CookieError
+from cherrypy import _cpreqbody, _cpconfig
+from cherrypy._cperror import format_exc, bare_error
+from cherrypy.lib import httputil, file_generator
+
+
+class Hook(object):
+ """A callback and its metadata: failsafe, priority, and kwargs."""
+
+ callback = None
+ """
+ The bare callable that this Hook object is wrapping, which will
+ be called when the Hook is called."""
+
+ failsafe = False
+ """
+ If True, the callback is guaranteed to run even if other callbacks
+ from the same call point raise exceptions."""
+
+ priority = 50
+ """
+ Defines the order of execution for a list of Hooks. Priority numbers
+ should be limited to the closed interval [0, 100], but values outside
+ this range are acceptable, as are fractional values."""
+
+ kwargs = {}
+ """
+ A set of keyword arguments that will be passed to the
+ callable on each call."""
+
+ def __init__(self, callback, failsafe=None, priority=None, **kwargs):
+ self.callback = callback
+
+ if failsafe is None:
+ failsafe = getattr(callback, "failsafe", False)
+ self.failsafe = failsafe
+
+ if priority is None:
+ priority = getattr(callback, "priority", 50)
+ self.priority = priority
+
+ self.kwargs = kwargs
+
+ def __cmp__(self, other):
+ return cmp(self.priority, other.priority)
+
+ def __call__(self):
+ """Run self.callback(**self.kwargs)."""
+ return self.callback(**self.kwargs)
+
+ def __repr__(self):
+ cls = self.__class__
+ return ("%s.%s(callback=%r, failsafe=%r, priority=%r, %s)"
+ % (cls.__module__, cls.__name__, self.callback,
+ self.failsafe, self.priority,
+ ", ".join(['%s=%r' % (k, v)
+ for k, v in self.kwargs.items()])))
+
+
+class HookMap(dict):
+ """A map of call points to lists of callbacks (Hook objects)."""
+
+ def __new__(cls, points=None):
+ d = dict.__new__(cls)
+ for p in points or []:
+ d[p] = []
+ return d
+
+ def __init__(self, *a, **kw):
+ pass
+
+ def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
+ """Append a new Hook made from the supplied arguments."""
+ self[point].append(Hook(callback, failsafe, priority, **kwargs))
+
+ def run(self, point):
+ """Execute all registered Hooks (callbacks) for the given point."""
+ exc = None
+ hooks = self[point]
+ hooks.sort()
+ for hook in hooks:
+ # Some hooks are guaranteed to run even if others at
+ # the same hookpoint fail. We will still log the failure,
+ # but proceed on to the next hook. The only way
+ # to stop all processing from one of these hooks is
+ # to raise SystemExit and stop the whole server.
+ if exc is None or hook.failsafe:
+ try:
+ hook()
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except (cherrypy.HTTPError, cherrypy.HTTPRedirect,
+ cherrypy.InternalRedirect):
+ exc = sys.exc_info()[1]
+ except:
+ exc = sys.exc_info()[1]
+ cherrypy.log(traceback=True, severity=40)
+ if exc:
+ raise
+
+ def __copy__(self):
+ newmap = self.__class__()
+ # We can't just use 'update' because we want copies of the
+ # mutable values (each is a list) as well.
+ for k, v in self.items():
+ newmap[k] = v[:]
+ return newmap
+ copy = __copy__
+
+ def __repr__(self):
+ cls = self.__class__
+ return "%s.%s(points=%r)" % (cls.__module__, cls.__name__, copykeys(self))
+
+
+# Config namespace handlers
+
+def hooks_namespace(k, v):
+ """Attach bare hooks declared in config."""
+ # Use split again to allow multiple hooks for a single
+ # hookpoint per path (e.g. "hooks.before_handler.1").
+ # Little-known fact you only get from reading source ;)
+ hookpoint = k.split(".", 1)[0]
+ if isinstance(v, basestring):
+ v = cherrypy.lib.attributes(v)
+ if not isinstance(v, Hook):
+ v = Hook(v)
+ cherrypy.serving.request.hooks[hookpoint].append(v)
+
+def request_namespace(k, v):
+ """Attach request attributes declared in config."""
+ # Provides config entries to set request.body attrs (like attempt_charsets).
+ if k[:5] == 'body.':
+ setattr(cherrypy.serving.request.body, k[5:], v)
+ else:
+ setattr(cherrypy.serving.request, k, v)
+
+def response_namespace(k, v):
+ """Attach response attributes declared in config."""
+ # Provides config entries to set default response headers
+ # http://cherrypy.org/ticket/889
+ if k[:8] == 'headers.':
+ cherrypy.serving.response.headers[k.split('.', 1)[1]] = v
+ else:
+ setattr(cherrypy.serving.response, k, v)
+
+def error_page_namespace(k, v):
+ """Attach error pages declared in config."""
+ if k != 'default':
+ k = int(k)
+ cherrypy.serving.request.error_page[k] = v
+
+
+hookpoints = ['on_start_resource', 'before_request_body',
+ 'before_handler', 'before_finalize',
+ 'on_end_resource', 'on_end_request',
+ 'before_error_response', 'after_error_response']
+
+
+class Request(object):
+ """An HTTP request.
+
+ This object represents the metadata of an HTTP request message;
+ that is, it contains attributes which describe the environment
+ in which the request URL, headers, and body were sent (if you
+ want tools to interpret the headers and body, those are elsewhere,
+ mostly in Tools). This 'metadata' consists of socket data,
+ transport characteristics, and the Request-Line. This object
+ also contains data regarding the configuration in effect for
+ the given URL, and the execution plan for generating a response.
+ """
+
+ prev = None
+ """
+ The previous Request object (if any). This should be None
+ unless we are processing an InternalRedirect."""
+
+ # Conversation/connection attributes
+ local = httputil.Host("127.0.0.1", 80)
+ "An httputil.Host(ip, port, hostname) object for the server socket."
+
+ remote = httputil.Host("127.0.0.1", 1111)
+ "An httputil.Host(ip, port, hostname) object for the client socket."
+
+ scheme = "http"
+ """
+ The protocol used between client and server. In most cases,
+ this will be either 'http' or 'https'."""
+
+ server_protocol = "HTTP/1.1"
+ """
+ The HTTP version for which the HTTP server is at least
+ conditionally compliant."""
+
+ base = ""
+ """The (scheme://host) portion of the requested URL.
+ In some cases (e.g. when proxying via mod_rewrite), this may contain
+ path segments which cherrypy.url uses when constructing url's, but
+ which otherwise are ignored by CherryPy. Regardless, this value
+ MUST NOT end in a slash."""
+
+ # Request-Line attributes
+ request_line = ""
+ """
+ The complete Request-Line received from the client. This is a
+ single string consisting of the request method, URI, and protocol
+ version (joined by spaces). Any final CRLF is removed."""
+
+ method = "GET"
+ """
+ Indicates the HTTP method to be performed on the resource identified
+ by the Request-URI. Common methods include GET, HEAD, POST, PUT, and
+ DELETE. CherryPy allows any extension method; however, various HTTP
+ servers and gateways may restrict the set of allowable methods.
+ CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
+
+ query_string = ""
+ """
+ The query component of the Request-URI, a string of information to be
+ interpreted by the resource. The query portion of a URI follows the
+ path component, and is separated by a '?'. For example, the URI
+ 'http://www.cherrypy.org/wiki?a=3&b=4' has the query component,
+ 'a=3&b=4'."""
+
+ query_string_encoding = 'utf8'
+ """
+ The encoding expected for query string arguments after % HEX HEX decoding).
+ If a query string is provided that cannot be decoded with this encoding,
+ 404 is raised (since technically it's a different URI). If you want
+ arbitrary encodings to not error, set this to 'Latin-1'; you can then
+ encode back to bytes and re-decode to whatever encoding you like later.
+ """
+
+ protocol = (1, 1)
+ """The HTTP protocol version corresponding to the set
+ of features which should be allowed in the response. If BOTH
+ the client's request message AND the server's level of HTTP
+ compliance is HTTP/1.1, this attribute will be the tuple (1, 1).
+ If either is 1.0, this attribute will be the tuple (1, 0).
+ Lower HTTP protocol versions are not explicitly supported."""
+
+ params = {}
+ """
+ A dict which combines query string (GET) and request entity (POST)
+ variables. This is populated in two stages: GET params are added
+ before the 'on_start_resource' hook, and POST params are added
+ between the 'before_request_body' and 'before_handler' hooks."""
+
+ # Message attributes
+ header_list = []
+ """
+ A list of the HTTP request headers as (name, value) tuples.
+ In general, you should use request.headers (a dict) instead."""
+
+ headers = httputil.HeaderMap()
+ """
+ A dict-like object containing the request headers. Keys are header
+ names (in Title-Case format); however, you may get and set them in
+ a case-insensitive manner. That is, headers['Content-Type'] and
+ headers['content-type'] refer to the same value. Values are header
+ values (decoded according to :rfc:`2047` if necessary). See also:
+ httputil.HeaderMap, httputil.HeaderElement."""
+
+ cookie = SimpleCookie()
+ """See help(Cookie)."""
+
+ rfile = None
+ """
+ If the request included an entity (body), it will be available
+ as a stream in this attribute. However, the rfile will normally
+ be read for you between the 'before_request_body' hook and the
+ 'before_handler' hook, and the resulting string is placed into
+ either request.params or the request.body attribute.
+
+ You may disable the automatic consumption of the rfile by setting
+ request.process_request_body to False, either in config for the desired
+ path, or in an 'on_start_resource' or 'before_request_body' hook.
+
+ WARNING: In almost every case, you should not attempt to read from the
+ rfile stream after CherryPy's automatic mechanism has read it. If you
+ turn off the automatic parsing of rfile, you should read exactly the
+ number of bytes specified in request.headers['Content-Length'].
+ Ignoring either of these warnings may result in a hung request thread
+ or in corruption of the next (pipelined) request.
+ """
+
+ process_request_body = True
+ """
+ If True, the rfile (if any) is automatically read and parsed,
+ and the result placed into request.params or request.body."""
+
+ methods_with_bodies = ("POST", "PUT")
+ """
+ A sequence of HTTP methods for which CherryPy will automatically
+ attempt to read a body from the rfile."""
+
+ body = None
+ """
+ If the request Content-Type is 'application/x-www-form-urlencoded'
+ or multipart, this will be None. Otherwise, this will be an instance
+ of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you
+ can .read()); this value is set between the 'before_request_body' and
+ 'before_handler' hooks (assuming that process_request_body is True)."""
+
+ # Dispatch attributes
+ dispatch = cherrypy.dispatch.Dispatcher()
+ """
+ The object which looks up the 'page handler' callable and collects
+ config for the current request based on the path_info, other
+ request attributes, and the application architecture. The core
+ calls the dispatcher as early as possible, passing it a 'path_info'
+ argument.
+
+ The default dispatcher discovers the page handler by matching path_info
+ to a hierarchical arrangement of objects, starting at request.app.root.
+ See help(cherrypy.dispatch) for more information."""
+
+ script_name = ""
+ """
+ The 'mount point' of the application which is handling this request.
+
+ This attribute MUST NOT end in a slash. If the script_name refers to
+ the root of the URI, it MUST be an empty string (not "/").
+ """
+
+ path_info = "/"
+ """
+ The 'relative path' portion of the Request-URI. This is relative
+ to the script_name ('mount point') of the application which is
+ handling this request."""
+
+ login = None
+ """
+ When authentication is used during the request processing this is
+ set to 'False' if it failed and to the 'username' value if it succeeded.
+ The default 'None' implies that no authentication happened."""
+
+ # Note that cherrypy.url uses "if request.app:" to determine whether
+ # the call is during a real HTTP request or not. So leave this None.
+ app = None
+ """The cherrypy.Application object which is handling this request."""
+
+ handler = None
+ """
+ The function, method, or other callable which CherryPy will call to
+ produce the response. The discovery of the handler and the arguments
+ it will receive are determined by the request.dispatch object.
+ By default, the handler is discovered by walking a tree of objects
+ starting at request.app.root, and is then passed all HTTP params
+ (from the query string and POST body) as keyword arguments."""
+
+ toolmaps = {}
+ """
+ A nested dict of all Toolboxes and Tools in effect for this request,
+ of the form: {Toolbox.namespace: {Tool.name: config dict}}."""
+
+ config = None
+ """
+ A flat dict of all configuration entries which apply to the
+ current request. These entries are collected from global config,
+ application config (based on request.path_info), and from handler
+ config (exactly how is governed by the request.dispatch object in
+ effect for this request; by default, handler config can be attached
+ anywhere in the tree between request.app.root and the final handler,
+ and inherits downward)."""
+
+ is_index = None
+ """
+ This will be True if the current request is mapped to an 'index'
+ resource handler (also, a 'default' handler if path_info ends with
+ a slash). The value may be used to automatically redirect the
+ user-agent to a 'more canonical' URL which either adds or removes
+ the trailing slash. See cherrypy.tools.trailing_slash."""
+
+ hooks = HookMap(hookpoints)
+ """
+ A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}.
+ Each key is a str naming the hook point, and each value is a list
+ of hooks which will be called at that hook point during this request.
+ The list of hooks is generally populated as early as possible (mostly
+ from Tools specified in config), but may be extended at any time.
+ See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools."""
+
+ error_response = cherrypy.HTTPError(500).set_response
+ """
+ The no-arg callable which will handle unexpected, untrapped errors
+ during request processing. This is not used for expected exceptions
+ (like NotFound, HTTPError, or HTTPRedirect) which are raised in
+ response to expected conditions (those should be customized either
+ via request.error_page or by overriding HTTPError.set_response).
+ By default, error_response uses HTTPError(500) to return a generic
+ error response to the user-agent."""
+
+ error_page = {}
+ """
+ A dict of {error code: response filename or callable} pairs.
+
+ The error code must be an int representing a given HTTP error code,
+ or the string 'default', which will be used if no matching entry
+ is found for a given numeric code.
+
+ If a filename is provided, the file should contain a Python string-
+ formatting template, and can expect by default to receive format
+ values with the mapping keys %(status)s, %(message)s, %(traceback)s,
+ and %(version)s. The set of format mappings can be extended by
+ overriding HTTPError.set_response.
+
+ If a callable is provided, it will be called by default with keyword
+ arguments 'status', 'message', 'traceback', and 'version', as for a
+ string-formatting template. The callable must return a string or iterable of
+ strings which will be set to response.body. It may also override headers or
+ perform any other processing.
+
+ If no entry is given for an error code, and no 'default' entry exists,
+ a default template will be used.
+ """
+
+ show_tracebacks = True
+ """
+ If True, unexpected errors encountered during request processing will
+ include a traceback in the response body."""
+
+ show_mismatched_params = True
+ """
+ If True, mismatched parameters encountered during PageHandler invocation
+ processing will be included in the response body."""
+
+ throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
+ """The sequence of exceptions which Request.run does not trap."""
+
+ throw_errors = False
+ """
+ If True, Request.run will not trap any errors (except HTTPRedirect and
+ HTTPError, which are more properly called 'exceptions', not errors)."""
+
+ closed = False
+ """True once the close method has been called, False otherwise."""
+
+ stage = None
+ """
+ A string containing the stage reached in the request-handling process.
+ This is useful when debugging a live server with hung requests."""
+
+ namespaces = _cpconfig.NamespaceSet(
+ **{"hooks": hooks_namespace,
+ "request": request_namespace,
+ "response": response_namespace,
+ "error_page": error_page_namespace,
+ "tools": cherrypy.tools,
+ })
+
+ def __init__(self, local_host, remote_host, scheme="http",
+ server_protocol="HTTP/1.1"):
+ """Populate a new Request object.
+
+ local_host should be an httputil.Host object with the server info.
+ remote_host should be an httputil.Host object with the client info.
+ scheme should be a string, either "http" or "https".
+ """
+ self.local = local_host
+ self.remote = remote_host
+ self.scheme = scheme
+ self.server_protocol = server_protocol
+
+ self.closed = False
+
+ # Put a *copy* of the class error_page into self.
+ self.error_page = self.error_page.copy()
+
+ # Put a *copy* of the class namespaces into self.
+ self.namespaces = self.namespaces.copy()
+
+ self.stage = None
+
+ def close(self):
+ """Run cleanup code. (Core)"""
+ if not self.closed:
+ self.closed = True
+ self.stage = 'on_end_request'
+ self.hooks.run('on_end_request')
+ self.stage = 'close'
+
+ def run(self, method, path, query_string, req_protocol, headers, rfile):
+ """Process the Request. (Core)
+
+ method, path, query_string, and req_protocol should be pulled directly
+ from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
+
+ path
+ This should be %XX-unquoted, but query_string should not be.
+ They both MUST be byte strings, not unicode strings.
+
+ headers
+ A list of (name, value) tuples.
+
+ rfile
+ A file-like object containing the HTTP request entity.
+
+ When run() is done, the returned object should have 3 attributes:
+
+ * status, e.g. "200 OK"
+ * header_list, a list of (name, value) tuples
+ * body, an iterable yielding strings
+
+ Consumer code (HTTP servers) should then access these response
+ attributes to build the outbound stream.
+
+ """
+ response = cherrypy.serving.response
+ self.stage = 'run'
+ try:
+ self.error_response = cherrypy.HTTPError(500).set_response
+
+ self.method = method
+ path = path or "/"
+ self.query_string = query_string or ''
+ self.params = {}
+
+ # Compare request and server HTTP protocol versions, in case our
+ # server does not support the requested protocol. Limit our output
+ # to min(req, server). We want the following output:
+ # request server actual written supported response
+ # protocol protocol response protocol feature set
+ # a 1.0 1.0 1.0 1.0
+ # b 1.0 1.1 1.1 1.0
+ # c 1.1 1.0 1.0 1.0
+ # d 1.1 1.1 1.1 1.1
+ # Notice that, in (b), the response will be "HTTP/1.1" even though
+ # the client only understands 1.0. RFC 2616 10.5.6 says we should
+ # only return 505 if the _major_ version is different.
+ rp = int(req_protocol[5]), int(req_protocol[7])
+ sp = int(self.server_protocol[5]), int(self.server_protocol[7])
+ self.protocol = min(rp, sp)
+ response.headers.protocol = self.protocol
+
+ # Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
+ url = path
+ if query_string:
+ url += '?' + query_string
+ self.request_line = '%s %s %s' % (method, url, req_protocol)
+
+ self.header_list = list(headers)
+ self.headers = httputil.HeaderMap()
+
+ self.rfile = rfile
+ self.body = None
+
+ self.cookie = SimpleCookie()
+ self.handler = None
+
+ # path_info should be the path from the
+ # app root (script_name) to the handler.
+ self.script_name = self.app.script_name
+ self.path_info = pi = path[len(self.script_name):]
+
+ self.stage = 'respond'
+ self.respond(pi)
+
+ except self.throws:
+ raise
+ except:
+ if self.throw_errors:
+ raise
+ else:
+ # Failure in setup, error handler or finalize. Bypass them.
+ # Can't use handle_error because we may not have hooks yet.
+ cherrypy.log(traceback=True, severity=40)
+ if self.show_tracebacks:
+ body = format_exc()
+ else:
+ body = ""
+ r = bare_error(body)
+ response.output_status, response.header_list, response.body = r
+
+ if self.method == "HEAD":
+ # HEAD requests MUST NOT return a message-body in the response.
+ response.body = []
+
+ try:
+ cherrypy.log.access()
+ except:
+ cherrypy.log.error(traceback=True)
+
+ if response.timed_out:
+ raise cherrypy.TimeoutError()
+
+ return response
+
+ # Uncomment for stage debugging
+ # stage = property(lambda self: self._stage, lambda self, v: print(v))
+
+ def respond(self, path_info):
+ """Generate a response for the resource at self.path_info. (Core)"""
+ response = cherrypy.serving.response
+ try:
+ try:
+ try:
+ if self.app is None:
+ raise cherrypy.NotFound()
+
+ # Get the 'Host' header, so we can HTTPRedirect properly.
+ self.stage = 'process_headers'
+ self.process_headers()
+
+ # Make a copy of the class hooks
+ self.hooks = self.__class__.hooks.copy()
+ self.toolmaps = {}
+
+ self.stage = 'get_resource'
+ self.get_resource(path_info)
+
+ self.body = _cpreqbody.RequestBody(
+ self.rfile, self.headers, request_params=self.params)
+
+ self.namespaces(self.config)
+
+ self.stage = 'on_start_resource'
+ self.hooks.run('on_start_resource')
+
+ # Parse the querystring
+ self.stage = 'process_query_string'
+ self.process_query_string()
+
+ # Process the body
+ if self.process_request_body:
+ if self.method not in self.methods_with_bodies:
+ self.process_request_body = False
+ self.stage = 'before_request_body'
+ self.hooks.run('before_request_body')
+ if self.process_request_body:
+ self.body.process()
+
+ # Run the handler
+ self.stage = 'before_handler'
+ self.hooks.run('before_handler')
+ if self.handler:
+ self.stage = 'handler'
+ response.body = self.handler()
+
+ # Finalize
+ self.stage = 'before_finalize'
+ self.hooks.run('before_finalize')
+ response.finalize()
+ except (cherrypy.HTTPRedirect, cherrypy.HTTPError):
+ inst = sys.exc_info()[1]
+ inst.set_response()
+ self.stage = 'before_finalize (HTTPError)'
+ self.hooks.run('before_finalize')
+ response.finalize()
+ finally:
+ self.stage = 'on_end_resource'
+ self.hooks.run('on_end_resource')
+ except self.throws:
+ raise
+ except:
+ if self.throw_errors:
+ raise
+ self.handle_error()
+
+ def process_query_string(self):
+ """Parse the query string into Python structures. (Core)"""
+ try:
+ p = httputil.parse_query_string(
+ self.query_string, encoding=self.query_string_encoding)
+ except UnicodeDecodeError:
+ raise cherrypy.HTTPError(
+ 404, "The given query string could not be processed. Query "
+ "strings for this resource must be encoded with %r." %
+ self.query_string_encoding)
+
+ # Python 2 only: keyword arguments must be byte strings (type 'str').
+ for key, value in p.items():
+ if isinstance(key, unicode):
+ del p[key]
+ p[key.encode(self.query_string_encoding)] = value
+ self.params.update(p)
+
+ def process_headers(self):
+ """Parse HTTP header data into Python structures. (Core)"""
+ # Process the headers into self.headers
+ headers = self.headers
+ for name, value in self.header_list:
+ # Call title() now (and use dict.__method__(headers))
+ # so title doesn't have to be called twice.
+ name = name.title()
+ value = value.strip()
+
+ # Warning: if there is more than one header entry for cookies (AFAIK,
+ # only Konqueror does that), only the last one will remain in headers
+ # (but they will be correctly stored in request.cookie).
+ if "=?" in value:
+ dict.__setitem__(headers, name, httputil.decode_TEXT(value))
+ else:
+ dict.__setitem__(headers, name, value)
+
+ # Handle cookies differently because on Konqueror, multiple
+ # cookies come on different lines with the same key
+ if name == 'Cookie':
+ try:
+ self.cookie.load(value)
+ except CookieError:
+ msg = "Illegal cookie name %s" % value.split('=')[0]
+ raise cherrypy.HTTPError(400, msg)
+
+ if not dict.__contains__(headers, 'Host'):
+ # All Internet-based HTTP/1.1 servers MUST respond with a 400
+ # (Bad Request) status code to any HTTP/1.1 request message
+ # which lacks a Host header field.
+ if self.protocol >= (1, 1):
+ msg = "HTTP/1.1 requires a 'Host' request header."
+ raise cherrypy.HTTPError(400, msg)
+ host = dict.get(headers, 'Host')
+ if not host:
+ host = self.local.name or self.local.ip
+ self.base = "%s://%s" % (self.scheme, host)
+
+ def get_resource(self, path):
+ """Call a dispatcher (which sets self.handler and .config). (Core)"""
+ # First, see if there is a custom dispatch at this URI. Custom
+ # dispatchers can only be specified in app.config, not in _cp_config
+ # (since custom dispatchers may not even have an app.root).
+ dispatch = self.app.find_config(path, "request.dispatch", self.dispatch)
+
+ # dispatch() should set self.handler and self.config
+ dispatch(path)
+
+ def handle_error(self):
+ """Handle the last unanticipated exception. (Core)"""
+ try:
+ self.hooks.run("before_error_response")
+ if self.error_response:
+ self.error_response()
+ self.hooks.run("after_error_response")
+ cherrypy.serving.response.finalize()
+ except cherrypy.HTTPRedirect:
+ inst = sys.exc_info()[1]
+ inst.set_response()
+ cherrypy.serving.response.finalize()
+
+ # ------------------------- Properties ------------------------- #
+
+ def _get_body_params(self):
+ warnings.warn(
+ "body_params is deprecated in CherryPy 3.2, will be removed in "
+ "CherryPy 3.3.",
+ DeprecationWarning
+ )
+ return self.body.params
+ body_params = property(_get_body_params,
+ doc= """
+ If the request Content-Type is 'application/x-www-form-urlencoded' or
+ multipart, this will be a dict of the params pulled from the entity
+ body; that is, it will be the portion of request.params that come
+ from the message body (sometimes called "POST params", although they
+ can be sent with various HTTP method verbs). This value is set between
+ the 'before_request_body' and 'before_handler' hooks (assuming that
+ process_request_body is True).
+
+ Deprecated in 3.2, will be removed for 3.3 in favor of
+ :attr:`request.body.params<cherrypy._cprequest.RequestBody.params>`.""")
+
+
+class ResponseBody(object):
+ """The body of the HTTP response (the response entity)."""
+
+ def __get__(self, obj, objclass=None):
+ if obj is None:
+ # When calling on the class instead of an instance...
+ return self
+ else:
+ return obj._body
+
+ def __set__(self, obj, value):
+ # Convert the given value to an iterable object.
+ if isinstance(value, basestring):
+ # strings get wrapped in a list because iterating over a single
+ # item list is much faster than iterating over every character
+ # in a long string.
+ if value:
+ value = [value]
+ else:
+ # [''] doesn't evaluate to False, so replace it with [].
+ value = []
+ # Don't use isinstance here; io.IOBase which has an ABC takes
+ # 1000 times as long as, say, isinstance(value, str)
+ elif hasattr(value, 'read'):
+ value = file_generator(value)
+ elif value is None:
+ value = []
+ obj._body = value
+
+
+class Response(object):
+ """An HTTP Response, including status, headers, and body."""
+
+ status = ""
+ """The HTTP Status-Code and Reason-Phrase."""
+
+ header_list = []
+ """
+ A list of the HTTP response headers as (name, value) tuples.
+ In general, you should use response.headers (a dict) instead. This
+ attribute is generated from response.headers and is not valid until
+ after the finalize phase."""
+
+ headers = httputil.HeaderMap()
+ """
+ A dict-like object containing the response headers. Keys are header
+ names (in Title-Case format); however, you may get and set them in
+ a case-insensitive manner. That is, headers['Content-Type'] and
+ headers['content-type'] refer to the same value. Values are header
+ values (decoded according to :rfc:`2047` if necessary).
+
+ .. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement`
+ """
+
+ cookie = SimpleCookie()
+ """See help(Cookie)."""
+
+ body = ResponseBody()
+ """The body (entity) of the HTTP response."""
+
+ time = None
+ """The value of time.time() when created. Use in HTTP dates."""
+
+ timeout = 300
+ """Seconds after which the response will be aborted."""
+
+ timed_out = False
+ """
+ Flag to indicate the response should be aborted, because it has
+ exceeded its timeout."""
+
+ stream = False
+ """If False, buffer the response body."""
+
+ def __init__(self):
+ self.status = None
+ self.header_list = None
+ self._body = []
+ self.time = time.time()
+
+ self.headers = httputil.HeaderMap()
+ # Since we know all our keys are titled strings, we can
+ # bypass HeaderMap.update and get a big speed boost.
+ dict.update(self.headers, {
+ "Content-Type": 'text/html',
+ "Server": "CherryPy/" + cherrypy.__version__,
+ "Date": httputil.HTTPDate(self.time),
+ })
+ self.cookie = SimpleCookie()
+
+ def collapse_body(self):
+ """Collapse self.body to a single string; replace it and return it."""
+ if isinstance(self.body, basestring):
+ return self.body
+
+ newbody = ''.join([chunk for chunk in self.body])
+
+ self.body = newbody
+ return newbody
+
+ def finalize(self):
+ """Transform headers (and cookies) into self.header_list. (Core)"""
+ try:
+ code, reason, _ = httputil.valid_status(self.status)
+ except ValueError:
+ raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0])
+
+ headers = self.headers
+
+ self.output_status = ntob(str(code), 'ascii') + ntob(" ") + headers.encode(reason)
+
+ if self.stream:
+ # The upshot: wsgiserver will chunk the response if
+ # you pop Content-Length (or set it explicitly to None).
+ # Note that lib.static sets C-L to the file's st_size.
+ if dict.get(headers, 'Content-Length') is None:
+ dict.pop(headers, 'Content-Length', None)
+ elif code < 200 or code in (204, 205, 304):
+ # "All 1xx (informational), 204 (no content),
+ # and 304 (not modified) responses MUST NOT
+ # include a message-body."
+ dict.pop(headers, 'Content-Length', None)
+ self.body = ntob("")
+ else:
+ # Responses which are not streamed should have a Content-Length,
+ # but allow user code to set Content-Length if desired.
+ if dict.get(headers, 'Content-Length') is None:
+ content = self.collapse_body()
+ dict.__setitem__(headers, 'Content-Length', len(content))
+
+ # Transform our header dict into a list of tuples.
+ self.header_list = h = headers.output()
+
+ cookie = self.cookie.output()
+ if cookie:
+ for line in cookie.split("\n"):
+ if line.endswith("\r"):
+ # Python 2.4 emits cookies joined by LF but 2.5+ by CRLF.
+ line = line[:-1]
+ name, value = line.split(": ", 1)
+ if isinstance(name, unicodestr):
+ name = name.encode("ISO-8859-1")
+ if isinstance(value, unicodestr):
+ value = headers.encode(value)
+ h.append((name, value))
+
+ def check_timeout(self):
+ """If now > self.time + self.timeout, set self.timed_out.
+
+ This purposefully sets a flag, rather than raising an error,
+ so that a monitor thread can interrupt the Response thread.
+ """
+ if time.time() > self.time + self.timeout:
+ self.timed_out = True
+
+
+
diff --git a/cherrypy/_cpserver.py b/cherrypy/_cpserver.py
new file mode 100755
index 0000000..c1695a6
--- /dev/null
+++ b/cherrypy/_cpserver.py
@@ -0,0 +1,195 @@
+"""Manage HTTP servers with CherryPy."""
+
+import warnings
+
+import cherrypy
+from cherrypy.lib import attributes
+from cherrypy._cpcompat import basestring
+
+# We import * because we want to export check_port
+# et al as attributes of this module.
+from cherrypy.process.servers import *
+
+
+class Server(ServerAdapter):
+ """An adapter for an HTTP server.
+
+ You can set attributes (like socket_host and socket_port)
+ on *this* object (which is probably cherrypy.server), and call
+ quickstart. For example::
+
+ cherrypy.server.socket_port = 80
+ cherrypy.quickstart()
+ """
+
+ socket_port = 8080
+ """The TCP port on which to listen for connections."""
+
+ _socket_host = '127.0.0.1'
+ def _get_socket_host(self):
+ return self._socket_host
+ def _set_socket_host(self, value):
+ if value == '':
+ raise ValueError("The empty string ('') is not an allowed value. "
+ "Use '0.0.0.0' instead to listen on all active "
+ "interfaces (INADDR_ANY).")
+ self._socket_host = value
+ socket_host = property(_get_socket_host, _set_socket_host,
+ doc="""The hostname or IP address on which to listen for connections.
+
+ Host values may be any IPv4 or IPv6 address, or any valid hostname.
+ The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if
+ your hosts file prefers IPv6). The string '0.0.0.0' is a special
+ IPv4 entry meaning "any active interface" (INADDR_ANY), and '::'
+ is the similar IN6ADDR_ANY for IPv6. The empty string or None are
+ not allowed.""")
+
+ socket_file = None
+ """If given, the name of the UNIX socket to use instead of TCP/IP.
+
+ When this option is not None, the `socket_host` and `socket_port` options
+ are ignored."""
+
+ socket_queue_size = 5
+ """The 'backlog' argument to socket.listen(); specifies the maximum number
+ of queued connections (default 5)."""
+
+ socket_timeout = 10
+ """The timeout in seconds for accepted connections (default 10)."""
+
+ shutdown_timeout = 5
+ """The time to wait for HTTP worker threads to clean up."""
+
+ protocol_version = 'HTTP/1.1'
+ """The version string to write in the Status-Line of all HTTP responses,
+ for example, "HTTP/1.1" (the default). Depending on the HTTP server used,
+ this should also limit the supported features used in the response."""
+
+ thread_pool = 10
+ """The number of worker threads to start up in the pool."""
+
+ thread_pool_max = -1
+ """The maximum size of the worker-thread pool. Use -1 to indicate no limit."""
+
+ max_request_header_size = 500 * 1024
+ """The maximum number of bytes allowable in the request headers. If exceeded,
+ the HTTP server should return "413 Request Entity Too Large"."""
+
+ max_request_body_size = 100 * 1024 * 1024
+ """The maximum number of bytes allowable in the request body. If exceeded,
+ the HTTP server should return "413 Request Entity Too Large"."""
+
+ instance = None
+ """If not None, this should be an HTTP server instance (such as
+ CPWSGIServer) which cherrypy.server will control. Use this when you need
+ more control over object instantiation than is available in the various
+ configuration options."""
+
+ ssl_context = None
+ """When using PyOpenSSL, an instance of SSL.Context."""
+
+ ssl_certificate = None
+ """The filename of the SSL certificate to use."""
+
+ ssl_certificate_chain = None
+ """When using PyOpenSSL, the certificate chain to pass to
+ Context.load_verify_locations."""
+
+ ssl_private_key = None
+ """The filename of the private key to use with SSL."""
+
+ ssl_module = 'pyopenssl'
+ """The name of a registered SSL adaptation module to use with the builtin
+ WSGI server. Builtin options are 'builtin' (to use the SSL library built
+ into recent versions of Python) and 'pyopenssl' (to use the PyOpenSSL
+ project, which you must install separately). You may also register your
+ own classes in the wsgiserver.ssl_adapters dict."""
+
+ nodelay = True
+ """If True (the default since 3.1), sets the TCP_NODELAY socket option."""
+
+ wsgi_version = (1, 0)
+ """The WSGI version tuple to use with the builtin WSGI server.
+ The provided options are (1, 0) [which includes support for PEP 3333,
+ which declares it covers WSGI version 1.0.1 but still mandates the
+ wsgi.version (1, 0)] and ('u', 0), an experimental unicode version.
+ You may create and register your own experimental versions of the WSGI
+ protocol by adding custom classes to the wsgiserver.wsgi_gateways dict."""
+
+ def __init__(self):
+ self.bus = cherrypy.engine
+ self.httpserver = None
+ self.interrupt = None
+ self.running = False
+
+ def httpserver_from_self(self, httpserver=None):
+ """Return a (httpserver, bind_addr) pair based on self attributes."""
+ if httpserver is None:
+ httpserver = self.instance
+ if httpserver is None:
+ from cherrypy import _cpwsgi_server
+ httpserver = _cpwsgi_server.CPWSGIServer(self)
+ if isinstance(httpserver, basestring):
+ # Is anyone using this? Can I add an arg?
+ httpserver = attributes(httpserver)(self)
+ return httpserver, self.bind_addr
+
+ def start(self):
+ """Start the HTTP server."""
+ if not self.httpserver:
+ self.httpserver, self.bind_addr = self.httpserver_from_self()
+ ServerAdapter.start(self)
+ start.priority = 75
+
+ def _get_bind_addr(self):
+ if self.socket_file:
+ return self.socket_file
+ if self.socket_host is None and self.socket_port is None:
+ return None
+ return (self.socket_host, self.socket_port)
+ def _set_bind_addr(self, value):
+ if value is None:
+ self.socket_file = None
+ self.socket_host = None
+ self.socket_port = None
+ elif isinstance(value, basestring):
+ self.socket_file = value
+ self.socket_host = None
+ self.socket_port = None
+ else:
+ try:
+ self.socket_host, self.socket_port = value
+ self.socket_file = None
+ except ValueError:
+ raise ValueError("bind_addr must be a (host, port) tuple "
+ "(for TCP sockets) or a string (for Unix "
+ "domain sockets), not %r" % value)
+ bind_addr = property(_get_bind_addr, _set_bind_addr,
+ doc='A (host, port) tuple for TCP sockets or a str for Unix domain sockets.')
+
+ def base(self):
+ """Return the base (scheme://host[:port] or sock file) for this server."""
+ if self.socket_file:
+ return self.socket_file
+
+ host = self.socket_host
+ if host in ('0.0.0.0', '::'):
+ # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY.
+ # Look up the host name, which should be the
+ # safest thing to spit out in a URL.
+ import socket
+ host = socket.gethostname()
+
+ port = self.socket_port
+
+ if self.ssl_certificate:
+ scheme = "https"
+ if port != 443:
+ host += ":%s" % port
+ else:
+ scheme = "http"
+ if port != 80:
+ host += ":%s" % port
+
+ return "%s://%s" % (scheme, host)
+
diff --git a/cherrypy/_cpthreadinglocal.py b/cherrypy/_cpthreadinglocal.py
new file mode 100755
index 0000000..34c17ac
--- /dev/null
+++ b/cherrypy/_cpthreadinglocal.py
@@ -0,0 +1,239 @@
+# This is a backport of Python-2.4's threading.local() implementation
+
+"""Thread-local objects
+
+(Note that this module provides a Python version of thread
+ threading.local class. Depending on the version of Python you're
+ using, there may be a faster one available. You should always import
+ the local class from threading.)
+
+Thread-local objects support the management of thread-local data.
+If you have data that you want to be local to a thread, simply create
+a thread-local object and use its attributes:
+
+ >>> mydata = local()
+ >>> mydata.number = 42
+ >>> mydata.number
+ 42
+
+You can also access the local-object's dictionary:
+
+ >>> mydata.__dict__
+ {'number': 42}
+ >>> mydata.__dict__.setdefault('widgets', [])
+ []
+ >>> mydata.widgets
+ []
+
+What's important about thread-local objects is that their data are
+local to a thread. If we access the data in a different thread:
+
+ >>> log = []
+ >>> def f():
+ ... items = mydata.__dict__.items()
+ ... items.sort()
+ ... log.append(items)
+ ... mydata.number = 11
+ ... log.append(mydata.number)
+
+ >>> import threading
+ >>> thread = threading.Thread(target=f)
+ >>> thread.start()
+ >>> thread.join()
+ >>> log
+ [[], 11]
+
+we get different data. Furthermore, changes made in the other thread
+don't affect data seen in this thread:
+
+ >>> mydata.number
+ 42
+
+Of course, values you get from a local object, including a __dict__
+attribute, are for whatever thread was current at the time the
+attribute was read. For that reason, you generally don't want to save
+these values across threads, as they apply only to the thread they
+came from.
+
+You can create custom local objects by subclassing the local class:
+
+ >>> class MyLocal(local):
+ ... number = 2
+ ... initialized = False
+ ... def __init__(self, **kw):
+ ... if self.initialized:
+ ... raise SystemError('__init__ called too many times')
+ ... self.initialized = True
+ ... self.__dict__.update(kw)
+ ... def squared(self):
+ ... return self.number ** 2
+
+This can be useful to support default values, methods and
+initialization. Note that if you define an __init__ method, it will be
+called each time the local object is used in a separate thread. This
+is necessary to initialize each thread's dictionary.
+
+Now if we create a local object:
+
+ >>> mydata = MyLocal(color='red')
+
+Now we have a default number:
+
+ >>> mydata.number
+ 2
+
+an initial color:
+
+ >>> mydata.color
+ 'red'
+ >>> del mydata.color
+
+And a method that operates on the data:
+
+ >>> mydata.squared()
+ 4
+
+As before, we can access the data in a separate thread:
+
+ >>> log = []
+ >>> thread = threading.Thread(target=f)
+ >>> thread.start()
+ >>> thread.join()
+ >>> log
+ [[('color', 'red'), ('initialized', True)], 11]
+
+without affecting this thread's data:
+
+ >>> mydata.number
+ 2
+ >>> mydata.color
+ Traceback (most recent call last):
+ ...
+ AttributeError: 'MyLocal' object has no attribute 'color'
+
+Note that subclasses can define slots, but they are not thread
+local. They are shared across threads:
+
+ >>> class MyLocal(local):
+ ... __slots__ = 'number'
+
+ >>> mydata = MyLocal()
+ >>> mydata.number = 42
+ >>> mydata.color = 'red'
+
+So, the separate thread:
+
+ >>> thread = threading.Thread(target=f)
+ >>> thread.start()
+ >>> thread.join()
+
+affects what we see:
+
+ >>> mydata.number
+ 11
+
+>>> del mydata
+"""
+
+# Threading import is at end
+
+class _localbase(object):
+ __slots__ = '_local__key', '_local__args', '_local__lock'
+
+ def __new__(cls, *args, **kw):
+ self = object.__new__(cls)
+ key = 'thread.local.' + str(id(self))
+ object.__setattr__(self, '_local__key', key)
+ object.__setattr__(self, '_local__args', (args, kw))
+ object.__setattr__(self, '_local__lock', RLock())
+
+ if args or kw and (cls.__init__ is object.__init__):
+ raise TypeError("Initialization arguments are not supported")
+
+ # We need to create the thread dict in anticipation of
+ # __init__ being called, to make sure we don't call it
+ # again ourselves.
+ dict = object.__getattribute__(self, '__dict__')
+ currentThread().__dict__[key] = dict
+
+ return self
+
+def _patch(self):
+ key = object.__getattribute__(self, '_local__key')
+ d = currentThread().__dict__.get(key)
+ if d is None:
+ d = {}
+ currentThread().__dict__[key] = d
+ object.__setattr__(self, '__dict__', d)
+
+ # we have a new instance dict, so call out __init__ if we have
+ # one
+ cls = type(self)
+ if cls.__init__ is not object.__init__:
+ args, kw = object.__getattribute__(self, '_local__args')
+ cls.__init__(self, *args, **kw)
+ else:
+ object.__setattr__(self, '__dict__', d)
+
+class local(_localbase):
+
+ def __getattribute__(self, name):
+ lock = object.__getattribute__(self, '_local__lock')
+ lock.acquire()
+ try:
+ _patch(self)
+ return object.__getattribute__(self, name)
+ finally:
+ lock.release()
+
+ def __setattr__(self, name, value):
+ lock = object.__getattribute__(self, '_local__lock')
+ lock.acquire()
+ try:
+ _patch(self)
+ return object.__setattr__(self, name, value)
+ finally:
+ lock.release()
+
+ def __delattr__(self, name):
+ lock = object.__getattribute__(self, '_local__lock')
+ lock.acquire()
+ try:
+ _patch(self)
+ return object.__delattr__(self, name)
+ finally:
+ lock.release()
+
+
+ def __del__():
+ threading_enumerate = enumerate
+ __getattribute__ = object.__getattribute__
+
+ def __del__(self):
+ key = __getattribute__(self, '_local__key')
+
+ try:
+ threads = list(threading_enumerate())
+ except:
+ # if enumerate fails, as it seems to do during
+ # shutdown, we'll skip cleanup under the assumption
+ # that there is nothing to clean up
+ return
+
+ for thread in threads:
+ try:
+ __dict__ = thread.__dict__
+ except AttributeError:
+ # Thread is dying, rest in peace
+ continue
+
+ if key in __dict__:
+ try:
+ del __dict__[key]
+ except KeyError:
+ pass # didn't have anything in this thread
+
+ return __del__
+ __del__ = __del__()
+
+from threading import currentThread, enumerate, RLock
diff --git a/cherrypy/_cptools.py b/cherrypy/_cptools.py
new file mode 100755
index 0000000..d3eab05
--- /dev/null
+++ b/cherrypy/_cptools.py
@@ -0,0 +1,510 @@
+"""CherryPy tools. A "tool" is any helper, adapted to CP.
+
+Tools are usually designed to be used in a variety of ways (although some
+may only offer one if they choose):
+
+ Library calls
+ All tools are callables that can be used wherever needed.
+ The arguments are straightforward and should be detailed within the
+ docstring.
+
+ Function decorators
+ All tools, when called, may be used as decorators which configure
+ individual CherryPy page handlers (methods on the CherryPy tree).
+ That is, "@tools.anytool()" should "turn on" the tool via the
+ decorated function's _cp_config attribute.
+
+ CherryPy config
+ If a tool exposes a "_setup" callable, it will be called
+ once per Request (if the feature is "turned on" via config).
+
+Tools may be implemented as any object with a namespace. The builtins
+are generally either modules or instances of the tools.Tool class.
+"""
+
+import sys
+import warnings
+
+import cherrypy
+
+
+def _getargs(func):
+ """Return the names of all static arguments to the given function."""
+ # Use this instead of importing inspect for less mem overhead.
+ import types
+ if sys.version_info >= (3, 0):
+ if isinstance(func, types.MethodType):
+ func = func.__func__
+ co = func.__code__
+ else:
+ if isinstance(func, types.MethodType):
+ func = func.im_func
+ co = func.func_code
+ return co.co_varnames[:co.co_argcount]
+
+
+_attr_error = ("CherryPy Tools cannot be turned on directly. Instead, turn them "
+ "on via config, or use them as decorators on your page handlers.")
+
+class Tool(object):
+ """A registered function for use with CherryPy request-processing hooks.
+
+ help(tool.callable) should give you more information about this Tool.
+ """
+
+ namespace = "tools"
+
+ def __init__(self, point, callable, name=None, priority=50):
+ self._point = point
+ self.callable = callable
+ self._name = name
+ self._priority = priority
+ self.__doc__ = self.callable.__doc__
+ self._setargs()
+
+ def _get_on(self):
+ raise AttributeError(_attr_error)
+ def _set_on(self, value):
+ raise AttributeError(_attr_error)
+ on = property(_get_on, _set_on)
+
+ def _setargs(self):
+ """Copy func parameter names to obj attributes."""
+ try:
+ for arg in _getargs(self.callable):
+ setattr(self, arg, None)
+ except (TypeError, AttributeError):
+ if hasattr(self.callable, "__call__"):
+ for arg in _getargs(self.callable.__call__):
+ setattr(self, arg, None)
+ # IronPython 1.0 raises NotImplementedError because
+ # inspect.getargspec tries to access Python bytecode
+ # in co_code attribute.
+ except NotImplementedError:
+ pass
+ # IronPython 1B1 may raise IndexError in some cases,
+ # but if we trap it here it doesn't prevent CP from working.
+ except IndexError:
+ pass
+
+ def _merged_args(self, d=None):
+ """Return a dict of configuration entries for this Tool."""
+ if d:
+ conf = d.copy()
+ else:
+ conf = {}
+
+ tm = cherrypy.serving.request.toolmaps[self.namespace]
+ if self._name in tm:
+ conf.update(tm[self._name])
+
+ if "on" in conf:
+ del conf["on"]
+
+ return conf
+
+ def __call__(self, *args, **kwargs):
+ """Compile-time decorator (turn on the tool in config).
+
+ For example::
+
+ @tools.proxy()
+ def whats_my_base(self):
+ return cherrypy.request.base
+ whats_my_base.exposed = True
+ """
+ if args:
+ raise TypeError("The %r Tool does not accept positional "
+ "arguments; you must use keyword arguments."
+ % self._name)
+ def tool_decorator(f):
+ if not hasattr(f, "_cp_config"):
+ f._cp_config = {}
+ subspace = self.namespace + "." + self._name + "."
+ f._cp_config[subspace + "on"] = True
+ for k, v in kwargs.items():
+ f._cp_config[subspace + k] = v
+ return f
+ return tool_decorator
+
+ def _setup(self):
+ """Hook this tool into cherrypy.request.
+
+ The standard CherryPy request object will automatically call this
+ method when the tool is "turned on" in config.
+ """
+ conf = self._merged_args()
+ p = conf.pop("priority", None)
+ if p is None:
+ p = getattr(self.callable, "priority", self._priority)
+ cherrypy.serving.request.hooks.attach(self._point, self.callable,
+ priority=p, **conf)
+
+
+class HandlerTool(Tool):
+ """Tool which is called 'before main', that may skip normal handlers.
+
+ If the tool successfully handles the request (by setting response.body),
+ if should return True. This will cause CherryPy to skip any 'normal' page
+ handler. If the tool did not handle the request, it should return False
+ to tell CherryPy to continue on and call the normal page handler. If the
+ tool is declared AS a page handler (see the 'handler' method), returning
+ False will raise NotFound.
+ """
+
+ def __init__(self, callable, name=None):
+ Tool.__init__(self, 'before_handler', callable, name)
+
+ def handler(self, *args, **kwargs):
+ """Use this tool as a CherryPy page handler.
+
+ For example::
+
+ class Root:
+ nav = tools.staticdir.handler(section="/nav", dir="nav",
+ root=absDir)
+ """
+ def handle_func(*a, **kw):
+ handled = self.callable(*args, **self._merged_args(kwargs))
+ if not handled:
+ raise cherrypy.NotFound()
+ return cherrypy.serving.response.body
+ handle_func.exposed = True
+ return handle_func
+
+ def _wrapper(self, **kwargs):
+ if self.callable(**kwargs):
+ cherrypy.serving.request.handler = None
+
+ def _setup(self):
+ """Hook this tool into cherrypy.request.
+
+ The standard CherryPy request object will automatically call this
+ method when the tool is "turned on" in config.
+ """
+ conf = self._merged_args()
+ p = conf.pop("priority", None)
+ if p is None:
+ p = getattr(self.callable, "priority", self._priority)
+ cherrypy.serving.request.hooks.attach(self._point, self._wrapper,
+ priority=p, **conf)
+
+
+class HandlerWrapperTool(Tool):
+ """Tool which wraps request.handler in a provided wrapper function.
+
+ The 'newhandler' arg must be a handler wrapper function that takes a
+ 'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all
+ page handler
+ functions, it must return an iterable for use as cherrypy.response.body.
+
+ For example, to allow your 'inner' page handlers to return dicts
+ which then get interpolated into a template::
+
+ def interpolator(next_handler, *args, **kwargs):
+ filename = cherrypy.request.config.get('template')
+ cherrypy.response.template = env.get_template(filename)
+ response_dict = next_handler(*args, **kwargs)
+ return cherrypy.response.template.render(**response_dict)
+ cherrypy.tools.jinja = HandlerWrapperTool(interpolator)
+ """
+
+ def __init__(self, newhandler, point='before_handler', name=None, priority=50):
+ self.newhandler = newhandler
+ self._point = point
+ self._name = name
+ self._priority = priority
+
+ def callable(self, debug=False):
+ innerfunc = cherrypy.serving.request.handler
+ def wrap(*args, **kwargs):
+ return self.newhandler(innerfunc, *args, **kwargs)
+ cherrypy.serving.request.handler = wrap
+
+
+class ErrorTool(Tool):
+ """Tool which is used to replace the default request.error_response."""
+
+ def __init__(self, callable, name=None):
+ Tool.__init__(self, None, callable, name)
+
+ def _wrapper(self):
+ self.callable(**self._merged_args())
+
+ def _setup(self):
+ """Hook this tool into cherrypy.request.
+
+ The standard CherryPy request object will automatically call this
+ method when the tool is "turned on" in config.
+ """
+ cherrypy.serving.request.error_response = self._wrapper
+
+
+# Builtin tools #
+
+from cherrypy.lib import cptools, encoding, auth, static, jsontools
+from cherrypy.lib import sessions as _sessions, xmlrpc as _xmlrpc
+from cherrypy.lib import caching as _caching
+from cherrypy.lib import auth_basic, auth_digest
+
+
+class SessionTool(Tool):
+ """Session Tool for CherryPy.
+
+ sessions.locking
+ When 'implicit' (the default), the session will be locked for you,
+ just before running the page handler.
+
+ When 'early', the session will be locked before reading the request
+ body. This is off by default for safety reasons; for example,
+ a large upload would block the session, denying an AJAX
+ progress meter (see http://www.cherrypy.org/ticket/630).
+
+ When 'explicit' (or any other value), you need to call
+ cherrypy.session.acquire_lock() yourself before using
+ session data.
+ """
+
+ def __init__(self):
+ # _sessions.init must be bound after headers are read
+ Tool.__init__(self, 'before_request_body', _sessions.init)
+
+ def _lock_session(self):
+ cherrypy.serving.session.acquire_lock()
+
+ def _setup(self):
+ """Hook this tool into cherrypy.request.
+
+ The standard CherryPy request object will automatically call this
+ method when the tool is "turned on" in config.
+ """
+ hooks = cherrypy.serving.request.hooks
+
+ conf = self._merged_args()
+
+ p = conf.pop("priority", None)
+ if p is None:
+ p = getattr(self.callable, "priority", self._priority)
+
+ hooks.attach(self._point, self.callable, priority=p, **conf)
+
+ locking = conf.pop('locking', 'implicit')
+ if locking == 'implicit':
+ hooks.attach('before_handler', self._lock_session)
+ elif locking == 'early':
+ # Lock before the request body (but after _sessions.init runs!)
+ hooks.attach('before_request_body', self._lock_session,
+ priority=60)
+ else:
+ # Don't lock
+ pass
+
+ hooks.attach('before_finalize', _sessions.save)
+ hooks.attach('on_end_request', _sessions.close)
+
+ def regenerate(self):
+ """Drop the current session and make a new one (with a new id)."""
+ sess = cherrypy.serving.session
+ sess.regenerate()
+
+ # Grab cookie-relevant tool args
+ conf = dict([(k, v) for k, v in self._merged_args().items()
+ if k in ('path', 'path_header', 'name', 'timeout',
+ 'domain', 'secure')])
+ _sessions.set_response_cookie(**conf)
+
+
+
+
+class XMLRPCController(object):
+ """A Controller (page handler collection) for XML-RPC.
+
+ To use it, have your controllers subclass this base class (it will
+ turn on the tool for you).
+
+ You can also supply the following optional config entries::
+
+ tools.xmlrpc.encoding: 'utf-8'
+ tools.xmlrpc.allow_none: 0
+
+ XML-RPC is a rather discontinuous layer over HTTP; dispatching to the
+ appropriate handler must first be performed according to the URL, and
+ then a second dispatch step must take place according to the RPC method
+ specified in the request body. It also allows a superfluous "/RPC2"
+ prefix in the URL, supplies its own handler args in the body, and
+ requires a 200 OK "Fault" response instead of 404 when the desired
+ method is not found.
+
+ Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
+ This Controller acts as the dispatch target for the first half (based
+ on the URL); it then reads the RPC method from the request body and
+ does its own second dispatch step based on that method. It also reads
+ body params, and returns a Fault on error.
+
+ The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
+ in your URL's, you can safely skip turning on the XMLRPCDispatcher.
+ Otherwise, you need to use declare it in config::
+
+ request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
+ """
+
+ # Note we're hard-coding this into the 'tools' namespace. We could do
+ # a huge amount of work to make it relocatable, but the only reason why
+ # would be if someone actually disabled the default_toolbox. Meh.
+ _cp_config = {'tools.xmlrpc.on': True}
+
+ def default(self, *vpath, **params):
+ rpcparams, rpcmethod = _xmlrpc.process_body()
+
+ subhandler = self
+ for attr in str(rpcmethod).split('.'):
+ subhandler = getattr(subhandler, attr, None)
+
+ if subhandler and getattr(subhandler, "exposed", False):
+ body = subhandler(*(vpath + rpcparams), **params)
+
+ else:
+ # http://www.cherrypy.org/ticket/533
+ # if a method is not found, an xmlrpclib.Fault should be returned
+ # raising an exception here will do that; see
+ # cherrypy.lib.xmlrpc.on_error
+ raise Exception('method "%s" is not supported' % attr)
+
+ conf = cherrypy.serving.request.toolmaps['tools'].get("xmlrpc", {})
+ _xmlrpc.respond(body,
+ conf.get('encoding', 'utf-8'),
+ conf.get('allow_none', 0))
+ return cherrypy.serving.response.body
+ default.exposed = True
+
+
+class SessionAuthTool(HandlerTool):
+
+ def _setargs(self):
+ for name in dir(cptools.SessionAuth):
+ if not name.startswith("__"):
+ setattr(self, name, None)
+
+
+class CachingTool(Tool):
+ """Caching Tool for CherryPy."""
+
+ def _wrapper(self, **kwargs):
+ request = cherrypy.serving.request
+ if _caching.get(**kwargs):
+ request.handler = None
+ else:
+ if request.cacheable:
+ # Note the devious technique here of adding hooks on the fly
+ request.hooks.attach('before_finalize', _caching.tee_output,
+ priority = 90)
+ _wrapper.priority = 20
+
+ def _setup(self):
+ """Hook caching into cherrypy.request."""
+ conf = self._merged_args()
+
+ p = conf.pop("priority", None)
+ cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
+ priority=p, **conf)
+
+
+
+class Toolbox(object):
+ """A collection of Tools.
+
+ This object also functions as a config namespace handler for itself.
+ Custom toolboxes should be added to each Application's toolboxes dict.
+ """
+
+ def __init__(self, namespace):
+ self.namespace = namespace
+
+ def __setattr__(self, name, value):
+ # If the Tool._name is None, supply it from the attribute name.
+ if isinstance(value, Tool):
+ if value._name is None:
+ value._name = name
+ value.namespace = self.namespace
+ object.__setattr__(self, name, value)
+
+ def __enter__(self):
+ """Populate request.toolmaps from tools specified in config."""
+ cherrypy.serving.request.toolmaps[self.namespace] = map = {}
+ def populate(k, v):
+ toolname, arg = k.split(".", 1)
+ bucket = map.setdefault(toolname, {})
+ bucket[arg] = v
+ return populate
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ """Run tool._setup() for each tool in our toolmap."""
+ map = cherrypy.serving.request.toolmaps.get(self.namespace)
+ if map:
+ for name, settings in map.items():
+ if settings.get("on", False):
+ tool = getattr(self, name)
+ tool._setup()
+
+
+class DeprecatedTool(Tool):
+
+ _name = None
+ warnmsg = "This Tool is deprecated."
+
+ def __init__(self, point, warnmsg=None):
+ self.point = point
+ if warnmsg is not None:
+ self.warnmsg = warnmsg
+
+ def __call__(self, *args, **kwargs):
+ warnings.warn(self.warnmsg)
+ def tool_decorator(f):
+ return f
+ return tool_decorator
+
+ def _setup(self):
+ warnings.warn(self.warnmsg)
+
+
+default_toolbox = _d = Toolbox("tools")
+_d.session_auth = SessionAuthTool(cptools.session_auth)
+_d.allow = Tool('on_start_resource', cptools.allow)
+_d.proxy = Tool('before_request_body', cptools.proxy, priority=30)
+_d.response_headers = Tool('on_start_resource', cptools.response_headers)
+_d.log_tracebacks = Tool('before_error_response', cptools.log_traceback)
+_d.log_headers = Tool('before_error_response', cptools.log_request_headers)
+_d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100)
+_d.err_redirect = ErrorTool(cptools.redirect)
+_d.etags = Tool('before_finalize', cptools.validate_etags, priority=75)
+_d.decode = Tool('before_request_body', encoding.decode)
+# the order of encoding, gzip, caching is important
+_d.encode = Tool('before_handler', encoding.ResponseEncoder, priority=70)
+_d.gzip = Tool('before_finalize', encoding.gzip, priority=80)
+_d.staticdir = HandlerTool(static.staticdir)
+_d.staticfile = HandlerTool(static.staticfile)
+_d.sessions = SessionTool()
+_d.xmlrpc = ErrorTool(_xmlrpc.on_error)
+_d.caching = CachingTool('before_handler', _caching.get, 'caching')
+_d.expires = Tool('before_finalize', _caching.expires)
+_d.tidy = DeprecatedTool('before_finalize',
+ "The tidy tool has been removed from the standard distribution of CherryPy. "
+ "The most recent version can be found at http://tools.cherrypy.org/browser.")
+_d.nsgmls = DeprecatedTool('before_finalize',
+ "The nsgmls tool has been removed from the standard distribution of CherryPy. "
+ "The most recent version can be found at http://tools.cherrypy.org/browser.")
+_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers)
+_d.referer = Tool('before_request_body', cptools.referer)
+_d.basic_auth = Tool('on_start_resource', auth.basic_auth)
+_d.digest_auth = Tool('on_start_resource', auth.digest_auth)
+_d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60)
+_d.flatten = Tool('before_finalize', cptools.flatten)
+_d.accept = Tool('on_start_resource', cptools.accept)
+_d.redirect = Tool('on_start_resource', cptools.redirect)
+_d.autovary = Tool('on_start_resource', cptools.autovary, priority=0)
+_d.json_in = Tool('before_request_body', jsontools.json_in, priority=30)
+_d.json_out = Tool('before_handler', jsontools.json_out, priority=30)
+_d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1)
+_d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1)
+
+del _d, cptools, encoding, auth, static
diff --git a/cherrypy/_cptree.py b/cherrypy/_cptree.py
new file mode 100755
index 0000000..67ce546
--- /dev/null
+++ b/cherrypy/_cptree.py
@@ -0,0 +1,279 @@
+"""CherryPy Application and Tree objects."""
+
+import os
+import cherrypy
+from cherrypy._cpcompat import ntou
+from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools
+from cherrypy.lib import httputil
+
+
+class Application(object):
+ """A CherryPy Application.
+
+ Servers and gateways should not instantiate Request objects directly.
+ Instead, they should ask an Application object for a request object.
+
+ An instance of this class may also be used as a WSGI callable
+ (WSGI application object) for itself.
+ """
+
+ root = None
+ """The top-most container of page handlers for this app. Handlers should
+ be arranged in a hierarchy of attributes, matching the expected URI
+ hierarchy; the default dispatcher then searches this hierarchy for a
+ matching handler. When using a dispatcher other than the default,
+ this value may be None."""
+
+ config = {}
+ """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
+ of {key: value} pairs."""
+
+ namespaces = _cpconfig.NamespaceSet()
+ toolboxes = {'tools': cherrypy.tools}
+
+ log = None
+ """A LogManager instance. See _cplogging."""
+
+ wsgiapp = None
+ """A CPWSGIApp instance. See _cpwsgi."""
+
+ request_class = _cprequest.Request
+ response_class = _cprequest.Response
+
+ relative_urls = False
+
+ def __init__(self, root, script_name="", config=None):
+ self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
+ self.root = root
+ self.script_name = script_name
+ self.wsgiapp = _cpwsgi.CPWSGIApp(self)
+
+ self.namespaces = self.namespaces.copy()
+ self.namespaces["log"] = lambda k, v: setattr(self.log, k, v)
+ self.namespaces["wsgi"] = self.wsgiapp.namespace_handler
+
+ self.config = self.__class__.config.copy()
+ if config:
+ self.merge(config)
+
+ def __repr__(self):
+ return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__,
+ self.root, self.script_name)
+
+ script_name_doc = """The URI "mount point" for this app. A mount point is that portion of
+ the URI which is constant for all URIs that are serviced by this
+ application; it does not include scheme, host, or proxy ("virtual host")
+ portions of the URI.
+
+ For example, if script_name is "/my/cool/app", then the URL
+ "http://www.example.com/my/cool/app/page1" might be handled by a
+ "page1" method on the root object.
+
+ The value of script_name MUST NOT end in a slash. If the script_name
+ refers to the root of the URI, it MUST be an empty string (not "/").
+
+ If script_name is explicitly set to None, then the script_name will be
+ provided for each call from request.wsgi_environ['SCRIPT_NAME'].
+ """
+ def _get_script_name(self):
+ if self._script_name is None:
+ # None signals that the script name should be pulled from WSGI environ.
+ return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip("/")
+ return self._script_name
+ def _set_script_name(self, value):
+ if value:
+ value = value.rstrip("/")
+ self._script_name = value
+ script_name = property(fget=_get_script_name, fset=_set_script_name,
+ doc=script_name_doc)
+
+ def merge(self, config):
+ """Merge the given config into self.config."""
+ _cpconfig.merge(self.config, config)
+
+ # Handle namespaces specified in config.
+ self.namespaces(self.config.get("/", {}))
+
+ def find_config(self, path, key, default=None):
+ """Return the most-specific value for key along path, or default."""
+ trail = path or "/"
+ while trail:
+ nodeconf = self.config.get(trail, {})
+
+ if key in nodeconf:
+ return nodeconf[key]
+
+ lastslash = trail.rfind("/")
+ if lastslash == -1:
+ break
+ elif lastslash == 0 and trail != "/":
+ trail = "/"
+ else:
+ trail = trail[:lastslash]
+
+ return default
+
+ def get_serving(self, local, remote, scheme, sproto):
+ """Create and return a Request and Response object."""
+ req = self.request_class(local, remote, scheme, sproto)
+ req.app = self
+
+ for name, toolbox in self.toolboxes.items():
+ req.namespaces[name] = toolbox
+
+ resp = self.response_class()
+ cherrypy.serving.load(req, resp)
+ cherrypy.engine.timeout_monitor.acquire()
+ cherrypy.engine.publish('acquire_thread')
+
+ return req, resp
+
+ def release_serving(self):
+ """Release the current serving (request and response)."""
+ req = cherrypy.serving.request
+
+ cherrypy.engine.timeout_monitor.release()
+
+ try:
+ req.close()
+ except:
+ cherrypy.log(traceback=True, severity=40)
+
+ cherrypy.serving.clear()
+
+ def __call__(self, environ, start_response):
+ return self.wsgiapp(environ, start_response)
+
+
+class Tree(object):
+ """A registry of CherryPy applications, mounted at diverse points.
+
+ An instance of this class may also be used as a WSGI callable
+ (WSGI application object), in which case it dispatches to all
+ mounted apps.
+ """
+
+ apps = {}
+ """
+ A dict of the form {script name: application}, where "script name"
+ is a string declaring the URI mount point (no trailing slash), and
+ "application" is an instance of cherrypy.Application (or an arbitrary
+ WSGI callable if you happen to be using a WSGI server)."""
+
+ def __init__(self):
+ self.apps = {}
+
+ def mount(self, root, script_name="", config=None):
+ """Mount a new app from a root object, script_name, and config.
+
+ root
+ An instance of a "controller class" (a collection of page
+ handler methods) which represents the root of the application.
+ This may also be an Application instance, or None if using
+ a dispatcher other than the default.
+
+ script_name
+ A string containing the "mount point" of the application.
+ This should start with a slash, and be the path portion of the
+ URL at which to mount the given root. For example, if root.index()
+ will handle requests to "http://www.example.com:8080/dept/app1/",
+ then the script_name argument would be "/dept/app1".
+
+ It MUST NOT end in a slash. If the script_name refers to the
+ root of the URI, it MUST be an empty string (not "/").
+
+ config
+ A file or dict containing application config.
+ """
+ if script_name is None:
+ raise TypeError(
+ "The 'script_name' argument may not be None. Application "
+ "objects may, however, possess a script_name of None (in "
+ "order to inpect the WSGI environ for SCRIPT_NAME upon each "
+ "request). You cannot mount such Applications on this Tree; "
+ "you must pass them to a WSGI server interface directly.")
+
+ # Next line both 1) strips trailing slash and 2) maps "/" -> "".
+ script_name = script_name.rstrip("/")
+
+ if isinstance(root, Application):
+ app = root
+ if script_name != "" and script_name != app.script_name:
+ raise ValueError("Cannot specify a different script name and "
+ "pass an Application instance to cherrypy.mount")
+ script_name = app.script_name
+ else:
+ app = Application(root, script_name)
+
+ # If mounted at "", add favicon.ico
+ if (script_name == "" and root is not None
+ and not hasattr(root, "favicon_ico")):
+ favicon = os.path.join(os.getcwd(), os.path.dirname(__file__),
+ "favicon.ico")
+ root.favicon_ico = tools.staticfile.handler(favicon)
+
+ if config:
+ app.merge(config)
+
+ self.apps[script_name] = app
+
+ return app
+
+ def graft(self, wsgi_callable, script_name=""):
+ """Mount a wsgi callable at the given script_name."""
+ # Next line both 1) strips trailing slash and 2) maps "/" -> "".
+ script_name = script_name.rstrip("/")
+ self.apps[script_name] = wsgi_callable
+
+ def script_name(self, path=None):
+ """The script_name of the app at the given path, or None.
+
+ If path is None, cherrypy.request is used.
+ """
+ if path is None:
+ try:
+ request = cherrypy.serving.request
+ path = httputil.urljoin(request.script_name,
+ request.path_info)
+ except AttributeError:
+ return None
+
+ while True:
+ if path in self.apps:
+ return path
+
+ if path == "":
+ return None
+
+ # Move one node up the tree and try again.
+ path = path[:path.rfind("/")]
+
+ def __call__(self, environ, start_response):
+ # If you're calling this, then you're probably setting SCRIPT_NAME
+ # to '' (some WSGI servers always set SCRIPT_NAME to '').
+ # Try to look up the app using the full path.
+ env1x = environ
+ if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
+ env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ)
+ path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''),
+ env1x.get('PATH_INFO', ''))
+ sn = self.script_name(path or "/")
+ if sn is None:
+ start_response('404 Not Found', [])
+ return []
+
+ app = self.apps[sn]
+
+ # Correct the SCRIPT_NAME and PATH_INFO environ entries.
+ environ = environ.copy()
+ if environ.get(u'wsgi.version') == (u'u', 0):
+ # Python 2/WSGI u.0: all strings MUST be of type unicode
+ enc = environ[u'wsgi.url_encoding']
+ environ[u'SCRIPT_NAME'] = sn.decode(enc)
+ environ[u'PATH_INFO'] = path[len(sn.rstrip("/")):].decode(enc)
+ else:
+ # Python 2/WSGI 1.x: all strings MUST be of type str
+ environ['SCRIPT_NAME'] = sn
+ environ['PATH_INFO'] = path[len(sn.rstrip("/")):]
+ return app(environ, start_response)
+
diff --git a/cherrypy/_cpwsgi.py b/cherrypy/_cpwsgi.py
new file mode 100755
index 0000000..aa4b763
--- /dev/null
+++ b/cherrypy/_cpwsgi.py
@@ -0,0 +1,347 @@
+"""WSGI interface (see PEP 333 and 3333).
+
+Note that WSGI environ keys and values are 'native strings'; that is,
+whatever the type of "" is. For Python 2, that's a byte string; for Python 3,
+it's a unicode string. But PEP 3333 says: "even if Python's str type is
+actually Unicode "under the hood", the content of native strings must
+still be translatable to bytes via the Latin-1 encoding!"
+"""
+
+import sys as _sys
+
+import cherrypy as _cherrypy
+from cherrypy._cpcompat import BytesIO
+from cherrypy import _cperror
+from cherrypy.lib import httputil
+
+
+def downgrade_wsgi_ux_to_1x(environ):
+ """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ."""
+ env1x = {}
+
+ url_encoding = environ[u'wsgi.url_encoding']
+ for k, v in environ.items():
+ if k in [u'PATH_INFO', u'SCRIPT_NAME', u'QUERY_STRING']:
+ v = v.encode(url_encoding)
+ elif isinstance(v, unicode):
+ v = v.encode('ISO-8859-1')
+ env1x[k.encode('ISO-8859-1')] = v
+
+ return env1x
+
+
+class VirtualHost(object):
+ """Select a different WSGI application based on the Host header.
+
+ This can be useful when running multiple sites within one CP server.
+ It allows several domains to point to different applications. For example::
+
+ root = Root()
+ RootApp = cherrypy.Application(root)
+ Domain2App = cherrypy.Application(root)
+ SecureApp = cherrypy.Application(Secure())
+
+ vhost = cherrypy._cpwsgi.VirtualHost(RootApp,
+ domains={'www.domain2.example': Domain2App,
+ 'www.domain2.example:443': SecureApp,
+ })
+
+ cherrypy.tree.graft(vhost)
+ """
+ default = None
+ """Required. The default WSGI application."""
+
+ use_x_forwarded_host = True
+ """If True (the default), any "X-Forwarded-Host"
+ request header will be used instead of the "Host" header. This
+ is commonly added by HTTP servers (such as Apache) when proxying."""
+
+ domains = {}
+ """A dict of {host header value: application} pairs.
+ The incoming "Host" request header is looked up in this dict,
+ and, if a match is found, the corresponding WSGI application
+ will be called instead of the default. Note that you often need
+ separate entries for "example.com" and "www.example.com".
+ In addition, "Host" headers may contain the port number.
+ """
+
+ def __init__(self, default, domains=None, use_x_forwarded_host=True):
+ self.default = default
+ self.domains = domains or {}
+ self.use_x_forwarded_host = use_x_forwarded_host
+
+ def __call__(self, environ, start_response):
+ domain = environ.get('HTTP_HOST', '')
+ if self.use_x_forwarded_host:
+ domain = environ.get("HTTP_X_FORWARDED_HOST", domain)
+
+ nextapp = self.domains.get(domain)
+ if nextapp is None:
+ nextapp = self.default
+ return nextapp(environ, start_response)
+
+
+class InternalRedirector(object):
+ """WSGI middleware that handles raised cherrypy.InternalRedirect."""
+
+ def __init__(self, nextapp, recursive=False):
+ self.nextapp = nextapp
+ self.recursive = recursive
+
+ def __call__(self, environ, start_response):
+ redirections = []
+ while True:
+ environ = environ.copy()
+ try:
+ return self.nextapp(environ, start_response)
+ except _cherrypy.InternalRedirect, ir:
+ sn = environ.get('SCRIPT_NAME', '')
+ path = environ.get('PATH_INFO', '')
+ qs = environ.get('QUERY_STRING', '')
+
+ # Add the *previous* path_info + qs to redirections.
+ old_uri = sn + path
+ if qs:
+ old_uri += "?" + qs
+ redirections.append(old_uri)
+
+ if not self.recursive:
+ # Check to see if the new URI has been redirected to already
+ new_uri = sn + ir.path
+ if ir.query_string:
+ new_uri += "?" + ir.query_string
+ if new_uri in redirections:
+ ir.request.close()
+ raise RuntimeError("InternalRedirector visited the "
+ "same URL twice: %r" % new_uri)
+
+ # Munge the environment and try again.
+ environ['REQUEST_METHOD'] = "GET"
+ environ['PATH_INFO'] = ir.path
+ environ['QUERY_STRING'] = ir.query_string
+ environ['wsgi.input'] = BytesIO()
+ environ['CONTENT_LENGTH'] = "0"
+ environ['cherrypy.previous_request'] = ir.request
+
+
+class ExceptionTrapper(object):
+ """WSGI middleware that traps exceptions."""
+
+ def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
+ self.nextapp = nextapp
+ self.throws = throws
+
+ def __call__(self, environ, start_response):
+ return _TrappedResponse(self.nextapp, environ, start_response, self.throws)
+
+
+class _TrappedResponse(object):
+
+ response = iter([])
+
+ def __init__(self, nextapp, environ, start_response, throws):
+ self.nextapp = nextapp
+ self.environ = environ
+ self.start_response = start_response
+ self.throws = throws
+ self.started_response = False
+ self.response = self.trap(self.nextapp, self.environ, self.start_response)
+ self.iter_response = iter(self.response)
+
+ def __iter__(self):
+ self.started_response = True
+ return self
+
+ def next(self):
+ return self.trap(self.iter_response.next)
+
+ def close(self):
+ if hasattr(self.response, 'close'):
+ self.response.close()
+
+ def trap(self, func, *args, **kwargs):
+ try:
+ return func(*args, **kwargs)
+ except self.throws:
+ raise
+ except StopIteration:
+ raise
+ except:
+ tb = _cperror.format_exc()
+ #print('trapped (started %s):' % self.started_response, tb)
+ _cherrypy.log(tb, severity=40)
+ if not _cherrypy.request.show_tracebacks:
+ tb = ""
+ s, h, b = _cperror.bare_error(tb)
+ if self.started_response:
+ # Empty our iterable (so future calls raise StopIteration)
+ self.iter_response = iter([])
+ else:
+ self.iter_response = iter(b)
+
+ try:
+ self.start_response(s, h, _sys.exc_info())
+ except:
+ # "The application must not trap any exceptions raised by
+ # start_response, if it called start_response with exc_info.
+ # Instead, it should allow such exceptions to propagate
+ # back to the server or gateway."
+ # But we still log and call close() to clean up ourselves.
+ _cherrypy.log(traceback=True, severity=40)
+ raise
+
+ if self.started_response:
+ return "".join(b)
+ else:
+ return b
+
+
+# WSGI-to-CP Adapter #
+
+
+class AppResponse(object):
+ """WSGI response iterable for CherryPy applications."""
+
+ def __init__(self, environ, start_response, cpapp):
+ if environ.get(u'wsgi.version') == (u'u', 0):
+ environ = downgrade_wsgi_ux_to_1x(environ)
+ self.environ = environ
+ self.cpapp = cpapp
+ try:
+ self.run()
+ except:
+ self.close()
+ raise
+ r = _cherrypy.serving.response
+ self.iter_response = iter(r.body)
+ self.write = start_response(r.output_status, r.header_list)
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ return self.iter_response.next()
+
+ def close(self):
+ """Close and de-reference the current request and response. (Core)"""
+ self.cpapp.release_serving()
+
+ def run(self):
+ """Create a Request object using environ."""
+ env = self.environ.get
+
+ local = httputil.Host('', int(env('SERVER_PORT', 80)),
+ env('SERVER_NAME', ''))
+ remote = httputil.Host(env('REMOTE_ADDR', ''),
+ int(env('REMOTE_PORT', -1) or -1),
+ env('REMOTE_HOST', ''))
+ scheme = env('wsgi.url_scheme')
+ sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
+ request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
+
+ # LOGON_USER is served by IIS, and is the name of the
+ # user after having been mapped to a local account.
+ # Both IIS and Apache set REMOTE_USER, when possible.
+ request.login = env('LOGON_USER') or env('REMOTE_USER') or None
+ request.multithread = self.environ['wsgi.multithread']
+ request.multiprocess = self.environ['wsgi.multiprocess']
+ request.wsgi_environ = self.environ
+ request.prev = env('cherrypy.previous_request', None)
+
+ meth = self.environ['REQUEST_METHOD']
+
+ path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''),
+ self.environ.get('PATH_INFO', ''))
+ qs = self.environ.get('QUERY_STRING', '')
+ rproto = self.environ.get('SERVER_PROTOCOL')
+ headers = self.translate_headers(self.environ)
+ rfile = self.environ['wsgi.input']
+ request.run(meth, path, qs, rproto, headers, rfile)
+
+ headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
+ 'CONTENT_LENGTH': 'Content-Length',
+ 'CONTENT_TYPE': 'Content-Type',
+ 'REMOTE_HOST': 'Remote-Host',
+ 'REMOTE_ADDR': 'Remote-Addr',
+ }
+
+ def translate_headers(self, environ):
+ """Translate CGI-environ header names to HTTP header names."""
+ for cgiName in environ:
+ # We assume all incoming header keys are uppercase already.
+ if cgiName in self.headerNames:
+ yield self.headerNames[cgiName], environ[cgiName]
+ elif cgiName[:5] == "HTTP_":
+ # Hackish attempt at recovering original header names.
+ translatedHeader = cgiName[5:].replace("_", "-")
+ yield translatedHeader, environ[cgiName]
+
+
+class CPWSGIApp(object):
+ """A WSGI application object for a CherryPy Application."""
+
+ pipeline = [('ExceptionTrapper', ExceptionTrapper),
+ ('InternalRedirector', InternalRedirector),
+ ]
+ """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
+ constructor that takes an initial, positional 'nextapp' argument,
+ plus optional keyword arguments, and returns a WSGI application
+ (that takes environ and start_response arguments). The 'name' can
+ be any you choose, and will correspond to keys in self.config."""
+
+ head = None
+ """Rather than nest all apps in the pipeline on each call, it's only
+ done the first time, and the result is memoized into self.head. Set
+ this to None again if you change self.pipeline after calling self."""
+
+ config = {}
+ """A dict whose keys match names listed in the pipeline. Each
+ value is a further dict which will be passed to the corresponding
+ named WSGI callable (from the pipeline) as keyword arguments."""
+
+ response_class = AppResponse
+ """The class to instantiate and return as the next app in the WSGI chain."""
+
+ def __init__(self, cpapp, pipeline=None):
+ self.cpapp = cpapp
+ self.pipeline = self.pipeline[:]
+ if pipeline:
+ self.pipeline.extend(pipeline)
+ self.config = self.config.copy()
+
+ def tail(self, environ, start_response):
+ """WSGI application callable for the actual CherryPy application.
+
+ You probably shouldn't call this; call self.__call__ instead,
+ so that any WSGI middleware in self.pipeline can run first.
+ """
+ return self.response_class(environ, start_response, self.cpapp)
+
+ def __call__(self, environ, start_response):
+ head = self.head
+ if head is None:
+ # Create and nest the WSGI apps in our pipeline (in reverse order).
+ # Then memoize the result in self.head.
+ head = self.tail
+ for name, callable in self.pipeline[::-1]:
+ conf = self.config.get(name, {})
+ head = callable(head, **conf)
+ self.head = head
+ return head(environ, start_response)
+
+ def namespace_handler(self, k, v):
+ """Config handler for the 'wsgi' namespace."""
+ if k == "pipeline":
+ # Note this allows multiple 'wsgi.pipeline' config entries
+ # (but each entry will be processed in a 'random' order).
+ # It should also allow developers to set default middleware
+ # in code (passed to self.__init__) that deployers can add to
+ # (but not remove) via config.
+ self.pipeline.extend(v)
+ elif k == "response_class":
+ self.response_class = v
+ else:
+ name, arg = k.split(".", 1)
+ bucket = self.config.setdefault(name, {})
+ bucket[arg] = v
+
diff --git a/cherrypy/_cpwsgi_server.py b/cherrypy/_cpwsgi_server.py
new file mode 100755
index 0000000..49fd5a1
--- /dev/null
+++ b/cherrypy/_cpwsgi_server.py
@@ -0,0 +1,54 @@
+"""WSGI server interface (see PEP 333). This adds some CP-specific bits to
+the framework-agnostic wsgiserver package.
+"""
+import sys
+
+import cherrypy
+from cherrypy import wsgiserver
+
+
+class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
+ """Wrapper for wsgiserver.CherryPyWSGIServer.
+
+ wsgiserver has been designed to not reference CherryPy in any way,
+ so that it can be used in other frameworks and applications. Therefore,
+ we wrap it here, so we can set our own mount points from cherrypy.tree
+ and apply some attributes from config -> cherrypy.server -> wsgiserver.
+ """
+
+ def __init__(self, server_adapter=cherrypy.server):
+ self.server_adapter = server_adapter
+ self.max_request_header_size = self.server_adapter.max_request_header_size or 0
+ self.max_request_body_size = self.server_adapter.max_request_body_size or 0
+
+ server_name = (self.server_adapter.socket_host or
+ self.server_adapter.socket_file or
+ None)
+
+ self.wsgi_version = self.server_adapter.wsgi_version
+ s = wsgiserver.CherryPyWSGIServer
+ s.__init__(self, server_adapter.bind_addr, cherrypy.tree,
+ self.server_adapter.thread_pool,
+ server_name,
+ max = self.server_adapter.thread_pool_max,
+ request_queue_size = self.server_adapter.socket_queue_size,
+ timeout = self.server_adapter.socket_timeout,
+ shutdown_timeout = self.server_adapter.shutdown_timeout,
+ )
+ self.protocol = self.server_adapter.protocol_version
+ self.nodelay = self.server_adapter.nodelay
+
+ ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
+ if self.server_adapter.ssl_context:
+ adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
+ self.ssl_adapter = adapter_class(
+ self.server_adapter.ssl_certificate,
+ self.server_adapter.ssl_private_key,
+ self.server_adapter.ssl_certificate_chain)
+ self.ssl_adapter.context = self.server_adapter.ssl_context
+ elif self.server_adapter.ssl_certificate:
+ adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
+ self.ssl_adapter = adapter_class(
+ self.server_adapter.ssl_certificate,
+ self.server_adapter.ssl_private_key,
+ self.server_adapter.ssl_certificate_chain)
diff --git a/cherrypy/cherryd b/cherrypy/cherryd
new file mode 100644
index 0000000..adb2a02
--- /dev/null
+++ b/cherrypy/cherryd
@@ -0,0 +1,109 @@
+#! /usr/bin/env python
+"""The CherryPy daemon."""
+
+import sys
+
+import cherrypy
+from cherrypy.process import plugins, servers
+from cherrypy import Application
+
+def start(configfiles=None, daemonize=False, environment=None,
+ fastcgi=False, scgi=False, pidfile=None, imports=None,
+ cgi=False):
+ """Subscribe all engine plugins and start the engine."""
+ sys.path = [''] + sys.path
+ for i in imports or []:
+ exec("import %s" % i)
+
+ for c in configfiles or []:
+ cherrypy.config.update(c)
+ # If there's only one app mounted, merge config into it.
+ if len(cherrypy.tree.apps) == 1:
+ for app in cherrypy.tree.apps.values():
+ if isinstance(app, Application):
+ app.merge(c)
+
+ engine = cherrypy.engine
+
+ if environment is not None:
+ cherrypy.config.update({'environment': environment})
+
+ # Only daemonize if asked to.
+ if daemonize:
+ # Don't print anything to stdout/sterr.
+ cherrypy.config.update({'log.screen': False})
+ plugins.Daemonizer(engine).subscribe()
+
+ if pidfile:
+ plugins.PIDFile(engine, pidfile).subscribe()
+
+ if hasattr(engine, "signal_handler"):
+ engine.signal_handler.subscribe()
+ if hasattr(engine, "console_control_handler"):
+ engine.console_control_handler.subscribe()
+
+ if (fastcgi and (scgi or cgi)) or (scgi and cgi):
+ cherrypy.log.error("You may only specify one of the cgi, fastcgi, and "
+ "scgi options.", 'ENGINE')
+ sys.exit(1)
+ elif fastcgi or scgi or cgi:
+ # Turn off autoreload when using *cgi.
+ cherrypy.config.update({'engine.autoreload_on': False})
+ # Turn off the default HTTP server (which is subscribed by default).
+ cherrypy.server.unsubscribe()
+
+ addr = cherrypy.server.bind_addr
+ if fastcgi:
+ f = servers.FlupFCGIServer(application=cherrypy.tree,
+ bindAddress=addr)
+ elif scgi:
+ f = servers.FlupSCGIServer(application=cherrypy.tree,
+ bindAddress=addr)
+ else:
+ f = servers.FlupCGIServer(application=cherrypy.tree,
+ bindAddress=addr)
+ s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr)
+ s.subscribe()
+
+ # Always start the engine; this will start all other services
+ try:
+ engine.start()
+ except:
+ # Assume the error has been logged already via bus.log.
+ sys.exit(1)
+ else:
+ engine.block()
+
+
+if __name__ == '__main__':
+ from optparse import OptionParser
+
+ p = OptionParser()
+ p.add_option('-c', '--config', action="append", dest='config',
+ help="specify config file(s)")
+ p.add_option('-d', action="store_true", dest='daemonize',
+ help="run the server as a daemon")
+ p.add_option('-e', '--environment', dest='environment', default=None,
+ help="apply the given config environment")
+ p.add_option('-f', action="store_true", dest='fastcgi',
+ help="start a fastcgi server instead of the default HTTP server")
+ p.add_option('-s', action="store_true", dest='scgi',
+ help="start a scgi server instead of the default HTTP server")
+ p.add_option('-x', action="store_true", dest='cgi',
+ help="start a cgi server instead of the default HTTP server")
+ p.add_option('-i', '--import', action="append", dest='imports',
+ help="specify modules to import")
+ p.add_option('-p', '--pidfile', dest='pidfile', default=None,
+ help="store the process id in the given file")
+ p.add_option('-P', '--Path', action="append", dest='Path',
+ help="add the given paths to sys.path")
+ options, args = p.parse_args()
+
+ if options.Path:
+ for p in options.Path:
+ sys.path.insert(0, p)
+
+ start(options.config, options.daemonize,
+ options.environment, options.fastcgi, options.scgi,
+ options.pidfile, options.imports, options.cgi)
+
diff --git a/cherrypy/favicon.ico b/cherrypy/favicon.ico
new file mode 100644
index 0000000..f0d7e61
--- /dev/null
+++ b/cherrypy/favicon.ico
Binary files differ
diff --git a/cherrypy/lib/__init__.py b/cherrypy/lib/__init__.py
new file mode 100755
index 0000000..611350c
--- /dev/null
+++ b/cherrypy/lib/__init__.py
@@ -0,0 +1,45 @@
+"""CherryPy Library"""
+
+# Deprecated in CherryPy 3.2 -- remove in CherryPy 3.3
+from cherrypy.lib.reprconf import _Builder, unrepr, modules, attributes
+
+class file_generator(object):
+ """Yield the given input (a file object) in chunks (default 64k). (Core)"""
+
+ def __init__(self, input, chunkSize=65536):
+ self.input = input
+ self.chunkSize = chunkSize
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ chunk = self.input.read(self.chunkSize)
+ if chunk:
+ return chunk
+ else:
+ if hasattr(self.input, 'close'):
+ self.input.close()
+ raise StopIteration()
+ next = __next__
+
+def file_generator_limited(fileobj, count, chunk_size=65536):
+ """Yield the given file object in chunks, stopping after `count`
+ bytes has been emitted. Default chunk size is 64kB. (Core)
+ """
+ remaining = count
+ while remaining > 0:
+ chunk = fileobj.read(min(chunk_size, remaining))
+ chunklen = len(chunk)
+ if chunklen == 0:
+ return
+ remaining -= chunklen
+ yield chunk
+
+def set_vary_header(response, header_name):
+ "Add a Vary header to a response"
+ varies = response.headers.get("Vary", "")
+ varies = [x.strip() for x in varies.split(",") if x.strip()]
+ if header_name not in varies:
+ varies.append(header_name)
+ response.headers['Vary'] = ", ".join(varies)
diff --git a/cherrypy/lib/auth.py b/cherrypy/lib/auth.py
new file mode 100755
index 0000000..7d2f6dc
--- /dev/null
+++ b/cherrypy/lib/auth.py
@@ -0,0 +1,87 @@
+import cherrypy
+from cherrypy.lib import httpauth
+
+
+def check_auth(users, encrypt=None, realm=None):
+ """If an authorization header contains credentials, return True, else False."""
+ request = cherrypy.serving.request
+ if 'authorization' in request.headers:
+ # make sure the provided credentials are correctly set
+ ah = httpauth.parseAuthorization(request.headers['authorization'])
+ if ah is None:
+ raise cherrypy.HTTPError(400, 'Bad Request')
+
+ if not encrypt:
+ encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5]
+
+ if hasattr(users, '__call__'):
+ try:
+ # backward compatibility
+ users = users() # expect it to return a dictionary
+
+ if not isinstance(users, dict):
+ raise ValueError("Authentication users must be a dictionary")
+
+ # fetch the user password
+ password = users.get(ah["username"], None)
+ except TypeError:
+ # returns a password (encrypted or clear text)
+ password = users(ah["username"])
+ else:
+ if not isinstance(users, dict):
+ raise ValueError("Authentication users must be a dictionary")
+
+ # fetch the user password
+ password = users.get(ah["username"], None)
+
+ # validate the authorization by re-computing it here
+ # and compare it with what the user-agent provided
+ if httpauth.checkResponse(ah, password, method=request.method,
+ encrypt=encrypt, realm=realm):
+ request.login = ah["username"]
+ return True
+
+ request.login = False
+ return False
+
+def basic_auth(realm, users, encrypt=None, debug=False):
+ """If auth fails, raise 401 with a basic authentication header.
+
+ realm
+ A string containing the authentication realm.
+
+ users
+ A dict of the form: {username: password} or a callable returning a dict.
+
+ encrypt
+ callable used to encrypt the password returned from the user-agent.
+ if None it defaults to a md5 encryption.
+
+ """
+ if check_auth(users, encrypt):
+ if debug:
+ cherrypy.log('Auth successful', 'TOOLS.BASIC_AUTH')
+ return
+
+ # inform the user-agent this path is protected
+ cherrypy.serving.response.headers['www-authenticate'] = httpauth.basicAuth(realm)
+
+ raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
+
+def digest_auth(realm, users, debug=False):
+ """If auth fails, raise 401 with a digest authentication header.
+
+ realm
+ A string containing the authentication realm.
+ users
+ A dict of the form: {username: password} or a callable returning a dict.
+ """
+ if check_auth(users, realm=realm):
+ if debug:
+ cherrypy.log('Auth successful', 'TOOLS.DIGEST_AUTH')
+ return
+
+ # inform the user-agent this path is protected
+ cherrypy.serving.response.headers['www-authenticate'] = httpauth.digestAuth(realm)
+
+ raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
diff --git a/cherrypy/lib/auth_basic.py b/cherrypy/lib/auth_basic.py
new file mode 100755
index 0000000..2c05e01
--- /dev/null
+++ b/cherrypy/lib/auth_basic.py
@@ -0,0 +1,87 @@
+# This file is part of CherryPy <http://www.cherrypy.org/>
+# -*- coding: utf-8 -*-
+# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
+
+__doc__ = """This module provides a CherryPy 3.x tool which implements
+the server-side of HTTP Basic Access Authentication, as described in :rfc:`2617`.
+
+Example usage, using the built-in checkpassword_dict function which uses a dict
+as the credentials store::
+
+ userpassdict = {'bird' : 'bebop', 'ornette' : 'wayout'}
+ checkpassword = cherrypy.lib.auth_basic.checkpassword_dict(userpassdict)
+ basic_auth = {'tools.auth_basic.on': True,
+ 'tools.auth_basic.realm': 'earth',
+ 'tools.auth_basic.checkpassword': checkpassword,
+ }
+ app_config = { '/' : basic_auth }
+
+"""
+
+__author__ = 'visteya'
+__date__ = 'April 2009'
+
+import binascii
+from cherrypy._cpcompat import base64_decode
+import cherrypy
+
+
+def checkpassword_dict(user_password_dict):
+ """Returns a checkpassword function which checks credentials
+ against a dictionary of the form: {username : password}.
+
+ If you want a simple dictionary-based authentication scheme, use
+ checkpassword_dict(my_credentials_dict) as the value for the
+ checkpassword argument to basic_auth().
+ """
+ def checkpassword(realm, user, password):
+ p = user_password_dict.get(user)
+ return p and p == password or False
+
+ return checkpassword
+
+
+def basic_auth(realm, checkpassword, debug=False):
+ """A CherryPy tool which hooks at before_handler to perform
+ HTTP Basic Access Authentication, as specified in :rfc:`2617`.
+
+ If the request has an 'authorization' header with a 'Basic' scheme, this
+ tool attempts to authenticate the credentials supplied in that header. If
+ the request has no 'authorization' header, or if it does but the scheme is
+ not 'Basic', or if authentication fails, the tool sends a 401 response with
+ a 'WWW-Authenticate' Basic header.
+
+ realm
+ A string containing the authentication realm.
+
+ checkpassword
+ A callable which checks the authentication credentials.
+ Its signature is checkpassword(realm, username, password). where
+ username and password are the values obtained from the request's
+ 'authorization' header. If authentication succeeds, checkpassword
+ returns True, else it returns False.
+
+ """
+
+ if '"' in realm:
+ raise ValueError('Realm cannot contain the " (quote) character.')
+ request = cherrypy.serving.request
+
+ auth_header = request.headers.get('authorization')
+ if auth_header is not None:
+ try:
+ scheme, params = auth_header.split(' ', 1)
+ if scheme.lower() == 'basic':
+ username, password = base64_decode(params).split(':', 1)
+ if checkpassword(realm, username, password):
+ if debug:
+ cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC')
+ request.login = username
+ return # successful authentication
+ except (ValueError, binascii.Error): # split() error, base64.decodestring() error
+ raise cherrypy.HTTPError(400, 'Bad Request')
+
+ # Respond with 401 status and a WWW-Authenticate header
+ cherrypy.serving.response.headers['www-authenticate'] = 'Basic realm="%s"' % realm
+ raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
+
diff --git a/cherrypy/lib/auth_digest.py b/cherrypy/lib/auth_digest.py
new file mode 100755
index 0000000..67578e0
--- /dev/null
+++ b/cherrypy/lib/auth_digest.py
@@ -0,0 +1,365 @@
+# This file is part of CherryPy <http://www.cherrypy.org/>
+# -*- coding: utf-8 -*-
+# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
+
+__doc__ = """An implementation of the server-side of HTTP Digest Access
+Authentication, which is described in :rfc:`2617`.
+
+Example usage, using the built-in get_ha1_dict_plain function which uses a dict
+of plaintext passwords as the credentials store::
+
+ userpassdict = {'alice' : '4x5istwelve'}
+ get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(userpassdict)
+ digest_auth = {'tools.auth_digest.on': True,
+ 'tools.auth_digest.realm': 'wonderland',
+ 'tools.auth_digest.get_ha1': get_ha1,
+ 'tools.auth_digest.key': 'a565c27146791cfb',
+ }
+ app_config = { '/' : digest_auth }
+"""
+
+__author__ = 'visteya'
+__date__ = 'April 2009'
+
+
+import time
+from cherrypy._cpcompat import parse_http_list, parse_keqv_list
+
+import cherrypy
+from cherrypy._cpcompat import md5, ntob
+md5_hex = lambda s: md5(ntob(s)).hexdigest()
+
+qop_auth = 'auth'
+qop_auth_int = 'auth-int'
+valid_qops = (qop_auth, qop_auth_int)
+
+valid_algorithms = ('MD5', 'MD5-sess')
+
+
+def TRACE(msg):
+ cherrypy.log(msg, context='TOOLS.AUTH_DIGEST')
+
+# Three helper functions for users of the tool, providing three variants
+# of get_ha1() functions for three different kinds of credential stores.
+def get_ha1_dict_plain(user_password_dict):
+ """Returns a get_ha1 function which obtains a plaintext password from a
+ dictionary of the form: {username : password}.
+
+ If you want a simple dictionary-based authentication scheme, with plaintext
+ passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the
+ get_ha1 argument to digest_auth().
+ """
+ def get_ha1(realm, username):
+ password = user_password_dict.get(username)
+ if password:
+ return md5_hex('%s:%s:%s' % (username, realm, password))
+ return None
+
+ return get_ha1
+
+def get_ha1_dict(user_ha1_dict):
+ """Returns a get_ha1 function which obtains a HA1 password hash from a
+ dictionary of the form: {username : HA1}.
+
+ If you want a dictionary-based authentication scheme, but with
+ pre-computed HA1 hashes instead of plain-text passwords, use
+ get_ha1_dict(my_userha1_dict) as the value for the get_ha1
+ argument to digest_auth().
+ """
+ def get_ha1(realm, username):
+ return user_ha1_dict.get(user)
+
+ return get_ha1
+
+def get_ha1_file_htdigest(filename):
+ """Returns a get_ha1 function which obtains a HA1 password hash from a
+ flat file with lines of the same format as that produced by the Apache
+ htdigest utility. For example, for realm 'wonderland', username 'alice',
+ and password '4x5istwelve', the htdigest line would be::
+
+ alice:wonderland:3238cdfe91a8b2ed8e39646921a02d4c
+
+ If you want to use an Apache htdigest file as the credentials store,
+ then use get_ha1_file_htdigest(my_htdigest_file) as the value for the
+ get_ha1 argument to digest_auth(). It is recommended that the filename
+ argument be an absolute path, to avoid problems.
+ """
+ def get_ha1(realm, username):
+ result = None
+ f = open(filename, 'r')
+ for line in f:
+ u, r, ha1 = line.rstrip().split(':')
+ if u == username and r == realm:
+ result = ha1
+ break
+ f.close()
+ return result
+
+ return get_ha1
+
+
+def synthesize_nonce(s, key, timestamp=None):
+ """Synthesize a nonce value which resists spoofing and can be checked for staleness.
+ Returns a string suitable as the value for 'nonce' in the www-authenticate header.
+
+ s
+ A string related to the resource, such as the hostname of the server.
+
+ key
+ A secret string known only to the server.
+
+ timestamp
+ An integer seconds-since-the-epoch timestamp
+
+ """
+ if timestamp is None:
+ timestamp = int(time.time())
+ h = md5_hex('%s:%s:%s' % (timestamp, s, key))
+ nonce = '%s:%s' % (timestamp, h)
+ return nonce
+
+
+def H(s):
+ """The hash function H"""
+ return md5_hex(s)
+
+
+class HttpDigestAuthorization (object):
+ """Class to parse a Digest Authorization header and perform re-calculation
+ of the digest.
+ """
+
+ def errmsg(self, s):
+ return 'Digest Authorization header: %s' % s
+
+ def __init__(self, auth_header, http_method, debug=False):
+ self.http_method = http_method
+ self.debug = debug
+ scheme, params = auth_header.split(" ", 1)
+ self.scheme = scheme.lower()
+ if self.scheme != 'digest':
+ raise ValueError('Authorization scheme is not "Digest"')
+
+ self.auth_header = auth_header
+
+ # make a dict of the params
+ items = parse_http_list(params)
+ paramsd = parse_keqv_list(items)
+
+ self.realm = paramsd.get('realm')
+ self.username = paramsd.get('username')
+ self.nonce = paramsd.get('nonce')
+ self.uri = paramsd.get('uri')
+ self.method = paramsd.get('method')
+ self.response = paramsd.get('response') # the response digest
+ self.algorithm = paramsd.get('algorithm', 'MD5')
+ self.cnonce = paramsd.get('cnonce')
+ self.opaque = paramsd.get('opaque')
+ self.qop = paramsd.get('qop') # qop
+ self.nc = paramsd.get('nc') # nonce count
+
+ # perform some correctness checks
+ if self.algorithm not in valid_algorithms:
+ raise ValueError(self.errmsg("Unsupported value for algorithm: '%s'" % self.algorithm))
+
+ has_reqd = self.username and \
+ self.realm and \
+ self.nonce and \
+ self.uri and \
+ self.response
+ if not has_reqd:
+ raise ValueError(self.errmsg("Not all required parameters are present."))
+
+ if self.qop:
+ if self.qop not in valid_qops:
+ raise ValueError(self.errmsg("Unsupported value for qop: '%s'" % self.qop))
+ if not (self.cnonce and self.nc):
+ raise ValueError(self.errmsg("If qop is sent then cnonce and nc MUST be present"))
+ else:
+ if self.cnonce or self.nc:
+ raise ValueError(self.errmsg("If qop is not sent, neither cnonce nor nc can be present"))
+
+
+ def __str__(self):
+ return 'authorization : %s' % self.auth_header
+
+ def validate_nonce(self, s, key):
+ """Validate the nonce.
+ Returns True if nonce was generated by synthesize_nonce() and the timestamp
+ is not spoofed, else returns False.
+
+ s
+ A string related to the resource, such as the hostname of the server.
+
+ key
+ A secret string known only to the server.
+
+ Both s and key must be the same values which were used to synthesize the nonce
+ we are trying to validate.
+ """
+ try:
+ timestamp, hashpart = self.nonce.split(':', 1)
+ s_timestamp, s_hashpart = synthesize_nonce(s, key, timestamp).split(':', 1)
+ is_valid = s_hashpart == hashpart
+ if self.debug:
+ TRACE('validate_nonce: %s' % is_valid)
+ return is_valid
+ except ValueError: # split() error
+ pass
+ return False
+
+
+ def is_nonce_stale(self, max_age_seconds=600):
+ """Returns True if a validated nonce is stale. The nonce contains a
+ timestamp in plaintext and also a secure hash of the timestamp. You should
+ first validate the nonce to ensure the plaintext timestamp is not spoofed.
+ """
+ try:
+ timestamp, hashpart = self.nonce.split(':', 1)
+ if int(timestamp) + max_age_seconds > int(time.time()):
+ return False
+ except ValueError: # int() error
+ pass
+ if self.debug:
+ TRACE("nonce is stale")
+ return True
+
+
+ def HA2(self, entity_body=''):
+ """Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3."""
+ # RFC 2617 3.2.2.3
+ # If the "qop" directive's value is "auth" or is unspecified, then A2 is:
+ # A2 = method ":" digest-uri-value
+ #
+ # If the "qop" value is "auth-int", then A2 is:
+ # A2 = method ":" digest-uri-value ":" H(entity-body)
+ if self.qop is None or self.qop == "auth":
+ a2 = '%s:%s' % (self.http_method, self.uri)
+ elif self.qop == "auth-int":
+ a2 = "%s:%s:%s" % (self.http_method, self.uri, H(entity_body))
+ else:
+ # in theory, this should never happen, since I validate qop in __init__()
+ raise ValueError(self.errmsg("Unrecognized value for qop!"))
+ return H(a2)
+
+
+ def request_digest(self, ha1, entity_body=''):
+ """Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1.
+
+ ha1
+ The HA1 string obtained from the credentials store.
+
+ entity_body
+ If 'qop' is set to 'auth-int', then A2 includes a hash
+ of the "entity body". The entity body is the part of the
+ message which follows the HTTP headers. See :rfc:`2617` section
+ 4.3. This refers to the entity the user agent sent in the request which
+ has the Authorization header. Typically GET requests don't have an entity,
+ and POST requests do.
+
+ """
+ ha2 = self.HA2(entity_body)
+ # Request-Digest -- RFC 2617 3.2.2.1
+ if self.qop:
+ req = "%s:%s:%s:%s:%s" % (self.nonce, self.nc, self.cnonce, self.qop, ha2)
+ else:
+ req = "%s:%s" % (self.nonce, ha2)
+
+ # RFC 2617 3.2.2.2
+ #
+ # If the "algorithm" directive's value is "MD5" or is unspecified, then A1 is:
+ # A1 = unq(username-value) ":" unq(realm-value) ":" passwd
+ #
+ # If the "algorithm" directive's value is "MD5-sess", then A1 is
+ # calculated only once - on the first request by the client following
+ # receipt of a WWW-Authenticate challenge from the server.
+ # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
+ # ":" unq(nonce-value) ":" unq(cnonce-value)
+ if self.algorithm == 'MD5-sess':
+ ha1 = H('%s:%s:%s' % (ha1, self.nonce, self.cnonce))
+
+ digest = H('%s:%s' % (ha1, req))
+ return digest
+
+
+
+def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth, stale=False):
+ """Constructs a WWW-Authenticate header for Digest authentication."""
+ if qop not in valid_qops:
+ raise ValueError("Unsupported value for qop: '%s'" % qop)
+ if algorithm not in valid_algorithms:
+ raise ValueError("Unsupported value for algorithm: '%s'" % algorithm)
+
+ if nonce is None:
+ nonce = synthesize_nonce(realm, key)
+ s = 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
+ realm, nonce, algorithm, qop)
+ if stale:
+ s += ', stale="true"'
+ return s
+
+
+def digest_auth(realm, get_ha1, key, debug=False):
+ """A CherryPy tool which hooks at before_handler to perform
+ HTTP Digest Access Authentication, as specified in :rfc:`2617`.
+
+ If the request has an 'authorization' header with a 'Digest' scheme, this
+ tool authenticates the credentials supplied in that header. If
+ the request has no 'authorization' header, or if it does but the scheme is
+ not "Digest", or if authentication fails, the tool sends a 401 response with
+ a 'WWW-Authenticate' Digest header.
+
+ realm
+ A string containing the authentication realm.
+
+ get_ha1
+ A callable which looks up a username in a credentials store
+ and returns the HA1 string, which is defined in the RFC to be
+ MD5(username : realm : password). The function's signature is:
+ ``get_ha1(realm, username)``
+ where username is obtained from the request's 'authorization' header.
+ If username is not found in the credentials store, get_ha1() returns
+ None.
+
+ key
+ A secret string known only to the server, used in the synthesis of nonces.
+
+ """
+ request = cherrypy.serving.request
+
+ auth_header = request.headers.get('authorization')
+ nonce_is_stale = False
+ if auth_header is not None:
+ try:
+ auth = HttpDigestAuthorization(auth_header, request.method, debug=debug)
+ except ValueError:
+ raise cherrypy.HTTPError(400, "The Authorization header could not be parsed.")
+
+ if debug:
+ TRACE(str(auth))
+
+ if auth.validate_nonce(realm, key):
+ ha1 = get_ha1(realm, auth.username)
+ if ha1 is not None:
+ # note that for request.body to be available we need to hook in at
+ # before_handler, not on_start_resource like 3.1.x digest_auth does.
+ digest = auth.request_digest(ha1, entity_body=request.body)
+ if digest == auth.response: # authenticated
+ if debug:
+ TRACE("digest matches auth.response")
+ # Now check if nonce is stale.
+ # The choice of ten minutes' lifetime for nonce is somewhat arbitrary
+ nonce_is_stale = auth.is_nonce_stale(max_age_seconds=600)
+ if not nonce_is_stale:
+ request.login = auth.username
+ if debug:
+ TRACE("authentication of %s successful" % auth.username)
+ return
+
+ # Respond with 401 status and a WWW-Authenticate header
+ header = www_authenticate(realm, key, stale=nonce_is_stale)
+ if debug:
+ TRACE(header)
+ cherrypy.serving.response.headers['WWW-Authenticate'] = header
+ raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
+
diff --git a/cherrypy/lib/caching.py b/cherrypy/lib/caching.py
new file mode 100755
index 0000000..435b9dc
--- /dev/null
+++ b/cherrypy/lib/caching.py
@@ -0,0 +1,465 @@
+"""
+CherryPy implements a simple caching system as a pluggable Tool. This tool tries
+to be an (in-process) HTTP/1.1-compliant cache. It's not quite there yet, but
+it's probably good enough for most sites.
+
+In general, GET responses are cached (along with selecting headers) and, if
+another request arrives for the same resource, the caching Tool will return 304
+Not Modified if possible, or serve the cached response otherwise. It also sets
+request.cached to True if serving a cached representation, and sets
+request.cacheable to False (so it doesn't get cached again).
+
+If POST, PUT, or DELETE requests are made for a cached resource, they invalidate
+(delete) any cached response.
+
+Usage
+=====
+
+Configuration file example::
+
+ [/]
+ tools.caching.on = True
+ tools.caching.delay = 3600
+
+You may use a class other than the default
+:class:`MemoryCache<cherrypy.lib.caching.MemoryCache>` by supplying the config
+entry ``cache_class``; supply the full dotted name of the replacement class
+as the config value. It must implement the basic methods ``get``, ``put``,
+``delete``, and ``clear``.
+
+You may set any attribute, including overriding methods, on the cache
+instance by providing them in config. The above sets the
+:attr:`delay<cherrypy.lib.caching.MemoryCache.delay>` attribute, for example.
+"""
+
+import datetime
+import sys
+import threading
+import time
+
+import cherrypy
+from cherrypy.lib import cptools, httputil
+from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted
+
+
+class Cache(object):
+ """Base class for Cache implementations."""
+
+ def get(self):
+ """Return the current variant if in the cache, else None."""
+ raise NotImplemented
+
+ def put(self, obj, size):
+ """Store the current variant in the cache."""
+ raise NotImplemented
+
+ def delete(self):
+ """Remove ALL cached variants of the current resource."""
+ raise NotImplemented
+
+ def clear(self):
+ """Reset the cache to its initial, empty state."""
+ raise NotImplemented
+
+
+
+# ------------------------------- Memory Cache ------------------------------- #
+
+
+class AntiStampedeCache(dict):
+ """A storage system for cached items which reduces stampede collisions."""
+
+ def wait(self, key, timeout=5, debug=False):
+ """Return the cached value for the given key, or None.
+
+ If timeout is not None, and the value is already
+ being calculated by another thread, wait until the given timeout has
+ elapsed. If the value is available before the timeout expires, it is
+ returned. If not, None is returned, and a sentinel placed in the cache
+ to signal other threads to wait.
+
+ If timeout is None, no waiting is performed nor sentinels used.
+ """
+ value = self.get(key)
+ if isinstance(value, threading._Event):
+ if timeout is None:
+ # Ignore the other thread and recalc it ourselves.
+ if debug:
+ cherrypy.log('No timeout', 'TOOLS.CACHING')
+ return None
+
+ # Wait until it's done or times out.
+ if debug:
+ cherrypy.log('Waiting up to %s seconds' % timeout, 'TOOLS.CACHING')
+ value.wait(timeout)
+ if value.result is not None:
+ # The other thread finished its calculation. Use it.
+ if debug:
+ cherrypy.log('Result!', 'TOOLS.CACHING')
+ return value.result
+ # Timed out. Stick an Event in the slot so other threads wait
+ # on this one to finish calculating the value.
+ if debug:
+ cherrypy.log('Timed out', 'TOOLS.CACHING')
+ e = threading.Event()
+ e.result = None
+ dict.__setitem__(self, key, e)
+
+ return None
+ elif value is None:
+ # Stick an Event in the slot so other threads wait
+ # on this one to finish calculating the value.
+ if debug:
+ cherrypy.log('Timed out', 'TOOLS.CACHING')
+ e = threading.Event()
+ e.result = None
+ dict.__setitem__(self, key, e)
+ return value
+
+ def __setitem__(self, key, value):
+ """Set the cached value for the given key."""
+ existing = self.get(key)
+ dict.__setitem__(self, key, value)
+ if isinstance(existing, threading._Event):
+ # Set Event.result so other threads waiting on it have
+ # immediate access without needing to poll the cache again.
+ existing.result = value
+ existing.set()
+
+
+class MemoryCache(Cache):
+ """An in-memory cache for varying response content.
+
+ Each key in self.store is a URI, and each value is an AntiStampedeCache.
+ The response for any given URI may vary based on the values of
+ "selecting request headers"; that is, those named in the Vary
+ response header. We assume the list of header names to be constant
+ for each URI throughout the lifetime of the application, and store
+ that list in ``self.store[uri].selecting_headers``.
+
+ The items contained in ``self.store[uri]`` have keys which are tuples of
+ request header values (in the same order as the names in its
+ selecting_headers), and values which are the actual responses.
+ """
+
+ maxobjects = 1000
+ """The maximum number of cached objects; defaults to 1000."""
+
+ maxobj_size = 100000
+ """The maximum size of each cached object in bytes; defaults to 100 KB."""
+
+ maxsize = 10000000
+ """The maximum size of the entire cache in bytes; defaults to 10 MB."""
+
+ delay = 600
+ """Seconds until the cached content expires; defaults to 600 (10 minutes)."""
+
+ antistampede_timeout = 5
+ """Seconds to wait for other threads to release a cache lock."""
+
+ expire_freq = 0.1
+ """Seconds to sleep between cache expiration sweeps."""
+
+ debug = False
+
+ def __init__(self):
+ self.clear()
+
+ # Run self.expire_cache in a separate daemon thread.
+ t = threading.Thread(target=self.expire_cache, name='expire_cache')
+ self.expiration_thread = t
+ set_daemon(t, True)
+ t.start()
+
+ def clear(self):
+ """Reset the cache to its initial, empty state."""
+ self.store = {}
+ self.expirations = {}
+ self.tot_puts = 0
+ self.tot_gets = 0
+ self.tot_hist = 0
+ self.tot_expires = 0
+ self.tot_non_modified = 0
+ self.cursize = 0
+
+ def expire_cache(self):
+ """Continuously examine cached objects, expiring stale ones.
+
+ This function is designed to be run in its own daemon thread,
+ referenced at ``self.expiration_thread``.
+ """
+ # It's possible that "time" will be set to None
+ # arbitrarily, so we check "while time" to avoid exceptions.
+ # See tickets #99 and #180 for more information.
+ while time:
+ now = time.time()
+ # Must make a copy of expirations so it doesn't change size
+ # during iteration
+ for expiration_time, objects in copyitems(self.expirations):
+ if expiration_time <= now:
+ for obj_size, uri, sel_header_values in objects:
+ try:
+ del self.store[uri][tuple(sel_header_values)]
+ self.tot_expires += 1
+ self.cursize -= obj_size
+ except KeyError:
+ # the key may have been deleted elsewhere
+ pass
+ del self.expirations[expiration_time]
+ time.sleep(self.expire_freq)
+
+ def get(self):
+ """Return the current variant if in the cache, else None."""
+ request = cherrypy.serving.request
+ self.tot_gets += 1
+
+ uri = cherrypy.url(qs=request.query_string)
+ uricache = self.store.get(uri)
+ if uricache is None:
+ return None
+
+ header_values = [request.headers.get(h, '')
+ for h in uricache.selecting_headers]
+ variant = uricache.wait(key=tuple(sorted(header_values)),
+ timeout=self.antistampede_timeout,
+ debug=self.debug)
+ if variant is not None:
+ self.tot_hist += 1
+ return variant
+
+ def put(self, variant, size):
+ """Store the current variant in the cache."""
+ request = cherrypy.serving.request
+ response = cherrypy.serving.response
+
+ uri = cherrypy.url(qs=request.query_string)
+ uricache = self.store.get(uri)
+ if uricache is None:
+ uricache = AntiStampedeCache()
+ uricache.selecting_headers = [
+ e.value for e in response.headers.elements('Vary')]
+ self.store[uri] = uricache
+
+ if len(self.store) < self.maxobjects:
+ total_size = self.cursize + size
+
+ # checks if there's space for the object
+ if (size < self.maxobj_size and total_size < self.maxsize):
+ # add to the expirations list
+ expiration_time = response.time + self.delay
+ bucket = self.expirations.setdefault(expiration_time, [])
+ bucket.append((size, uri, uricache.selecting_headers))
+
+ # add to the cache
+ header_values = [request.headers.get(h, '')
+ for h in uricache.selecting_headers]
+ uricache[tuple(sorted(header_values))] = variant
+ self.tot_puts += 1
+ self.cursize = total_size
+
+ def delete(self):
+ """Remove ALL cached variants of the current resource."""
+ uri = cherrypy.url(qs=cherrypy.serving.request.query_string)
+ self.store.pop(uri, None)
+
+
+def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
+ """Try to obtain cached output. If fresh enough, raise HTTPError(304).
+
+ If POST, PUT, or DELETE:
+ * invalidates (deletes) any cached response for this resource
+ * sets request.cached = False
+ * sets request.cacheable = False
+
+ else if a cached copy exists:
+ * sets request.cached = True
+ * sets request.cacheable = False
+ * sets response.headers to the cached values
+ * checks the cached Last-Modified response header against the
+ current If-(Un)Modified-Since request headers; raises 304
+ if necessary.
+ * sets response.status and response.body to the cached values
+ * returns True
+
+ otherwise:
+ * sets request.cached = False
+ * sets request.cacheable = True
+ * returns False
+ """
+ request = cherrypy.serving.request
+ response = cherrypy.serving.response
+
+ if not hasattr(cherrypy, "_cache"):
+ # Make a process-wide Cache object.
+ cherrypy._cache = kwargs.pop("cache_class", MemoryCache)()
+
+ # Take all remaining kwargs and set them on the Cache object.
+ for k, v in kwargs.items():
+ setattr(cherrypy._cache, k, v)
+ cherrypy._cache.debug = debug
+
+ # POST, PUT, DELETE should invalidate (delete) the cached copy.
+ # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
+ if request.method in invalid_methods:
+ if debug:
+ cherrypy.log('request.method %r in invalid_methods %r' %
+ (request.method, invalid_methods), 'TOOLS.CACHING')
+ cherrypy._cache.delete()
+ request.cached = False
+ request.cacheable = False
+ return False
+
+ if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]:
+ request.cached = False
+ request.cacheable = True
+ return False
+
+ cache_data = cherrypy._cache.get()
+ request.cached = bool(cache_data)
+ request.cacheable = not request.cached
+ if request.cached:
+ # Serve the cached copy.
+ max_age = cherrypy._cache.delay
+ for v in [e.value for e in request.headers.elements('Cache-Control')]:
+ atoms = v.split('=', 1)
+ directive = atoms.pop(0)
+ if directive == 'max-age':
+ if len(atoms) != 1 or not atoms[0].isdigit():
+ raise cherrypy.HTTPError(400, "Invalid Cache-Control header")
+ max_age = int(atoms[0])
+ break
+ elif directive == 'no-cache':
+ if debug:
+ cherrypy.log('Ignoring cache due to Cache-Control: no-cache',
+ 'TOOLS.CACHING')
+ request.cached = False
+ request.cacheable = True
+ return False
+
+ if debug:
+ cherrypy.log('Reading response from cache', 'TOOLS.CACHING')
+ s, h, b, create_time = cache_data
+ age = int(response.time - create_time)
+ if (age > max_age):
+ if debug:
+ cherrypy.log('Ignoring cache due to age > %d' % max_age,
+ 'TOOLS.CACHING')
+ request.cached = False
+ request.cacheable = True
+ return False
+
+ # Copy the response headers. See http://www.cherrypy.org/ticket/721.
+ response.headers = rh = httputil.HeaderMap()
+ for k in h:
+ dict.__setitem__(rh, k, dict.__getitem__(h, k))
+
+ # Add the required Age header
+ response.headers["Age"] = str(age)
+
+ try:
+ # Note that validate_since depends on a Last-Modified header;
+ # this was put into the cached copy, and should have been
+ # resurrected just above (response.headers = cache_data[1]).
+ cptools.validate_since()
+ except cherrypy.HTTPRedirect:
+ x = sys.exc_info()[1]
+ if x.status == 304:
+ cherrypy._cache.tot_non_modified += 1
+ raise
+
+ # serve it & get out from the request
+ response.status = s
+ response.body = b
+ else:
+ if debug:
+ cherrypy.log('request is not cached', 'TOOLS.CACHING')
+ return request.cached
+
+
+def tee_output():
+ """Tee response output to cache storage. Internal."""
+ # Used by CachingTool by attaching to request.hooks
+
+ request = cherrypy.serving.request
+ if 'no-store' in request.headers.values('Cache-Control'):
+ return
+
+ def tee(body):
+ """Tee response.body into a list."""
+ if ('no-cache' in response.headers.values('Pragma') or
+ 'no-store' in response.headers.values('Cache-Control')):
+ for chunk in body:
+ yield chunk
+ return
+
+ output = []
+ for chunk in body:
+ output.append(chunk)
+ yield chunk
+
+ # save the cache data
+ body = ntob('').join(output)
+ cherrypy._cache.put((response.status, response.headers or {},
+ body, response.time), len(body))
+
+ response = cherrypy.serving.response
+ response.body = tee(response.body)
+
+
+def expires(secs=0, force=False, debug=False):
+ """Tool for influencing cache mechanisms using the 'Expires' header.
+
+ secs
+ Must be either an int or a datetime.timedelta, and indicates the
+ number of seconds between response.time and when the response should
+ expire. The 'Expires' header will be set to response.time + secs.
+ If secs is zero, the 'Expires' header is set one year in the past, and
+ the following "cache prevention" headers are also set:
+
+ * Pragma: no-cache
+ * Cache-Control': no-cache, must-revalidate
+
+ force
+ If False, the following headers are checked:
+
+ * Etag
+ * Last-Modified
+ * Age
+ * Expires
+
+ If any are already present, none of the above response headers are set.
+
+ """
+
+ response = cherrypy.serving.response
+ headers = response.headers
+
+ cacheable = False
+ if not force:
+ # some header names that indicate that the response can be cached
+ for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'):
+ if indicator in headers:
+ cacheable = True
+ break
+
+ if not cacheable and not force:
+ if debug:
+ cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES')
+ else:
+ if debug:
+ cherrypy.log('request is cacheable', 'TOOLS.EXPIRES')
+ if isinstance(secs, datetime.timedelta):
+ secs = (86400 * secs.days) + secs.seconds
+
+ if secs == 0:
+ if force or ("Pragma" not in headers):
+ headers["Pragma"] = "no-cache"
+ if cherrypy.serving.request.protocol >= (1, 1):
+ if force or "Cache-Control" not in headers:
+ headers["Cache-Control"] = "no-cache, must-revalidate"
+ # Set an explicit Expires date in the past.
+ expiry = httputil.HTTPDate(1169942400.0)
+ else:
+ expiry = httputil.HTTPDate(response.time + secs)
+ if force or "Expires" not in headers:
+ headers["Expires"] = expiry
diff --git a/cherrypy/lib/covercp.py b/cherrypy/lib/covercp.py
new file mode 100755
index 0000000..9b701b5
--- /dev/null
+++ b/cherrypy/lib/covercp.py
@@ -0,0 +1,365 @@
+"""Code-coverage tools for CherryPy.
+
+To use this module, or the coverage tools in the test suite,
+you need to download 'coverage.py', either Gareth Rees' `original
+implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_
+or Ned Batchelder's `enhanced version:
+<http://www.nedbatchelder.com/code/modules/coverage.html>`_
+
+To turn on coverage tracing, use the following code::
+
+ cherrypy.engine.subscribe('start', covercp.start)
+
+DO NOT subscribe anything on the 'start_thread' channel, as previously
+recommended. Calling start once in the main thread should be sufficient
+to start coverage on all threads. Calling start again in each thread
+effectively clears any coverage data gathered up to that point.
+
+Run your code, then use the ``covercp.serve()`` function to browse the
+results in a web browser. If you run this module from the command line,
+it will call ``serve()`` for you.
+"""
+
+import re
+import sys
+import cgi
+from cherrypy._cpcompat import quote_plus
+import os, os.path
+localFile = os.path.join(os.path.dirname(__file__), "coverage.cache")
+
+the_coverage = None
+try:
+ from coverage import coverage
+ the_coverage = coverage(data_file=localFile)
+ def start():
+ the_coverage.start()
+except ImportError:
+ # Setting the_coverage to None will raise errors
+ # that need to be trapped downstream.
+ the_coverage = None
+
+ import warnings
+ warnings.warn("No code coverage will be performed; coverage.py could not be imported.")
+
+ def start():
+ pass
+start.priority = 20
+
+TEMPLATE_MENU = """<html>
+<head>
+ <title>CherryPy Coverage Menu</title>
+ <style>
+ body {font: 9pt Arial, serif;}
+ #tree {
+ font-size: 8pt;
+ font-family: Andale Mono, monospace;
+ white-space: pre;
+ }
+ #tree a:active, a:focus {
+ background-color: black;
+ padding: 1px;
+ color: white;
+ border: 0px solid #9999FF;
+ -moz-outline-style: none;
+ }
+ .fail { color: red;}
+ .pass { color: #888;}
+ #pct { text-align: right;}
+ h3 {
+ font-size: small;
+ font-weight: bold;
+ font-style: italic;
+ margin-top: 5px;
+ }
+ input { border: 1px solid #ccc; padding: 2px; }
+ .directory {
+ color: #933;
+ font-style: italic;
+ font-weight: bold;
+ font-size: 10pt;
+ }
+ .file {
+ color: #400;
+ }
+ a { text-decoration: none; }
+ #crumbs {
+ color: white;
+ font-size: 8pt;
+ font-family: Andale Mono, monospace;
+ width: 100%;
+ background-color: black;
+ }
+ #crumbs a {
+ color: #f88;
+ }
+ #options {
+ line-height: 2.3em;
+ border: 1px solid black;
+ background-color: #eee;
+ padding: 4px;
+ }
+ #exclude {
+ width: 100%;
+ margin-bottom: 3px;
+ border: 1px solid #999;
+ }
+ #submit {
+ background-color: black;
+ color: white;
+ border: 0;
+ margin-bottom: -9px;
+ }
+ </style>
+</head>
+<body>
+<h2>CherryPy Coverage</h2>"""
+
+TEMPLATE_FORM = """
+<div id="options">
+<form action='menu' method=GET>
+ <input type='hidden' name='base' value='%(base)s' />
+ Show percentages <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
+ Hide files over <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
+ Exclude files matching<br />
+ <input type='text' id='exclude' name='exclude' value='%(exclude)s' size='20' />
+ <br />
+
+ <input type='submit' value='Change view' id="submit"/>
+</form>
+</div>"""
+
+TEMPLATE_FRAMESET = """<html>
+<head><title>CherryPy coverage data</title></head>
+<frameset cols='250, 1*'>
+ <frame src='menu?base=%s' />
+ <frame name='main' src='' />
+</frameset>
+</html>
+"""
+
+TEMPLATE_COVERAGE = """<html>
+<head>
+ <title>Coverage for %(name)s</title>
+ <style>
+ h2 { margin-bottom: .25em; }
+ p { margin: .25em; }
+ .covered { color: #000; background-color: #fff; }
+ .notcovered { color: #fee; background-color: #500; }
+ .excluded { color: #00f; background-color: #fff; }
+ table .covered, table .notcovered, table .excluded
+ { font-family: Andale Mono, monospace;
+ font-size: 10pt; white-space: pre; }
+
+ .lineno { background-color: #eee;}
+ .notcovered .lineno { background-color: #000;}
+ table { border-collapse: collapse;
+ </style>
+</head>
+<body>
+<h2>%(name)s</h2>
+<p>%(fullpath)s</p>
+<p>Coverage: %(pc)s%%</p>"""
+
+TEMPLATE_LOC_COVERED = """<tr class="covered">
+ <td class="lineno">%s&nbsp;</td>
+ <td>%s</td>
+</tr>\n"""
+TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered">
+ <td class="lineno">%s&nbsp;</td>
+ <td>%s</td>
+</tr>\n"""
+TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
+ <td class="lineno">%s&nbsp;</td>
+ <td>%s</td>
+</tr>\n"""
+
+TEMPLATE_ITEM = "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
+
+def _percent(statements, missing):
+ s = len(statements)
+ e = s - len(missing)
+ if s > 0:
+ return int(round(100.0 * e / s))
+ return 0
+
+def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
+ coverage=the_coverage):
+
+ # Show the directory name and any of our children
+ dirs = [k for k, v in root.items() if v]
+ dirs.sort()
+ for name in dirs:
+ newpath = os.path.join(path, name)
+
+ if newpath.lower().startswith(base):
+ relpath = newpath[len(base):]
+ yield "| " * relpath.count(os.sep)
+ yield "<a class='directory' href='menu?base=%s&exclude=%s'>%s</a>\n" % \
+ (newpath, quote_plus(exclude), name)
+
+ for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude, coverage=coverage):
+ yield chunk
+
+ # Now list the files
+ if path.lower().startswith(base):
+ relpath = path[len(base):]
+ files = [k for k, v in root.items() if not v]
+ files.sort()
+ for name in files:
+ newpath = os.path.join(path, name)
+
+ pc_str = ""
+ if showpct:
+ try:
+ _, statements, _, missing, _ = coverage.analysis2(newpath)
+ except:
+ # Yes, we really want to pass on all errors.
+ pass
+ else:
+ pc = _percent(statements, missing)
+ pc_str = ("%3d%% " % pc).replace(' ','&nbsp;')
+ if pc < float(pct) or pc == -1:
+ pc_str = "<span class='fail'>%s</span>" % pc_str
+ else:
+ pc_str = "<span class='pass'>%s</span>" % pc_str
+
+ yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1),
+ pc_str, newpath, name)
+
+def _skip_file(path, exclude):
+ if exclude:
+ return bool(re.search(exclude, path))
+
+def _graft(path, tree):
+ d = tree
+
+ p = path
+ atoms = []
+ while True:
+ p, tail = os.path.split(p)
+ if not tail:
+ break
+ atoms.append(tail)
+ atoms.append(p)
+ if p != "/":
+ atoms.append("/")
+
+ atoms.reverse()
+ for node in atoms:
+ if node:
+ d = d.setdefault(node, {})
+
+def get_tree(base, exclude, coverage=the_coverage):
+ """Return covered module names as a nested dict."""
+ tree = {}
+ runs = coverage.data.executed_files()
+ for path in runs:
+ if not _skip_file(path, exclude) and not os.path.isdir(path):
+ _graft(path, tree)
+ return tree
+
+class CoverStats(object):
+
+ def __init__(self, coverage, root=None):
+ self.coverage = coverage
+ if root is None:
+ # Guess initial depth. Files outside this path will not be
+ # reachable from the web interface.
+ import cherrypy
+ root = os.path.dirname(cherrypy.__file__)
+ self.root = root
+
+ def index(self):
+ return TEMPLATE_FRAMESET % self.root.lower()
+ index.exposed = True
+
+ def menu(self, base="/", pct="50", showpct="",
+ exclude=r'python\d\.\d|test|tut\d|tutorial'):
+
+ # The coverage module uses all-lower-case names.
+ base = base.lower().rstrip(os.sep)
+
+ yield TEMPLATE_MENU
+ yield TEMPLATE_FORM % locals()
+
+ # Start by showing links for parent paths
+ yield "<div id='crumbs'>"
+ path = ""
+ atoms = base.split(os.sep)
+ atoms.pop()
+ for atom in atoms:
+ path += atom + os.sep
+ yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
+ % (path, quote_plus(exclude), atom, os.sep))
+ yield "</div>"
+
+ yield "<div id='tree'>"
+
+ # Then display the tree
+ tree = get_tree(base, exclude, self.coverage)
+ if not tree:
+ yield "<p>No modules covered.</p>"
+ else:
+ for chunk in _show_branch(tree, base, "/", pct,
+ showpct=='checked', exclude, coverage=self.coverage):
+ yield chunk
+
+ yield "</div>"
+ yield "</body></html>"
+ menu.exposed = True
+
+ def annotated_file(self, filename, statements, excluded, missing):
+ source = open(filename, 'r')
+ buffer = []
+ for lineno, line in enumerate(source.readlines()):
+ lineno += 1
+ line = line.strip("\n\r")
+ empty_the_buffer = True
+ if lineno in excluded:
+ template = TEMPLATE_LOC_EXCLUDED
+ elif lineno in missing:
+ template = TEMPLATE_LOC_NOT_COVERED
+ elif lineno in statements:
+ template = TEMPLATE_LOC_COVERED
+ else:
+ empty_the_buffer = False
+ buffer.append((lineno, line))
+ if empty_the_buffer:
+ for lno, pastline in buffer:
+ yield template % (lno, cgi.escape(pastline))
+ buffer = []
+ yield template % (lineno, cgi.escape(line))
+
+ def report(self, name):
+ filename, statements, excluded, missing, _ = self.coverage.analysis2(name)
+ pc = _percent(statements, missing)
+ yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name),
+ fullpath=name,
+ pc=pc)
+ yield '<table>\n'
+ for line in self.annotated_file(filename, statements, excluded,
+ missing):
+ yield line
+ yield '</table>'
+ yield '</body>'
+ yield '</html>'
+ report.exposed = True
+
+
+def serve(path=localFile, port=8080, root=None):
+ if coverage is None:
+ raise ImportError("The coverage module could not be imported.")
+ from coverage import coverage
+ cov = coverage(data_file = path)
+ cov.load()
+
+ import cherrypy
+ cherrypy.config.update({'server.socket_port': int(port),
+ 'server.thread_pool': 10,
+ 'environment': "production",
+ })
+ cherrypy.quickstart(CoverStats(cov, root))
+
+if __name__ == "__main__":
+ serve(*tuple(sys.argv[1:]))
+
diff --git a/cherrypy/lib/cpstats.py b/cherrypy/lib/cpstats.py
new file mode 100755
index 0000000..79d5c3a
--- /dev/null
+++ b/cherrypy/lib/cpstats.py
@@ -0,0 +1,661 @@
+"""CPStats, a package for collecting and reporting on program statistics.
+
+Overview
+========
+
+Statistics about program operation are an invaluable monitoring and debugging
+tool. Unfortunately, the gathering and reporting of these critical values is
+usually ad-hoc. This package aims to add a centralized place for gathering
+statistical performance data, a structure for recording that data which
+provides for extrapolation of that data into more useful information,
+and a method of serving that data to both human investigators and
+monitoring software. Let's examine each of those in more detail.
+
+Data Gathering
+--------------
+
+Just as Python's `logging` module provides a common importable for gathering
+and sending messages, performance statistics would benefit from a similar
+common mechanism, and one that does *not* require each package which wishes
+to collect stats to import a third-party module. Therefore, we choose to
+re-use the `logging` module by adding a `statistics` object to it.
+
+That `logging.statistics` object is a nested dict. It is not a custom class,
+because that would 1) require libraries and applications to import a third-
+party module in order to participate, 2) inhibit innovation in extrapolation
+approaches and in reporting tools, and 3) be slow. There are, however, some
+specifications regarding the structure of the dict.
+
+ {
+ +----"SQLAlchemy": {
+ | "Inserts": 4389745,
+ | "Inserts per Second":
+ | lambda s: s["Inserts"] / (time() - s["Start"]),
+ | C +---"Table Statistics": {
+ | o | "widgets": {-----------+
+ N | l | "Rows": 1.3M, | Record
+ a | l | "Inserts": 400, |
+ m | e | },---------------------+
+ e | c | "froobles": {
+ s | t | "Rows": 7845,
+ p | i | "Inserts": 0,
+ a | o | },
+ c | n +---},
+ e | "Slow Queries":
+ | [{"Query": "SELECT * FROM widgets;",
+ | "Processing Time": 47.840923343,
+ | },
+ | ],
+ +----},
+ }
+
+The `logging.statistics` dict has four levels. The topmost level is nothing
+more than a set of names to introduce modularity, usually along the lines of
+package names. If the SQLAlchemy project wanted to participate, for example,
+it might populate the item `logging.statistics['SQLAlchemy']`, whose value
+would be a second-layer dict we call a "namespace". Namespaces help multiple
+packages to avoid collisions over key names, and make reports easier to read,
+to boot. The maintainers of SQLAlchemy should feel free to use more than one
+namespace if needed (such as 'SQLAlchemy ORM'). Note that there are no case
+or other syntax constraints on the namespace names; they should be chosen
+to be maximally readable by humans (neither too short nor too long).
+
+Each namespace, then, is a dict of named statistical values, such as
+'Requests/sec' or 'Uptime'. You should choose names which will look
+good on a report: spaces and capitalization are just fine.
+
+In addition to scalars, values in a namespace MAY be a (third-layer)
+dict, or a list, called a "collection". For example, the CherryPy StatsTool
+keeps track of what each request is doing (or has most recently done)
+in a 'Requests' collection, where each key is a thread ID; each
+value in the subdict MUST be a fourth dict (whew!) of statistical data about
+each thread. We call each subdict in the collection a "record". Similarly,
+the StatsTool also keeps a list of slow queries, where each record contains
+data about each slow query, in order.
+
+Values in a namespace or record may also be functions, which brings us to:
+
+Extrapolation
+-------------
+
+The collection of statistical data needs to be fast, as close to unnoticeable
+as possible to the host program. That requires us to minimize I/O, for example,
+but in Python it also means we need to minimize function calls. So when you
+are designing your namespace and record values, try to insert the most basic
+scalar values you already have on hand.
+
+When it comes time to report on the gathered data, however, we usually have
+much more freedom in what we can calculate. Therefore, whenever reporting
+tools (like the provided StatsPage CherryPy class) fetch the contents of
+`logging.statistics` for reporting, they first call `extrapolate_statistics`
+(passing the whole `statistics` dict as the only argument). This makes a
+deep copy of the statistics dict so that the reporting tool can both iterate
+over it and even change it without harming the original. But it also expands
+any functions in the dict by calling them. For example, you might have a
+'Current Time' entry in the namespace with the value "lambda scope: time.time()".
+The "scope" parameter is the current namespace dict (or record, if we're
+currently expanding one of those instead), allowing you access to existing
+static entries. If you're truly evil, you can even modify more than one entry
+at a time.
+
+However, don't try to calculate an entry and then use its value in further
+extrapolations; the order in which the functions are called is not guaranteed.
+This can lead to a certain amount of duplicated work (or a redesign of your
+schema), but that's better than complicating the spec.
+
+After the whole thing has been extrapolated, it's time for:
+
+Reporting
+---------
+
+The StatsPage class grabs the `logging.statistics` dict, extrapolates it all,
+and then transforms it to HTML for easy viewing. Each namespace gets its own
+header and attribute table, plus an extra table for each collection. This is
+NOT part of the statistics specification; other tools can format how they like.
+
+You can control which columns are output and how they are formatted by updating
+StatsPage.formatting, which is a dict that mirrors the keys and nesting of
+`logging.statistics`. The difference is that, instead of data values, it has
+formatting values. Use None for a given key to indicate to the StatsPage that a
+given column should not be output. Use a string with formatting (such as '%.3f')
+to interpolate the value(s), or use a callable (such as lambda v: v.isoformat())
+for more advanced formatting. Any entry which is not mentioned in the formatting
+dict is output unchanged.
+
+Monitoring
+----------
+
+Although the HTML output takes pains to assign unique id's to each <td> with
+statistical data, you're probably better off fetching /cpstats/data, which
+outputs the whole (extrapolated) `logging.statistics` dict in JSON format.
+That is probably easier to parse, and doesn't have any formatting controls,
+so you get the "original" data in a consistently-serialized format.
+Note: there's no treatment yet for datetime objects. Try time.time() instead
+for now if you can. Nagios will probably thank you.
+
+Turning Collection Off
+----------------------
+
+It is recommended each namespace have an "Enabled" item which, if False,
+stops collection (but not reporting) of statistical data. Applications
+SHOULD provide controls to pause and resume collection by setting these
+entries to False or True, if present.
+
+
+Usage
+=====
+
+To collect statistics on CherryPy applications:
+
+ from cherrypy.lib import cpstats
+ appconfig['/']['tools.cpstats.on'] = True
+
+To collect statistics on your own code:
+
+ import logging
+ # Initialize the repository
+ if not hasattr(logging, 'statistics'): logging.statistics = {}
+ # Initialize my namespace
+ mystats = logging.statistics.setdefault('My Stuff', {})
+ # Initialize my namespace's scalars and collections
+ mystats.update({
+ 'Enabled': True,
+ 'Start Time': time.time(),
+ 'Important Events': 0,
+ 'Events/Second': lambda s: (
+ (s['Important Events'] / (time.time() - s['Start Time']))),
+ })
+ ...
+ for event in events:
+ ...
+ # Collect stats
+ if mystats.get('Enabled', False):
+ mystats['Important Events'] += 1
+
+To report statistics:
+
+ root.cpstats = cpstats.StatsPage()
+
+To format statistics reports:
+
+ See 'Reporting', above.
+
+"""
+
+# -------------------------------- Statistics -------------------------------- #
+
+import logging
+if not hasattr(logging, 'statistics'): logging.statistics = {}
+
+def extrapolate_statistics(scope):
+ """Return an extrapolated copy of the given scope."""
+ c = {}
+ for k, v in list(scope.items()):
+ if isinstance(v, dict):
+ v = extrapolate_statistics(v)
+ elif isinstance(v, (list, tuple)):
+ v = [extrapolate_statistics(record) for record in v]
+ elif hasattr(v, '__call__'):
+ v = v(scope)
+ c[k] = v
+ return c
+
+
+# --------------------- CherryPy Applications Statistics --------------------- #
+
+import threading
+import time
+
+import cherrypy
+
+appstats = logging.statistics.setdefault('CherryPy Applications', {})
+appstats.update({
+ 'Enabled': True,
+ 'Bytes Read/Request': lambda s: (s['Total Requests'] and
+ (s['Total Bytes Read'] / float(s['Total Requests'])) or 0.0),
+ 'Bytes Read/Second': lambda s: s['Total Bytes Read'] / s['Uptime'](s),
+ 'Bytes Written/Request': lambda s: (s['Total Requests'] and
+ (s['Total Bytes Written'] / float(s['Total Requests'])) or 0.0),
+ 'Bytes Written/Second': lambda s: s['Total Bytes Written'] / s['Uptime'](s),
+ 'Current Time': lambda s: time.time(),
+ 'Current Requests': 0,
+ 'Requests/Second': lambda s: float(s['Total Requests']) / s['Uptime'](s),
+ 'Server Version': cherrypy.__version__,
+ 'Start Time': time.time(),
+ 'Total Bytes Read': 0,
+ 'Total Bytes Written': 0,
+ 'Total Requests': 0,
+ 'Total Time': 0,
+ 'Uptime': lambda s: time.time() - s['Start Time'],
+ 'Requests': {},
+ })
+
+proc_time = lambda s: time.time() - s['Start Time']
+
+
+class ByteCountWrapper(object):
+ """Wraps a file-like object, counting the number of bytes read."""
+
+ def __init__(self, rfile):
+ self.rfile = rfile
+ self.bytes_read = 0
+
+ def read(self, size=-1):
+ data = self.rfile.read(size)
+ self.bytes_read += len(data)
+ return data
+
+ def readline(self, size=-1):
+ data = self.rfile.readline(size)
+ self.bytes_read += len(data)
+ return data
+
+ def readlines(self, sizehint=0):
+ # Shamelessly stolen from StringIO
+ total = 0
+ lines = []
+ line = self.readline()
+ while line:
+ lines.append(line)
+ total += len(line)
+ if 0 < sizehint <= total:
+ break
+ line = self.readline()
+ return lines
+
+ def close(self):
+ self.rfile.close()
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ data = self.rfile.next()
+ self.bytes_read += len(data)
+ return data
+
+
+average_uriset_time = lambda s: s['Count'] and (s['Sum'] / s['Count']) or 0
+
+
+class StatsTool(cherrypy.Tool):
+ """Record various information about the current request."""
+
+ def __init__(self):
+ cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop)
+
+ def _setup(self):
+ """Hook this tool into cherrypy.request.
+
+ The standard CherryPy request object will automatically call this
+ method when the tool is "turned on" in config.
+ """
+ if appstats.get('Enabled', False):
+ cherrypy.Tool._setup(self)
+ self.record_start()
+
+ def record_start(self):
+ """Record the beginning of a request."""
+ request = cherrypy.serving.request
+ if not hasattr(request.rfile, 'bytes_read'):
+ request.rfile = ByteCountWrapper(request.rfile)
+ request.body.fp = request.rfile
+
+ r = request.remote
+
+ appstats['Current Requests'] += 1
+ appstats['Total Requests'] += 1
+ appstats['Requests'][threading._get_ident()] = {
+ 'Bytes Read': None,
+ 'Bytes Written': None,
+ # Use a lambda so the ip gets updated by tools.proxy later
+ 'Client': lambda s: '%s:%s' % (r.ip, r.port),
+ 'End Time': None,
+ 'Processing Time': proc_time,
+ 'Request-Line': request.request_line,
+ 'Response Status': None,
+ 'Start Time': time.time(),
+ }
+
+ def record_stop(self, uriset=None, slow_queries=1.0, slow_queries_count=100,
+ debug=False, **kwargs):
+ """Record the end of a request."""
+ w = appstats['Requests'][threading._get_ident()]
+
+ r = cherrypy.request.rfile.bytes_read
+ w['Bytes Read'] = r
+ appstats['Total Bytes Read'] += r
+
+ if cherrypy.response.stream:
+ w['Bytes Written'] = 'chunked'
+ else:
+ cl = int(cherrypy.response.headers.get('Content-Length', 0))
+ w['Bytes Written'] = cl
+ appstats['Total Bytes Written'] += cl
+
+ w['Response Status'] = cherrypy.response.status
+
+ w['End Time'] = time.time()
+ p = w['End Time'] - w['Start Time']
+ w['Processing Time'] = p
+ appstats['Total Time'] += p
+
+ appstats['Current Requests'] -= 1
+
+ if debug:
+ cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS')
+
+ if uriset:
+ rs = appstats.setdefault('URI Set Tracking', {})
+ r = rs.setdefault(uriset, {
+ 'Min': None, 'Max': None, 'Count': 0, 'Sum': 0,
+ 'Avg': average_uriset_time})
+ if r['Min'] is None or p < r['Min']:
+ r['Min'] = p
+ if r['Max'] is None or p > r['Max']:
+ r['Max'] = p
+ r['Count'] += 1
+ r['Sum'] += p
+
+ if slow_queries and p > slow_queries:
+ sq = appstats.setdefault('Slow Queries', [])
+ sq.append(w.copy())
+ if len(sq) > slow_queries_count:
+ sq.pop(0)
+
+
+import cherrypy
+cherrypy.tools.cpstats = StatsTool()
+
+
+# ---------------------- CherryPy Statistics Reporting ---------------------- #
+
+import os
+thisdir = os.path.abspath(os.path.dirname(__file__))
+
+try:
+ import json
+except ImportError:
+ try:
+ import simplejson as json
+ except ImportError:
+ json = None
+
+
+missing = object()
+
+locale_date = lambda v: time.strftime('%c', time.gmtime(v))
+iso_format = lambda v: time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v))
+
+def pause_resume(ns):
+ def _pause_resume(enabled):
+ pause_disabled = ''
+ resume_disabled = ''
+ if enabled:
+ resume_disabled = 'disabled="disabled" '
+ else:
+ pause_disabled = 'disabled="disabled" '
+ return """
+ <form action="pause" method="POST" style="display:inline">
+ <input type="hidden" name="namespace" value="%s" />
+ <input type="submit" value="Pause" %s/>
+ </form>
+ <form action="resume" method="POST" style="display:inline">
+ <input type="hidden" name="namespace" value="%s" />
+ <input type="submit" value="Resume" %s/>
+ </form>
+ """ % (ns, pause_disabled, ns, resume_disabled)
+ return _pause_resume
+
+
+class StatsPage(object):
+
+ formatting = {
+ 'CherryPy Applications': {
+ 'Enabled': pause_resume('CherryPy Applications'),
+ 'Bytes Read/Request': '%.3f',
+ 'Bytes Read/Second': '%.3f',
+ 'Bytes Written/Request': '%.3f',
+ 'Bytes Written/Second': '%.3f',
+ 'Current Time': iso_format,
+ 'Requests/Second': '%.3f',
+ 'Start Time': iso_format,
+ 'Total Time': '%.3f',
+ 'Uptime': '%.3f',
+ 'Slow Queries': {
+ 'End Time': None,
+ 'Processing Time': '%.3f',
+ 'Start Time': iso_format,
+ },
+ 'URI Set Tracking': {
+ 'Avg': '%.3f',
+ 'Max': '%.3f',
+ 'Min': '%.3f',
+ 'Sum': '%.3f',
+ },
+ 'Requests': {
+ 'Bytes Read': '%s',
+ 'Bytes Written': '%s',
+ 'End Time': None,
+ 'Processing Time': '%.3f',
+ 'Start Time': None,
+ },
+ },
+ 'CherryPy WSGIServer': {
+ 'Enabled': pause_resume('CherryPy WSGIServer'),
+ 'Connections/second': '%.3f',
+ 'Start time': iso_format,
+ },
+ }
+
+
+ def index(self):
+ # Transform the raw data into pretty output for HTML
+ yield """
+<html>
+<head>
+ <title>Statistics</title>
+<style>
+
+th, td {
+ padding: 0.25em 0.5em;
+ border: 1px solid #666699;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+table.stats1 {
+ width: 100%;
+}
+
+table.stats1 th {
+ font-weight: bold;
+ text-align: right;
+ background-color: #CCD5DD;
+}
+
+table.stats2, h2 {
+ margin-left: 50px;
+}
+
+table.stats2 th {
+ font-weight: bold;
+ text-align: center;
+ background-color: #CCD5DD;
+}
+
+</style>
+</head>
+<body>
+"""
+ for title, scalars, collections in self.get_namespaces():
+ yield """
+<h1>%s</h1>
+
+<table class='stats1'>
+ <tbody>
+""" % title
+ for i, (key, value) in enumerate(scalars):
+ colnum = i % 3
+ if colnum == 0: yield """
+ <tr>"""
+ yield """
+ <th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" % vars()
+ if colnum == 2: yield """
+ </tr>"""
+
+ if colnum == 0: yield """
+ <th></th><td></td>
+ <th></th><td></td>
+ </tr>"""
+ elif colnum == 1: yield """
+ <th></th><td></td>
+ </tr>"""
+ yield """
+ </tbody>
+</table>"""
+
+ for subtitle, headers, subrows in collections:
+ yield """
+<h2>%s</h2>
+<table class='stats2'>
+ <thead>
+ <tr>""" % subtitle
+ for key in headers:
+ yield """
+ <th>%s</th>""" % key
+ yield """
+ </tr>
+ </thead>
+ <tbody>"""
+ for subrow in subrows:
+ yield """
+ <tr>"""
+ for value in subrow:
+ yield """
+ <td>%s</td>""" % value
+ yield """
+ </tr>"""
+ yield """
+ </tbody>
+</table>"""
+ yield """
+</body>
+</html>
+"""
+ index.exposed = True
+
+ def get_namespaces(self):
+ """Yield (title, scalars, collections) for each namespace."""
+ s = extrapolate_statistics(logging.statistics)
+ for title, ns in sorted(s.items()):
+ scalars = []
+ collections = []
+ ns_fmt = self.formatting.get(title, {})
+ for k, v in sorted(ns.items()):
+ fmt = ns_fmt.get(k, {})
+ if isinstance(v, dict):
+ headers, subrows = self.get_dict_collection(v, fmt)
+ collections.append((k, ['ID'] + headers, subrows))
+ elif isinstance(v, (list, tuple)):
+ headers, subrows = self.get_list_collection(v, fmt)
+ collections.append((k, headers, subrows))
+ else:
+ format = ns_fmt.get(k, missing)
+ if format is None:
+ # Don't output this column.
+ continue
+ if hasattr(format, '__call__'):
+ v = format(v)
+ elif format is not missing:
+ v = format % v
+ scalars.append((k, v))
+ yield title, scalars, collections
+
+ def get_dict_collection(self, v, formatting):
+ """Return ([headers], [rows]) for the given collection."""
+ # E.g., the 'Requests' dict.
+ headers = []
+ for record in v.itervalues():
+ for k3 in record:
+ format = formatting.get(k3, missing)
+ if format is None:
+ # Don't output this column.
+ continue
+ if k3 not in headers:
+ headers.append(k3)
+ headers.sort()
+
+ subrows = []
+ for k2, record in sorted(v.items()):
+ subrow = [k2]
+ for k3 in headers:
+ v3 = record.get(k3, '')
+ format = formatting.get(k3, missing)
+ if format is None:
+ # Don't output this column.
+ continue
+ if hasattr(format, '__call__'):
+ v3 = format(v3)
+ elif format is not missing:
+ v3 = format % v3
+ subrow.append(v3)
+ subrows.append(subrow)
+
+ return headers, subrows
+
+ def get_list_collection(self, v, formatting):
+ """Return ([headers], [subrows]) for the given collection."""
+ # E.g., the 'Slow Queries' list.
+ headers = []
+ for record in v:
+ for k3 in record:
+ format = formatting.get(k3, missing)
+ if format is None:
+ # Don't output this column.
+ continue
+ if k3 not in headers:
+ headers.append(k3)
+ headers.sort()
+
+ subrows = []
+ for record in v:
+ subrow = []
+ for k3 in headers:
+ v3 = record.get(k3, '')
+ format = formatting.get(k3, missing)
+ if format is None:
+ # Don't output this column.
+ continue
+ if hasattr(format, '__call__'):
+ v3 = format(v3)
+ elif format is not missing:
+ v3 = format % v3
+ subrow.append(v3)
+ subrows.append(subrow)
+
+ return headers, subrows
+
+ if json is not None:
+ def data(self):
+ s = extrapolate_statistics(logging.statistics)
+ cherrypy.response.headers['Content-Type'] = 'application/json'
+ return json.dumps(s, sort_keys=True, indent=4)
+ data.exposed = True
+
+ def pause(self, namespace):
+ logging.statistics.get(namespace, {})['Enabled'] = False
+ raise cherrypy.HTTPRedirect('./')
+ pause.exposed = True
+ pause.cp_config = {'tools.allow.on': True,
+ 'tools.allow.methods': ['POST']}
+
+ def resume(self, namespace):
+ logging.statistics.get(namespace, {})['Enabled'] = True
+ raise cherrypy.HTTPRedirect('./')
+ resume.exposed = True
+ resume.cp_config = {'tools.allow.on': True,
+ 'tools.allow.methods': ['POST']}
+
diff --git a/cherrypy/lib/cptools.py b/cherrypy/lib/cptools.py
new file mode 100755
index 0000000..3eedf97
--- /dev/null
+++ b/cherrypy/lib/cptools.py
@@ -0,0 +1,611 @@
+"""Functions for builtin CherryPy tools."""
+
+import logging
+import re
+
+import cherrypy
+from cherrypy._cpcompat import basestring, ntob, md5, set
+from cherrypy.lib import httputil as _httputil
+
+
+# Conditional HTTP request support #
+
+def validate_etags(autotags=False, debug=False):
+ """Validate the current ETag against If-Match, If-None-Match headers.
+
+ If autotags is True, an ETag response-header value will be provided
+ from an MD5 hash of the response body (unless some other code has
+ already provided an ETag header). If False (the default), the ETag
+ will not be automatic.
+
+ WARNING: the autotags feature is not designed for URL's which allow
+ methods other than GET. For example, if a POST to the same URL returns
+ no content, the automatic ETag will be incorrect, breaking a fundamental
+ use for entity tags in a possibly destructive fashion. Likewise, if you
+ raise 304 Not Modified, the response body will be empty, the ETag hash
+ will be incorrect, and your application will break.
+ See :rfc:`2616` Section 14.24.
+ """
+ response = cherrypy.serving.response
+
+ # Guard against being run twice.
+ if hasattr(response, "ETag"):
+ return
+
+ status, reason, msg = _httputil.valid_status(response.status)
+
+ etag = response.headers.get('ETag')
+
+ # Automatic ETag generation. See warning in docstring.
+ if etag:
+ if debug:
+ cherrypy.log('ETag already set: %s' % etag, 'TOOLS.ETAGS')
+ elif not autotags:
+ if debug:
+ cherrypy.log('Autotags off', 'TOOLS.ETAGS')
+ elif status != 200:
+ if debug:
+ cherrypy.log('Status not 200', 'TOOLS.ETAGS')
+ else:
+ etag = response.collapse_body()
+ etag = '"%s"' % md5(etag).hexdigest()
+ if debug:
+ cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS')
+ response.headers['ETag'] = etag
+
+ response.ETag = etag
+
+ # "If the request would, without the If-Match header field, result in
+ # anything other than a 2xx or 412 status, then the If-Match header
+ # MUST be ignored."
+ if debug:
+ cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS')
+ if status >= 200 and status <= 299:
+ request = cherrypy.serving.request
+
+ conditions = request.headers.elements('If-Match') or []
+ conditions = [str(x) for x in conditions]
+ if debug:
+ cherrypy.log('If-Match conditions: %s' % repr(conditions),
+ 'TOOLS.ETAGS')
+ if conditions and not (conditions == ["*"] or etag in conditions):
+ raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did "
+ "not match %r" % (etag, conditions))
+
+ conditions = request.headers.elements('If-None-Match') or []
+ conditions = [str(x) for x in conditions]
+ if debug:
+ cherrypy.log('If-None-Match conditions: %s' % repr(conditions),
+ 'TOOLS.ETAGS')
+ if conditions == ["*"] or etag in conditions:
+ if debug:
+ cherrypy.log('request.method: %s' % request.method, 'TOOLS.ETAGS')
+ if request.method in ("GET", "HEAD"):
+ raise cherrypy.HTTPRedirect([], 304)
+ else:
+ raise cherrypy.HTTPError(412, "If-None-Match failed: ETag %r "
+ "matched %r" % (etag, conditions))
+
+def validate_since():
+ """Validate the current Last-Modified against If-Modified-Since headers.
+
+ If no code has set the Last-Modified response header, then no validation
+ will be performed.
+ """
+ response = cherrypy.serving.response
+ lastmod = response.headers.get('Last-Modified')
+ if lastmod:
+ status, reason, msg = _httputil.valid_status(response.status)
+
+ request = cherrypy.serving.request
+
+ since = request.headers.get('If-Unmodified-Since')
+ if since and since != lastmod:
+ if (status >= 200 and status <= 299) or status == 412:
+ raise cherrypy.HTTPError(412)
+
+ since = request.headers.get('If-Modified-Since')
+ if since and since == lastmod:
+ if (status >= 200 and status <= 299) or status == 304:
+ if request.method in ("GET", "HEAD"):
+ raise cherrypy.HTTPRedirect([], 304)
+ else:
+ raise cherrypy.HTTPError(412)
+
+
+# Tool code #
+
+def allow(methods=None, debug=False):
+ """Raise 405 if request.method not in methods (default GET/HEAD).
+
+ The given methods are case-insensitive, and may be in any order.
+ If only one method is allowed, you may supply a single string;
+ if more than one, supply a list of strings.
+
+ Regardless of whether the current method is allowed or not, this
+ also emits an 'Allow' response header, containing the given methods.
+ """
+ if not isinstance(methods, (tuple, list)):
+ methods = [methods]
+ methods = [m.upper() for m in methods if m]
+ if not methods:
+ methods = ['GET', 'HEAD']
+ elif 'GET' in methods and 'HEAD' not in methods:
+ methods.append('HEAD')
+
+ cherrypy.response.headers['Allow'] = ', '.join(methods)
+ if cherrypy.request.method not in methods:
+ if debug:
+ cherrypy.log('request.method %r not in methods %r' %
+ (cherrypy.request.method, methods), 'TOOLS.ALLOW')
+ raise cherrypy.HTTPError(405)
+ else:
+ if debug:
+ cherrypy.log('request.method %r in methods %r' %
+ (cherrypy.request.method, methods), 'TOOLS.ALLOW')
+
+
+def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
+ scheme='X-Forwarded-Proto', debug=False):
+ """Change the base URL (scheme://host[:port][/path]).
+
+ For running a CP server behind Apache, lighttpd, or other HTTP server.
+
+ If you want the new request.base to include path info (not just the host),
+ you must explicitly set base to the full base path, and ALSO set 'local'
+ to '', so that the X-Forwarded-Host request header (which never includes
+ path info) does not override it. Regardless, the value for 'base' MUST
+ NOT end in a slash.
+
+ cherrypy.request.remote.ip (the IP address of the client) will be
+ rewritten if the header specified by the 'remote' arg is valid.
+ By default, 'remote' is set to 'X-Forwarded-For'. If you do not
+ want to rewrite remote.ip, set the 'remote' arg to an empty string.
+ """
+
+ request = cherrypy.serving.request
+
+ if scheme:
+ s = request.headers.get(scheme, None)
+ if debug:
+ cherrypy.log('Testing scheme %r:%r' % (scheme, s), 'TOOLS.PROXY')
+ if s == 'on' and 'ssl' in scheme.lower():
+ # This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header
+ scheme = 'https'
+ else:
+ # This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https'
+ scheme = s
+ if not scheme:
+ scheme = request.base[:request.base.find("://")]
+
+ if local:
+ lbase = request.headers.get(local, None)
+ if debug:
+ cherrypy.log('Testing local %r:%r' % (local, lbase), 'TOOLS.PROXY')
+ if lbase is not None:
+ base = lbase.split(',')[0]
+ if not base:
+ port = request.local.port
+ if port == 80:
+ base = '127.0.0.1'
+ else:
+ base = '127.0.0.1:%s' % port
+
+ if base.find("://") == -1:
+ # add http:// or https:// if needed
+ base = scheme + "://" + base
+
+ request.base = base
+
+ if remote:
+ xff = request.headers.get(remote)
+ if debug:
+ cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY')
+ if xff:
+ if remote == 'X-Forwarded-For':
+ # See http://bob.pythonmac.org/archives/2005/09/23/apache-x-forwarded-for-caveat/
+ xff = xff.split(',')[-1].strip()
+ request.remote.ip = xff
+
+
+def ignore_headers(headers=('Range',), debug=False):
+ """Delete request headers whose field names are included in 'headers'.
+
+ This is a useful tool for working behind certain HTTP servers;
+ for example, Apache duplicates the work that CP does for 'Range'
+ headers, and will doubly-truncate the response.
+ """
+ request = cherrypy.serving.request
+ for name in headers:
+ if name in request.headers:
+ if debug:
+ cherrypy.log('Ignoring request header %r' % name,
+ 'TOOLS.IGNORE_HEADERS')
+ del request.headers[name]
+
+
+def response_headers(headers=None, debug=False):
+ """Set headers on the response."""
+ if debug:
+ cherrypy.log('Setting response headers: %s' % repr(headers),
+ 'TOOLS.RESPONSE_HEADERS')
+ for name, value in (headers or []):
+ cherrypy.serving.response.headers[name] = value
+response_headers.failsafe = True
+
+
+def referer(pattern, accept=True, accept_missing=False, error=403,
+ message='Forbidden Referer header.', debug=False):
+ """Raise HTTPError if Referer header does/does not match the given pattern.
+
+ pattern
+ A regular expression pattern to test against the Referer.
+
+ accept
+ If True, the Referer must match the pattern; if False,
+ the Referer must NOT match the pattern.
+
+ accept_missing
+ If True, permit requests with no Referer header.
+
+ error
+ The HTTP error code to return to the client on failure.
+
+ message
+ A string to include in the response body on failure.
+
+ """
+ try:
+ ref = cherrypy.serving.request.headers['Referer']
+ match = bool(re.match(pattern, ref))
+ if debug:
+ cherrypy.log('Referer %r matches %r' % (ref, pattern),
+ 'TOOLS.REFERER')
+ if accept == match:
+ return
+ except KeyError:
+ if debug:
+ cherrypy.log('No Referer header', 'TOOLS.REFERER')
+ if accept_missing:
+ return
+
+ raise cherrypy.HTTPError(error, message)
+
+
+class SessionAuth(object):
+ """Assert that the user is logged in."""
+
+ session_key = "username"
+ debug = False
+
+ def check_username_and_password(self, username, password):
+ pass
+
+ def anonymous(self):
+ """Provide a temporary user name for anonymous users."""
+ pass
+
+ def on_login(self, username):
+ pass
+
+ def on_logout(self, username):
+ pass
+
+ def on_check(self, username):
+ pass
+
+ def login_screen(self, from_page='..', username='', error_msg='', **kwargs):
+ return ntob("""<html><body>
+Message: %(error_msg)s
+<form method="post" action="do_login">
+ Login: <input type="text" name="username" value="%(username)s" size="10" /><br />
+ Password: <input type="password" name="password" size="10" /><br />
+ <input type="hidden" name="from_page" value="%(from_page)s" /><br />
+ <input type="submit" />
+</form>
+</body></html>""" % {'from_page': from_page, 'username': username,
+ 'error_msg': error_msg}, "utf-8")
+
+ def do_login(self, username, password, from_page='..', **kwargs):
+ """Login. May raise redirect, or return True if request handled."""
+ response = cherrypy.serving.response
+ error_msg = self.check_username_and_password(username, password)
+ if error_msg:
+ body = self.login_screen(from_page, username, error_msg)
+ response.body = body
+ if "Content-Length" in response.headers:
+ # Delete Content-Length header so finalize() recalcs it.
+ del response.headers["Content-Length"]
+ return True
+ else:
+ cherrypy.serving.request.login = username
+ cherrypy.session[self.session_key] = username
+ self.on_login(username)
+ raise cherrypy.HTTPRedirect(from_page or "/")
+
+ def do_logout(self, from_page='..', **kwargs):
+ """Logout. May raise redirect, or return True if request handled."""
+ sess = cherrypy.session
+ username = sess.get(self.session_key)
+ sess[self.session_key] = None
+ if username:
+ cherrypy.serving.request.login = None
+ self.on_logout(username)
+ raise cherrypy.HTTPRedirect(from_page)
+
+ def do_check(self):
+ """Assert username. May raise redirect, or return True if request handled."""
+ sess = cherrypy.session
+ request = cherrypy.serving.request
+ response = cherrypy.serving.response
+
+ username = sess.get(self.session_key)
+ if not username:
+ sess[self.session_key] = username = self.anonymous()
+ if self.debug:
+ cherrypy.log('No session[username], trying anonymous', 'TOOLS.SESSAUTH')
+ if not username:
+ url = cherrypy.url(qs=request.query_string)
+ if self.debug:
+ cherrypy.log('No username, routing to login_screen with '
+ 'from_page %r' % url, 'TOOLS.SESSAUTH')
+ response.body = self.login_screen(url)
+ if "Content-Length" in response.headers:
+ # Delete Content-Length header so finalize() recalcs it.
+ del response.headers["Content-Length"]
+ return True
+ if self.debug:
+ cherrypy.log('Setting request.login to %r' % username, 'TOOLS.SESSAUTH')
+ request.login = username
+ self.on_check(username)
+
+ def run(self):
+ request = cherrypy.serving.request
+ response = cherrypy.serving.response
+
+ path = request.path_info
+ if path.endswith('login_screen'):
+ if self.debug:
+ cherrypy.log('routing %r to login_screen' % path, 'TOOLS.SESSAUTH')
+ return self.login_screen(**request.params)
+ elif path.endswith('do_login'):
+ if request.method != 'POST':
+ response.headers['Allow'] = "POST"
+ if self.debug:
+ cherrypy.log('do_login requires POST', 'TOOLS.SESSAUTH')
+ raise cherrypy.HTTPError(405)
+ if self.debug:
+ cherrypy.log('routing %r to do_login' % path, 'TOOLS.SESSAUTH')
+ return self.do_login(**request.params)
+ elif path.endswith('do_logout'):
+ if request.method != 'POST':
+ response.headers['Allow'] = "POST"
+ raise cherrypy.HTTPError(405)
+ if self.debug:
+ cherrypy.log('routing %r to do_logout' % path, 'TOOLS.SESSAUTH')
+ return self.do_logout(**request.params)
+ else:
+ if self.debug:
+ cherrypy.log('No special path, running do_check', 'TOOLS.SESSAUTH')
+ return self.do_check()
+
+
+def session_auth(**kwargs):
+ sa = SessionAuth()
+ for k, v in kwargs.items():
+ setattr(sa, k, v)
+ return sa.run()
+session_auth.__doc__ = """Session authentication hook.
+
+Any attribute of the SessionAuth class may be overridden via a keyword arg
+to this function:
+
+""" + "\n".join(["%s: %s" % (k, type(getattr(SessionAuth, k)).__name__)
+ for k in dir(SessionAuth) if not k.startswith("__")])
+
+
+def log_traceback(severity=logging.ERROR, debug=False):
+ """Write the last error's traceback to the cherrypy error log."""
+ cherrypy.log("", "HTTP", severity=severity, traceback=True)
+
+def log_request_headers(debug=False):
+ """Write request headers to the cherrypy error log."""
+ h = [" %s: %s" % (k, v) for k, v in cherrypy.serving.request.header_list]
+ cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), "HTTP")
+
+def log_hooks(debug=False):
+ """Write request.hooks to the cherrypy error log."""
+ request = cherrypy.serving.request
+
+ msg = []
+ # Sort by the standard points if possible.
+ from cherrypy import _cprequest
+ points = _cprequest.hookpoints
+ for k in request.hooks.keys():
+ if k not in points:
+ points.append(k)
+
+ for k in points:
+ msg.append(" %s:" % k)
+ v = request.hooks.get(k, [])
+ v.sort()
+ for h in v:
+ msg.append(" %r" % h)
+ cherrypy.log('\nRequest Hooks for ' + cherrypy.url() +
+ ':\n' + '\n'.join(msg), "HTTP")
+
+def redirect(url='', internal=True, debug=False):
+ """Raise InternalRedirect or HTTPRedirect to the given url."""
+ if debug:
+ cherrypy.log('Redirecting %sto: %s' %
+ ({True: 'internal ', False: ''}[internal], url),
+ 'TOOLS.REDIRECT')
+ if internal:
+ raise cherrypy.InternalRedirect(url)
+ else:
+ raise cherrypy.HTTPRedirect(url)
+
+def trailing_slash(missing=True, extra=False, status=None, debug=False):
+ """Redirect if path_info has (missing|extra) trailing slash."""
+ request = cherrypy.serving.request
+ pi = request.path_info
+
+ if debug:
+ cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' %
+ (request.is_index, missing, extra, pi),
+ 'TOOLS.TRAILING_SLASH')
+ if request.is_index is True:
+ if missing:
+ if not pi.endswith('/'):
+ new_url = cherrypy.url(pi + '/', request.query_string)
+ raise cherrypy.HTTPRedirect(new_url, status=status or 301)
+ elif request.is_index is False:
+ if extra:
+ # If pi == '/', don't redirect to ''!
+ if pi.endswith('/') and pi != '/':
+ new_url = cherrypy.url(pi[:-1], request.query_string)
+ raise cherrypy.HTTPRedirect(new_url, status=status or 301)
+
+def flatten(debug=False):
+ """Wrap response.body in a generator that recursively iterates over body.
+
+ This allows cherrypy.response.body to consist of 'nested generators';
+ that is, a set of generators that yield generators.
+ """
+ import types
+ def flattener(input):
+ numchunks = 0
+ for x in input:
+ if not isinstance(x, types.GeneratorType):
+ numchunks += 1
+ yield x
+ else:
+ for y in flattener(x):
+ numchunks += 1
+ yield y
+ if debug:
+ cherrypy.log('Flattened %d chunks' % numchunks, 'TOOLS.FLATTEN')
+ response = cherrypy.serving.response
+ response.body = flattener(response.body)
+
+
+def accept(media=None, debug=False):
+ """Return the client's preferred media-type (from the given Content-Types).
+
+ If 'media' is None (the default), no test will be performed.
+
+ If 'media' is provided, it should be the Content-Type value (as a string)
+ or values (as a list or tuple of strings) which the current resource
+ can emit. The client's acceptable media ranges (as declared in the
+ Accept request header) will be matched in order to these Content-Type
+ values; the first such string is returned. That is, the return value
+ will always be one of the strings provided in the 'media' arg (or None
+ if 'media' is None).
+
+ If no match is found, then HTTPError 406 (Not Acceptable) is raised.
+ Note that most web browsers send */* as a (low-quality) acceptable
+ media range, which should match any Content-Type. In addition, "...if
+ no Accept header field is present, then it is assumed that the client
+ accepts all media types."
+
+ Matching types are checked in order of client preference first,
+ and then in the order of the given 'media' values.
+
+ Note that this function does not honor accept-params (other than "q").
+ """
+ if not media:
+ return
+ if isinstance(media, basestring):
+ media = [media]
+ request = cherrypy.serving.request
+
+ # Parse the Accept request header, and try to match one
+ # of the requested media-ranges (in order of preference).
+ ranges = request.headers.elements('Accept')
+ if not ranges:
+ # Any media type is acceptable.
+ if debug:
+ cherrypy.log('No Accept header elements', 'TOOLS.ACCEPT')
+ return media[0]
+ else:
+ # Note that 'ranges' is sorted in order of preference
+ for element in ranges:
+ if element.qvalue > 0:
+ if element.value == "*/*":
+ # Matches any type or subtype
+ if debug:
+ cherrypy.log('Match due to */*', 'TOOLS.ACCEPT')
+ return media[0]
+ elif element.value.endswith("/*"):
+ # Matches any subtype
+ mtype = element.value[:-1] # Keep the slash
+ for m in media:
+ if m.startswith(mtype):
+ if debug:
+ cherrypy.log('Match due to %s' % element.value,
+ 'TOOLS.ACCEPT')
+ return m
+ else:
+ # Matches exact value
+ if element.value in media:
+ if debug:
+ cherrypy.log('Match due to %s' % element.value,
+ 'TOOLS.ACCEPT')
+ return element.value
+
+ # No suitable media-range found.
+ ah = request.headers.get('Accept')
+ if ah is None:
+ msg = "Your client did not send an Accept header."
+ else:
+ msg = "Your client sent this Accept header: %s." % ah
+ msg += (" But this resource only emits these media types: %s." %
+ ", ".join(media))
+ raise cherrypy.HTTPError(406, msg)
+
+
+class MonitoredHeaderMap(_httputil.HeaderMap):
+
+ def __init__(self):
+ self.accessed_headers = set()
+
+ def __getitem__(self, key):
+ self.accessed_headers.add(key)
+ return _httputil.HeaderMap.__getitem__(self, key)
+
+ def __contains__(self, key):
+ self.accessed_headers.add(key)
+ return _httputil.HeaderMap.__contains__(self, key)
+
+ def get(self, key, default=None):
+ self.accessed_headers.add(key)
+ return _httputil.HeaderMap.get(self, key, default=default)
+
+ def has_key(self, key):
+ self.accessed_headers.add(key)
+ return _httputil.HeaderMap.has_key(self, key)
+
+
+def autovary(ignore=None, debug=False):
+ """Auto-populate the Vary response header based on request.header access."""
+ request = cherrypy.serving.request
+
+ req_h = request.headers
+ request.headers = MonitoredHeaderMap()
+ request.headers.update(req_h)
+ if ignore is None:
+ ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type'])
+
+ def set_response_header():
+ resp_h = cherrypy.serving.response.headers
+ v = set([e.value for e in resp_h.elements('Vary')])
+ if debug:
+ cherrypy.log('Accessed headers: %s' % request.headers.accessed_headers,
+ 'TOOLS.AUTOVARY')
+ v = v.union(request.headers.accessed_headers)
+ v = v.difference(ignore)
+ v = list(v)
+ v.sort()
+ resp_h['Vary'] = ', '.join(v)
+ request.hooks.attach('before_finalize', set_response_header, 95)
+
diff --git a/cherrypy/lib/encoding.py b/cherrypy/lib/encoding.py
new file mode 100755
index 0000000..6459746
--- /dev/null
+++ b/cherrypy/lib/encoding.py
@@ -0,0 +1,388 @@
+import struct
+import time
+
+import cherrypy
+from cherrypy._cpcompat import basestring, BytesIO, ntob, set, unicodestr
+from cherrypy.lib import file_generator
+from cherrypy.lib import set_vary_header
+
+
+def decode(encoding=None, default_encoding='utf-8'):
+ """Replace or extend the list of charsets used to decode a request entity.
+
+ Either argument may be a single string or a list of strings.
+
+ encoding
+ If not None, restricts the set of charsets attempted while decoding
+ a request entity to the given set (even if a different charset is given in
+ the Content-Type request header).
+
+ default_encoding
+ Only in effect if the 'encoding' argument is not given.
+ If given, the set of charsets attempted while decoding a request entity is
+ *extended* with the given value(s).
+
+ """
+ body = cherrypy.request.body
+ if encoding is not None:
+ if not isinstance(encoding, list):
+ encoding = [encoding]
+ body.attempt_charsets = encoding
+ elif default_encoding:
+ if not isinstance(default_encoding, list):
+ default_encoding = [default_encoding]
+ body.attempt_charsets = body.attempt_charsets + default_encoding
+
+
+class ResponseEncoder:
+
+ default_encoding = 'utf-8'
+ failmsg = "Response body could not be encoded with %r."
+ encoding = None
+ errors = 'strict'
+ text_only = True
+ add_charset = True
+ debug = False
+
+ def __init__(self, **kwargs):
+ for k, v in kwargs.items():
+ setattr(self, k, v)
+
+ self.attempted_charsets = set()
+ request = cherrypy.serving.request
+ if request.handler is not None:
+ # Replace request.handler with self
+ if self.debug:
+ cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE')
+ self.oldhandler = request.handler
+ request.handler = self
+
+ def encode_stream(self, encoding):
+ """Encode a streaming response body.
+
+ Use a generator wrapper, and just pray it works as the stream is
+ being written out.
+ """
+ if encoding in self.attempted_charsets:
+ return False
+ self.attempted_charsets.add(encoding)
+
+ def encoder(body):
+ for chunk in body:
+ if isinstance(chunk, unicodestr):
+ chunk = chunk.encode(encoding, self.errors)
+ yield chunk
+ self.body = encoder(self.body)
+ return True
+
+ def encode_string(self, encoding):
+ """Encode a buffered response body."""
+ if encoding in self.attempted_charsets:
+ return False
+ self.attempted_charsets.add(encoding)
+
+ try:
+ body = []
+ for chunk in self.body:
+ if isinstance(chunk, unicodestr):
+ chunk = chunk.encode(encoding, self.errors)
+ body.append(chunk)
+ self.body = body
+ except (LookupError, UnicodeError):
+ return False
+ else:
+ return True
+
+ def find_acceptable_charset(self):
+ request = cherrypy.serving.request
+ response = cherrypy.serving.response
+
+ if self.debug:
+ cherrypy.log('response.stream %r' % response.stream, 'TOOLS.ENCODE')
+ if response.stream:
+ encoder = self.encode_stream
+ else:
+ encoder = self.encode_string
+ if "Content-Length" in response.headers:
+ # Delete Content-Length header so finalize() recalcs it.
+ # Encoded strings may be of different lengths from their
+ # unicode equivalents, and even from each other. For example:
+ # >>> t = u"\u7007\u3040"
+ # >>> len(t)
+ # 2
+ # >>> len(t.encode("UTF-8"))
+ # 6
+ # >>> len(t.encode("utf7"))
+ # 8
+ del response.headers["Content-Length"]
+
+ # Parse the Accept-Charset request header, and try to provide one
+ # of the requested charsets (in order of user preference).
+ encs = request.headers.elements('Accept-Charset')
+ charsets = [enc.value.lower() for enc in encs]
+ if self.debug:
+ cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE')
+
+ if self.encoding is not None:
+ # If specified, force this encoding to be used, or fail.
+ encoding = self.encoding.lower()
+ if self.debug:
+ cherrypy.log('Specified encoding %r' % encoding, 'TOOLS.ENCODE')
+ if (not charsets) or "*" in charsets or encoding in charsets:
+ if self.debug:
+ cherrypy.log('Attempting encoding %r' % encoding, 'TOOLS.ENCODE')
+ if encoder(encoding):
+ return encoding
+ else:
+ if not encs:
+ if self.debug:
+ cherrypy.log('Attempting default encoding %r' %
+ self.default_encoding, 'TOOLS.ENCODE')
+ # Any character-set is acceptable.
+ if encoder(self.default_encoding):
+ return self.default_encoding
+ else:
+ raise cherrypy.HTTPError(500, self.failmsg % self.default_encoding)
+ else:
+ for element in encs:
+ if element.qvalue > 0:
+ if element.value == "*":
+ # Matches any charset. Try our default.
+ if self.debug:
+ cherrypy.log('Attempting default encoding due '
+ 'to %r' % element, 'TOOLS.ENCODE')
+ if encoder(self.default_encoding):
+ return self.default_encoding
+ else:
+ encoding = element.value
+ if self.debug:
+ cherrypy.log('Attempting encoding %s (qvalue >'
+ '0)' % element, 'TOOLS.ENCODE')
+ if encoder(encoding):
+ return encoding
+
+ if "*" not in charsets:
+ # If no "*" is present in an Accept-Charset field, then all
+ # character sets not explicitly mentioned get a quality
+ # value of 0, except for ISO-8859-1, which gets a quality
+ # value of 1 if not explicitly mentioned.
+ iso = 'iso-8859-1'
+ if iso not in charsets:
+ if self.debug:
+ cherrypy.log('Attempting ISO-8859-1 encoding',
+ 'TOOLS.ENCODE')
+ if encoder(iso):
+ return iso
+
+ # No suitable encoding found.
+ ac = request.headers.get('Accept-Charset')
+ if ac is None:
+ msg = "Your client did not send an Accept-Charset header."
+ else:
+ msg = "Your client sent this Accept-Charset header: %s." % ac
+ msg += " We tried these charsets: %s." % ", ".join(self.attempted_charsets)
+ raise cherrypy.HTTPError(406, msg)
+
+ def __call__(self, *args, **kwargs):
+ response = cherrypy.serving.response
+ self.body = self.oldhandler(*args, **kwargs)
+
+ if isinstance(self.body, basestring):
+ # strings get wrapped in a list because iterating over a single
+ # item list is much faster than iterating over every character
+ # in a long string.
+ if self.body:
+ self.body = [self.body]
+ else:
+ # [''] doesn't evaluate to False, so replace it with [].
+ self.body = []
+ elif hasattr(self.body, 'read'):
+ self.body = file_generator(self.body)
+ elif self.body is None:
+ self.body = []
+
+ ct = response.headers.elements("Content-Type")
+ if self.debug:
+ cherrypy.log('Content-Type: %r' % [str(h) for h in ct], 'TOOLS.ENCODE')
+ if ct:
+ ct = ct[0]
+ if self.text_only:
+ if ct.value.lower().startswith("text/"):
+ if self.debug:
+ cherrypy.log('Content-Type %s starts with "text/"' % ct,
+ 'TOOLS.ENCODE')
+ do_find = True
+ else:
+ if self.debug:
+ cherrypy.log('Not finding because Content-Type %s does '
+ 'not start with "text/"' % ct,
+ 'TOOLS.ENCODE')
+ do_find = False
+ else:
+ if self.debug:
+ cherrypy.log('Finding because not text_only', 'TOOLS.ENCODE')
+ do_find = True
+
+ if do_find:
+ # Set "charset=..." param on response Content-Type header
+ ct.params['charset'] = self.find_acceptable_charset()
+ if self.add_charset:
+ if self.debug:
+ cherrypy.log('Setting Content-Type %s' % ct,
+ 'TOOLS.ENCODE')
+ response.headers["Content-Type"] = str(ct)
+
+ return self.body
+
+# GZIP
+
+def compress(body, compress_level):
+ """Compress 'body' at the given compress_level."""
+ import zlib
+
+ # See http://www.gzip.org/zlib/rfc-gzip.html
+ yield ntob('\x1f\x8b') # ID1 and ID2: gzip marker
+ yield ntob('\x08') # CM: compression method
+ yield ntob('\x00') # FLG: none set
+ # MTIME: 4 bytes
+ yield struct.pack("<L", int(time.time()) & int('FFFFFFFF', 16))
+ yield ntob('\x02') # XFL: max compression, slowest algo
+ yield ntob('\xff') # OS: unknown
+
+ crc = zlib.crc32(ntob(""))
+ size = 0
+ zobj = zlib.compressobj(compress_level,
+ zlib.DEFLATED, -zlib.MAX_WBITS,
+ zlib.DEF_MEM_LEVEL, 0)
+ for line in body:
+ size += len(line)
+ crc = zlib.crc32(line, crc)
+ yield zobj.compress(line)
+ yield zobj.flush()
+
+ # CRC32: 4 bytes
+ yield struct.pack("<L", crc & int('FFFFFFFF', 16))
+ # ISIZE: 4 bytes
+ yield struct.pack("<L", size & int('FFFFFFFF', 16))
+
+def decompress(body):
+ import gzip
+
+ zbuf = BytesIO()
+ zbuf.write(body)
+ zbuf.seek(0)
+ zfile = gzip.GzipFile(mode='rb', fileobj=zbuf)
+ data = zfile.read()
+ zfile.close()
+ return data
+
+
+def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
+ """Try to gzip the response body if Content-Type in mime_types.
+
+ cherrypy.response.headers['Content-Type'] must be set to one of the
+ values in the mime_types arg before calling this function.
+
+ The provided list of mime-types must be of one of the following form:
+ * type/subtype
+ * type/*
+ * type/*+subtype
+
+ No compression is performed if any of the following hold:
+ * The client sends no Accept-Encoding request header
+ * No 'gzip' or 'x-gzip' is present in the Accept-Encoding header
+ * No 'gzip' or 'x-gzip' with a qvalue > 0 is present
+ * The 'identity' value is given with a qvalue > 0.
+
+ """
+ request = cherrypy.serving.request
+ response = cherrypy.serving.response
+
+ set_vary_header(response, "Accept-Encoding")
+
+ if not response.body:
+ # Response body is empty (might be a 304 for instance)
+ if debug:
+ cherrypy.log('No response body', context='TOOLS.GZIP')
+ return
+
+ # If returning cached content (which should already have been gzipped),
+ # don't re-zip.
+ if getattr(request, "cached", False):
+ if debug:
+ cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP')
+ return
+
+ acceptable = request.headers.elements('Accept-Encoding')
+ if not acceptable:
+ # If no Accept-Encoding field is present in a request,
+ # the server MAY assume that the client will accept any
+ # content coding. In this case, if "identity" is one of
+ # the available content-codings, then the server SHOULD use
+ # the "identity" content-coding, unless it has additional
+ # information that a different content-coding is meaningful
+ # to the client.
+ if debug:
+ cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP')
+ return
+
+ ct = response.headers.get('Content-Type', '').split(';')[0]
+ for coding in acceptable:
+ if coding.value == 'identity' and coding.qvalue != 0:
+ if debug:
+ cherrypy.log('Non-zero identity qvalue: %s' % coding,
+ context='TOOLS.GZIP')
+ return
+ if coding.value in ('gzip', 'x-gzip'):
+ if coding.qvalue == 0:
+ if debug:
+ cherrypy.log('Zero gzip qvalue: %s' % coding,
+ context='TOOLS.GZIP')
+ return
+
+ if ct not in mime_types:
+ # If the list of provided mime-types contains tokens
+ # such as 'text/*' or 'application/*+xml',
+ # we go through them and find the most appropriate one
+ # based on the given content-type.
+ # The pattern matching is only caring about the most
+ # common cases, as stated above, and doesn't support
+ # for extra parameters.
+ found = False
+ if '/' in ct:
+ ct_media_type, ct_sub_type = ct.split('/')
+ for mime_type in mime_types:
+ if '/' in mime_type:
+ media_type, sub_type = mime_type.split('/')
+ if ct_media_type == media_type:
+ if sub_type == '*':
+ found = True
+ break
+ elif '+' in sub_type and '+' in ct_sub_type:
+ ct_left, ct_right = ct_sub_type.split('+')
+ left, right = sub_type.split('+')
+ if left == '*' and ct_right == right:
+ found = True
+ break
+
+ if not found:
+ if debug:
+ cherrypy.log('Content-Type %s not in mime_types %r' %
+ (ct, mime_types), context='TOOLS.GZIP')
+ return
+
+ if debug:
+ cherrypy.log('Gzipping', context='TOOLS.GZIP')
+ # Return a generator that compresses the page
+ response.headers['Content-Encoding'] = 'gzip'
+ response.body = compress(response.body, compress_level)
+ if "Content-Length" in response.headers:
+ # Delete Content-Length header so finalize() recalcs it.
+ del response.headers["Content-Length"]
+
+ return
+
+ if debug:
+ cherrypy.log('No acceptable encoding found.', context='GZIP')
+ cherrypy.HTTPError(406, "identity, gzip").set_response()
+
diff --git a/cherrypy/lib/http.py b/cherrypy/lib/http.py
new file mode 100755
index 0000000..4661d69
--- /dev/null
+++ b/cherrypy/lib/http.py
@@ -0,0 +1,7 @@
+import warnings
+warnings.warn('cherrypy.lib.http has been deprecated and will be removed '
+ 'in CherryPy 3.3 use cherrypy.lib.httputil instead.',
+ DeprecationWarning)
+
+from cherrypy.lib.httputil import *
+
diff --git a/cherrypy/lib/httpauth.py b/cherrypy/lib/httpauth.py
new file mode 100755
index 0000000..ad7c6eb
--- /dev/null
+++ b/cherrypy/lib/httpauth.py
@@ -0,0 +1,354 @@
+"""
+This module defines functions to implement HTTP Digest Authentication (:rfc:`2617`).
+This has full compliance with 'Digest' and 'Basic' authentication methods. In
+'Digest' it supports both MD5 and MD5-sess algorithms.
+
+Usage:
+ First use 'doAuth' to request the client authentication for a
+ certain resource. You should send an httplib.UNAUTHORIZED response to the
+ client so he knows he has to authenticate itself.
+
+ Then use 'parseAuthorization' to retrieve the 'auth_map' used in
+ 'checkResponse'.
+
+ To use 'checkResponse' you must have already verified the password associated
+ with the 'username' key in 'auth_map' dict. Then you use the 'checkResponse'
+ function to verify if the password matches the one sent by the client.
+
+SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms
+SUPPORTED_QOP - list of supported 'Digest' 'qop'.
+"""
+__version__ = 1, 0, 1
+__author__ = "Tiago Cogumbreiro <cogumbreiro@users.sf.net>"
+__credits__ = """
+ Peter van Kampen for its recipe which implement most of Digest authentication:
+ http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378
+"""
+
+__license__ = """
+Copyright (c) 2005, Tiago Cogumbreiro <cogumbreiro@users.sf.net>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of Sylvain Hellegouarch nor the names of his contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+"""
+
+__all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse",
+ "parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey",
+ "calculateNonce", "SUPPORTED_QOP")
+
+################################################################################
+import time
+from cherrypy._cpcompat import base64_decode, ntob, md5
+from cherrypy._cpcompat import parse_http_list, parse_keqv_list
+
+MD5 = "MD5"
+MD5_SESS = "MD5-sess"
+AUTH = "auth"
+AUTH_INT = "auth-int"
+
+SUPPORTED_ALGORITHM = (MD5, MD5_SESS)
+SUPPORTED_QOP = (AUTH, AUTH_INT)
+
+################################################################################
+# doAuth
+#
+DIGEST_AUTH_ENCODERS = {
+ MD5: lambda val: md5(ntob(val)).hexdigest(),
+ MD5_SESS: lambda val: md5(ntob(val)).hexdigest(),
+# SHA: lambda val: sha.new(ntob(val)).hexdigest (),
+}
+
+def calculateNonce (realm, algorithm = MD5):
+ """This is an auxaliary function that calculates 'nonce' value. It is used
+ to handle sessions."""
+
+ global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS
+ assert algorithm in SUPPORTED_ALGORITHM
+
+ try:
+ encoder = DIGEST_AUTH_ENCODERS[algorithm]
+ except KeyError:
+ raise NotImplementedError ("The chosen algorithm (%s) does not have "\
+ "an implementation yet" % algorithm)
+
+ return encoder ("%d:%s" % (time.time(), realm))
+
+def digestAuth (realm, algorithm = MD5, nonce = None, qop = AUTH):
+ """Challenges the client for a Digest authentication."""
+ global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS, SUPPORTED_QOP
+ assert algorithm in SUPPORTED_ALGORITHM
+ assert qop in SUPPORTED_QOP
+
+ if nonce is None:
+ nonce = calculateNonce (realm, algorithm)
+
+ return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
+ realm, nonce, algorithm, qop
+ )
+
+def basicAuth (realm):
+ """Challengenes the client for a Basic authentication."""
+ assert '"' not in realm, "Realms cannot contain the \" (quote) character."
+
+ return 'Basic realm="%s"' % realm
+
+def doAuth (realm):
+ """'doAuth' function returns the challenge string b giving priority over
+ Digest and fallback to Basic authentication when the browser doesn't
+ support the first one.
+
+ This should be set in the HTTP header under the key 'WWW-Authenticate'."""
+
+ return digestAuth (realm) + " " + basicAuth (realm)
+
+
+################################################################################
+# Parse authorization parameters
+#
+def _parseDigestAuthorization (auth_params):
+ # Convert the auth params to a dict
+ items = parse_http_list(auth_params)
+ params = parse_keqv_list(items)
+
+ # Now validate the params
+
+ # Check for required parameters
+ required = ["username", "realm", "nonce", "uri", "response"]
+ for k in required:
+ if k not in params:
+ return None
+
+ # If qop is sent then cnonce and nc MUST be present
+ if "qop" in params and not ("cnonce" in params \
+ and "nc" in params):
+ return None
+
+ # If qop is not sent, neither cnonce nor nc can be present
+ if ("cnonce" in params or "nc" in params) and \
+ "qop" not in params:
+ return None
+
+ return params
+
+
+def _parseBasicAuthorization (auth_params):
+ username, password = base64_decode(auth_params).split(":", 1)
+ return {"username": username, "password": password}
+
+AUTH_SCHEMES = {
+ "basic": _parseBasicAuthorization,
+ "digest": _parseDigestAuthorization,
+}
+
+def parseAuthorization (credentials):
+ """parseAuthorization will convert the value of the 'Authorization' key in
+ the HTTP header to a map itself. If the parsing fails 'None' is returned.
+ """
+
+ global AUTH_SCHEMES
+
+ auth_scheme, auth_params = credentials.split(" ", 1)
+ auth_scheme = auth_scheme.lower ()
+
+ parser = AUTH_SCHEMES[auth_scheme]
+ params = parser (auth_params)
+
+ if params is None:
+ return
+
+ assert "auth_scheme" not in params
+ params["auth_scheme"] = auth_scheme
+ return params
+
+
+################################################################################
+# Check provided response for a valid password
+#
+def md5SessionKey (params, password):
+ """
+ If the "algorithm" directive's value is "MD5-sess", then A1
+ [the session key] is calculated only once - on the first request by the
+ client following receipt of a WWW-Authenticate challenge from the server.
+
+ This creates a 'session key' for the authentication of subsequent
+ requests and responses which is different for each "authentication
+ session", thus limiting the amount of material hashed with any one
+ key.
+
+ Because the server need only use the hash of the user
+ credentials in order to create the A1 value, this construction could
+ be used in conjunction with a third party authentication service so
+ that the web server would not need the actual password value. The
+ specification of such a protocol is beyond the scope of this
+ specification.
+"""
+
+ keys = ("username", "realm", "nonce", "cnonce")
+ params_copy = {}
+ for key in keys:
+ params_copy[key] = params[key]
+
+ params_copy["algorithm"] = MD5_SESS
+ return _A1 (params_copy, password)
+
+def _A1(params, password):
+ algorithm = params.get ("algorithm", MD5)
+ H = DIGEST_AUTH_ENCODERS[algorithm]
+
+ if algorithm == MD5:
+ # If the "algorithm" directive's value is "MD5" or is
+ # unspecified, then A1 is:
+ # A1 = unq(username-value) ":" unq(realm-value) ":" passwd
+ return "%s:%s:%s" % (params["username"], params["realm"], password)
+
+ elif algorithm == MD5_SESS:
+
+ # This is A1 if qop is set
+ # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
+ # ":" unq(nonce-value) ":" unq(cnonce-value)
+ h_a1 = H ("%s:%s:%s" % (params["username"], params["realm"], password))
+ return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"])
+
+
+def _A2(params, method, kwargs):
+ # If the "qop" directive's value is "auth" or is unspecified, then A2 is:
+ # A2 = Method ":" digest-uri-value
+
+ qop = params.get ("qop", "auth")
+ if qop == "auth":
+ return method + ":" + params["uri"]
+ elif qop == "auth-int":
+ # If the "qop" value is "auth-int", then A2 is:
+ # A2 = Method ":" digest-uri-value ":" H(entity-body)
+ entity_body = kwargs.get ("entity_body", "")
+ H = kwargs["H"]
+
+ return "%s:%s:%s" % (
+ method,
+ params["uri"],
+ H(entity_body)
+ )
+
+ else:
+ raise NotImplementedError ("The 'qop' method is unknown: %s" % qop)
+
+def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwargs):
+ """
+ Generates a response respecting the algorithm defined in RFC 2617
+ """
+ params = auth_map
+
+ algorithm = params.get ("algorithm", MD5)
+
+ H = DIGEST_AUTH_ENCODERS[algorithm]
+ KD = lambda secret, data: H(secret + ":" + data)
+
+ qop = params.get ("qop", None)
+
+ H_A2 = H(_A2(params, method, kwargs))
+
+ if algorithm == MD5_SESS and A1 is not None:
+ H_A1 = H(A1)
+ else:
+ H_A1 = H(_A1(params, password))
+
+ if qop in ("auth", "auth-int"):
+ # If the "qop" value is "auth" or "auth-int":
+ # request-digest = <"> < KD ( H(A1), unq(nonce-value)
+ # ":" nc-value
+ # ":" unq(cnonce-value)
+ # ":" unq(qop-value)
+ # ":" H(A2)
+ # ) <">
+ request = "%s:%s:%s:%s:%s" % (
+ params["nonce"],
+ params["nc"],
+ params["cnonce"],
+ params["qop"],
+ H_A2,
+ )
+ elif qop is None:
+ # If the "qop" directive is not present (this construction is
+ # for compatibility with RFC 2069):
+ # request-digest =
+ # <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
+ request = "%s:%s" % (params["nonce"], H_A2)
+
+ return KD(H_A1, request)
+
+def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs):
+ """This function is used to verify the response given by the client when
+ he tries to authenticate.
+ Optional arguments:
+ entity_body - when 'qop' is set to 'auth-int' you MUST provide the
+ raw data you are going to send to the client (usually the
+ HTML page.
+ request_uri - the uri from the request line compared with the 'uri'
+ directive of the authorization map. They must represent
+ the same resource (unused at this time).
+ """
+
+ if auth_map['realm'] != kwargs.get('realm', None):
+ return False
+
+ response = _computeDigestResponse(auth_map, password, method, A1,**kwargs)
+
+ return response == auth_map["response"]
+
+def _checkBasicResponse (auth_map, password, method='GET', encrypt=None, **kwargs):
+ # Note that the Basic response doesn't provide the realm value so we cannot
+ # test it
+ try:
+ return encrypt(auth_map["password"], auth_map["username"]) == password
+ except TypeError:
+ return encrypt(auth_map["password"]) == password
+
+AUTH_RESPONSES = {
+ "basic": _checkBasicResponse,
+ "digest": _checkDigestResponse,
+}
+
+def checkResponse (auth_map, password, method = "GET", encrypt=None, **kwargs):
+ """'checkResponse' compares the auth_map with the password and optionally
+ other arguments that each implementation might need.
+
+ If the response is of type 'Basic' then the function has the following
+ signature::
+
+ checkBasicResponse (auth_map, password) -> bool
+
+ If the response is of type 'Digest' then the function has the following
+ signature::
+
+ checkDigestResponse (auth_map, password, method = 'GET', A1 = None) -> bool
+
+ The 'A1' argument is only used in MD5_SESS algorithm based responses.
+ Check md5SessionKey() for more info.
+ """
+ checker = AUTH_RESPONSES[auth_map["auth_scheme"]]
+ return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs)
+
+
+
+
diff --git a/cherrypy/lib/httputil.py b/cherrypy/lib/httputil.py
new file mode 100755
index 0000000..e005875
--- /dev/null
+++ b/cherrypy/lib/httputil.py
@@ -0,0 +1,469 @@
+"""HTTP library functions.
+
+This module contains functions for building an HTTP application
+framework: any one, not just one whose name starts with "Ch". ;) If you
+reference any modules from some popular framework inside *this* module,
+FuManChu will personally hang you up by your thumbs and submit you
+to a public caning.
+"""
+
+from binascii import b2a_base64
+from cherrypy._cpcompat import BaseHTTPRequestHandler, HTTPDate, ntob, ntou, reversed, sorted
+from cherrypy._cpcompat import basestring, iteritems, unicodestr, unquote_qs
+response_codes = BaseHTTPRequestHandler.responses.copy()
+
+# From http://www.cherrypy.org/ticket/361
+response_codes[500] = ('Internal Server Error',
+ 'The server encountered an unexpected condition '
+ 'which prevented it from fulfilling the request.')
+response_codes[503] = ('Service Unavailable',
+ 'The server is currently unable to handle the '
+ 'request due to a temporary overloading or '
+ 'maintenance of the server.')
+
+import re
+import urllib
+
+
+
+def urljoin(*atoms):
+ """Return the given path \*atoms, joined into a single URL.
+
+ This will correctly join a SCRIPT_NAME and PATH_INFO into the
+ original URL, even if either atom is blank.
+ """
+ url = "/".join([x for x in atoms if x])
+ while "//" in url:
+ url = url.replace("//", "/")
+ # Special-case the final url of "", and return "/" instead.
+ return url or "/"
+
+def protocol_from_http(protocol_str):
+ """Return a protocol tuple from the given 'HTTP/x.y' string."""
+ return int(protocol_str[5]), int(protocol_str[7])
+
+def get_ranges(headervalue, content_length):
+ """Return a list of (start, stop) indices from a Range header, or None.
+
+ Each (start, stop) tuple will be composed of two ints, which are suitable
+ for use in a slicing operation. That is, the header "Range: bytes=3-6",
+ if applied against a Python string, is requesting resource[3:7]. This
+ function will return the list [(3, 7)].
+
+ If this function returns an empty list, you should return HTTP 416.
+ """
+
+ if not headervalue:
+ return None
+
+ result = []
+ bytesunit, byteranges = headervalue.split("=", 1)
+ for brange in byteranges.split(","):
+ start, stop = [x.strip() for x in brange.split("-", 1)]
+ if start:
+ if not stop:
+ stop = content_length - 1
+ start, stop = int(start), int(stop)
+ if start >= content_length:
+ # From rfc 2616 sec 14.16:
+ # "If the server receives a request (other than one
+ # including an If-Range request-header field) with an
+ # unsatisfiable Range request-header field (that is,
+ # all of whose byte-range-spec values have a first-byte-pos
+ # value greater than the current length of the selected
+ # resource), it SHOULD return a response code of 416
+ # (Requested range not satisfiable)."
+ continue
+ if stop < start:
+ # From rfc 2616 sec 14.16:
+ # "If the server ignores a byte-range-spec because it
+ # is syntactically invalid, the server SHOULD treat
+ # the request as if the invalid Range header field
+ # did not exist. (Normally, this means return a 200
+ # response containing the full entity)."
+ return None
+ result.append((start, stop + 1))
+ else:
+ if not stop:
+ # See rfc quote above.
+ return None
+ # Negative subscript (last N bytes)
+ result.append((content_length - int(stop), content_length))
+
+ return result
+
+
+class HeaderElement(object):
+ """An element (with parameters) from an HTTP header's element list."""
+
+ def __init__(self, value, params=None):
+ self.value = value
+ if params is None:
+ params = {}
+ self.params = params
+
+ def __cmp__(self, other):
+ return cmp(self.value, other.value)
+
+ def __str__(self):
+ p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)]
+ return "%s%s" % (self.value, "".join(p))
+
+ def __unicode__(self):
+ return ntou(self.__str__())
+
+ def parse(elementstr):
+ """Transform 'token;key=val' to ('token', {'key': 'val'})."""
+ # Split the element into a value and parameters. The 'value' may
+ # be of the form, "token=token", but we don't split that here.
+ atoms = [x.strip() for x in elementstr.split(";") if x.strip()]
+ if not atoms:
+ initial_value = ''
+ else:
+ initial_value = atoms.pop(0).strip()
+ params = {}
+ for atom in atoms:
+ atom = [x.strip() for x in atom.split("=", 1) if x.strip()]
+ key = atom.pop(0)
+ if atom:
+ val = atom[0]
+ else:
+ val = ""
+ params[key] = val
+ return initial_value, params
+ parse = staticmethod(parse)
+
+ def from_str(cls, elementstr):
+ """Construct an instance from a string of the form 'token;key=val'."""
+ ival, params = cls.parse(elementstr)
+ return cls(ival, params)
+ from_str = classmethod(from_str)
+
+
+q_separator = re.compile(r'; *q *=')
+
+class AcceptElement(HeaderElement):
+ """An element (with parameters) from an Accept* header's element list.
+
+ AcceptElement objects are comparable; the more-preferred object will be
+ "less than" the less-preferred object. They are also therefore sortable;
+ if you sort a list of AcceptElement objects, they will be listed in
+ priority order; the most preferred value will be first. Yes, it should
+ have been the other way around, but it's too late to fix now.
+ """
+
+ def from_str(cls, elementstr):
+ qvalue = None
+ # The first "q" parameter (if any) separates the initial
+ # media-range parameter(s) (if any) from the accept-params.
+ atoms = q_separator.split(elementstr, 1)
+ media_range = atoms.pop(0).strip()
+ if atoms:
+ # The qvalue for an Accept header can have extensions. The other
+ # headers cannot, but it's easier to parse them as if they did.
+ qvalue = HeaderElement.from_str(atoms[0].strip())
+
+ media_type, params = cls.parse(media_range)
+ if qvalue is not None:
+ params["q"] = qvalue
+ return cls(media_type, params)
+ from_str = classmethod(from_str)
+
+ def qvalue(self):
+ val = self.params.get("q", "1")
+ if isinstance(val, HeaderElement):
+ val = val.value
+ return float(val)
+ qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
+
+ def __cmp__(self, other):
+ diff = cmp(self.qvalue, other.qvalue)
+ if diff == 0:
+ diff = cmp(str(self), str(other))
+ return diff
+
+
+def header_elements(fieldname, fieldvalue):
+ """Return a sorted HeaderElement list from a comma-separated header string."""
+ if not fieldvalue:
+ return []
+
+ result = []
+ for element in fieldvalue.split(","):
+ if fieldname.startswith("Accept") or fieldname == 'TE':
+ hv = AcceptElement.from_str(element)
+ else:
+ hv = HeaderElement.from_str(element)
+ result.append(hv)
+
+ return list(reversed(sorted(result)))
+
+def decode_TEXT(value):
+ r"""Decode :rfc:`2047` TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> u"f\xfcr")."""
+ from email.Header import decode_header
+ atoms = decode_header(value)
+ decodedvalue = ""
+ for atom, charset in atoms:
+ if charset is not None:
+ atom = atom.decode(charset)
+ decodedvalue += atom
+ return decodedvalue
+
+def valid_status(status):
+ """Return legal HTTP status Code, Reason-phrase and Message.
+
+ The status arg must be an int, or a str that begins with an int.
+
+ If status is an int, or a str and no reason-phrase is supplied,
+ a default reason-phrase will be provided.
+ """
+
+ if not status:
+ status = 200
+
+ status = str(status)
+ parts = status.split(" ", 1)
+ if len(parts) == 1:
+ # No reason supplied.
+ code, = parts
+ reason = None
+ else:
+ code, reason = parts
+ reason = reason.strip()
+
+ try:
+ code = int(code)
+ except ValueError:
+ raise ValueError("Illegal response status from server "
+ "(%s is non-numeric)." % repr(code))
+
+ if code < 100 or code > 599:
+ raise ValueError("Illegal response status from server "
+ "(%s is out of range)." % repr(code))
+
+ if code not in response_codes:
+ # code is unknown but not illegal
+ default_reason, message = "", ""
+ else:
+ default_reason, message = response_codes[code]
+
+ if reason is None:
+ reason = default_reason
+
+ return code, reason, message
+
+
+def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
+ """Parse a query given as a string argument.
+
+ Arguments:
+
+ qs: URL-encoded query string to be parsed
+
+ keep_blank_values: flag indicating whether blank values in
+ URL encoded queries should be treated as blank strings. A
+ true value indicates that blanks should be retained as blank
+ strings. The default false value indicates that blank values
+ are to be ignored and treated as if they were not included.
+
+ strict_parsing: flag indicating what to do with parsing errors. If
+ false (the default), errors are silently ignored. If true,
+ errors raise a ValueError exception.
+
+ Returns a dict, as G-d intended.
+ """
+ pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
+ d = {}
+ for name_value in pairs:
+ if not name_value and not strict_parsing:
+ continue
+ nv = name_value.split('=', 1)
+ if len(nv) != 2:
+ if strict_parsing:
+ raise ValueError("bad query field: %r" % (name_value,))
+ # Handle case of a control-name with no equal sign
+ if keep_blank_values:
+ nv.append('')
+ else:
+ continue
+ if len(nv[1]) or keep_blank_values:
+ name = unquote_qs(nv[0], encoding)
+ value = unquote_qs(nv[1], encoding)
+ if name in d:
+ if not isinstance(d[name], list):
+ d[name] = [d[name]]
+ d[name].append(value)
+ else:
+ d[name] = value
+ return d
+
+
+image_map_pattern = re.compile(r"[0-9]+,[0-9]+")
+
+def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
+ """Build a params dictionary from a query_string.
+
+ Duplicate key/value pairs in the provided query_string will be
+ returned as {'key': [val1, val2, ...]}. Single key/values will
+ be returned as strings: {'key': 'value'}.
+ """
+ if image_map_pattern.match(query_string):
+ # Server-side image map. Map the coords to 'x' and 'y'
+ # (like CGI::Request does).
+ pm = query_string.split(",")
+ pm = {'x': int(pm[0]), 'y': int(pm[1])}
+ else:
+ pm = _parse_qs(query_string, keep_blank_values, encoding=encoding)
+ return pm
+
+
+class CaseInsensitiveDict(dict):
+ """A case-insensitive dict subclass.
+
+ Each key is changed on entry to str(key).title().
+ """
+
+ def __getitem__(self, key):
+ return dict.__getitem__(self, str(key).title())
+
+ def __setitem__(self, key, value):
+ dict.__setitem__(self, str(key).title(), value)
+
+ def __delitem__(self, key):
+ dict.__delitem__(self, str(key).title())
+
+ def __contains__(self, key):
+ return dict.__contains__(self, str(key).title())
+
+ def get(self, key, default=None):
+ return dict.get(self, str(key).title(), default)
+
+ def has_key(self, key):
+ return dict.has_key(self, str(key).title())
+
+ def update(self, E):
+ for k in E.keys():
+ self[str(k).title()] = E[k]
+
+ def fromkeys(cls, seq, value=None):
+ newdict = cls()
+ for k in seq:
+ newdict[str(k).title()] = value
+ return newdict
+ fromkeys = classmethod(fromkeys)
+
+ def setdefault(self, key, x=None):
+ key = str(key).title()
+ try:
+ return self[key]
+ except KeyError:
+ self[key] = x
+ return x
+
+ def pop(self, key, default):
+ return dict.pop(self, str(key).title(), default)
+
+
+# TEXT = <any OCTET except CTLs, but including LWS>
+#
+# A CRLF is allowed in the definition of TEXT only as part of a header
+# field continuation. It is expected that the folding LWS will be
+# replaced with a single SP before interpretation of the TEXT value."
+header_translate_table = ''.join([chr(i) for i in xrange(256)])
+header_translate_deletechars = ''.join([chr(i) for i in xrange(32)]) + chr(127)
+
+
+class HeaderMap(CaseInsensitiveDict):
+ """A dict subclass for HTTP request and response headers.
+
+ Each key is changed on entry to str(key).title(). This allows headers
+ to be case-insensitive and avoid duplicates.
+
+ Values are header values (decoded according to :rfc:`2047` if necessary).
+ """
+
+ protocol=(1, 1)
+ encodings = ["ISO-8859-1"]
+
+ # Someday, when http-bis is done, this will probably get dropped
+ # since few servers, clients, or intermediaries do it. But until then,
+ # we're going to obey the spec as is.
+ # "Words of *TEXT MAY contain characters from character sets other than
+ # ISO-8859-1 only when encoded according to the rules of RFC 2047."
+ use_rfc_2047 = True
+
+ def elements(self, key):
+ """Return a sorted list of HeaderElements for the given header."""
+ key = str(key).title()
+ value = self.get(key)
+ return header_elements(key, value)
+
+ def values(self, key):
+ """Return a sorted list of HeaderElement.value for the given header."""
+ return [e.value for e in self.elements(key)]
+
+ def output(self):
+ """Transform self into a list of (name, value) tuples."""
+ header_list = []
+ for k, v in self.items():
+ if isinstance(k, unicodestr):
+ k = self.encode(k)
+
+ if not isinstance(v, basestring):
+ v = str(v)
+
+ if isinstance(v, unicodestr):
+ v = self.encode(v)
+
+ # See header_translate_* constants above.
+ # Replace only if you really know what you're doing.
+ k = k.translate(header_translate_table, header_translate_deletechars)
+ v = v.translate(header_translate_table, header_translate_deletechars)
+
+ header_list.append((k, v))
+ return header_list
+
+ def encode(self, v):
+ """Return the given header name or value, encoded for HTTP output."""
+ for enc in self.encodings:
+ try:
+ return v.encode(enc)
+ except UnicodeEncodeError:
+ continue
+
+ if self.protocol == (1, 1) and self.use_rfc_2047:
+ # Encode RFC-2047 TEXT
+ # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
+ # We do our own here instead of using the email module
+ # because we never want to fold lines--folding has
+ # been deprecated by the HTTP working group.
+ v = b2a_base64(v.encode('utf-8'))
+ return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?='))
+
+ raise ValueError("Could not encode header part %r using "
+ "any of the encodings %r." %
+ (v, self.encodings))
+
+
+class Host(object):
+ """An internet address.
+
+ name
+ Should be the client's host name. If not available (because no DNS
+ lookup is performed), the IP address should be used instead.
+
+ """
+
+ ip = "0.0.0.0"
+ port = 80
+ name = "unknown.tld"
+
+ def __init__(self, ip, port, name=None):
+ self.ip = ip
+ self.port = port
+ if name is None:
+ name = ip
+ self.name = name
+
+ def __repr__(self):
+ return "httputil.Host(%r, %r, %r)" % (self.ip, self.port, self.name)
diff --git a/cherrypy/lib/jsontools.py b/cherrypy/lib/jsontools.py
new file mode 100755
index 0000000..09042e4
--- /dev/null
+++ b/cherrypy/lib/jsontools.py
@@ -0,0 +1,87 @@
+import sys
+import cherrypy
+from cherrypy._cpcompat import basestring, ntou, json, json_encode, json_decode
+
+def json_processor(entity):
+ """Read application/json data into request.json."""
+ if not entity.headers.get(ntou("Content-Length"), ntou("")):
+ raise cherrypy.HTTPError(411)
+
+ body = entity.fp.read()
+ try:
+ cherrypy.serving.request.json = json_decode(body.decode('utf-8'))
+ except ValueError:
+ raise cherrypy.HTTPError(400, 'Invalid JSON document')
+
+def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
+ force=True, debug=False, processor = json_processor):
+ """Add a processor to parse JSON request entities:
+ The default processor places the parsed data into request.json.
+
+ Incoming request entities which match the given content_type(s) will
+ be deserialized from JSON to the Python equivalent, and the result
+ stored at cherrypy.request.json. The 'content_type' argument may
+ be a Content-Type string or a list of allowable Content-Type strings.
+
+ If the 'force' argument is True (the default), then entities of other
+ content types will not be allowed; "415 Unsupported Media Type" is
+ raised instead.
+
+ Supply your own processor to use a custom decoder, or to handle the parsed
+ data differently. The processor can be configured via
+ tools.json_in.processor or via the decorator method.
+
+ Note that the deserializer requires the client send a Content-Length
+ request header, or it will raise "411 Length Required". If for any
+ other reason the request entity cannot be deserialized from JSON,
+ it will raise "400 Bad Request: Invalid JSON document".
+
+ You must be using Python 2.6 or greater, or have the 'simplejson'
+ package importable; otherwise, ValueError is raised during processing.
+ """
+ request = cherrypy.serving.request
+ if isinstance(content_type, basestring):
+ content_type = [content_type]
+
+ if force:
+ if debug:
+ cherrypy.log('Removing body processors %s' %
+ repr(request.body.processors.keys()), 'TOOLS.JSON_IN')
+ request.body.processors.clear()
+ request.body.default_proc = cherrypy.HTTPError(
+ 415, 'Expected an entity of content type %s' %
+ ', '.join(content_type))
+
+ for ct in content_type:
+ if debug:
+ cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN')
+ request.body.processors[ct] = processor
+
+def json_handler(*args, **kwargs):
+ value = cherrypy.serving.request._json_inner_handler(*args, **kwargs)
+ return json_encode(value)
+
+def json_out(content_type='application/json', debug=False, handler=json_handler):
+ """Wrap request.handler to serialize its output to JSON. Sets Content-Type.
+
+ If the given content_type is None, the Content-Type response header
+ is not set.
+
+ Provide your own handler to use a custom encoder. For example
+ cherrypy.config['tools.json_out.handler'] = <function>, or
+ @json_out(handler=function).
+
+ You must be using Python 2.6 or greater, or have the 'simplejson'
+ package importable; otherwise, ValueError is raised during processing.
+ """
+ request = cherrypy.serving.request
+ if debug:
+ cherrypy.log('Replacing %s with JSON handler' % request.handler,
+ 'TOOLS.JSON_OUT')
+ request._json_inner_handler = request.handler
+ request.handler = handler
+ if content_type is not None:
+ if debug:
+ cherrypy.log('Setting Content-Type to %s' % ct, 'TOOLS.JSON_OUT')
+ cherrypy.serving.response.headers['Content-Type'] = content_type
+
diff --git a/cherrypy/lib/profiler.py b/cherrypy/lib/profiler.py
new file mode 100755
index 0000000..785d58a
--- /dev/null
+++ b/cherrypy/lib/profiler.py
@@ -0,0 +1,208 @@
+"""Profiler tools for CherryPy.
+
+CherryPy users
+==============
+
+You can profile any of your pages as follows::
+
+ from cherrypy.lib import profiler
+
+ class Root:
+ p = profile.Profiler("/path/to/profile/dir")
+
+ def index(self):
+ self.p.run(self._index)
+ index.exposed = True
+
+ def _index(self):
+ return "Hello, world!"
+
+ cherrypy.tree.mount(Root())
+
+You can also turn on profiling for all requests
+using the ``make_app`` function as WSGI middleware.
+
+CherryPy developers
+===================
+
+This module can be used whenever you make changes to CherryPy,
+to get a quick sanity-check on overall CP performance. Use the
+``--profile`` flag when running the test suite. Then, use the ``serve()``
+function to browse the results in a web browser. If you run this
+module from the command line, it will call ``serve()`` for you.
+
+"""
+
+
+def new_func_strip_path(func_name):
+ """Make profiler output more readable by adding ``__init__`` modules' parents"""
+ filename, line, name = func_name
+ if filename.endswith("__init__.py"):
+ return os.path.basename(filename[:-12]) + filename[-12:], line, name
+ return os.path.basename(filename), line, name
+
+try:
+ import profile
+ import pstats
+ pstats.func_strip_path = new_func_strip_path
+except ImportError:
+ profile = None
+ pstats = None
+
+import os, os.path
+import sys
+import warnings
+
+from cherrypy._cpcompat import BytesIO
+
+_count = 0
+
+class Profiler(object):
+
+ def __init__(self, path=None):
+ if not path:
+ path = os.path.join(os.path.dirname(__file__), "profile")
+ self.path = path
+ if not os.path.exists(path):
+ os.makedirs(path)
+
+ def run(self, func, *args, **params):
+ """Dump profile data into self.path."""
+ global _count
+ c = _count = _count + 1
+ path = os.path.join(self.path, "cp_%04d.prof" % c)
+ prof = profile.Profile()
+ result = prof.runcall(func, *args, **params)
+ prof.dump_stats(path)
+ return result
+
+ def statfiles(self):
+ """:rtype: list of available profiles.
+ """
+ return [f for f in os.listdir(self.path)
+ if f.startswith("cp_") and f.endswith(".prof")]
+
+ def stats(self, filename, sortby='cumulative'):
+ """:rtype stats(index): output of print_stats() for the given profile.
+ """
+ sio = BytesIO()
+ if sys.version_info >= (2, 5):
+ s = pstats.Stats(os.path.join(self.path, filename), stream=sio)
+ s.strip_dirs()
+ s.sort_stats(sortby)
+ s.print_stats()
+ else:
+ # pstats.Stats before Python 2.5 didn't take a 'stream' arg,
+ # but just printed to stdout. So re-route stdout.
+ s = pstats.Stats(os.path.join(self.path, filename))
+ s.strip_dirs()
+ s.sort_stats(sortby)
+ oldout = sys.stdout
+ try:
+ sys.stdout = sio
+ s.print_stats()
+ finally:
+ sys.stdout = oldout
+ response = sio.getvalue()
+ sio.close()
+ return response
+
+ def index(self):
+ return """<html>
+ <head><title>CherryPy profile data</title></head>
+ <frameset cols='200, 1*'>
+ <frame src='menu' />
+ <frame name='main' src='' />
+ </frameset>
+ </html>
+ """
+ index.exposed = True
+
+ def menu(self):
+ yield "<h2>Profiling runs</h2>"
+ yield "<p>Click on one of the runs below to see profiling data.</p>"
+ runs = self.statfiles()
+ runs.sort()
+ for i in runs:
+ yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (i, i)
+ menu.exposed = True
+
+ def report(self, filename):
+ import cherrypy
+ cherrypy.response.headers['Content-Type'] = 'text/plain'
+ return self.stats(filename)
+ report.exposed = True
+
+
+class ProfileAggregator(Profiler):
+
+ def __init__(self, path=None):
+ Profiler.__init__(self, path)
+ global _count
+ self.count = _count = _count + 1
+ self.profiler = profile.Profile()
+
+ def run(self, func, *args):
+ path = os.path.join(self.path, "cp_%04d.prof" % self.count)
+ result = self.profiler.runcall(func, *args)
+ self.profiler.dump_stats(path)
+ return result
+
+
+class make_app:
+ def __init__(self, nextapp, path=None, aggregate=False):
+ """Make a WSGI middleware app which wraps 'nextapp' with profiling.
+
+ nextapp
+ the WSGI application to wrap, usually an instance of
+ cherrypy.Application.
+
+ path
+ where to dump the profiling output.
+
+ aggregate
+ if True, profile data for all HTTP requests will go in
+ a single file. If False (the default), each HTTP request will
+ dump its profile data into a separate file.
+
+ """
+ if profile is None or pstats is None:
+ msg = ("Your installation of Python does not have a profile module. "
+ "If you're on Debian, try `sudo apt-get install python-profiler`. "
+ "See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.")
+ warnings.warn(msg)
+
+ self.nextapp = nextapp
+ self.aggregate = aggregate
+ if aggregate:
+ self.profiler = ProfileAggregator(path)
+ else:
+ self.profiler = Profiler(path)
+
+ def __call__(self, environ, start_response):
+ def gather():
+ result = []
+ for line in self.nextapp(environ, start_response):
+ result.append(line)
+ return result
+ return self.profiler.run(gather)
+
+
+def serve(path=None, port=8080):
+ if profile is None or pstats is None:
+ msg = ("Your installation of Python does not have a profile module. "
+ "If you're on Debian, try `sudo apt-get install python-profiler`. "
+ "See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.")
+ warnings.warn(msg)
+
+ import cherrypy
+ cherrypy.config.update({'server.socket_port': int(port),
+ 'server.thread_pool': 10,
+ 'environment': "production",
+ })
+ cherrypy.quickstart(Profiler(path))
+
+
+if __name__ == "__main__":
+ serve(*tuple(sys.argv[1:]))
+
diff --git a/cherrypy/lib/reprconf.py b/cherrypy/lib/reprconf.py
new file mode 100755
index 0000000..e18949e
--- /dev/null
+++ b/cherrypy/lib/reprconf.py
@@ -0,0 +1,351 @@
+"""Generic configuration system using unrepr.
+
+Configuration data may be supplied as a Python dictionary, as a filename,
+or as an open file object. When you supply a filename or file, Python's
+builtin ConfigParser is used (with some extensions).
+
+Namespaces
+----------
+
+Configuration keys are separated into namespaces by the first "." in the key.
+
+The only key that cannot exist in a namespace is the "environment" entry.
+This special entry 'imports' other config entries from a template stored in
+the Config.environments dict.
+
+You can define your own namespaces to be called when new config is merged
+by adding a named handler to Config.namespaces. The name can be any string,
+and the handler must be either a callable or a context manager.
+"""
+
+try:
+ # Python 3.0+
+ from configparser import ConfigParser
+except ImportError:
+ from ConfigParser import ConfigParser
+
+try:
+ set
+except NameError:
+ from sets import Set as set
+import sys
+
+def as_dict(config):
+ """Return a dict from 'config' whether it is a dict, file, or filename."""
+ if isinstance(config, basestring):
+ config = Parser().dict_from_file(config)
+ elif hasattr(config, 'read'):
+ config = Parser().dict_from_file(config)
+ return config
+
+
+class NamespaceSet(dict):
+ """A dict of config namespace names and handlers.
+
+ Each config entry should begin with a namespace name; the corresponding
+ namespace handler will be called once for each config entry in that
+ namespace, and will be passed two arguments: the config key (with the
+ namespace removed) and the config value.
+
+ Namespace handlers may be any Python callable; they may also be
+ Python 2.5-style 'context managers', in which case their __enter__
+ method should return a callable to be used as the handler.
+ See cherrypy.tools (the Toolbox class) for an example.
+ """
+
+ def __call__(self, config):
+ """Iterate through config and pass it to each namespace handler.
+
+ config
+ A flat dict, where keys use dots to separate
+ namespaces, and values are arbitrary.
+
+ The first name in each config key is used to look up the corresponding
+ namespace handler. For example, a config entry of {'tools.gzip.on': v}
+ will call the 'tools' namespace handler with the args: ('gzip.on', v)
+ """
+ # Separate the given config into namespaces
+ ns_confs = {}
+ for k in config:
+ if "." in k:
+ ns, name = k.split(".", 1)
+ bucket = ns_confs.setdefault(ns, {})
+ bucket[name] = config[k]
+
+ # I chose __enter__ and __exit__ so someday this could be
+ # rewritten using Python 2.5's 'with' statement:
+ # for ns, handler in self.iteritems():
+ # with handler as callable:
+ # for k, v in ns_confs.get(ns, {}).iteritems():
+ # callable(k, v)
+ for ns, handler in self.items():
+ exit = getattr(handler, "__exit__", None)
+ if exit:
+ callable = handler.__enter__()
+ no_exc = True
+ try:
+ try:
+ for k, v in ns_confs.get(ns, {}).items():
+ callable(k, v)
+ except:
+ # The exceptional case is handled here
+ no_exc = False
+ if exit is None:
+ raise
+ if not exit(*sys.exc_info()):
+ raise
+ # The exception is swallowed if exit() returns true
+ finally:
+ # The normal and non-local-goto cases are handled here
+ if no_exc and exit:
+ exit(None, None, None)
+ else:
+ for k, v in ns_confs.get(ns, {}).items():
+ handler(k, v)
+
+ def __repr__(self):
+ return "%s.%s(%s)" % (self.__module__, self.__class__.__name__,
+ dict.__repr__(self))
+
+ def __copy__(self):
+ newobj = self.__class__()
+ newobj.update(self)
+ return newobj
+ copy = __copy__
+
+
+class Config(dict):
+ """A dict-like set of configuration data, with defaults and namespaces.
+
+ May take a file, filename, or dict.
+ """
+
+ defaults = {}
+ environments = {}
+ namespaces = NamespaceSet()
+
+ def __init__(self, file=None, **kwargs):
+ self.reset()
+ if file is not None:
+ self.update(file)
+ if kwargs:
+ self.update(kwargs)
+
+ def reset(self):
+ """Reset self to default values."""
+ self.clear()
+ dict.update(self, self.defaults)
+
+ def update(self, config):
+ """Update self from a dict, file or filename."""
+ if isinstance(config, basestring):
+ # Filename
+ config = Parser().dict_from_file(config)
+ elif hasattr(config, 'read'):
+ # Open file object
+ config = Parser().dict_from_file(config)
+ else:
+ config = config.copy()
+ self._apply(config)
+
+ def _apply(self, config):
+ """Update self from a dict."""
+ which_env = config.get('environment')
+ if which_env:
+ env = self.environments[which_env]
+ for k in env:
+ if k not in config:
+ config[k] = env[k]
+
+ dict.update(self, config)
+ self.namespaces(config)
+
+ def __setitem__(self, k, v):
+ dict.__setitem__(self, k, v)
+ self.namespaces({k: v})
+
+
+class Parser(ConfigParser):
+ """Sub-class of ConfigParser that keeps the case of options and that
+ raises an exception if the file cannot be read.
+ """
+
+ def optionxform(self, optionstr):
+ return optionstr
+
+ def read(self, filenames):
+ if isinstance(filenames, basestring):
+ filenames = [filenames]
+ for filename in filenames:
+ # try:
+ # fp = open(filename)
+ # except IOError:
+ # continue
+ fp = open(filename)
+ try:
+ self._read(fp, filename)
+ finally:
+ fp.close()
+
+ def as_dict(self, raw=False, vars=None):
+ """Convert an INI file to a dictionary"""
+ # Load INI file into a dict
+ result = {}
+ for section in self.sections():
+ if section not in result:
+ result[section] = {}
+ for option in self.options(section):
+ value = self.get(section, option, raw, vars)
+ try:
+ value = unrepr(value)
+ except Exception, x:
+ msg = ("Config error in section: %r, option: %r, "
+ "value: %r. Config values must be valid Python." %
+ (section, option, value))
+ raise ValueError(msg, x.__class__.__name__, x.args)
+ result[section][option] = value
+ return result
+
+ def dict_from_file(self, file):
+ if hasattr(file, 'read'):
+ self.readfp(file)
+ else:
+ self.read(file)
+ return self.as_dict()
+
+
+# public domain "unrepr" implementation, found on the web and then improved.
+
+class _Builder:
+
+ def build(self, o):
+ m = getattr(self, 'build_' + o.__class__.__name__, None)
+ if m is None:
+ raise TypeError("unrepr does not recognize %s" %
+ repr(o.__class__.__name__))
+ return m(o)
+
+ def build_Subscript(self, o):
+ expr, flags, subs = o.getChildren()
+ expr = self.build(expr)
+ subs = self.build(subs)
+ return expr[subs]
+
+ def build_CallFunc(self, o):
+ children = map(self.build, o.getChildren())
+ callee = children.pop(0)
+ kwargs = children.pop() or {}
+ starargs = children.pop() or ()
+ args = tuple(children) + tuple(starargs)
+ return callee(*args, **kwargs)
+
+ def build_List(self, o):
+ return map(self.build, o.getChildren())
+
+ def build_Const(self, o):
+ return o.value
+
+ def build_Dict(self, o):
+ d = {}
+ i = iter(map(self.build, o.getChildren()))
+ for el in i:
+ d[el] = i.next()
+ return d
+
+ def build_Tuple(self, o):
+ return tuple(self.build_List(o))
+
+ def build_Name(self, o):
+ name = o.name
+ if name == 'None':
+ return None
+ if name == 'True':
+ return True
+ if name == 'False':
+ return False
+
+ # See if the Name is a package or module. If it is, import it.
+ try:
+ return modules(name)
+ except ImportError:
+ pass
+
+ # See if the Name is in builtins.
+ try:
+ import __builtin__
+ return getattr(__builtin__, name)
+ except AttributeError:
+ pass
+
+ raise TypeError("unrepr could not resolve the name %s" % repr(name))
+
+ def build_Add(self, o):
+ left, right = map(self.build, o.getChildren())
+ return left + right
+
+ def build_Getattr(self, o):
+ parent = self.build(o.expr)
+ return getattr(parent, o.attrname)
+
+ def build_NoneType(self, o):
+ return None
+
+ def build_UnarySub(self, o):
+ return -self.build(o.getChildren()[0])
+
+ def build_UnaryAdd(self, o):
+ return self.build(o.getChildren()[0])
+
+
+def _astnode(s):
+ """Return a Python ast Node compiled from a string."""
+ try:
+ import compiler
+ except ImportError:
+ # Fallback to eval when compiler package is not available,
+ # e.g. IronPython 1.0.
+ return eval(s)
+
+ p = compiler.parse("__tempvalue__ = " + s)
+ return p.getChildren()[1].getChildren()[0].getChildren()[1]
+
+
+def unrepr(s):
+ """Return a Python object compiled from a string."""
+ if not s:
+ return s
+ obj = _astnode(s)
+ return _Builder().build(obj)
+
+
+def modules(modulePath):
+ """Load a module and retrieve a reference to that module."""
+ try:
+ mod = sys.modules[modulePath]
+ if mod is None:
+ raise KeyError()
+ except KeyError:
+ # The last [''] is important.
+ mod = __import__(modulePath, globals(), locals(), [''])
+ return mod
+
+def attributes(full_attribute_name):
+ """Load a module and retrieve an attribute of that module."""
+
+ # Parse out the path, module, and attribute
+ last_dot = full_attribute_name.rfind(".")
+ attr_name = full_attribute_name[last_dot + 1:]
+ mod_path = full_attribute_name[:last_dot]
+
+ mod = modules(mod_path)
+ # Let an AttributeError propagate outward.
+ try:
+ attr = getattr(mod, attr_name)
+ except AttributeError:
+ raise AttributeError("'%s' object has no attribute '%s'"
+ % (mod_path, attr_name))
+
+ # Return a reference to the attribute.
+ return attr
+
+
diff --git a/cherrypy/lib/sessions.py b/cherrypy/lib/sessions.py
new file mode 100755
index 0000000..42c2800
--- /dev/null
+++ b/cherrypy/lib/sessions.py
@@ -0,0 +1,832 @@
+"""Session implementation for CherryPy.
+
+You need to edit your config file to use sessions. Here's an example::
+
+ [/]
+ tools.sessions.on = True
+ tools.sessions.storage_type = "file"
+ tools.sessions.storage_path = "/home/site/sessions"
+ tools.sessions.timeout = 60
+
+This sets the session to be stored in files in the directory /home/site/sessions,
+and the session timeout to 60 minutes. If you omit ``storage_type`` the sessions
+will be saved in RAM. ``tools.sessions.on`` is the only required line for
+working sessions, the rest are optional.
+
+By default, the session ID is passed in a cookie, so the client's browser must
+have cookies enabled for your site.
+
+To set data for the current session, use
+``cherrypy.session['fieldname'] = 'fieldvalue'``;
+to get data use ``cherrypy.session.get('fieldname')``.
+
+================
+Locking sessions
+================
+
+By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means
+the session is locked early and unlocked late. If you want to control when the
+session data is locked and unlocked, set ``tools.sessions.locking = 'explicit'``.
+Then call ``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``.
+Regardless of which mode you use, the session is guaranteed to be unlocked when
+the request is complete.
+
+=================
+Expiring Sessions
+=================
+
+You can force a session to expire with :func:`cherrypy.lib.sessions.expire`.
+Simply call that function at the point you want the session to expire, and it
+will cause the session cookie to expire client-side.
+
+===========================
+Session Fixation Protection
+===========================
+
+If CherryPy receives, via a request cookie, a session id that it does not
+recognize, it will reject that id and create a new one to return in the
+response cookie. This `helps prevent session fixation attacks
+<http://en.wikipedia.org/wiki/Session_fixation#Regenerate_SID_on_each_request>`_.
+However, CherryPy "recognizes" a session id by looking up the saved session
+data for that id. Therefore, if you never save any session data,
+**you will get a new session id for every request**.
+
+================
+Sharing Sessions
+================
+
+If you run multiple instances of CherryPy (for example via mod_python behind
+Apache prefork), you most likely cannot use the RAM session backend, since each
+instance of CherryPy will have its own memory space. Use a different backend
+instead, and verify that all instances are pointing at the same file or db
+location. Alternately, you might try a load balancer which makes sessions
+"sticky". Google is your friend, there.
+
+================
+Expiration Dates
+================
+
+The response cookie will possess an expiration date to inform the client at
+which point to stop sending the cookie back in requests. If the server time
+and client time differ, expect sessions to be unreliable. **Make sure the
+system time of your server is accurate**.
+
+CherryPy defaults to a 60-minute session timeout, which also applies to the
+cookie which is sent to the client. Unfortunately, some versions of Safari
+("4 public beta" on Windows XP at least) appear to have a bug in their parsing
+of the GMT expiration date--they appear to interpret the date as one hour in
+the past. Sixty minutes minus one hour is pretty close to zero, so you may
+experience this bug as a new session id for every request, unless the requests
+are less than one second apart. To fix, try increasing the session.timeout.
+
+On the other extreme, some users report Firefox sending cookies after their
+expiration date, although this was on a system with an inaccurate system time.
+Maybe FF doesn't trust system time.
+"""
+
+import datetime
+import os
+import random
+import time
+import threading
+import types
+from warnings import warn
+
+import cherrypy
+from cherrypy._cpcompat import copyitems, pickle, random20
+from cherrypy.lib import httputil
+
+
+missing = object()
+
+class Session(object):
+ """A CherryPy dict-like Session object (one per request)."""
+
+ _id = None
+
+ id_observers = None
+ "A list of callbacks to which to pass new id's."
+
+ def _get_id(self):
+ return self._id
+ def _set_id(self, value):
+ self._id = value
+ for o in self.id_observers:
+ o(value)
+ id = property(_get_id, _set_id, doc="The current session ID.")
+
+ timeout = 60
+ "Number of minutes after which to delete session data."
+
+ locked = False
+ """
+ If True, this session instance has exclusive read/write access
+ to session data."""
+
+ loaded = False
+ """
+ If True, data has been retrieved from storage. This should happen
+ automatically on the first attempt to access session data."""
+
+ clean_thread = None
+ "Class-level Monitor which calls self.clean_up."
+
+ clean_freq = 5
+ "The poll rate for expired session cleanup in minutes."
+
+ originalid = None
+ "The session id passed by the client. May be missing or unsafe."
+
+ missing = False
+ "True if the session requested by the client did not exist."
+
+ regenerated = False
+ """
+ True if the application called session.regenerate(). This is not set by
+ internal calls to regenerate the session id."""
+
+ debug=False
+
+ def __init__(self, id=None, **kwargs):
+ self.id_observers = []
+ self._data = {}
+
+ for k, v in kwargs.items():
+ setattr(self, k, v)
+
+ self.originalid = id
+ self.missing = False
+ if id is None:
+ if self.debug:
+ cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS')
+ self._regenerate()
+ else:
+ self.id = id
+ if not self._exists():
+ if self.debug:
+ cherrypy.log('Expired or malicious session %r; '
+ 'making a new one' % id, 'TOOLS.SESSIONS')
+ # Expired or malicious session. Make a new one.
+ # See http://www.cherrypy.org/ticket/709.
+ self.id = None
+ self.missing = True
+ self._regenerate()
+
+ def regenerate(self):
+ """Replace the current session (with a new id)."""
+ self.regenerated = True
+ self._regenerate()
+
+ def _regenerate(self):
+ if self.id is not None:
+ self.delete()
+
+ old_session_was_locked = self.locked
+ if old_session_was_locked:
+ self.release_lock()
+
+ self.id = None
+ while self.id is None:
+ self.id = self.generate_id()
+ # Assert that the generated id is not already stored.
+ if self._exists():
+ self.id = None
+
+ if old_session_was_locked:
+ self.acquire_lock()
+
+ def clean_up(self):
+ """Clean up expired sessions."""
+ pass
+
+ def generate_id(self):
+ """Return a new session id."""
+ return random20()
+
+ def save(self):
+ """Save session data."""
+ try:
+ # If session data has never been loaded then it's never been
+ # accessed: no need to save it
+ if self.loaded:
+ t = datetime.timedelta(seconds = self.timeout * 60)
+ expiration_time = datetime.datetime.now() + t
+ if self.debug:
+ cherrypy.log('Saving with expiry %s' % expiration_time,
+ 'TOOLS.SESSIONS')
+ self._save(expiration_time)
+
+ finally:
+ if self.locked:
+ # Always release the lock if the user didn't release it
+ self.release_lock()
+
+ def load(self):
+ """Copy stored session data into this session instance."""
+ data = self._load()
+ # data is either None or a tuple (session_data, expiration_time)
+ if data is None or data[1] < datetime.datetime.now():
+ if self.debug:
+ cherrypy.log('Expired session, flushing data', 'TOOLS.SESSIONS')
+ self._data = {}
+ else:
+ self._data = data[0]
+ self.loaded = True
+
+ # Stick the clean_thread in the class, not the instance.
+ # The instances are created and destroyed per-request.
+ cls = self.__class__
+ if self.clean_freq and not cls.clean_thread:
+ # clean_up is in instancemethod and not a classmethod,
+ # so that tool config can be accessed inside the method.
+ t = cherrypy.process.plugins.Monitor(
+ cherrypy.engine, self.clean_up, self.clean_freq * 60,
+ name='Session cleanup')
+ t.subscribe()
+ cls.clean_thread = t
+ t.start()
+
+ def delete(self):
+ """Delete stored session data."""
+ self._delete()
+
+ def __getitem__(self, key):
+ if not self.loaded: self.load()
+ return self._data[key]
+
+ def __setitem__(self, key, value):
+ if not self.loaded: self.load()
+ self._data[key] = value
+
+ def __delitem__(self, key):
+ if not self.loaded: self.load()
+ del self._data[key]
+
+ def pop(self, key, default=missing):
+ """Remove the specified key and return the corresponding value.
+ If key is not found, default is returned if given,
+ otherwise KeyError is raised.
+ """
+ if not self.loaded: self.load()
+ if default is missing:
+ return self._data.pop(key)
+ else:
+ return self._data.pop(key, default)
+
+ def __contains__(self, key):
+ if not self.loaded: self.load()
+ return key in self._data
+
+ def has_key(self, key):
+ """D.has_key(k) -> True if D has a key k, else False."""
+ if not self.loaded: self.load()
+ return key in self._data
+
+ def get(self, key, default=None):
+ """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None."""
+ if not self.loaded: self.load()
+ return self._data.get(key, default)
+
+ def update(self, d):
+ """D.update(E) -> None. Update D from E: for k in E: D[k] = E[k]."""
+ if not self.loaded: self.load()
+ self._data.update(d)
+
+ def setdefault(self, key, default=None):
+ """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D."""
+ if not self.loaded: self.load()
+ return self._data.setdefault(key, default)
+
+ def clear(self):
+ """D.clear() -> None. Remove all items from D."""
+ if not self.loaded: self.load()
+ self._data.clear()
+
+ def keys(self):
+ """D.keys() -> list of D's keys."""
+ if not self.loaded: self.load()
+ return self._data.keys()
+
+ def items(self):
+ """D.items() -> list of D's (key, value) pairs, as 2-tuples."""
+ if not self.loaded: self.load()
+ return self._data.items()
+
+ def values(self):
+ """D.values() -> list of D's values."""
+ if not self.loaded: self.load()
+ return self._data.values()
+
+
+class RamSession(Session):
+
+ # Class-level objects. Don't rebind these!
+ cache = {}
+ locks = {}
+
+ def clean_up(self):
+ """Clean up expired sessions."""
+ now = datetime.datetime.now()
+ for id, (data, expiration_time) in copyitems(self.cache):
+ if expiration_time <= now:
+ try:
+ del self.cache[id]
+ except KeyError:
+ pass
+ try:
+ del self.locks[id]
+ except KeyError:
+ pass
+
+ def _exists(self):
+ return self.id in self.cache
+
+ def _load(self):
+ return self.cache.get(self.id)
+
+ def _save(self, expiration_time):
+ self.cache[self.id] = (self._data, expiration_time)
+
+ def _delete(self):
+ self.cache.pop(self.id, None)
+
+ def acquire_lock(self):
+ """Acquire an exclusive lock on the currently-loaded session data."""
+ self.locked = True
+ self.locks.setdefault(self.id, threading.RLock()).acquire()
+
+ def release_lock(self):
+ """Release the lock on the currently-loaded session data."""
+ self.locks[self.id].release()
+ self.locked = False
+
+ def __len__(self):
+ """Return the number of active sessions."""
+ return len(self.cache)
+
+
+class FileSession(Session):
+ """Implementation of the File backend for sessions
+
+ storage_path
+ The folder where session data will be saved. Each session
+ will be saved as pickle.dump(data, expiration_time) in its own file;
+ the filename will be self.SESSION_PREFIX + self.id.
+
+ """
+
+ SESSION_PREFIX = 'session-'
+ LOCK_SUFFIX = '.lock'
+ pickle_protocol = pickle.HIGHEST_PROTOCOL
+
+ def __init__(self, id=None, **kwargs):
+ # The 'storage_path' arg is required for file-based sessions.
+ kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
+ Session.__init__(self, id=id, **kwargs)
+
+ def setup(cls, **kwargs):
+ """Set up the storage system for file-based sessions.
+
+ This should only be called once per process; this will be done
+ automatically when using sessions.init (as the built-in Tool does).
+ """
+ # The 'storage_path' arg is required for file-based sessions.
+ kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
+
+ for k, v in kwargs.items():
+ setattr(cls, k, v)
+
+ # Warn if any lock files exist at startup.
+ lockfiles = [fname for fname in os.listdir(cls.storage_path)
+ if (fname.startswith(cls.SESSION_PREFIX)
+ and fname.endswith(cls.LOCK_SUFFIX))]
+ if lockfiles:
+ plural = ('', 's')[len(lockfiles) > 1]
+ warn("%s session lockfile%s found at startup. If you are "
+ "only running one process, then you may need to "
+ "manually delete the lockfiles found at %r."
+ % (len(lockfiles), plural, cls.storage_path))
+ setup = classmethod(setup)
+
+ def _get_file_path(self):
+ f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
+ if not os.path.abspath(f).startswith(self.storage_path):
+ raise cherrypy.HTTPError(400, "Invalid session id in cookie.")
+ return f
+
+ def _exists(self):
+ path = self._get_file_path()
+ return os.path.exists(path)
+
+ def _load(self, path=None):
+ if path is None:
+ path = self._get_file_path()
+ try:
+ f = open(path, "rb")
+ try:
+ return pickle.load(f)
+ finally:
+ f.close()
+ except (IOError, EOFError):
+ return None
+
+ def _save(self, expiration_time):
+ f = open(self._get_file_path(), "wb")
+ try:
+ pickle.dump((self._data, expiration_time), f, self.pickle_protocol)
+ finally:
+ f.close()
+
+ def _delete(self):
+ try:
+ os.unlink(self._get_file_path())
+ except OSError:
+ pass
+
+ def acquire_lock(self, path=None):
+ """Acquire an exclusive lock on the currently-loaded session data."""
+ if path is None:
+ path = self._get_file_path()
+ path += self.LOCK_SUFFIX
+ while True:
+ try:
+ lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL)
+ except OSError:
+ time.sleep(0.1)
+ else:
+ os.close(lockfd)
+ break
+ self.locked = True
+
+ def release_lock(self, path=None):
+ """Release the lock on the currently-loaded session data."""
+ if path is None:
+ path = self._get_file_path()
+ os.unlink(path + self.LOCK_SUFFIX)
+ self.locked = False
+
+ def clean_up(self):
+ """Clean up expired sessions."""
+ now = datetime.datetime.now()
+ # Iterate over all session files in self.storage_path
+ for fname in os.listdir(self.storage_path):
+ if (fname.startswith(self.SESSION_PREFIX)
+ and not fname.endswith(self.LOCK_SUFFIX)):
+ # We have a session file: lock and load it and check
+ # if it's expired. If it fails, nevermind.
+ path = os.path.join(self.storage_path, fname)
+ self.acquire_lock(path)
+ try:
+ contents = self._load(path)
+ # _load returns None on IOError
+ if contents is not None:
+ data, expiration_time = contents
+ if expiration_time < now:
+ # Session expired: deleting it
+ os.unlink(path)
+ finally:
+ self.release_lock(path)
+
+ def __len__(self):
+ """Return the number of active sessions."""
+ return len([fname for fname in os.listdir(self.storage_path)
+ if (fname.startswith(self.SESSION_PREFIX)
+ and not fname.endswith(self.LOCK_SUFFIX))])
+
+
+class PostgresqlSession(Session):
+ """ Implementation of the PostgreSQL backend for sessions. It assumes
+ a table like this::
+
+ create table session (
+ id varchar(40),
+ data text,
+ expiration_time timestamp
+ )
+
+ You must provide your own get_db function.
+ """
+
+ pickle_protocol = pickle.HIGHEST_PROTOCOL
+
+ def __init__(self, id=None, **kwargs):
+ Session.__init__(self, id, **kwargs)
+ self.cursor = self.db.cursor()
+
+ def setup(cls, **kwargs):
+ """Set up the storage system for Postgres-based sessions.
+
+ This should only be called once per process; this will be done
+ automatically when using sessions.init (as the built-in Tool does).
+ """
+ for k, v in kwargs.items():
+ setattr(cls, k, v)
+
+ self.db = self.get_db()
+ setup = classmethod(setup)
+
+ def __del__(self):
+ if self.cursor:
+ self.cursor.close()
+ self.db.commit()
+
+ def _exists(self):
+ # Select session data from table
+ self.cursor.execute('select data, expiration_time from session '
+ 'where id=%s', (self.id,))
+ rows = self.cursor.fetchall()
+ return bool(rows)
+
+ def _load(self):
+ # Select session data from table
+ self.cursor.execute('select data, expiration_time from session '
+ 'where id=%s', (self.id,))
+ rows = self.cursor.fetchall()
+ if not rows:
+ return None
+
+ pickled_data, expiration_time = rows[0]
+ data = pickle.loads(pickled_data)
+ return data, expiration_time
+
+ def _save(self, expiration_time):
+ pickled_data = pickle.dumps(self._data, self.pickle_protocol)
+ self.cursor.execute('update session set data = %s, '
+ 'expiration_time = %s where id = %s',
+ (pickled_data, expiration_time, self.id))
+
+ def _delete(self):
+ self.cursor.execute('delete from session where id=%s', (self.id,))
+
+ def acquire_lock(self):
+ """Acquire an exclusive lock on the currently-loaded session data."""
+ # We use the "for update" clause to lock the row
+ self.locked = True
+ self.cursor.execute('select id from session where id=%s for update',
+ (self.id,))
+
+ def release_lock(self):
+ """Release the lock on the currently-loaded session data."""
+ # We just close the cursor and that will remove the lock
+ # introduced by the "for update" clause
+ self.cursor.close()
+ self.locked = False
+
+ def clean_up(self):
+ """Clean up expired sessions."""
+ self.cursor.execute('delete from session where expiration_time < %s',
+ (datetime.datetime.now(),))
+
+
+class MemcachedSession(Session):
+
+ # The most popular memcached client for Python isn't thread-safe.
+ # Wrap all .get and .set operations in a single lock.
+ mc_lock = threading.RLock()
+
+ # This is a seperate set of locks per session id.
+ locks = {}
+
+ servers = ['127.0.0.1:11211']
+
+ def setup(cls, **kwargs):
+ """Set up the storage system for memcached-based sessions.
+
+ This should only be called once per process; this will be done
+ automatically when using sessions.init (as the built-in Tool does).
+ """
+ for k, v in kwargs.items():
+ setattr(cls, k, v)
+
+ import memcache
+ cls.cache = memcache.Client(cls.servers)
+ setup = classmethod(setup)
+
+ def _exists(self):
+ self.mc_lock.acquire()
+ try:
+ return bool(self.cache.get(self.id))
+ finally:
+ self.mc_lock.release()
+
+ def _load(self):
+ self.mc_lock.acquire()
+ try:
+ return self.cache.get(self.id)
+ finally:
+ self.mc_lock.release()
+
+ def _save(self, expiration_time):
+ # Send the expiration time as "Unix time" (seconds since 1/1/1970)
+ td = int(time.mktime(expiration_time.timetuple()))
+ self.mc_lock.acquire()
+ try:
+ if not self.cache.set(self.id, (self._data, expiration_time), td):
+ raise AssertionError("Session data for id %r not set." % self.id)
+ finally:
+ self.mc_lock.release()
+
+ def _delete(self):
+ self.cache.delete(self.id)
+
+ def acquire_lock(self):
+ """Acquire an exclusive lock on the currently-loaded session data."""
+ self.locked = True
+ self.locks.setdefault(self.id, threading.RLock()).acquire()
+
+ def release_lock(self):
+ """Release the lock on the currently-loaded session data."""
+ self.locks[self.id].release()
+ self.locked = False
+
+ def __len__(self):
+ """Return the number of active sessions."""
+ raise NotImplementedError
+
+
+# Hook functions (for CherryPy tools)
+
+def save():
+ """Save any changed session data."""
+
+ if not hasattr(cherrypy.serving, "session"):
+ return
+ request = cherrypy.serving.request
+ response = cherrypy.serving.response
+
+ # Guard against running twice
+ if hasattr(request, "_sessionsaved"):
+ return
+ request._sessionsaved = True
+
+ if response.stream:
+ # If the body is being streamed, we have to save the data
+ # *after* the response has been written out
+ request.hooks.attach('on_end_request', cherrypy.session.save)
+ else:
+ # If the body is not being streamed, we save the data now
+ # (so we can release the lock).
+ if isinstance(response.body, types.GeneratorType):
+ response.collapse_body()
+ cherrypy.session.save()
+save.failsafe = True
+
+def close():
+ """Close the session object for this request."""
+ sess = getattr(cherrypy.serving, "session", None)
+ if getattr(sess, "locked", False):
+ # If the session is still locked we release the lock
+ sess.release_lock()
+close.failsafe = True
+close.priority = 90
+
+
+def init(storage_type='ram', path=None, path_header=None, name='session_id',
+ timeout=60, domain=None, secure=False, clean_freq=5,
+ persistent=True, debug=False, **kwargs):
+ """Initialize session object (using cookies).
+
+ storage_type
+ One of 'ram', 'file', 'postgresql'. This will be used
+ to look up the corresponding class in cherrypy.lib.sessions
+ globals. For example, 'file' will use the FileSession class.
+
+ path
+ The 'path' value to stick in the response cookie metadata.
+
+ path_header
+ If 'path' is None (the default), then the response
+ cookie 'path' will be pulled from request.headers[path_header].
+
+ name
+ The name of the cookie.
+
+ timeout
+ The expiration timeout (in minutes) for the stored session data.
+ If 'persistent' is True (the default), this is also the timeout
+ for the cookie.
+
+ domain
+ The cookie domain.
+
+ secure
+ If False (the default) the cookie 'secure' value will not
+ be set. If True, the cookie 'secure' value will be set (to 1).
+
+ clean_freq (minutes)
+ The poll rate for expired session cleanup.
+
+ persistent
+ If True (the default), the 'timeout' argument will be used
+ to expire the cookie. If False, the cookie will not have an expiry,
+ and the cookie will be a "session cookie" which expires when the
+ browser is closed.
+
+ Any additional kwargs will be bound to the new Session instance,
+ and may be specific to the storage type. See the subclass of Session
+ you're using for more information.
+ """
+
+ request = cherrypy.serving.request
+
+ # Guard against running twice
+ if hasattr(request, "_session_init_flag"):
+ return
+ request._session_init_flag = True
+
+ # Check if request came with a session ID
+ id = None
+ if name in request.cookie:
+ id = request.cookie[name].value
+ if debug:
+ cherrypy.log('ID obtained from request.cookie: %r' % id,
+ 'TOOLS.SESSIONS')
+
+ # Find the storage class and call setup (first time only).
+ storage_class = storage_type.title() + 'Session'
+ storage_class = globals()[storage_class]
+ if not hasattr(cherrypy, "session"):
+ if hasattr(storage_class, "setup"):
+ storage_class.setup(**kwargs)
+
+ # Create and attach a new Session instance to cherrypy.serving.
+ # It will possess a reference to (and lock, and lazily load)
+ # the requested session data.
+ kwargs['timeout'] = timeout
+ kwargs['clean_freq'] = clean_freq
+ cherrypy.serving.session = sess = storage_class(id, **kwargs)
+ sess.debug = debug
+ def update_cookie(id):
+ """Update the cookie every time the session id changes."""
+ cherrypy.serving.response.cookie[name] = id
+ sess.id_observers.append(update_cookie)
+
+ # Create cherrypy.session which will proxy to cherrypy.serving.session
+ if not hasattr(cherrypy, "session"):
+ cherrypy.session = cherrypy._ThreadLocalProxy('session')
+
+ if persistent:
+ cookie_timeout = timeout
+ else:
+ # See http://support.microsoft.com/kb/223799/EN-US/
+ # and http://support.mozilla.com/en-US/kb/Cookies
+ cookie_timeout = None
+ set_response_cookie(path=path, path_header=path_header, name=name,
+ timeout=cookie_timeout, domain=domain, secure=secure)
+
+
+def set_response_cookie(path=None, path_header=None, name='session_id',
+ timeout=60, domain=None, secure=False):
+ """Set a response cookie for the client.
+
+ path
+ the 'path' value to stick in the response cookie metadata.
+
+ path_header
+ if 'path' is None (the default), then the response
+ cookie 'path' will be pulled from request.headers[path_header].
+
+ name
+ the name of the cookie.
+
+ timeout
+ the expiration timeout for the cookie. If 0 or other boolean
+ False, no 'expires' param will be set, and the cookie will be a
+ "session cookie" which expires when the browser is closed.
+
+ domain
+ the cookie domain.
+
+ secure
+ if False (the default) the cookie 'secure' value will not
+ be set. If True, the cookie 'secure' value will be set (to 1).
+
+ """
+ # Set response cookie
+ cookie = cherrypy.serving.response.cookie
+ cookie[name] = cherrypy.serving.session.id
+ cookie[name]['path'] = (path or cherrypy.serving.request.headers.get(path_header)
+ or '/')
+
+ # We'd like to use the "max-age" param as indicated in
+ # http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
+ # save it to disk and the session is lost if people close
+ # the browser. So we have to use the old "expires" ... sigh ...
+## cookie[name]['max-age'] = timeout * 60
+ if timeout:
+ e = time.time() + (timeout * 60)
+ cookie[name]['expires'] = httputil.HTTPDate(e)
+ if domain is not None:
+ cookie[name]['domain'] = domain
+ if secure:
+ cookie[name]['secure'] = 1
+
+
+def expire():
+ """Expire the current session cookie."""
+ name = cherrypy.serving.request.config.get('tools.sessions.name', 'session_id')
+ one_year = 60 * 60 * 24 * 365
+ e = time.time() - one_year
+ cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e)
+
+
diff --git a/cherrypy/lib/static.py b/cherrypy/lib/static.py
new file mode 100755
index 0000000..cb9a68c
--- /dev/null
+++ b/cherrypy/lib/static.py
@@ -0,0 +1,352 @@
+import logging
+import mimetypes
+mimetypes.init()
+mimetypes.types_map['.dwg']='image/x-dwg'
+mimetypes.types_map['.ico']='image/x-icon'
+mimetypes.types_map['.bz2']='application/x-bzip2'
+mimetypes.types_map['.gz']='application/x-gzip'
+
+import os
+import re
+import stat
+import time
+
+import cherrypy
+from cherrypy._cpcompat import ntob, unquote
+from cherrypy.lib import cptools, httputil, file_generator_limited
+
+
+def serve_file(path, content_type=None, disposition=None, name=None, debug=False):
+ """Set status, headers, and body in order to serve the given path.
+
+ The Content-Type header will be set to the content_type arg, if provided.
+ If not provided, the Content-Type will be guessed by the file extension
+ of the 'path' argument.
+
+ If disposition is not None, the Content-Disposition header will be set
+ to "<disposition>; filename=<name>". If name is None, it will be set
+ to the basename of path. If disposition is None, no Content-Disposition
+ header will be written.
+ """
+
+ response = cherrypy.serving.response
+
+ # If path is relative, users should fix it by making path absolute.
+ # That is, CherryPy should not guess where the application root is.
+ # It certainly should *not* use cwd (since CP may be invoked from a
+ # variety of paths). If using tools.staticdir, you can make your relative
+ # paths become absolute by supplying a value for "tools.staticdir.root".
+ if not os.path.isabs(path):
+ msg = "'%s' is not an absolute path." % path
+ if debug:
+ cherrypy.log(msg, 'TOOLS.STATICFILE')
+ raise ValueError(msg)
+
+ try:
+ st = os.stat(path)
+ except OSError:
+ if debug:
+ cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
+ raise cherrypy.NotFound()
+
+ # Check if path is a directory.
+ if stat.S_ISDIR(st.st_mode):
+ # Let the caller deal with it as they like.
+ if debug:
+ cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC')
+ raise cherrypy.NotFound()
+
+ # Set the Last-Modified response header, so that
+ # modified-since validation code can work.
+ response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
+ cptools.validate_since()
+
+ if content_type is None:
+ # Set content-type based on filename extension
+ ext = ""
+ i = path.rfind('.')
+ if i != -1:
+ ext = path[i:].lower()
+ content_type = mimetypes.types_map.get(ext, None)
+ if content_type is not None:
+ response.headers['Content-Type'] = content_type
+ if debug:
+ cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
+
+ cd = None
+ if disposition is not None:
+ if name is None:
+ name = os.path.basename(path)
+ cd = '%s; filename="%s"' % (disposition, name)
+ response.headers["Content-Disposition"] = cd
+ if debug:
+ cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
+
+ # Set Content-Length and use an iterable (file object)
+ # this way CP won't load the whole file in memory
+ content_length = st.st_size
+ fileobj = open(path, 'rb')
+ return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
+
+def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
+ debug=False):
+ """Set status, headers, and body in order to serve the given file object.
+
+ The Content-Type header will be set to the content_type arg, if provided.
+
+ If disposition is not None, the Content-Disposition header will be set
+ to "<disposition>; filename=<name>". If name is None, 'filename' will
+ not be set. If disposition is None, no Content-Disposition header will
+ be written.
+
+ CAUTION: If the request contains a 'Range' header, one or more seek()s will
+ be performed on the file object. This may cause undesired behavior if
+ the file object is not seekable. It could also produce undesired results
+ if the caller set the read position of the file object prior to calling
+ serve_fileobj(), expecting that the data would be served starting from that
+ position.
+ """
+
+ response = cherrypy.serving.response
+
+ try:
+ st = os.fstat(fileobj.fileno())
+ except AttributeError:
+ if debug:
+ cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC')
+ content_length = None
+ else:
+ # Set the Last-Modified response header, so that
+ # modified-since validation code can work.
+ response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
+ cptools.validate_since()
+ content_length = st.st_size
+
+ if content_type is not None:
+ response.headers['Content-Type'] = content_type
+ if debug:
+ cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
+
+ cd = None
+ if disposition is not None:
+ if name is None:
+ cd = disposition
+ else:
+ cd = '%s; filename="%s"' % (disposition, name)
+ response.headers["Content-Disposition"] = cd
+ if debug:
+ cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
+
+ return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
+
+def _serve_fileobj(fileobj, content_type, content_length, debug=False):
+ """Internal. Set response.body to the given file object, perhaps ranged."""
+ response = cherrypy.serving.response
+
+ # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
+ request = cherrypy.serving.request
+ if request.protocol >= (1, 1):
+ response.headers["Accept-Ranges"] = "bytes"
+ r = httputil.get_ranges(request.headers.get('Range'), content_length)
+ if r == []:
+ response.headers['Content-Range'] = "bytes */%s" % content_length
+ message = "Invalid Range (first-byte-pos greater than Content-Length)"
+ if debug:
+ cherrypy.log(message, 'TOOLS.STATIC')
+ raise cherrypy.HTTPError(416, message)
+
+ if r:
+ if len(r) == 1:
+ # Return a single-part response.
+ start, stop = r[0]
+ if stop > content_length:
+ stop = content_length
+ r_len = stop - start
+ if debug:
+ cherrypy.log('Single part; start: %r, stop: %r' % (start, stop),
+ 'TOOLS.STATIC')
+ response.status = "206 Partial Content"
+ response.headers['Content-Range'] = (
+ "bytes %s-%s/%s" % (start, stop - 1, content_length))
+ response.headers['Content-Length'] = r_len
+ fileobj.seek(start)
+ response.body = file_generator_limited(fileobj, r_len)
+ else:
+ # Return a multipart/byteranges response.
+ response.status = "206 Partial Content"
+ from mimetools import choose_boundary
+ boundary = choose_boundary()
+ ct = "multipart/byteranges; boundary=%s" % boundary
+ response.headers['Content-Type'] = ct
+ if "Content-Length" in response.headers:
+ # Delete Content-Length header so finalize() recalcs it.
+ del response.headers["Content-Length"]
+
+ def file_ranges():
+ # Apache compatibility:
+ yield ntob("\r\n")
+
+ for start, stop in r:
+ if debug:
+ cherrypy.log('Multipart; start: %r, stop: %r' % (start, stop),
+ 'TOOLS.STATIC')
+ yield ntob("--" + boundary, 'ascii')
+ yield ntob("\r\nContent-type: %s" % content_type, 'ascii')
+ yield ntob("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
+ % (start, stop - 1, content_length), 'ascii')
+ fileobj.seek(start)
+ for chunk in file_generator_limited(fileobj, stop-start):
+ yield chunk
+ yield ntob("\r\n")
+ # Final boundary
+ yield ntob("--" + boundary + "--", 'ascii')
+
+ # Apache compatibility:
+ yield ntob("\r\n")
+ response.body = file_ranges()
+ return response.body
+ else:
+ if debug:
+ cherrypy.log('No byteranges requested', 'TOOLS.STATIC')
+
+ # Set Content-Length and use an iterable (file object)
+ # this way CP won't load the whole file in memory
+ response.headers['Content-Length'] = content_length
+ response.body = fileobj
+ return response.body
+
+def serve_download(path, name=None):
+ """Serve 'path' as an application/x-download attachment."""
+ # This is such a common idiom I felt it deserved its own wrapper.
+ return serve_file(path, "application/x-download", "attachment", name)
+
+
+def _attempt(filename, content_types, debug=False):
+ if debug:
+ cherrypy.log('Attempting %r (content_types %r)' %
+ (filename, content_types), 'TOOLS.STATICDIR')
+ try:
+ # you can set the content types for a
+ # complete directory per extension
+ content_type = None
+ if content_types:
+ r, ext = os.path.splitext(filename)
+ content_type = content_types.get(ext[1:], None)
+ serve_file(filename, content_type=content_type, debug=debug)
+ return True
+ except cherrypy.NotFound:
+ # If we didn't find the static file, continue handling the
+ # request. We might find a dynamic handler instead.
+ if debug:
+ cherrypy.log('NotFound', 'TOOLS.STATICFILE')
+ return False
+
+def staticdir(section, dir, root="", match="", content_types=None, index="",
+ debug=False):
+ """Serve a static resource from the given (root +) dir.
+
+ match
+ If given, request.path_info will be searched for the given
+ regular expression before attempting to serve static content.
+
+ content_types
+ If given, it should be a Python dictionary of
+ {file-extension: content-type} pairs, where 'file-extension' is
+ a string (e.g. "gif") and 'content-type' is the value to write
+ out in the Content-Type response header (e.g. "image/gif").
+
+ index
+ If provided, it should be the (relative) name of a file to
+ serve for directory requests. For example, if the dir argument is
+ '/home/me', the Request-URI is 'myapp', and the index arg is
+ 'index.html', the file '/home/me/myapp/index.html' will be sought.
+ """
+ request = cherrypy.serving.request
+ if request.method not in ('GET', 'HEAD'):
+ if debug:
+ cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR')
+ return False
+
+ if match and not re.search(match, request.path_info):
+ if debug:
+ cherrypy.log('request.path_info %r does not match pattern %r' %
+ (request.path_info, match), 'TOOLS.STATICDIR')
+ return False
+
+ # Allow the use of '~' to refer to a user's home directory.
+ dir = os.path.expanduser(dir)
+
+ # If dir is relative, make absolute using "root".
+ if not os.path.isabs(dir):
+ if not root:
+ msg = "Static dir requires an absolute dir (or root)."
+ if debug:
+ cherrypy.log(msg, 'TOOLS.STATICDIR')
+ raise ValueError(msg)
+ dir = os.path.join(root, dir)
+
+ # Determine where we are in the object tree relative to 'section'
+ # (where the static tool was defined).
+ if section == 'global':
+ section = "/"
+ section = section.rstrip(r"\/")
+ branch = request.path_info[len(section) + 1:]
+ branch = unquote(branch.lstrip(r"\/"))
+
+ # If branch is "", filename will end in a slash
+ filename = os.path.join(dir, branch)
+ if debug:
+ cherrypy.log('Checking file %r to fulfill %r' %
+ (filename, request.path_info), 'TOOLS.STATICDIR')
+
+ # There's a chance that the branch pulled from the URL might
+ # have ".." or similar uplevel attacks in it. Check that the final
+ # filename is a child of dir.
+ if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
+ raise cherrypy.HTTPError(403) # Forbidden
+
+ handled = _attempt(filename, content_types)
+ if not handled:
+ # Check for an index file if a folder was requested.
+ if index:
+ handled = _attempt(os.path.join(filename, index), content_types)
+ if handled:
+ request.is_index = filename[-1] in (r"\/")
+ return handled
+
+def staticfile(filename, root=None, match="", content_types=None, debug=False):
+ """Serve a static resource from the given (root +) filename.
+
+ match
+ If given, request.path_info will be searched for the given
+ regular expression before attempting to serve static content.
+
+ content_types
+ If given, it should be a Python dictionary of
+ {file-extension: content-type} pairs, where 'file-extension' is
+ a string (e.g. "gif") and 'content-type' is the value to write
+ out in the Content-Type response header (e.g. "image/gif").
+
+ """
+ request = cherrypy.serving.request
+ if request.method not in ('GET', 'HEAD'):
+ if debug:
+ cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE')
+ return False
+
+ if match and not re.search(match, request.path_info):
+ if debug:
+ cherrypy.log('request.path_info %r does not match pattern %r' %
+ (request.path_info, match), 'TOOLS.STATICFILE')
+ return False
+
+ # If filename is relative, make absolute using "root".
+ if not os.path.isabs(filename):
+ if not root:
+ msg = "Static tool requires an absolute filename (got '%s')." % filename
+ if debug:
+ cherrypy.log(msg, 'TOOLS.STATICFILE')
+ raise ValueError(msg)
+ filename = os.path.join(root, filename)
+
+ return _attempt(filename, content_types, debug=debug)
diff --git a/cherrypy/lib/xmlrpc.py b/cherrypy/lib/xmlrpc.py
new file mode 100755
index 0000000..8a5ef54
--- /dev/null
+++ b/cherrypy/lib/xmlrpc.py
@@ -0,0 +1,49 @@
+import sys
+
+import cherrypy
+
+
+def process_body():
+ """Return (params, method) from request body."""
+ try:
+ import xmlrpclib
+ return xmlrpclib.loads(cherrypy.request.body.read())
+ except Exception:
+ return ('ERROR PARAMS', ), 'ERRORMETHOD'
+
+
+def patched_path(path):
+ """Return 'path', doctored for RPC."""
+ if not path.endswith('/'):
+ path += '/'
+ if path.startswith('/RPC2/'):
+ # strip the first /rpc2
+ path = path[5:]
+ return path
+
+
+def _set_response(body):
+ # The XML-RPC spec (http://www.xmlrpc.com/spec) says:
+ # "Unless there's a lower-level error, always return 200 OK."
+ # Since Python's xmlrpclib interprets a non-200 response
+ # as a "Protocol Error", we'll just return 200 every time.
+ response = cherrypy.response
+ response.status = '200 OK'
+ response.body = body
+ response.headers['Content-Type'] = 'text/xml'
+ response.headers['Content-Length'] = len(body)
+
+
+def respond(body, encoding='utf-8', allow_none=0):
+ from xmlrpclib import Fault, dumps
+ if not isinstance(body, Fault):
+ body = (body,)
+ _set_response(dumps(body, methodresponse=1,
+ encoding=encoding,
+ allow_none=allow_none))
+
+def on_error(*args, **kwargs):
+ body = str(sys.exc_info()[1])
+ from xmlrpclib import Fault, dumps
+ _set_response(dumps(Fault(1, body)))
+
diff --git a/cherrypy/process/__init__.py b/cherrypy/process/__init__.py
new file mode 100755
index 0000000..f15b123
--- /dev/null
+++ b/cherrypy/process/__init__.py
@@ -0,0 +1,14 @@
+"""Site container for an HTTP server.
+
+A Web Site Process Bus object is used to connect applications, servers,
+and frameworks with site-wide services such as daemonization, process
+reload, signal handling, drop privileges, PID file management, logging
+for all of these, and many more.
+
+The 'plugins' module defines a few abstract and concrete services for
+use with the bus. Some use tool-specific channels; see the documentation
+for each class.
+"""
+
+from cherrypy.process.wspbus import bus
+from cherrypy.process import plugins, servers
diff --git a/cherrypy/process/plugins.py b/cherrypy/process/plugins.py
new file mode 100755
index 0000000..488958e
--- /dev/null
+++ b/cherrypy/process/plugins.py
@@ -0,0 +1,681 @@
+"""Site services for use with a Web Site Process Bus."""
+
+import os
+import re
+import signal as _signal
+import sys
+import time
+import threading
+
+from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident, ntob, set
+
+# _module__file__base is used by Autoreload to make
+# absolute any filenames retrieved from sys.modules which are not
+# already absolute paths. This is to work around Python's quirk
+# of importing the startup script and using a relative filename
+# for it in sys.modules.
+#
+# Autoreload examines sys.modules afresh every time it runs. If an application
+# changes the current directory by executing os.chdir(), then the next time
+# Autoreload runs, it will not be able to find any filenames which are
+# not absolute paths, because the current directory is not the same as when the
+# module was first imported. Autoreload will then wrongly conclude the file has
+# "changed", and initiate the shutdown/re-exec sequence.
+# See ticket #917.
+# For this workaround to have a decent probability of success, this module
+# needs to be imported as early as possible, before the app has much chance
+# to change the working directory.
+_module__file__base = os.getcwd()
+
+
+class SimplePlugin(object):
+ """Plugin base class which auto-subscribes methods for known channels."""
+
+ bus = None
+ """A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine."""
+
+ def __init__(self, bus):
+ self.bus = bus
+
+ def subscribe(self):
+ """Register this object as a (multi-channel) listener on the bus."""
+ for channel in self.bus.listeners:
+ # Subscribe self.start, self.exit, etc. if present.
+ method = getattr(self, channel, None)
+ if method is not None:
+ self.bus.subscribe(channel, method)
+
+ def unsubscribe(self):
+ """Unregister this object as a listener on the bus."""
+ for channel in self.bus.listeners:
+ # Unsubscribe self.start, self.exit, etc. if present.
+ method = getattr(self, channel, None)
+ if method is not None:
+ self.bus.unsubscribe(channel, method)
+
+
+
+class SignalHandler(object):
+ """Register bus channels (and listeners) for system signals.
+
+ You can modify what signals your application listens for, and what it does
+ when it receives signals, by modifying :attr:`SignalHandler.handlers`,
+ a dict of {signal name: callback} pairs. The default set is::
+
+ handlers = {'SIGTERM': self.bus.exit,
+ 'SIGHUP': self.handle_SIGHUP,
+ 'SIGUSR1': self.bus.graceful,
+ }
+
+ The :func:`SignalHandler.handle_SIGHUP`` method calls
+ :func:`bus.restart()<cherrypy.process.wspbus.Bus.restart>`
+ if the process is daemonized, but
+ :func:`bus.exit()<cherrypy.process.wspbus.Bus.exit>`
+ if the process is attached to a TTY. This is because Unix window
+ managers tend to send SIGHUP to terminal windows when the user closes them.
+
+ Feel free to add signals which are not available on every platform. The
+ :class:`SignalHandler` will ignore errors raised from attempting to register
+ handlers for unknown signals.
+ """
+
+ handlers = {}
+ """A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit)."""
+
+ signals = {}
+ """A map from signal numbers to names."""
+
+ for k, v in vars(_signal).items():
+ if k.startswith('SIG') and not k.startswith('SIG_'):
+ signals[v] = k
+ del k, v
+
+ def __init__(self, bus):
+ self.bus = bus
+ # Set default handlers
+ self.handlers = {'SIGTERM': self.bus.exit,
+ 'SIGHUP': self.handle_SIGHUP,
+ 'SIGUSR1': self.bus.graceful,
+ }
+
+ if sys.platform[:4] == 'java':
+ del self.handlers['SIGUSR1']
+ self.handlers['SIGUSR2'] = self.bus.graceful
+ self.bus.log("SIGUSR1 cannot be set on the JVM platform. "
+ "Using SIGUSR2 instead.")
+ self.handlers['SIGINT'] = self._jython_SIGINT_handler
+
+ self._previous_handlers = {}
+
+ def _jython_SIGINT_handler(self, signum=None, frame=None):
+ # See http://bugs.jython.org/issue1313
+ self.bus.log('Keyboard Interrupt: shutting down bus')
+ self.bus.exit()
+
+ def subscribe(self):
+ """Subscribe self.handlers to signals."""
+ for sig, func in self.handlers.items():
+ try:
+ self.set_handler(sig, func)
+ except ValueError:
+ pass
+
+ def unsubscribe(self):
+ """Unsubscribe self.handlers from signals."""
+ for signum, handler in self._previous_handlers.items():
+ signame = self.signals[signum]
+
+ if handler is None:
+ self.bus.log("Restoring %s handler to SIG_DFL." % signame)
+ handler = _signal.SIG_DFL
+ else:
+ self.bus.log("Restoring %s handler %r." % (signame, handler))
+
+ try:
+ our_handler = _signal.signal(signum, handler)
+ if our_handler is None:
+ self.bus.log("Restored old %s handler %r, but our "
+ "handler was not registered." %
+ (signame, handler), level=30)
+ except ValueError:
+ self.bus.log("Unable to restore %s handler %r." %
+ (signame, handler), level=40, traceback=True)
+
+ def set_handler(self, signal, listener=None):
+ """Subscribe a handler for the given signal (number or name).
+
+ If the optional 'listener' argument is provided, it will be
+ subscribed as a listener for the given signal's channel.
+
+ If the given signal name or number is not available on the current
+ platform, ValueError is raised.
+ """
+ if isinstance(signal, basestring):
+ signum = getattr(_signal, signal, None)
+ if signum is None:
+ raise ValueError("No such signal: %r" % signal)
+ signame = signal
+ else:
+ try:
+ signame = self.signals[signal]
+ except KeyError:
+ raise ValueError("No such signal: %r" % signal)
+ signum = signal
+
+ prev = _signal.signal(signum, self._handle_signal)
+ self._previous_handlers[signum] = prev
+
+ if listener is not None:
+ self.bus.log("Listening for %s." % signame)
+ self.bus.subscribe(signame, listener)
+
+ def _handle_signal(self, signum=None, frame=None):
+ """Python signal handler (self.set_handler subscribes it for you)."""
+ signame = self.signals[signum]
+ self.bus.log("Caught signal %s." % signame)
+ self.bus.publish(signame)
+
+ def handle_SIGHUP(self):
+ """Restart if daemonized, else exit."""
+ if os.isatty(sys.stdin.fileno()):
+ # not daemonized (may be foreground or background)
+ self.bus.log("SIGHUP caught but not daemonized. Exiting.")
+ self.bus.exit()
+ else:
+ self.bus.log("SIGHUP caught while daemonized. Restarting.")
+ self.bus.restart()
+
+
+try:
+ import pwd, grp
+except ImportError:
+ pwd, grp = None, None
+
+
+class DropPrivileges(SimplePlugin):
+ """Drop privileges. uid/gid arguments not available on Windows.
+
+ Special thanks to Gavin Baker: http://antonym.org/node/100.
+ """
+
+ def __init__(self, bus, umask=None, uid=None, gid=None):
+ SimplePlugin.__init__(self, bus)
+ self.finalized = False
+ self.uid = uid
+ self.gid = gid
+ self.umask = umask
+
+ def _get_uid(self):
+ return self._uid
+ def _set_uid(self, val):
+ if val is not None:
+ if pwd is None:
+ self.bus.log("pwd module not available; ignoring uid.",
+ level=30)
+ val = None
+ elif isinstance(val, basestring):
+ val = pwd.getpwnam(val)[2]
+ self._uid = val
+ uid = property(_get_uid, _set_uid,
+ doc="The uid under which to run. Availability: Unix.")
+
+ def _get_gid(self):
+ return self._gid
+ def _set_gid(self, val):
+ if val is not None:
+ if grp is None:
+ self.bus.log("grp module not available; ignoring gid.",
+ level=30)
+ val = None
+ elif isinstance(val, basestring):
+ val = grp.getgrnam(val)[2]
+ self._gid = val
+ gid = property(_get_gid, _set_gid,
+ doc="The gid under which to run. Availability: Unix.")
+
+ def _get_umask(self):
+ return self._umask
+ def _set_umask(self, val):
+ if val is not None:
+ try:
+ os.umask
+ except AttributeError:
+ self.bus.log("umask function not available; ignoring umask.",
+ level=30)
+ val = None
+ self._umask = val
+ umask = property(_get_umask, _set_umask,
+ doc="""The default permission mode for newly created files and directories.
+
+ Usually expressed in octal format, for example, ``0644``.
+ Availability: Unix, Windows.
+ """)
+
+ def start(self):
+ # uid/gid
+ def current_ids():
+ """Return the current (uid, gid) if available."""
+ name, group = None, None
+ if pwd:
+ name = pwd.getpwuid(os.getuid())[0]
+ if grp:
+ group = grp.getgrgid(os.getgid())[0]
+ return name, group
+
+ if self.finalized:
+ if not (self.uid is None and self.gid is None):
+ self.bus.log('Already running as uid: %r gid: %r' %
+ current_ids())
+ else:
+ if self.uid is None and self.gid is None:
+ if pwd or grp:
+ self.bus.log('uid/gid not set', level=30)
+ else:
+ self.bus.log('Started as uid: %r gid: %r' % current_ids())
+ if self.gid is not None:
+ os.setgid(self.gid)
+ os.setgroups([])
+ if self.uid is not None:
+ os.setuid(self.uid)
+ self.bus.log('Running as uid: %r gid: %r' % current_ids())
+
+ # umask
+ if self.finalized:
+ if self.umask is not None:
+ self.bus.log('umask already set to: %03o' % self.umask)
+ else:
+ if self.umask is None:
+ self.bus.log('umask not set', level=30)
+ else:
+ old_umask = os.umask(self.umask)
+ self.bus.log('umask old: %03o, new: %03o' %
+ (old_umask, self.umask))
+
+ self.finalized = True
+ # This is slightly higher than the priority for server.start
+ # in order to facilitate the most common use: starting on a low
+ # port (which requires root) and then dropping to another user.
+ start.priority = 77
+
+
+class Daemonizer(SimplePlugin):
+ """Daemonize the running script.
+
+ Use this with a Web Site Process Bus via::
+
+ Daemonizer(bus).subscribe()
+
+ When this component finishes, the process is completely decoupled from
+ the parent environment. Please note that when this component is used,
+ the return code from the parent process will still be 0 if a startup
+ error occurs in the forked children. Errors in the initial daemonizing
+ process still return proper exit codes. Therefore, if you use this
+ plugin to daemonize, don't use the return code as an accurate indicator
+ of whether the process fully started. In fact, that return code only
+ indicates if the process succesfully finished the first fork.
+ """
+
+ def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
+ stderr='/dev/null'):
+ SimplePlugin.__init__(self, bus)
+ self.stdin = stdin
+ self.stdout = stdout
+ self.stderr = stderr
+ self.finalized = False
+
+ def start(self):
+ if self.finalized:
+ self.bus.log('Already deamonized.')
+
+ # forking has issues with threads:
+ # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html
+ # "The general problem with making fork() work in a multi-threaded
+ # world is what to do with all of the threads..."
+ # So we check for active threads:
+ if threading.activeCount() != 1:
+ self.bus.log('There are %r active threads. '
+ 'Daemonizing now may cause strange failures.' %
+ threading.enumerate(), level=30)
+
+ # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
+ # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
+ # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
+
+ # Finish up with the current stdout/stderr
+ sys.stdout.flush()
+ sys.stderr.flush()
+
+ # Do first fork.
+ try:
+ pid = os.fork()
+ if pid == 0:
+ # This is the child process. Continue.
+ pass
+ else:
+ # This is the first parent. Exit, now that we've forked.
+ self.bus.log('Forking once.')
+ os._exit(0)
+ except OSError:
+ # Python raises OSError rather than returning negative numbers.
+ exc = sys.exc_info()[1]
+ sys.exit("%s: fork #1 failed: (%d) %s\n"
+ % (sys.argv[0], exc.errno, exc.strerror))
+
+ os.setsid()
+
+ # Do second fork
+ try:
+ pid = os.fork()
+ if pid > 0:
+ self.bus.log('Forking twice.')
+ os._exit(0) # Exit second parent
+ except OSError:
+ exc = sys.exc_info()[1]
+ sys.exit("%s: fork #2 failed: (%d) %s\n"
+ % (sys.argv[0], exc.errno, exc.strerror))
+
+ os.chdir("/")
+ os.umask(0)
+
+ si = open(self.stdin, "r")
+ so = open(self.stdout, "a+")
+ se = open(self.stderr, "a+")
+
+ # os.dup2(fd, fd2) will close fd2 if necessary,
+ # so we don't explicitly close stdin/out/err.
+ # See http://docs.python.org/lib/os-fd-ops.html
+ os.dup2(si.fileno(), sys.stdin.fileno())
+ os.dup2(so.fileno(), sys.stdout.fileno())
+ os.dup2(se.fileno(), sys.stderr.fileno())
+
+ self.bus.log('Daemonized to PID: %s' % os.getpid())
+ self.finalized = True
+ start.priority = 65
+
+
+class PIDFile(SimplePlugin):
+ """Maintain a PID file via a WSPBus."""
+
+ def __init__(self, bus, pidfile):
+ SimplePlugin.__init__(self, bus)
+ self.pidfile = pidfile
+ self.finalized = False
+
+ def start(self):
+ pid = os.getpid()
+ if self.finalized:
+ self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
+ else:
+ open(self.pidfile, "wb").write(ntob("%s" % pid, 'utf8'))
+ self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
+ self.finalized = True
+ start.priority = 70
+
+ def exit(self):
+ try:
+ os.remove(self.pidfile)
+ self.bus.log('PID file removed: %r.' % self.pidfile)
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except:
+ pass
+
+
+class PerpetualTimer(threading._Timer):
+ """A responsive subclass of threading._Timer whose run() method repeats.
+
+ Use this timer only when you really need a very interruptible timer;
+ this checks its 'finished' condition up to 20 times a second, which can
+ results in pretty high CPU usage
+ """
+
+ def run(self):
+ while True:
+ self.finished.wait(self.interval)
+ if self.finished.isSet():
+ return
+ try:
+ self.function(*self.args, **self.kwargs)
+ except Exception:
+ self.bus.log("Error in perpetual timer thread function %r." %
+ self.function, level=40, traceback=True)
+ # Quit on first error to avoid massive logs.
+ raise
+
+
+class BackgroundTask(threading.Thread):
+ """A subclass of threading.Thread whose run() method repeats.
+
+ Use this class for most repeating tasks. It uses time.sleep() to wait
+ for each interval, which isn't very responsive; that is, even if you call
+ self.cancel(), you'll have to wait until the sleep() call finishes before
+ the thread stops. To compensate, it defaults to being daemonic, which means
+ it won't delay stopping the whole process.
+ """
+
+ def __init__(self, interval, function, args=[], kwargs={}):
+ threading.Thread.__init__(self)
+ self.interval = interval
+ self.function = function
+ self.args = args
+ self.kwargs = kwargs
+ self.running = False
+
+ def cancel(self):
+ self.running = False
+
+ def run(self):
+ self.running = True
+ while self.running:
+ time.sleep(self.interval)
+ if not self.running:
+ return
+ try:
+ self.function(*self.args, **self.kwargs)
+ except Exception:
+ self.bus.log("Error in background task thread function %r." %
+ self.function, level=40, traceback=True)
+ # Quit on first error to avoid massive logs.
+ raise
+
+ def _set_daemon(self):
+ return True
+
+
+class Monitor(SimplePlugin):
+ """WSPBus listener to periodically run a callback in its own thread."""
+
+ callback = None
+ """The function to call at intervals."""
+
+ frequency = 60
+ """The time in seconds between callback runs."""
+
+ thread = None
+ """A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>` thread."""
+
+ def __init__(self, bus, callback, frequency=60, name=None):
+ SimplePlugin.__init__(self, bus)
+ self.callback = callback
+ self.frequency = frequency
+ self.thread = None
+ self.name = name
+
+ def start(self):
+ """Start our callback in its own background thread."""
+ if self.frequency > 0:
+ threadname = self.name or self.__class__.__name__
+ if self.thread is None:
+ self.thread = BackgroundTask(self.frequency, self.callback)
+ self.thread.bus = self.bus
+ self.thread.setName(threadname)
+ self.thread.start()
+ self.bus.log("Started monitor thread %r." % threadname)
+ else:
+ self.bus.log("Monitor thread %r already started." % threadname)
+ start.priority = 70
+
+ def stop(self):
+ """Stop our callback's background task thread."""
+ if self.thread is None:
+ self.bus.log("No thread running for %s." % self.name or self.__class__.__name__)
+ else:
+ if self.thread is not threading.currentThread():
+ name = self.thread.getName()
+ self.thread.cancel()
+ if not get_daemon(self.thread):
+ self.bus.log("Joining %r" % name)
+ self.thread.join()
+ self.bus.log("Stopped thread %r." % name)
+ self.thread = None
+
+ def graceful(self):
+ """Stop the callback's background task thread and restart it."""
+ self.stop()
+ self.start()
+
+
+class Autoreloader(Monitor):
+ """Monitor which re-executes the process when files change.
+
+ This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`)
+ if any of the files it monitors change (or is deleted). By default, the
+ autoreloader monitors all imported modules; you can add to the
+ set by adding to ``autoreload.files``::
+
+ cherrypy.engine.autoreload.files.add(myFile)
+
+ If there are imported files you do *not* wish to monitor, you can adjust the
+ ``match`` attribute, a regular expression. For example, to stop monitoring
+ cherrypy itself::
+
+ cherrypy.engine.autoreload.match = r'^(?!cherrypy).+'
+
+ Like all :class:`Monitor<cherrypy.process.plugins.Monitor>` plugins,
+ the autoreload plugin takes a ``frequency`` argument. The default is
+ 1 second; that is, the autoreloader will examine files once each second.
+ """
+
+ files = None
+ """The set of files to poll for modifications."""
+
+ frequency = 1
+ """The interval in seconds at which to poll for modified files."""
+
+ match = '.*'
+ """A regular expression by which to match filenames."""
+
+ def __init__(self, bus, frequency=1, match='.*'):
+ self.mtimes = {}
+ self.files = set()
+ self.match = match
+ Monitor.__init__(self, bus, self.run, frequency)
+
+ def start(self):
+ """Start our own background task thread for self.run."""
+ if self.thread is None:
+ self.mtimes = {}
+ Monitor.start(self)
+ start.priority = 70
+
+ def sysfiles(self):
+ """Return a Set of sys.modules filenames to monitor."""
+ files = set()
+ for k, m in sys.modules.items():
+ if re.match(self.match, k):
+ if hasattr(m, '__loader__') and hasattr(m.__loader__, 'archive'):
+ f = m.__loader__.archive
+ else:
+ f = getattr(m, '__file__', None)
+ if f is not None and not os.path.isabs(f):
+ # ensure absolute paths so a os.chdir() in the app doesn't break me
+ f = os.path.normpath(os.path.join(_module__file__base, f))
+ files.add(f)
+ return files
+
+ def run(self):
+ """Reload the process if registered files have been modified."""
+ for filename in self.sysfiles() | self.files:
+ if filename:
+ if filename.endswith('.pyc'):
+ filename = filename[:-1]
+
+ oldtime = self.mtimes.get(filename, 0)
+ if oldtime is None:
+ # Module with no .py file. Skip it.
+ continue
+
+ try:
+ mtime = os.stat(filename).st_mtime
+ except OSError:
+ # Either a module with no .py file, or it's been deleted.
+ mtime = None
+
+ if filename not in self.mtimes:
+ # If a module has no .py file, this will be None.
+ self.mtimes[filename] = mtime
+ else:
+ if mtime is None or mtime > oldtime:
+ # The file has been deleted or modified.
+ self.bus.log("Restarting because %s changed." % filename)
+ self.thread.cancel()
+ self.bus.log("Stopped thread %r." % self.thread.getName())
+ self.bus.restart()
+ return
+
+
+class ThreadManager(SimplePlugin):
+ """Manager for HTTP request threads.
+
+ If you have control over thread creation and destruction, publish to
+ the 'acquire_thread' and 'release_thread' channels (for each thread).
+ This will register/unregister the current thread and publish to
+ 'start_thread' and 'stop_thread' listeners in the bus as needed.
+
+ If threads are created and destroyed by code you do not control
+ (e.g., Apache), then, at the beginning of every HTTP request,
+ publish to 'acquire_thread' only. You should not publish to
+ 'release_thread' in this case, since you do not know whether
+ the thread will be re-used or not. The bus will call
+ 'stop_thread' listeners for you when it stops.
+ """
+
+ threads = None
+ """A map of {thread ident: index number} pairs."""
+
+ def __init__(self, bus):
+ self.threads = {}
+ SimplePlugin.__init__(self, bus)
+ self.bus.listeners.setdefault('acquire_thread', set())
+ self.bus.listeners.setdefault('start_thread', set())
+ self.bus.listeners.setdefault('release_thread', set())
+ self.bus.listeners.setdefault('stop_thread', set())
+
+ def acquire_thread(self):
+ """Run 'start_thread' listeners for the current thread.
+
+ If the current thread has already been seen, any 'start_thread'
+ listeners will not be run again.
+ """
+ thread_ident = get_thread_ident()
+ if thread_ident not in self.threads:
+ # We can't just use get_ident as the thread ID
+ # because some platforms reuse thread ID's.
+ i = len(self.threads) + 1
+ self.threads[thread_ident] = i
+ self.bus.publish('start_thread', i)
+
+ def release_thread(self):
+ """Release the current thread and run 'stop_thread' listeners."""
+ thread_ident = get_thread_ident()
+ i = self.threads.pop(thread_ident, None)
+ if i is not None:
+ self.bus.publish('stop_thread', i)
+
+ def stop(self):
+ """Release all threads and run all 'stop_thread' listeners."""
+ for thread_ident, i in self.threads.items():
+ self.bus.publish('stop_thread', i)
+ self.threads.clear()
+ graceful = stop
+
diff --git a/cherrypy/process/servers.py b/cherrypy/process/servers.py
new file mode 100755
index 0000000..272e843
--- /dev/null
+++ b/cherrypy/process/servers.py
@@ -0,0 +1,418 @@
+"""
+Starting in CherryPy 3.1, cherrypy.server is implemented as an
+:ref:`Engine Plugin<plugins>`. It's an instance of
+:class:`cherrypy._cpserver.Server`, which is a subclass of
+:class:`cherrypy.process.servers.ServerAdapter`. The ``ServerAdapter`` class
+is designed to control other servers, as well.
+
+Multiple servers/ports
+======================
+
+If you need to start more than one HTTP server (to serve on multiple ports, or
+protocols, etc.), you can manually register each one and then start them all
+with engine.start::
+
+ s1 = ServerAdapter(cherrypy.engine, MyWSGIServer(host='0.0.0.0', port=80))
+ s2 = ServerAdapter(cherrypy.engine, another.HTTPServer(host='127.0.0.1', SSL=True))
+ s1.subscribe()
+ s2.subscribe()
+ cherrypy.engine.start()
+
+.. index:: SCGI
+
+FastCGI/SCGI
+============
+
+There are also Flup\ **F**\ CGIServer and Flup\ **S**\ CGIServer classes in
+:mod:`cherrypy.process.servers`. To start an fcgi server, for example,
+wrap an instance of it in a ServerAdapter::
+
+ addr = ('0.0.0.0', 4000)
+ f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=addr)
+ s = servers.ServerAdapter(cherrypy.engine, httpserver=f, bind_addr=addr)
+ s.subscribe()
+
+The :doc:`cherryd</deployguide/cherryd>` startup script will do the above for
+you via its `-f` flag.
+Note that you need to download and install `flup <http://trac.saddi.com/flup>`_
+yourself, whether you use ``cherryd`` or not.
+
+.. _fastcgi:
+.. index:: FastCGI
+
+FastCGI
+-------
+
+A very simple setup lets your cherry run with FastCGI.
+You just need the flup library,
+plus a running Apache server (with ``mod_fastcgi``) or lighttpd server.
+
+CherryPy code
+^^^^^^^^^^^^^
+
+hello.py::
+
+ #!/usr/bin/python
+ import cherrypy
+
+ class HelloWorld:
+ \"""Sample request handler class.\"""
+ def index(self):
+ return "Hello world!"
+ index.exposed = True
+
+ cherrypy.tree.mount(HelloWorld())
+ # CherryPy autoreload must be disabled for the flup server to work
+ cherrypy.config.update({'engine.autoreload_on':False})
+
+Then run :doc:`/deployguide/cherryd` with the '-f' arg::
+
+ cherryd -c <myconfig> -d -f -i hello.py
+
+Apache
+^^^^^^
+
+At the top level in httpd.conf::
+
+ FastCgiIpcDir /tmp
+ FastCgiServer /path/to/cherry.fcgi -idle-timeout 120 -processes 4
+
+And inside the relevant VirtualHost section::
+
+ # FastCGI config
+ AddHandler fastcgi-script .fcgi
+ ScriptAliasMatch (.*$) /path/to/cherry.fcgi$1
+
+Lighttpd
+^^^^^^^^
+
+For `Lighttpd <http://www.lighttpd.net/>`_ you can follow these
+instructions. Within ``lighttpd.conf`` make sure ``mod_fastcgi`` is
+active within ``server.modules``. Then, within your ``$HTTP["host"]``
+directive, configure your fastcgi script like the following::
+
+ $HTTP["url"] =~ "" {
+ fastcgi.server = (
+ "/" => (
+ "script.fcgi" => (
+ "bin-path" => "/path/to/your/script.fcgi",
+ "socket" => "/tmp/script.sock",
+ "check-local" => "disable",
+ "disable-time" => 1,
+ "min-procs" => 1,
+ "max-procs" => 1, # adjust as needed
+ ),
+ ),
+ )
+ } # end of $HTTP["url"] =~ "^/"
+
+Please see `Lighttpd FastCGI Docs
+<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for an explanation
+of the possible configuration options.
+"""
+
+import sys
+import time
+
+
+class ServerAdapter(object):
+ """Adapter for an HTTP server.
+
+ If you need to start more than one HTTP server (to serve on multiple
+ ports, or protocols, etc.), you can manually register each one and then
+ start them all with bus.start:
+
+ s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
+ s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
+ s1.subscribe()
+ s2.subscribe()
+ bus.start()
+ """
+
+ def __init__(self, bus, httpserver=None, bind_addr=None):
+ self.bus = bus
+ self.httpserver = httpserver
+ self.bind_addr = bind_addr
+ self.interrupt = None
+ self.running = False
+
+ def subscribe(self):
+ self.bus.subscribe('start', self.start)
+ self.bus.subscribe('stop', self.stop)
+
+ def unsubscribe(self):
+ self.bus.unsubscribe('start', self.start)
+ self.bus.unsubscribe('stop', self.stop)
+
+ def start(self):
+ """Start the HTTP server."""
+ if self.bind_addr is None:
+ on_what = "unknown interface (dynamic?)"
+ elif isinstance(self.bind_addr, tuple):
+ host, port = self.bind_addr
+ on_what = "%s:%s" % (host, port)
+ else:
+ on_what = "socket file: %s" % self.bind_addr
+
+ if self.running:
+ self.bus.log("Already serving on %s" % on_what)
+ return
+
+ self.interrupt = None
+ if not self.httpserver:
+ raise ValueError("No HTTP server has been created.")
+
+ # Start the httpserver in a new thread.
+ if isinstance(self.bind_addr, tuple):
+ wait_for_free_port(*self.bind_addr)
+
+ import threading
+ t = threading.Thread(target=self._start_http_thread)
+ t.setName("HTTPServer " + t.getName())
+ t.start()
+
+ self.wait()
+ self.running = True
+ self.bus.log("Serving on %s" % on_what)
+ start.priority = 75
+
+ def _start_http_thread(self):
+ """HTTP servers MUST be running in new threads, so that the
+ main thread persists to receive KeyboardInterrupt's. If an
+ exception is raised in the httpserver's thread then it's
+ trapped here, and the bus (and therefore our httpserver)
+ are shut down.
+ """
+ try:
+ self.httpserver.start()
+ except KeyboardInterrupt:
+ self.bus.log("<Ctrl-C> hit: shutting down HTTP server")
+ self.interrupt = sys.exc_info()[1]
+ self.bus.exit()
+ except SystemExit:
+ self.bus.log("SystemExit raised: shutting down HTTP server")
+ self.interrupt = sys.exc_info()[1]
+ self.bus.exit()
+ raise
+ except:
+ self.interrupt = sys.exc_info()[1]
+ self.bus.log("Error in HTTP server: shutting down",
+ traceback=True, level=40)
+ self.bus.exit()
+ raise
+
+ def wait(self):
+ """Wait until the HTTP server is ready to receive requests."""
+ while not getattr(self.httpserver, "ready", False):
+ if self.interrupt:
+ raise self.interrupt
+ time.sleep(.1)
+
+ # Wait for port to be occupied
+ if isinstance(self.bind_addr, tuple):
+ host, port = self.bind_addr
+ wait_for_occupied_port(host, port)
+
+ def stop(self):
+ """Stop the HTTP server."""
+ if self.running:
+ # stop() MUST block until the server is *truly* stopped.
+ self.httpserver.stop()
+ # Wait for the socket to be truly freed.
+ if isinstance(self.bind_addr, tuple):
+ wait_for_free_port(*self.bind_addr)
+ self.running = False
+ self.bus.log("HTTP Server %s shut down" % self.httpserver)
+ else:
+ self.bus.log("HTTP Server %s already shut down" % self.httpserver)
+ stop.priority = 25
+
+ def restart(self):
+ """Restart the HTTP server."""
+ self.stop()
+ self.start()
+
+
+class FlupCGIServer(object):
+ """Adapter for a flup.server.cgi.WSGIServer."""
+
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+ self.ready = False
+
+ def start(self):
+ """Start the CGI server."""
+ # We have to instantiate the server class here because its __init__
+ # starts a threadpool. If we do it too early, daemonize won't work.
+ from flup.server.cgi import WSGIServer
+
+ self.cgiserver = WSGIServer(*self.args, **self.kwargs)
+ self.ready = True
+ self.cgiserver.run()
+
+ def stop(self):
+ """Stop the HTTP server."""
+ self.ready = False
+
+
+class FlupFCGIServer(object):
+ """Adapter for a flup.server.fcgi.WSGIServer."""
+
+ def __init__(self, *args, **kwargs):
+ if kwargs.get('bindAddress', None) is None:
+ import socket
+ if not hasattr(socket, 'fromfd'):
+ raise ValueError(
+ 'Dynamic FCGI server not available on this platform. '
+ 'You must use a static or external one by providing a '
+ 'legal bindAddress.')
+ self.args = args
+ self.kwargs = kwargs
+ self.ready = False
+
+ def start(self):
+ """Start the FCGI server."""
+ # We have to instantiate the server class here because its __init__
+ # starts a threadpool. If we do it too early, daemonize won't work.
+ from flup.server.fcgi import WSGIServer
+ self.fcgiserver = WSGIServer(*self.args, **self.kwargs)
+ # TODO: report this bug upstream to flup.
+ # If we don't set _oldSIGs on Windows, we get:
+ # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
+ # line 108, in run
+ # self._restoreSignalHandlers()
+ # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
+ # line 156, in _restoreSignalHandlers
+ # for signum,handler in self._oldSIGs:
+ # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
+ self.fcgiserver._installSignalHandlers = lambda: None
+ self.fcgiserver._oldSIGs = []
+ self.ready = True
+ self.fcgiserver.run()
+
+ def stop(self):
+ """Stop the HTTP server."""
+ # Forcibly stop the fcgi server main event loop.
+ self.fcgiserver._keepGoing = False
+ # Force all worker threads to die off.
+ self.fcgiserver._threadPool.maxSpare = self.fcgiserver._threadPool._idleCount
+ self.ready = False
+
+
+class FlupSCGIServer(object):
+ """Adapter for a flup.server.scgi.WSGIServer."""
+
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+ self.ready = False
+
+ def start(self):
+ """Start the SCGI server."""
+ # We have to instantiate the server class here because its __init__
+ # starts a threadpool. If we do it too early, daemonize won't work.
+ from flup.server.scgi import WSGIServer
+ self.scgiserver = WSGIServer(*self.args, **self.kwargs)
+ # TODO: report this bug upstream to flup.
+ # If we don't set _oldSIGs on Windows, we get:
+ # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
+ # line 108, in run
+ # self._restoreSignalHandlers()
+ # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
+ # line 156, in _restoreSignalHandlers
+ # for signum,handler in self._oldSIGs:
+ # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
+ self.scgiserver._installSignalHandlers = lambda: None
+ self.scgiserver._oldSIGs = []
+ self.ready = True
+ self.scgiserver.run()
+
+ def stop(self):
+ """Stop the HTTP server."""
+ self.ready = False
+ # Forcibly stop the scgi server main event loop.
+ self.scgiserver._keepGoing = False
+ # Force all worker threads to die off.
+ self.scgiserver._threadPool.maxSpare = 0
+
+
+def client_host(server_host):
+ """Return the host on which a client can connect to the given listener."""
+ if server_host == '0.0.0.0':
+ # 0.0.0.0 is INADDR_ANY, which should answer on localhost.
+ return '127.0.0.1'
+ if server_host in ('::', '::0', '::0.0.0.0'):
+ # :: is IN6ADDR_ANY, which should answer on localhost.
+ # ::0 and ::0.0.0.0 are non-canonical but common ways to write IN6ADDR_ANY.
+ return '::1'
+ return server_host
+
+def check_port(host, port, timeout=1.0):
+ """Raise an error if the given port is not free on the given host."""
+ if not host:
+ raise ValueError("Host values of '' or None are not allowed.")
+ host = client_host(host)
+ port = int(port)
+
+ import socket
+
+ # AF_INET or AF_INET6 socket
+ # Get the correct address family for our host (allows IPv6 addresses)
+ try:
+ info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
+ socket.SOCK_STREAM)
+ except socket.gaierror:
+ if ':' in host:
+ info = [(socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0))]
+ else:
+ info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (host, port))]
+
+ for res in info:
+ af, socktype, proto, canonname, sa = res
+ s = None
+ try:
+ s = socket.socket(af, socktype, proto)
+ # See http://groups.google.com/group/cherrypy-users/
+ # browse_frm/thread/bbfe5eb39c904fe0
+ s.settimeout(timeout)
+ s.connect((host, port))
+ s.close()
+ raise IOError("Port %s is in use on %s; perhaps the previous "
+ "httpserver did not shut down properly." %
+ (repr(port), repr(host)))
+ except socket.error:
+ if s:
+ s.close()
+
+def wait_for_free_port(host, port):
+ """Wait for the specified port to become free (drop requests)."""
+ if not host:
+ raise ValueError("Host values of '' or None are not allowed.")
+
+ for trial in range(50):
+ try:
+ # we are expecting a free port, so reduce the timeout
+ check_port(host, port, timeout=0.1)
+ except IOError:
+ # Give the old server thread time to free the port.
+ time.sleep(0.1)
+ else:
+ return
+
+ raise IOError("Port %r not free on %r" % (port, host))
+
+def wait_for_occupied_port(host, port):
+ """Wait for the specified port to become active (receive requests)."""
+ if not host:
+ raise ValueError("Host values of '' or None are not allowed.")
+
+ for trial in range(50):
+ try:
+ check_port(host, port)
+ except IOError:
+ return
+ else:
+ time.sleep(.1)
+
+ raise IOError("Port %r not bound on %r" % (port, host))
diff --git a/cherrypy/process/win32.py b/cherrypy/process/win32.py
new file mode 100755
index 0000000..83f99a5
--- /dev/null
+++ b/cherrypy/process/win32.py
@@ -0,0 +1,174 @@
+"""Windows service. Requires pywin32."""
+
+import os
+import win32api
+import win32con
+import win32event
+import win32service
+import win32serviceutil
+
+from cherrypy.process import wspbus, plugins
+
+
+class ConsoleCtrlHandler(plugins.SimplePlugin):
+ """A WSPBus plugin for handling Win32 console events (like Ctrl-C)."""
+
+ def __init__(self, bus):
+ self.is_set = False
+ plugins.SimplePlugin.__init__(self, bus)
+
+ def start(self):
+ if self.is_set:
+ self.bus.log('Handler for console events already set.', level=40)
+ return
+
+ result = win32api.SetConsoleCtrlHandler(self.handle, 1)
+ if result == 0:
+ self.bus.log('Could not SetConsoleCtrlHandler (error %r)' %
+ win32api.GetLastError(), level=40)
+ else:
+ self.bus.log('Set handler for console events.', level=40)
+ self.is_set = True
+
+ def stop(self):
+ if not self.is_set:
+ self.bus.log('Handler for console events already off.', level=40)
+ return
+
+ try:
+ result = win32api.SetConsoleCtrlHandler(self.handle, 0)
+ except ValueError:
+ # "ValueError: The object has not been registered"
+ result = 1
+
+ if result == 0:
+ self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' %
+ win32api.GetLastError(), level=40)
+ else:
+ self.bus.log('Removed handler for console events.', level=40)
+ self.is_set = False
+
+ def handle(self, event):
+ """Handle console control events (like Ctrl-C)."""
+ if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT,
+ win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT,
+ win32con.CTRL_CLOSE_EVENT):
+ self.bus.log('Console event %s: shutting down bus' % event)
+
+ # Remove self immediately so repeated Ctrl-C doesn't re-call it.
+ try:
+ self.stop()
+ except ValueError:
+ pass
+
+ self.bus.exit()
+ # 'First to return True stops the calls'
+ return 1
+ return 0
+
+
+class Win32Bus(wspbus.Bus):
+ """A Web Site Process Bus implementation for Win32.
+
+ Instead of time.sleep, this bus blocks using native win32event objects.
+ """
+
+ def __init__(self):
+ self.events = {}
+ wspbus.Bus.__init__(self)
+
+ def _get_state_event(self, state):
+ """Return a win32event for the given state (creating it if needed)."""
+ try:
+ return self.events[state]
+ except KeyError:
+ event = win32event.CreateEvent(None, 0, 0,
+ "WSPBus %s Event (pid=%r)" %
+ (state.name, os.getpid()))
+ self.events[state] = event
+ return event
+
+ def _get_state(self):
+ return self._state
+ def _set_state(self, value):
+ self._state = value
+ event = self._get_state_event(value)
+ win32event.PulseEvent(event)
+ state = property(_get_state, _set_state)
+
+ def wait(self, state, interval=0.1, channel=None):
+ """Wait for the given state(s), KeyboardInterrupt or SystemExit.
+
+ Since this class uses native win32event objects, the interval
+ argument is ignored.
+ """
+ if isinstance(state, (tuple, list)):
+ # Don't wait for an event that beat us to the punch ;)
+ if self.state not in state:
+ events = tuple([self._get_state_event(s) for s in state])
+ win32event.WaitForMultipleObjects(events, 0, win32event.INFINITE)
+ else:
+ # Don't wait for an event that beat us to the punch ;)
+ if self.state != state:
+ event = self._get_state_event(state)
+ win32event.WaitForSingleObject(event, win32event.INFINITE)
+
+
+class _ControlCodes(dict):
+ """Control codes used to "signal" a service via ControlService.
+
+ User-defined control codes are in the range 128-255. We generally use
+ the standard Python value for the Linux signal and add 128. Example:
+
+ >>> signal.SIGUSR1
+ 10
+ control_codes['graceful'] = 128 + 10
+ """
+
+ def key_for(self, obj):
+ """For the given value, return its corresponding key."""
+ for key, val in self.items():
+ if val is obj:
+ return key
+ raise ValueError("The given object could not be found: %r" % obj)
+
+control_codes = _ControlCodes({'graceful': 138})
+
+
+def signal_child(service, command):
+ if command == 'stop':
+ win32serviceutil.StopService(service)
+ elif command == 'restart':
+ win32serviceutil.RestartService(service)
+ else:
+ win32serviceutil.ControlService(service, control_codes[command])
+
+
+class PyWebService(win32serviceutil.ServiceFramework):
+ """Python Web Service."""
+
+ _svc_name_ = "Python Web Service"
+ _svc_display_name_ = "Python Web Service"
+ _svc_deps_ = None # sequence of service names on which this depends
+ _exe_name_ = "pywebsvc"
+ _exe_args_ = None # Default to no arguments
+
+ # Only exists on Windows 2000 or later, ignored on windows NT
+ _svc_description_ = "Python Web Service"
+
+ def SvcDoRun(self):
+ from cherrypy import process
+ process.bus.start()
+ process.bus.block()
+
+ def SvcStop(self):
+ from cherrypy import process
+ self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
+ process.bus.exit()
+
+ def SvcOther(self, control):
+ process.bus.publish(control_codes.key_for(control))
+
+
+if __name__ == '__main__':
+ win32serviceutil.HandleCommandLine(PyWebService)
diff --git a/cherrypy/process/wspbus.py b/cherrypy/process/wspbus.py
new file mode 100755
index 0000000..46cd75a
--- /dev/null
+++ b/cherrypy/process/wspbus.py
@@ -0,0 +1,393 @@
+"""An implementation of the Web Site Process Bus.
+
+This module is completely standalone, depending only on the stdlib.
+
+Web Site Process Bus
+--------------------
+
+A Bus object is used to contain and manage site-wide behavior:
+daemonization, HTTP server start/stop, process reload, signal handling,
+drop privileges, PID file management, logging for all of these,
+and many more.
+
+In addition, a Bus object provides a place for each web framework
+to register code that runs in response to site-wide events (like
+process start and stop), or which controls or otherwise interacts with
+the site-wide components mentioned above. For example, a framework which
+uses file-based templates would add known template filenames to an
+autoreload component.
+
+Ideally, a Bus object will be flexible enough to be useful in a variety
+of invocation scenarios:
+
+ 1. The deployer starts a site from the command line via a
+ framework-neutral deployment script; applications from multiple frameworks
+ are mixed in a single site. Command-line arguments and configuration
+ files are used to define site-wide components such as the HTTP server,
+ WSGI component graph, autoreload behavior, signal handling, etc.
+ 2. The deployer starts a site via some other process, such as Apache;
+ applications from multiple frameworks are mixed in a single site.
+ Autoreload and signal handling (from Python at least) are disabled.
+ 3. The deployer starts a site via a framework-specific mechanism;
+ for example, when running tests, exploring tutorials, or deploying
+ single applications from a single framework. The framework controls
+ which site-wide components are enabled as it sees fit.
+
+The Bus object in this package uses topic-based publish-subscribe
+messaging to accomplish all this. A few topic channels are built in
+('start', 'stop', 'exit', 'graceful', 'log', and 'main'). Frameworks and
+site containers are free to define their own. If a message is sent to a
+channel that has not been defined or has no listeners, there is no effect.
+
+In general, there should only ever be a single Bus object per process.
+Frameworks and site containers share a single Bus object by publishing
+messages and subscribing listeners.
+
+The Bus object works as a finite state machine which models the current
+state of the process. Bus methods move it from one state to another;
+those methods then publish to subscribed listeners on the channel for
+the new state.::
+
+ O
+ |
+ V
+ STOPPING --> STOPPED --> EXITING -> X
+ A A |
+ | \___ |
+ | \ |
+ | V V
+ STARTED <-- STARTING
+
+"""
+
+import atexit
+import os
+import sys
+import threading
+import time
+import traceback as _traceback
+import warnings
+
+from cherrypy._cpcompat import set
+
+# Here I save the value of os.getcwd(), which, if I am imported early enough,
+# will be the directory from which the startup script was run. This is needed
+# by _do_execv(), to change back to the original directory before execv()ing a
+# new process. This is a defense against the application having changed the
+# current working directory (which could make sys.executable "not found" if
+# sys.executable is a relative-path, and/or cause other problems).
+_startup_cwd = os.getcwd()
+
+class ChannelFailures(Exception):
+ """Exception raised when errors occur in a listener during Bus.publish()."""
+ delimiter = '\n'
+
+ def __init__(self, *args, **kwargs):
+ # Don't use 'super' here; Exceptions are old-style in Py2.4
+ # See http://www.cherrypy.org/ticket/959
+ Exception.__init__(self, *args, **kwargs)
+ self._exceptions = list()
+
+ def handle_exception(self):
+ """Append the current exception to self."""
+ self._exceptions.append(sys.exc_info())
+
+ def get_instances(self):
+ """Return a list of seen exception instances."""
+ return [instance for cls, instance, traceback in self._exceptions]
+
+ def __str__(self):
+ exception_strings = map(repr, self.get_instances())
+ return self.delimiter.join(exception_strings)
+
+ __repr__ = __str__
+
+ def __nonzero__(self):
+ return bool(self._exceptions)
+
+# Use a flag to indicate the state of the bus.
+class _StateEnum(object):
+ class State(object):
+ name = None
+ def __repr__(self):
+ return "states.%s" % self.name
+
+ def __setattr__(self, key, value):
+ if isinstance(value, self.State):
+ value.name = key
+ object.__setattr__(self, key, value)
+states = _StateEnum()
+states.STOPPED = states.State()
+states.STARTING = states.State()
+states.STARTED = states.State()
+states.STOPPING = states.State()
+states.EXITING = states.State()
+
+
+class Bus(object):
+ """Process state-machine and messenger for HTTP site deployment.
+
+ All listeners for a given channel are guaranteed to be called even
+ if others at the same channel fail. Each failure is logged, but
+ execution proceeds on to the next listener. The only way to stop all
+ processing from inside a listener is to raise SystemExit and stop the
+ whole server.
+ """
+
+ states = states
+ state = states.STOPPED
+ execv = False
+
+ def __init__(self):
+ self.execv = False
+ self.state = states.STOPPED
+ self.listeners = dict(
+ [(channel, set()) for channel
+ in ('start', 'stop', 'exit', 'graceful', 'log', 'main')])
+ self._priorities = {}
+
+ def subscribe(self, channel, callback, priority=None):
+ """Add the given callback at the given channel (if not present)."""
+ if channel not in self.listeners:
+ self.listeners[channel] = set()
+ self.listeners[channel].add(callback)
+
+ if priority is None:
+ priority = getattr(callback, 'priority', 50)
+ self._priorities[(channel, callback)] = priority
+
+ def unsubscribe(self, channel, callback):
+ """Discard the given callback (if present)."""
+ listeners = self.listeners.get(channel)
+ if listeners and callback in listeners:
+ listeners.discard(callback)
+ del self._priorities[(channel, callback)]
+
+ def publish(self, channel, *args, **kwargs):
+ """Return output of all subscribers for the given channel."""
+ if channel not in self.listeners:
+ return []
+
+ exc = ChannelFailures()
+ output = []
+
+ items = [(self._priorities[(channel, listener)], listener)
+ for listener in self.listeners[channel]]
+ items.sort()
+ for priority, listener in items:
+ try:
+ output.append(listener(*args, **kwargs))
+ except KeyboardInterrupt:
+ raise
+ except SystemExit, e:
+ # If we have previous errors ensure the exit code is non-zero
+ if exc and e.code == 0:
+ e.code = 1
+ raise
+ except:
+ exc.handle_exception()
+ if channel == 'log':
+ # Assume any further messages to 'log' will fail.
+ pass
+ else:
+ self.log("Error in %r listener %r" % (channel, listener),
+ level=40, traceback=True)
+ if exc:
+ raise exc
+ return output
+
+ def _clean_exit(self):
+ """An atexit handler which asserts the Bus is not running."""
+ if self.state != states.EXITING:
+ warnings.warn(
+ "The main thread is exiting, but the Bus is in the %r state; "
+ "shutting it down automatically now. You must either call "
+ "bus.block() after start(), or call bus.exit() before the "
+ "main thread exits." % self.state, RuntimeWarning)
+ self.exit()
+
+ def start(self):
+ """Start all services."""
+ atexit.register(self._clean_exit)
+
+ self.state = states.STARTING
+ self.log('Bus STARTING')
+ try:
+ self.publish('start')
+ self.state = states.STARTED
+ self.log('Bus STARTED')
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except:
+ self.log("Shutting down due to error in start listener:",
+ level=40, traceback=True)
+ e_info = sys.exc_info()
+ try:
+ self.exit()
+ except:
+ # Any stop/exit errors will be logged inside publish().
+ pass
+ raise e_info[0], e_info[1], e_info[2]
+
+ def exit(self):
+ """Stop all services and prepare to exit the process."""
+ exitstate = self.state
+ try:
+ self.stop()
+
+ self.state = states.EXITING
+ self.log('Bus EXITING')
+ self.publish('exit')
+ # This isn't strictly necessary, but it's better than seeing
+ # "Waiting for child threads to terminate..." and then nothing.
+ self.log('Bus EXITED')
+ except:
+ # This method is often called asynchronously (whether thread,
+ # signal handler, console handler, or atexit handler), so we
+ # can't just let exceptions propagate out unhandled.
+ # Assume it's been logged and just die.
+ os._exit(70) # EX_SOFTWARE
+
+ if exitstate == states.STARTING:
+ # exit() was called before start() finished, possibly due to
+ # Ctrl-C because a start listener got stuck. In this case,
+ # we could get stuck in a loop where Ctrl-C never exits the
+ # process, so we just call os.exit here.
+ os._exit(70) # EX_SOFTWARE
+
+ def restart(self):
+ """Restart the process (may close connections).
+
+ This method does not restart the process from the calling thread;
+ instead, it stops the bus and asks the main thread to call execv.
+ """
+ self.execv = True
+ self.exit()
+
+ def graceful(self):
+ """Advise all services to reload."""
+ self.log('Bus graceful')
+ self.publish('graceful')
+
+ def block(self, interval=0.1):
+ """Wait for the EXITING state, KeyboardInterrupt or SystemExit.
+
+ This function is intended to be called only by the main thread.
+ After waiting for the EXITING state, it also waits for all threads
+ to terminate, and then calls os.execv if self.execv is True. This
+ design allows another thread to call bus.restart, yet have the main
+ thread perform the actual execv call (required on some platforms).
+ """
+ try:
+ self.wait(states.EXITING, interval=interval, channel='main')
+ except (KeyboardInterrupt, IOError):
+ # The time.sleep call might raise
+ # "IOError: [Errno 4] Interrupted function call" on KBInt.
+ self.log('Keyboard Interrupt: shutting down bus')
+ self.exit()
+ except SystemExit:
+ self.log('SystemExit raised: shutting down bus')
+ self.exit()
+ raise
+
+ # Waiting for ALL child threads to finish is necessary on OS X.
+ # See http://www.cherrypy.org/ticket/581.
+ # It's also good to let them all shut down before allowing
+ # the main thread to call atexit handlers.
+ # See http://www.cherrypy.org/ticket/751.
+ self.log("Waiting for child threads to terminate...")
+ for t in threading.enumerate():
+ if t != threading.currentThread() and t.isAlive():
+ # Note that any dummy (external) threads are always daemonic.
+ if hasattr(threading.Thread, "daemon"):
+ # Python 2.6+
+ d = t.daemon
+ else:
+ d = t.isDaemon()
+ if not d:
+ self.log("Waiting for thread %s." % t.getName())
+ t.join()
+
+ if self.execv:
+ self._do_execv()
+
+ def wait(self, state, interval=0.1, channel=None):
+ """Poll for the given state(s) at intervals; publish to channel."""
+ if isinstance(state, (tuple, list)):
+ states = state
+ else:
+ states = [state]
+
+ def _wait():
+ while self.state not in states:
+ time.sleep(interval)
+ self.publish(channel)
+
+ # From http://psyco.sourceforge.net/psycoguide/bugs.html:
+ # "The compiled machine code does not include the regular polling
+ # done by Python, meaning that a KeyboardInterrupt will not be
+ # detected before execution comes back to the regular Python
+ # interpreter. Your program cannot be interrupted if caught
+ # into an infinite Psyco-compiled loop."
+ try:
+ sys.modules['psyco'].cannotcompile(_wait)
+ except (KeyError, AttributeError):
+ pass
+
+ _wait()
+
+ def _do_execv(self):
+ """Re-execute the current process.
+
+ This must be called from the main thread, because certain platforms
+ (OS X) don't allow execv to be called in a child thread very well.
+ """
+ args = sys.argv[:]
+ self.log('Re-spawning %s' % ' '.join(args))
+
+ if sys.platform[:4] == 'java':
+ from _systemrestart import SystemRestart
+ raise SystemRestart
+ else:
+ args.insert(0, sys.executable)
+ if sys.platform == 'win32':
+ args = ['"%s"' % arg for arg in args]
+
+ os.chdir(_startup_cwd)
+ os.execv(sys.executable, args)
+
+ def stop(self):
+ """Stop all services."""
+ self.state = states.STOPPING
+ self.log('Bus STOPPING')
+ self.publish('stop')
+ self.state = states.STOPPED
+ self.log('Bus STOPPED')
+
+ def start_with_callback(self, func, args=None, kwargs=None):
+ """Start 'func' in a new thread T, then start self (and return T)."""
+ if args is None:
+ args = ()
+ if kwargs is None:
+ kwargs = {}
+ args = (func,) + args
+
+ def _callback(func, *a, **kw):
+ self.wait(states.STARTED)
+ func(*a, **kw)
+ t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
+ t.setName('Bus Callback ' + t.getName())
+ t.start()
+
+ self.start()
+
+ return t
+
+ def log(self, msg="", level=20, traceback=False):
+ """Log the given message. Append the last traceback if requested."""
+ if traceback:
+ exc = sys.exc_info()
+ msg += "\n" + "".join(_traceback.format_exception(*exc))
+ self.publish('log', msg, level)
+
+bus = Bus()
diff --git a/cherrypy/scaffold/__init__.py b/cherrypy/scaffold/__init__.py
new file mode 100755
index 0000000..00964ac
--- /dev/null
+++ b/cherrypy/scaffold/__init__.py
@@ -0,0 +1,61 @@
+"""<MyProject>, a CherryPy application.
+
+Use this as a base for creating new CherryPy applications. When you want
+to make a new app, copy and paste this folder to some other location
+(maybe site-packages) and rename it to the name of your project,
+then tweak as desired.
+
+Even before any tweaking, this should serve a few demonstration pages.
+Change to this directory and run:
+
+ ../cherryd -c site.conf
+
+"""
+
+import cherrypy
+from cherrypy import tools, url
+
+import os
+local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+
+class Root:
+
+ _cp_config = {'tools.log_tracebacks.on': True,
+ }
+
+ def index(self):
+ return """<html>
+<body>Try some <a href='%s?a=7'>other</a> path,
+or a <a href='%s?n=14'>default</a> path.<br />
+Or, just look at the pretty picture:<br />
+<img src='%s' />
+</body></html>""" % (url("other"), url("else"),
+ url("files/made_with_cherrypy_small.png"))
+ index.exposed = True
+
+ def default(self, *args, **kwargs):
+ return "args: %s kwargs: %s" % (args, kwargs)
+ default.exposed = True
+
+ def other(self, a=2, b='bananas', c=None):
+ cherrypy.response.headers['Content-Type'] = 'text/plain'
+ if c is None:
+ return "Have %d %s." % (int(a), b)
+ else:
+ return "Have %d %s, %s." % (int(a), b, c)
+ other.exposed = True
+
+ files = cherrypy.tools.staticdir.handler(
+ section="/files",
+ dir=os.path.join(local_dir, "static"),
+ # Ignore .php files, etc.
+ match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$',
+ )
+
+
+root = Root()
+
+# Uncomment the following to use your own favicon instead of CP's default.
+#favicon_path = os.path.join(local_dir, "favicon.ico")
+#root.favicon_ico = tools.staticfile.handler(filename=favicon_path)
diff --git a/cherrypy/scaffold/example.conf b/cherrypy/scaffold/example.conf
new file mode 100644
index 0000000..93a6e53
--- /dev/null
+++ b/cherrypy/scaffold/example.conf
@@ -0,0 +1,3 @@
+[/]
+log.error_file: "error.log"
+log.access_file: "access.log" \ No newline at end of file
diff --git a/cherrypy/scaffold/site.conf b/cherrypy/scaffold/site.conf
new file mode 100644
index 0000000..6ed3898
--- /dev/null
+++ b/cherrypy/scaffold/site.conf
@@ -0,0 +1,14 @@
+[global]
+# Uncomment this when you're done developing
+#environment: "production"
+
+server.socket_host: "0.0.0.0"
+server.socket_port: 8088
+
+# Uncomment the following lines to run on HTTPS at the same time
+#server.2.socket_host: "0.0.0.0"
+#server.2.socket_port: 8433
+#server.2.ssl_certificate: '../test/test.pem'
+#server.2.ssl_private_key: '../test/test.pem'
+
+tree.myapp: cherrypy.Application(scaffold.root, "/", "example.conf")
diff --git a/cherrypy/scaffold/static/made_with_cherrypy_small.png b/cherrypy/scaffold/static/made_with_cherrypy_small.png
new file mode 100644
index 0000000..c3aafee
--- /dev/null
+++ b/cherrypy/scaffold/static/made_with_cherrypy_small.png
Binary files differ
diff --git a/cherrypy/test/__init__.py b/cherrypy/test/__init__.py
new file mode 100755
index 0000000..e4c400d
--- /dev/null
+++ b/cherrypy/test/__init__.py
@@ -0,0 +1,25 @@
+"""Regression test suite for CherryPy.
+
+Run 'nosetests -s test/' to exercise all tests.
+
+The '-s' flag instructs nose to output stdout messages, wihch is crucial to
+the 'interactive' mode of webtest.py. If you run these tests without the '-s'
+flag, don't be surprised if the test seems to hang: it's waiting for your
+interactive input.
+"""
+
+import sys
+def newexit():
+ raise SystemExit('Exit called')
+
+def setup():
+ # We want to monkey patch sys.exit so that we can get some
+ # information about where exit is being called.
+ newexit._old = sys.exit
+ sys.exit = newexit
+
+def teardown():
+ try:
+ sys.exit = sys.exit._old
+ except AttributeError:
+ sys.exit = sys._exit
diff --git a/cherrypy/test/_test_decorators.py b/cherrypy/test/_test_decorators.py
new file mode 100755
index 0000000..5bcbc1e
--- /dev/null
+++ b/cherrypy/test/_test_decorators.py
@@ -0,0 +1,41 @@
+"""Test module for the @-decorator syntax, which is version-specific"""
+
+from cherrypy import expose, tools
+from cherrypy._cpcompat import ntob
+
+
+class ExposeExamples(object):
+
+ @expose
+ def no_call(self):
+ return "Mr E. R. Bradshaw"
+
+ @expose()
+ def call_empty(self):
+ return "Mrs. B.J. Smegma"
+
+ @expose("call_alias")
+ def nesbitt(self):
+ return "Mr Nesbitt"
+
+ @expose(["alias1", "alias2"])
+ def andrews(self):
+ return "Mr Ken Andrews"
+
+ @expose(alias="alias3")
+ def watson(self):
+ return "Mr. and Mrs. Watson"
+
+
+class ToolExamples(object):
+
+ @expose
+ @tools.response_headers(headers=[('Content-Type', 'application/data')])
+ def blah(self):
+ yield ntob("blah")
+ # This is here to demonstrate that _cp_config = {...} overwrites
+ # the _cp_config attribute added by the Tool decorator. You have
+ # to write _cp_config[k] = v or _cp_config.update(...) instead.
+ blah._cp_config['response.stream'] = True
+
+
diff --git a/cherrypy/test/_test_states_demo.py b/cherrypy/test/_test_states_demo.py
new file mode 100755
index 0000000..3f8f196
--- /dev/null
+++ b/cherrypy/test/_test_states_demo.py
@@ -0,0 +1,66 @@
+import os
+import sys
+import time
+starttime = time.time()
+
+import cherrypy
+
+
+class Root:
+
+ def index(self):
+ return "Hello World"
+ index.exposed = True
+
+ def mtimes(self):
+ return repr(cherrypy.engine.publish("Autoreloader", "mtimes"))
+ mtimes.exposed = True
+
+ def pid(self):
+ return str(os.getpid())
+ pid.exposed = True
+
+ def start(self):
+ return repr(starttime)
+ start.exposed = True
+
+ def exit(self):
+ # This handler might be called before the engine is STARTED if an
+ # HTTP worker thread handles it before the HTTP server returns
+ # control to engine.start. We avoid that race condition here
+ # by waiting for the Bus to be STARTED.
+ cherrypy.engine.wait(state=cherrypy.engine.states.STARTED)
+ cherrypy.engine.exit()
+ exit.exposed = True
+
+
+def unsub_sig():
+ cherrypy.log("unsubsig: %s" % cherrypy.config.get('unsubsig', False))
+ if cherrypy.config.get('unsubsig', False):
+ cherrypy.log("Unsubscribing the default cherrypy signal handler")
+ cherrypy.engine.signal_handler.unsubscribe()
+ try:
+ from signal import signal, SIGTERM
+ except ImportError:
+ pass
+ else:
+ def old_term_handler(signum=None, frame=None):
+ cherrypy.log("I am an old SIGTERM handler.")
+ sys.exit(0)
+ cherrypy.log("Subscribing the new one.")
+ signal(SIGTERM, old_term_handler)
+cherrypy.engine.subscribe('start', unsub_sig, priority=100)
+
+
+def starterror():
+ if cherrypy.config.get('starterror', False):
+ zerodiv = 1 / 0
+cherrypy.engine.subscribe('start', starterror, priority=6)
+
+def log_test_case_name():
+ if cherrypy.config.get('test_case_name', False):
+ cherrypy.log("STARTED FROM: %s" % cherrypy.config.get('test_case_name'))
+cherrypy.engine.subscribe('start', log_test_case_name, priority=6)
+
+
+cherrypy.tree.mount(Root(), '/', {'/': {}})
diff --git a/cherrypy/test/benchmark.py b/cherrypy/test/benchmark.py
new file mode 100755
index 0000000..90536a5
--- /dev/null
+++ b/cherrypy/test/benchmark.py
@@ -0,0 +1,409 @@
+"""CherryPy Benchmark Tool
+
+ Usage:
+ benchmark.py --null --notests --help --cpmodpy --modpython --ab=path --apache=path
+
+ --null: use a null Request object (to bench the HTTP server only)
+ --notests: start the server but do not run the tests; this allows
+ you to check the tested pages with a browser
+ --help: show this help message
+ --cpmodpy: run tests via apache on 8080 (with the builtin _cpmodpy)
+ --modpython: run tests via apache on 8080 (with modpython_gateway)
+ --ab=path: Use the ab script/executable at 'path' (see below)
+ --apache=path: Use the apache script/exe at 'path' (see below)
+
+ To run the benchmarks, the Apache Benchmark tool "ab" must either be on
+ your system path, or specified via the --ab=path option.
+
+ To run the modpython tests, the "apache" executable or script must be
+ on your system path, or provided via the --apache=path option. On some
+ platforms, "apache" may be called "apachectl" or "apache2ctl"--create
+ a symlink to them if needed.
+"""
+
+import getopt
+import os
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+import re
+import sys
+import time
+import traceback
+
+import cherrypy
+from cherrypy._cpcompat import ntob
+from cherrypy import _cperror, _cpmodpy
+from cherrypy.lib import httputil
+
+
+AB_PATH = ""
+APACHE_PATH = "apache"
+SCRIPT_NAME = "/cpbench/users/rdelon/apps/blog"
+
+__all__ = ['ABSession', 'Root', 'print_report',
+ 'run_standard_benchmarks', 'safe_threads',
+ 'size_report', 'startup', 'thread_report',
+ ]
+
+size_cache = {}
+
+class Root:
+
+ def index(self):
+ return """<html>
+<head>
+ <title>CherryPy Benchmark</title>
+</head>
+<body>
+ <ul>
+ <li><a href="hello">Hello, world! (14 byte dynamic)</a></li>
+ <li><a href="static/index.html">Static file (14 bytes static)</a></li>
+ <li><form action="sizer">Response of length:
+ <input type='text' name='size' value='10' /></form>
+ </li>
+ </ul>
+</body>
+</html>"""
+ index.exposed = True
+
+ def hello(self):
+ return "Hello, world\r\n"
+ hello.exposed = True
+
+ def sizer(self, size):
+ resp = size_cache.get(size, None)
+ if resp is None:
+ size_cache[size] = resp = "X" * int(size)
+ return resp
+ sizer.exposed = True
+
+
+cherrypy.config.update({
+ 'log.error.file': '',
+ 'environment': 'production',
+ 'server.socket_host': '127.0.0.1',
+ 'server.socket_port': 8080,
+ 'server.max_request_header_size': 0,
+ 'server.max_request_body_size': 0,
+ 'engine.deadlock_poll_freq': 0,
+ })
+
+# Cheat mode on ;)
+del cherrypy.config['tools.log_tracebacks.on']
+del cherrypy.config['tools.log_headers.on']
+del cherrypy.config['tools.trailing_slash.on']
+
+appconf = {
+ '/static': {
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': 'static',
+ 'tools.staticdir.root': curdir,
+ },
+ }
+app = cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf)
+
+
+class NullRequest:
+ """A null HTTP request class, returning 200 and an empty body."""
+
+ def __init__(self, local, remote, scheme="http"):
+ pass
+
+ def close(self):
+ pass
+
+ def run(self, method, path, query_string, protocol, headers, rfile):
+ cherrypy.response.status = "200 OK"
+ cherrypy.response.header_list = [("Content-Type", 'text/html'),
+ ("Server", "Null CherryPy"),
+ ("Date", httputil.HTTPDate()),
+ ("Content-Length", "0"),
+ ]
+ cherrypy.response.body = [""]
+ return cherrypy.response
+
+
+class NullResponse:
+ pass
+
+
+class ABSession:
+ """A session of 'ab', the Apache HTTP server benchmarking tool.
+
+Example output from ab:
+
+This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0
+Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
+Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/
+
+Benchmarking 127.0.0.1 (be patient)
+Completed 100 requests
+Completed 200 requests
+Completed 300 requests
+Completed 400 requests
+Completed 500 requests
+Completed 600 requests
+Completed 700 requests
+Completed 800 requests
+Completed 900 requests
+
+
+Server Software: CherryPy/3.1beta
+Server Hostname: 127.0.0.1
+Server Port: 8080
+
+Document Path: /static/index.html
+Document Length: 14 bytes
+
+Concurrency Level: 10
+Time taken for tests: 9.643867 seconds
+Complete requests: 1000
+Failed requests: 0
+Write errors: 0
+Total transferred: 189000 bytes
+HTML transferred: 14000 bytes
+Requests per second: 103.69 [#/sec] (mean)
+Time per request: 96.439 [ms] (mean)
+Time per request: 9.644 [ms] (mean, across all concurrent requests)
+Transfer rate: 19.08 [Kbytes/sec] received
+
+Connection Times (ms)
+ min mean[+/-sd] median max
+Connect: 0 0 2.9 0 10
+Processing: 20 94 7.3 90 130
+Waiting: 0 43 28.1 40 100
+Total: 20 95 7.3 100 130
+
+Percentage of the requests served within a certain time (ms)
+ 50% 100
+ 66% 100
+ 75% 100
+ 80% 100
+ 90% 100
+ 95% 100
+ 98% 100
+ 99% 110
+ 100% 130 (longest request)
+Finished 1000 requests
+"""
+
+ parse_patterns = [('complete_requests', 'Completed',
+ ntob(r'^Complete requests:\s*(\d+)')),
+ ('failed_requests', 'Failed',
+ ntob(r'^Failed requests:\s*(\d+)')),
+ ('requests_per_second', 'req/sec',
+ ntob(r'^Requests per second:\s*([0-9.]+)')),
+ ('time_per_request_concurrent', 'msec/req',
+ ntob(r'^Time per request:\s*([0-9.]+).*concurrent requests\)$')),
+ ('transfer_rate', 'KB/sec',
+ ntob(r'^Transfer rate:\s*([0-9.]+)')),
+ ]
+
+ def __init__(self, path=SCRIPT_NAME + "/hello", requests=1000, concurrency=10):
+ self.path = path
+ self.requests = requests
+ self.concurrency = concurrency
+
+ def args(self):
+ port = cherrypy.server.socket_port
+ assert self.concurrency > 0
+ assert self.requests > 0
+ # Don't use "localhost".
+ # Cf http://mail.python.org/pipermail/python-win32/2008-March/007050.html
+ return ("-k -n %s -c %s http://127.0.0.1:%s%s" %
+ (self.requests, self.concurrency, port, self.path))
+
+ def run(self):
+ # Parse output of ab, setting attributes on self
+ try:
+ self.output = _cpmodpy.read_process(AB_PATH or "ab", self.args())
+ except:
+ print(_cperror.format_exc())
+ raise
+
+ for attr, name, pattern in self.parse_patterns:
+ val = re.search(pattern, self.output, re.MULTILINE)
+ if val:
+ val = val.group(1)
+ setattr(self, attr, val)
+ else:
+ setattr(self, attr, None)
+
+
+safe_threads = (25, 50, 100, 200, 400)
+if sys.platform in ("win32",):
+ # For some reason, ab crashes with > 50 threads on my Win2k laptop.
+ safe_threads = (10, 20, 30, 40, 50)
+
+
+def thread_report(path=SCRIPT_NAME + "/hello", concurrency=safe_threads):
+ sess = ABSession(path)
+ attrs, names, patterns = list(zip(*sess.parse_patterns))
+ avg = dict.fromkeys(attrs, 0.0)
+
+ yield ('threads',) + names
+ for c in concurrency:
+ sess.concurrency = c
+ sess.run()
+ row = [c]
+ for attr in attrs:
+ val = getattr(sess, attr)
+ if val is None:
+ print(sess.output)
+ row = None
+ break
+ val = float(val)
+ avg[attr] += float(val)
+ row.append(val)
+ if row:
+ yield row
+
+ # Add a row of averages.
+ yield ["Average"] + [str(avg[attr] / len(concurrency)) for attr in attrs]
+
+def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000),
+ concurrency=50):
+ sess = ABSession(concurrency=concurrency)
+ attrs, names, patterns = list(zip(*sess.parse_patterns))
+ yield ('bytes',) + names
+ for sz in sizes:
+ sess.path = "%s/sizer?size=%s" % (SCRIPT_NAME, sz)
+ sess.run()
+ yield [sz] + [getattr(sess, attr) for attr in attrs]
+
+def print_report(rows):
+ for row in rows:
+ print("")
+ for i, val in enumerate(row):
+ sys.stdout.write(str(val).rjust(10) + " | ")
+ print("")
+
+
+def run_standard_benchmarks():
+ print("")
+ print("Client Thread Report (1000 requests, 14 byte response body, "
+ "%s server threads):" % cherrypy.server.thread_pool)
+ print_report(thread_report())
+
+ print("")
+ print("Client Thread Report (1000 requests, 14 bytes via staticdir, "
+ "%s server threads):" % cherrypy.server.thread_pool)
+ print_report(thread_report("%s/static/index.html" % SCRIPT_NAME))
+
+ print("")
+ print("Size Report (1000 requests, 50 client threads, "
+ "%s server threads):" % cherrypy.server.thread_pool)
+ print_report(size_report())
+
+
+# modpython and other WSGI #
+
+def startup_modpython(req=None):
+ """Start the CherryPy app server in 'serverless' mode (for modpython/WSGI)."""
+ if cherrypy.engine.state == cherrypy._cpengine.STOPPED:
+ if req:
+ if "nullreq" in req.get_options():
+ cherrypy.engine.request_class = NullRequest
+ cherrypy.engine.response_class = NullResponse
+ ab_opt = req.get_options().get("ab", "")
+ if ab_opt:
+ global AB_PATH
+ AB_PATH = ab_opt
+ cherrypy.engine.start()
+ if cherrypy.engine.state == cherrypy._cpengine.STARTING:
+ cherrypy.engine.wait()
+ return 0 # apache.OK
+
+
+def run_modpython(use_wsgi=False):
+ print("Starting mod_python...")
+ pyopts = []
+
+ # Pass the null and ab=path options through Apache
+ if "--null" in opts:
+ pyopts.append(("nullreq", ""))
+
+ if "--ab" in opts:
+ pyopts.append(("ab", opts["--ab"]))
+
+ s = _cpmodpy.ModPythonServer
+ if use_wsgi:
+ pyopts.append(("wsgi.application", "cherrypy::tree"))
+ pyopts.append(("wsgi.startup", "cherrypy.test.benchmark::startup_modpython"))
+ handler = "modpython_gateway::handler"
+ s = s(port=8080, opts=pyopts, apache_path=APACHE_PATH, handler=handler)
+ else:
+ pyopts.append(("cherrypy.setup", "cherrypy.test.benchmark::startup_modpython"))
+ s = s(port=8080, opts=pyopts, apache_path=APACHE_PATH)
+
+ try:
+ s.start()
+ run()
+ finally:
+ s.stop()
+
+
+
+if __name__ == '__main__':
+ longopts = ['cpmodpy', 'modpython', 'null', 'notests',
+ 'help', 'ab=', 'apache=']
+ try:
+ switches, args = getopt.getopt(sys.argv[1:], "", longopts)
+ opts = dict(switches)
+ except getopt.GetoptError:
+ print(__doc__)
+ sys.exit(2)
+
+ if "--help" in opts:
+ print(__doc__)
+ sys.exit(0)
+
+ if "--ab" in opts:
+ AB_PATH = opts['--ab']
+
+ if "--notests" in opts:
+ # Return without stopping the server, so that the pages
+ # can be tested from a standard web browser.
+ def run():
+ port = cherrypy.server.socket_port
+ print("You may now open http://127.0.0.1:%s%s/" %
+ (port, SCRIPT_NAME))
+
+ if "--null" in opts:
+ print("Using null Request object")
+ else:
+ def run():
+ end = time.time() - start
+ print("Started in %s seconds" % end)
+ if "--null" in opts:
+ print("\nUsing null Request object")
+ try:
+ try:
+ run_standard_benchmarks()
+ except:
+ print(_cperror.format_exc())
+ raise
+ finally:
+ cherrypy.engine.exit()
+
+ print("Starting CherryPy app server...")
+
+ class NullWriter(object):
+ """Suppresses the printing of socket errors."""
+ def write(self, data):
+ pass
+ sys.stderr = NullWriter()
+
+ start = time.time()
+
+ if "--cpmodpy" in opts:
+ run_modpython()
+ elif "--modpython" in opts:
+ run_modpython(use_wsgi=True)
+ else:
+ if "--null" in opts:
+ cherrypy.server.request_class = NullRequest
+ cherrypy.server.response_class = NullResponse
+
+ cherrypy.engine.start_with_callback(run)
+ cherrypy.engine.block()
diff --git a/cherrypy/test/checkerdemo.py b/cherrypy/test/checkerdemo.py
new file mode 100755
index 0000000..32a7dee
--- /dev/null
+++ b/cherrypy/test/checkerdemo.py
@@ -0,0 +1,47 @@
+"""Demonstration app for cherrypy.checker.
+
+This application is intentionally broken and badly designed.
+To demonstrate the output of the CherryPy Checker, simply execute
+this module.
+"""
+
+import os
+import cherrypy
+thisdir = os.path.dirname(os.path.abspath(__file__))
+
+class Root:
+ pass
+
+if __name__ == '__main__':
+ conf = {'/base': {'tools.staticdir.root': thisdir,
+ # Obsolete key.
+ 'throw_errors': True,
+ },
+ # This entry should be OK.
+ '/base/static': {'tools.staticdir.on': True,
+ 'tools.staticdir.dir': 'static'},
+ # Warn on missing folder.
+ '/base/js': {'tools.staticdir.on': True,
+ 'tools.staticdir.dir': 'js'},
+ # Warn on dir with an abs path even though we provide root.
+ '/base/static2': {'tools.staticdir.on': True,
+ 'tools.staticdir.dir': '/static'},
+ # Warn on dir with a relative path with no root.
+ '/static3': {'tools.staticdir.on': True,
+ 'tools.staticdir.dir': 'static'},
+ # Warn on unknown namespace
+ '/unknown': {'toobles.gzip.on': True},
+ # Warn special on cherrypy.<known ns>.*
+ '/cpknown': {'cherrypy.tools.encode.on': True},
+ # Warn on mismatched types
+ '/conftype': {'request.show_tracebacks': 14},
+ # Warn on unknown tool.
+ '/web': {'tools.unknown.on': True},
+ # Warn on server.* in app config.
+ '/app1': {'server.socket_host': '0.0.0.0'},
+ # Warn on 'localhost'
+ 'global': {'server.socket_host': 'localhost'},
+ # Warn on '[name]'
+ '[/extra_brackets]': {},
+ }
+ cherrypy.quickstart(Root(), config=conf)
diff --git a/cherrypy/test/helper.py b/cherrypy/test/helper.py
new file mode 100755
index 0000000..ff9e06c
--- /dev/null
+++ b/cherrypy/test/helper.py
@@ -0,0 +1,476 @@
+"""A library of helper functions for the CherryPy test suite."""
+
+import datetime
+import logging
+log = logging.getLogger(__name__)
+import os
+thisdir = os.path.abspath(os.path.dirname(__file__))
+serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem')
+
+import re
+import sys
+import time
+import warnings
+
+import cherrypy
+from cherrypy._cpcompat import basestring, copyitems, HTTPSConnection, ntob
+from cherrypy.lib import httputil
+from cherrypy.lib.reprconf import unrepr
+from cherrypy.test import webtest
+
+import nose
+
+_testconfig = None
+
+def get_tst_config(overconf = {}):
+ global _testconfig
+ if _testconfig is None:
+ conf = {
+ 'scheme': 'http',
+ 'protocol': "HTTP/1.1",
+ 'port': 8080,
+ 'host': '127.0.0.1',
+ 'validate': False,
+ 'conquer': False,
+ 'server': 'wsgi',
+ }
+ try:
+ import testconfig
+ _conf = testconfig.config.get('supervisor', None)
+ if _conf is not None:
+ for k, v in _conf.items():
+ if isinstance(v, basestring):
+ _conf[k] = unrepr(v)
+ conf.update(_conf)
+ except ImportError:
+ pass
+ _testconfig = conf
+ conf = _testconfig.copy()
+ conf.update(overconf)
+
+ return conf
+
+class Supervisor(object):
+ """Base class for modeling and controlling servers during testing."""
+
+ def __init__(self, **kwargs):
+ for k, v in kwargs.items():
+ if k == 'port':
+ setattr(self, k, int(v))
+ setattr(self, k, v)
+
+
+log_to_stderr = lambda msg, level: sys.stderr.write(msg + os.linesep)
+
+class LocalSupervisor(Supervisor):
+ """Base class for modeling/controlling servers which run in the same process.
+
+ When the server side runs in a different process, start/stop can dump all
+ state between each test module easily. When the server side runs in the
+ same process as the client, however, we have to do a bit more work to ensure
+ config and mounted apps are reset between tests.
+ """
+
+ using_apache = False
+ using_wsgi = False
+
+ def __init__(self, **kwargs):
+ for k, v in kwargs.items():
+ setattr(self, k, v)
+
+ cherrypy.server.httpserver = self.httpserver_class
+
+ engine = cherrypy.engine
+ if hasattr(engine, "signal_handler"):
+ engine.signal_handler.subscribe()
+ if hasattr(engine, "console_control_handler"):
+ engine.console_control_handler.subscribe()
+ #engine.subscribe('log', log_to_stderr)
+
+ def start(self, modulename=None):
+ """Load and start the HTTP server."""
+ if modulename:
+ # Unhook httpserver so cherrypy.server.start() creates a new
+ # one (with config from setup_server, if declared).
+ cherrypy.server.httpserver = None
+
+ cherrypy.engine.start()
+
+ self.sync_apps()
+
+ def sync_apps(self):
+ """Tell the server about any apps which the setup functions mounted."""
+ pass
+
+ def stop(self):
+ td = getattr(self, 'teardown', None)
+ if td:
+ td()
+
+ cherrypy.engine.exit()
+
+ for name, server in copyitems(getattr(cherrypy, 'servers', {})):
+ server.unsubscribe()
+ del cherrypy.servers[name]
+
+
+class NativeServerSupervisor(LocalSupervisor):
+ """Server supervisor for the builtin HTTP server."""
+
+ httpserver_class = "cherrypy._cpnative_server.CPHTTPServer"
+ using_apache = False
+ using_wsgi = False
+
+ def __str__(self):
+ return "Builtin HTTP Server on %s:%s" % (self.host, self.port)
+
+
+class LocalWSGISupervisor(LocalSupervisor):
+ """Server supervisor for the builtin WSGI server."""
+
+ httpserver_class = "cherrypy._cpwsgi_server.CPWSGIServer"
+ using_apache = False
+ using_wsgi = True
+
+ def __str__(self):
+ return "Builtin WSGI Server on %s:%s" % (self.host, self.port)
+
+ def sync_apps(self):
+ """Hook a new WSGI app into the origin server."""
+ cherrypy.server.httpserver.wsgi_app = self.get_app()
+
+ def get_app(self, app=None):
+ """Obtain a new (decorated) WSGI app to hook into the origin server."""
+ if app is None:
+ app = cherrypy.tree
+
+ if self.conquer:
+ try:
+ import wsgiconq
+ except ImportError:
+ warnings.warn("Error importing wsgiconq. pyconquer will not run.")
+ else:
+ app = wsgiconq.WSGILogger(app, c_calls=True)
+
+ if self.validate:
+ try:
+ from wsgiref import validate
+ except ImportError:
+ warnings.warn("Error importing wsgiref. The validator will not run.")
+ else:
+ #wraps the app in the validator
+ app = validate.validator(app)
+
+ return app
+
+
+def get_cpmodpy_supervisor(**options):
+ from cherrypy.test import modpy
+ sup = modpy.ModPythonSupervisor(**options)
+ sup.template = modpy.conf_cpmodpy
+ return sup
+
+def get_modpygw_supervisor(**options):
+ from cherrypy.test import modpy
+ sup = modpy.ModPythonSupervisor(**options)
+ sup.template = modpy.conf_modpython_gateway
+ sup.using_wsgi = True
+ return sup
+
+def get_modwsgi_supervisor(**options):
+ from cherrypy.test import modwsgi
+ return modwsgi.ModWSGISupervisor(**options)
+
+def get_modfcgid_supervisor(**options):
+ from cherrypy.test import modfcgid
+ return modfcgid.ModFCGISupervisor(**options)
+
+def get_modfastcgi_supervisor(**options):
+ from cherrypy.test import modfastcgi
+ return modfastcgi.ModFCGISupervisor(**options)
+
+def get_wsgi_u_supervisor(**options):
+ cherrypy.server.wsgi_version = ('u', 0)
+ return LocalWSGISupervisor(**options)
+
+
+class CPWebCase(webtest.WebCase):
+
+ script_name = ""
+ scheme = "http"
+
+ available_servers = {'wsgi': LocalWSGISupervisor,
+ 'wsgi_u': get_wsgi_u_supervisor,
+ 'native': NativeServerSupervisor,
+ 'cpmodpy': get_cpmodpy_supervisor,
+ 'modpygw': get_modpygw_supervisor,
+ 'modwsgi': get_modwsgi_supervisor,
+ 'modfcgid': get_modfcgid_supervisor,
+ 'modfastcgi': get_modfastcgi_supervisor,
+ }
+ default_server = "wsgi"
+
+ def _setup_server(cls, supervisor, conf):
+ v = sys.version.split()[0]
+ log.info("Python version used to run this test script: %s" % v)
+ log.info("CherryPy version: %s" % cherrypy.__version__)
+ if supervisor.scheme == "https":
+ ssl = " (ssl)"
+ else:
+ ssl = ""
+ log.info("HTTP server version: %s%s" % (supervisor.protocol, ssl))
+ log.info("PID: %s" % os.getpid())
+
+ cherrypy.server.using_apache = supervisor.using_apache
+ cherrypy.server.using_wsgi = supervisor.using_wsgi
+
+ if sys.platform[:4] == 'java':
+ cherrypy.config.update({'server.nodelay': False})
+
+ if isinstance(conf, basestring):
+ parser = cherrypy.lib.reprconf.Parser()
+ conf = parser.dict_from_file(conf).get('global', {})
+ else:
+ conf = conf or {}
+ baseconf = conf.copy()
+ baseconf.update({'server.socket_host': supervisor.host,
+ 'server.socket_port': supervisor.port,
+ 'server.protocol_version': supervisor.protocol,
+ 'environment': "test_suite",
+ })
+ if supervisor.scheme == "https":
+ #baseconf['server.ssl_module'] = 'builtin'
+ baseconf['server.ssl_certificate'] = serverpem
+ baseconf['server.ssl_private_key'] = serverpem
+
+ # helper must be imported lazily so the coverage tool
+ # can run against module-level statements within cherrypy.
+ # Also, we have to do "from cherrypy.test import helper",
+ # exactly like each test module does, because a relative import
+ # would stick a second instance of webtest in sys.modules,
+ # and we wouldn't be able to globally override the port anymore.
+ if supervisor.scheme == "https":
+ webtest.WebCase.HTTP_CONN = HTTPSConnection
+ return baseconf
+ _setup_server = classmethod(_setup_server)
+
+ def setup_class(cls):
+ ''
+ #Creates a server
+ conf = get_tst_config()
+ supervisor_factory = cls.available_servers.get(conf.get('server', 'wsgi'))
+ if supervisor_factory is None:
+ raise RuntimeError('Unknown server in config: %s' % conf['server'])
+ supervisor = supervisor_factory(**conf)
+
+ #Copied from "run_test_suite"
+ cherrypy.config.reset()
+ baseconf = cls._setup_server(supervisor, conf)
+ cherrypy.config.update(baseconf)
+ setup_client()
+
+ if hasattr(cls, 'setup_server'):
+ # Clear the cherrypy tree and clear the wsgi server so that
+ # it can be updated with the new root
+ cherrypy.tree = cherrypy._cptree.Tree()
+ cherrypy.server.httpserver = None
+ cls.setup_server()
+ supervisor.start(cls.__module__)
+
+ cls.supervisor = supervisor
+ setup_class = classmethod(setup_class)
+
+ def teardown_class(cls):
+ ''
+ if hasattr(cls, 'setup_server'):
+ cls.supervisor.stop()
+ teardown_class = classmethod(teardown_class)
+
+ def prefix(self):
+ return self.script_name.rstrip("/")
+
+ def base(self):
+ if ((self.scheme == "http" and self.PORT == 80) or
+ (self.scheme == "https" and self.PORT == 443)):
+ port = ""
+ else:
+ port = ":%s" % self.PORT
+
+ return "%s://%s%s%s" % (self.scheme, self.HOST, port,
+ self.script_name.rstrip("/"))
+
+ def exit(self):
+ sys.exit()
+
+ def getPage(self, url, headers=None, method="GET", body=None, protocol=None):
+ """Open the url. Return status, headers, body."""
+ if self.script_name:
+ url = httputil.urljoin(self.script_name, url)
+ return webtest.WebCase.getPage(self, url, headers, method, body, protocol)
+
+ def skip(self, msg='skipped '):
+ raise nose.SkipTest(msg)
+
+ def assertErrorPage(self, status, message=None, pattern=''):
+ """Compare the response body with a built in error page.
+
+ The function will optionally look for the regexp pattern,
+ within the exception embedded in the error page."""
+
+ # This will never contain a traceback
+ page = cherrypy._cperror.get_error_page(status, message=message)
+
+ # First, test the response body without checking the traceback.
+ # Stick a match-all group (.*) in to grab the traceback.
+ esc = re.escape
+ epage = esc(page)
+ epage = epage.replace(esc('<pre id="traceback"></pre>'),
+ esc('<pre id="traceback">') + '(.*)' + esc('</pre>'))
+ m = re.match(ntob(epage, self.encoding), self.body, re.DOTALL)
+ if not m:
+ self._handlewebError('Error page does not match; expected:\n' + page)
+ return
+
+ # Now test the pattern against the traceback
+ if pattern is None:
+ # Special-case None to mean that there should be *no* traceback.
+ if m and m.group(1):
+ self._handlewebError('Error page contains traceback')
+ else:
+ if (m is None) or (
+ not re.search(ntob(re.escape(pattern), self.encoding),
+ m.group(1))):
+ msg = 'Error page does not contain %s in traceback'
+ self._handlewebError(msg % repr(pattern))
+
+ date_tolerance = 2
+
+ def assertEqualDates(self, dt1, dt2, seconds=None):
+ """Assert abs(dt1 - dt2) is within Y seconds."""
+ if seconds is None:
+ seconds = self.date_tolerance
+
+ if dt1 > dt2:
+ diff = dt1 - dt2
+ else:
+ diff = dt2 - dt1
+ if not diff < datetime.timedelta(seconds=seconds):
+ raise AssertionError('%r and %r are not within %r seconds.' %
+ (dt1, dt2, seconds))
+
+
+def setup_client():
+ """Set up the WebCase classes to match the server's socket settings."""
+ webtest.WebCase.PORT = cherrypy.server.socket_port
+ webtest.WebCase.HOST = cherrypy.server.socket_host
+ if cherrypy.server.ssl_certificate:
+ CPWebCase.scheme = 'https'
+
+# --------------------------- Spawning helpers --------------------------- #
+
+
+class CPProcess(object):
+
+ pid_file = os.path.join(thisdir, 'test.pid')
+ config_file = os.path.join(thisdir, 'test.conf')
+ config_template = """[global]
+server.socket_host: '%(host)s'
+server.socket_port: %(port)s
+checker.on: False
+log.screen: False
+log.error_file: r'%(error_log)s'
+log.access_file: r'%(access_log)s'
+%(ssl)s
+%(extra)s
+"""
+ error_log = os.path.join(thisdir, 'test.error.log')
+ access_log = os.path.join(thisdir, 'test.access.log')
+
+ def __init__(self, wait=False, daemonize=False, ssl=False, socket_host=None, socket_port=None):
+ self.wait = wait
+ self.daemonize = daemonize
+ self.ssl = ssl
+ self.host = socket_host or cherrypy.server.socket_host
+ self.port = socket_port or cherrypy.server.socket_port
+
+ def write_conf(self, extra=""):
+ if self.ssl:
+ serverpem = os.path.join(thisdir, 'test.pem')
+ ssl = """
+server.ssl_certificate: r'%s'
+server.ssl_private_key: r'%s'
+""" % (serverpem, serverpem)
+ else:
+ ssl = ""
+
+ conf = self.config_template % {
+ 'host': self.host,
+ 'port': self.port,
+ 'error_log': self.error_log,
+ 'access_log': self.access_log,
+ 'ssl': ssl,
+ 'extra': extra,
+ }
+ f = open(self.config_file, 'wb')
+ f.write(ntob(conf, 'utf-8'))
+ f.close()
+
+ def start(self, imports=None):
+ """Start cherryd in a subprocess."""
+ cherrypy._cpserver.wait_for_free_port(self.host, self.port)
+
+ args = [sys.executable, os.path.join(thisdir, '..', 'cherryd'),
+ '-c', self.config_file, '-p', self.pid_file]
+
+ if not isinstance(imports, (list, tuple)):
+ imports = [imports]
+ for i in imports:
+ if i:
+ args.append('-i')
+ args.append(i)
+
+ if self.daemonize:
+ args.append('-d')
+
+ env = os.environ.copy()
+ # Make sure we import the cherrypy package in which this module is defined.
+ grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..'))
+ if env.get('PYTHONPATH', ''):
+ env['PYTHONPATH'] = os.pathsep.join((grandparentdir, env['PYTHONPATH']))
+ else:
+ env['PYTHONPATH'] = grandparentdir
+ if self.wait:
+ self.exit_code = os.spawnve(os.P_WAIT, sys.executable, args, env)
+ else:
+ os.spawnve(os.P_NOWAIT, sys.executable, args, env)
+ cherrypy._cpserver.wait_for_occupied_port(self.host, self.port)
+
+ # Give the engine a wee bit more time to finish STARTING
+ if self.daemonize:
+ time.sleep(2)
+ else:
+ time.sleep(1)
+
+ def get_pid(self):
+ return int(open(self.pid_file, 'rb').read())
+
+ def join(self):
+ """Wait for the process to exit."""
+ try:
+ try:
+ # Mac, UNIX
+ os.wait()
+ except AttributeError:
+ # Windows
+ try:
+ pid = self.get_pid()
+ except IOError:
+ # Assume the subprocess deleted the pidfile on shutdown.
+ pass
+ else:
+ os.waitpid(pid, 0)
+ except OSError:
+ x = sys.exc_info()[1]
+ if x.args != (10, 'No child processes'):
+ raise
+
diff --git a/cherrypy/test/logtest.py b/cherrypy/test/logtest.py
new file mode 100755
index 0000000..c093da2
--- /dev/null
+++ b/cherrypy/test/logtest.py
@@ -0,0 +1,181 @@
+"""logtest, a unittest.TestCase helper for testing log output."""
+
+import sys
+import time
+
+import cherrypy
+
+
+try:
+ # On Windows, msvcrt.getch reads a single char without output.
+ import msvcrt
+ def getchar():
+ return msvcrt.getch()
+except ImportError:
+ # Unix getchr
+ import tty, termios
+ def getchar():
+ fd = sys.stdin.fileno()
+ old_settings = termios.tcgetattr(fd)
+ try:
+ tty.setraw(sys.stdin.fileno())
+ ch = sys.stdin.read(1)
+ finally:
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
+ return ch
+
+
+class LogCase(object):
+ """unittest.TestCase mixin for testing log messages.
+
+ logfile: a filename for the desired log. Yes, I know modes are evil,
+ but it makes the test functions so much cleaner to set this once.
+
+ lastmarker: the last marker in the log. This can be used to search for
+ messages since the last marker.
+
+ markerPrefix: a string with which to prefix log markers. This should be
+ unique enough from normal log output to use for marker identification.
+ """
+
+ logfile = None
+ lastmarker = None
+ markerPrefix = "test suite marker: "
+
+ def _handleLogError(self, msg, data, marker, pattern):
+ print("")
+ print(" ERROR: %s" % msg)
+
+ if not self.interactive:
+ raise self.failureException(msg)
+
+ p = " Show: [L]og [M]arker [P]attern; [I]gnore, [R]aise, or sys.e[X]it >> "
+ print p,
+ # ARGH
+ sys.stdout.flush()
+ while True:
+ i = getchar().upper()
+ if i not in "MPLIRX":
+ continue
+ print(i.upper()) # Also prints new line
+ if i == "L":
+ for x, line in enumerate(data):
+ if (x + 1) % self.console_height == 0:
+ # The \r and comma should make the next line overwrite
+ print "<-- More -->\r",
+ m = getchar().lower()
+ # Erase our "More" prompt
+ print " \r",
+ if m == "q":
+ break
+ print(line.rstrip())
+ elif i == "M":
+ print(repr(marker or self.lastmarker))
+ elif i == "P":
+ print(repr(pattern))
+ elif i == "I":
+ # return without raising the normal exception
+ return
+ elif i == "R":
+ raise self.failureException(msg)
+ elif i == "X":
+ self.exit()
+ print p,
+
+ def exit(self):
+ sys.exit()
+
+ def emptyLog(self):
+ """Overwrite self.logfile with 0 bytes."""
+ open(self.logfile, 'wb').write("")
+
+ def markLog(self, key=None):
+ """Insert a marker line into the log and set self.lastmarker."""
+ if key is None:
+ key = str(time.time())
+ self.lastmarker = key
+
+ open(self.logfile, 'ab+').write("%s%s\n" % (self.markerPrefix, key))
+
+ def _read_marked_region(self, marker=None):
+ """Return lines from self.logfile in the marked region.
+
+ If marker is None, self.lastmarker is used. If the log hasn't
+ been marked (using self.markLog), the entire log will be returned.
+ """
+## # Give the logger time to finish writing?
+## time.sleep(0.5)
+
+ logfile = self.logfile
+ marker = marker or self.lastmarker
+ if marker is None:
+ return open(logfile, 'rb').readlines()
+
+ data = []
+ in_region = False
+ for line in open(logfile, 'rb'):
+ if in_region:
+ if (line.startswith(self.markerPrefix) and not marker in line):
+ break
+ else:
+ data.append(line)
+ elif marker in line:
+ in_region = True
+ return data
+
+ def assertInLog(self, line, marker=None):
+ """Fail if the given (partial) line is not in the log.
+
+ The log will be searched from the given marker to the next marker.
+ If marker is None, self.lastmarker is used. If the log hasn't
+ been marked (using self.markLog), the entire log will be searched.
+ """
+ data = self._read_marked_region(marker)
+ for logline in data:
+ if line in logline:
+ return
+ msg = "%r not found in log" % line
+ self._handleLogError(msg, data, marker, line)
+
+ def assertNotInLog(self, line, marker=None):
+ """Fail if the given (partial) line is in the log.
+
+ The log will be searched from the given marker to the next marker.
+ If marker is None, self.lastmarker is used. If the log hasn't
+ been marked (using self.markLog), the entire log will be searched.
+ """
+ data = self._read_marked_region(marker)
+ for logline in data:
+ if line in logline:
+ msg = "%r found in log" % line
+ self._handleLogError(msg, data, marker, line)
+
+ def assertLog(self, sliceargs, lines, marker=None):
+ """Fail if log.readlines()[sliceargs] is not contained in 'lines'.
+
+ The log will be searched from the given marker to the next marker.
+ If marker is None, self.lastmarker is used. If the log hasn't
+ been marked (using self.markLog), the entire log will be searched.
+ """
+ data = self._read_marked_region(marker)
+ if isinstance(sliceargs, int):
+ # Single arg. Use __getitem__ and allow lines to be str or list.
+ if isinstance(lines, (tuple, list)):
+ lines = lines[0]
+ if lines not in data[sliceargs]:
+ msg = "%r not found on log line %r" % (lines, sliceargs)
+ self._handleLogError(msg, [data[sliceargs]], marker, lines)
+ else:
+ # Multiple args. Use __getslice__ and require lines to be list.
+ if isinstance(lines, tuple):
+ lines = list(lines)
+ elif isinstance(lines, basestring):
+ raise TypeError("The 'lines' arg must be a list when "
+ "'sliceargs' is a tuple.")
+
+ start, stop = sliceargs
+ for line, logline in zip(lines, data[start:stop]):
+ if line not in logline:
+ msg = "%r not found in log" % line
+ self._handleLogError(msg, data[start:stop], marker, line)
+
diff --git a/cherrypy/test/modfastcgi.py b/cherrypy/test/modfastcgi.py
new file mode 100755
index 0000000..95acf14
--- /dev/null
+++ b/cherrypy/test/modfastcgi.py
@@ -0,0 +1,135 @@
+"""Wrapper for mod_fastcgi, for use as a CherryPy HTTP server when testing.
+
+To autostart fastcgi, the "apache" executable or script must be
+on your system path, or you must override the global APACHE_PATH.
+On some platforms, "apache" may be called "apachectl", "apache2ctl",
+or "httpd"--create a symlink to them if needed.
+
+You'll also need the WSGIServer from flup.servers.
+See http://projects.amor.org/misc/wiki/ModPythonGateway
+
+
+KNOWN BUGS
+==========
+
+1. Apache processes Range headers automatically; CherryPy's truncated
+ output is then truncated again by Apache. See test_core.testRanges.
+ This was worked around in http://www.cherrypy.org/changeset/1319.
+2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
+ See test_core.testHTTPMethods.
+3. Max request header and body settings do not work with Apache.
+4. Apache replaces status "reason phrases" automatically. For example,
+ CherryPy may set "304 Not modified" but Apache will write out
+ "304 Not Modified" (capital "M").
+5. Apache does not allow custom error codes as per the spec.
+6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
+ Request-URI too early.
+7. mod_python will not read request bodies which use the "chunked"
+ transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
+ instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
+ mod_python's requestobject.c).
+8. Apache will output a "Content-Length: 0" response header even if there's
+ no response entity body. This isn't really a bug; it just differs from
+ the CherryPy default.
+"""
+
+import os
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+import re
+import sys
+import time
+
+import cherrypy
+from cherrypy.process import plugins, servers
+from cherrypy.test import helper
+
+
+def read_process(cmd, args=""):
+ pipein, pipeout = os.popen4("%s %s" % (cmd, args))
+ try:
+ firstline = pipeout.readline()
+ if (re.search(r"(not recognized|No such file|not found)", firstline,
+ re.IGNORECASE)):
+ raise IOError('%s must be on your system path.' % cmd)
+ output = firstline + pipeout.read()
+ finally:
+ pipeout.close()
+ return output
+
+
+APACHE_PATH = "apache2ctl"
+CONF_PATH = "fastcgi.conf"
+
+conf_fastcgi = """
+# Apache2 server conf file for testing CherryPy with mod_fastcgi.
+# fumanchu: I had to hard-code paths due to crazy Debian layouts :(
+ServerRoot /usr/lib/apache2
+User #1000
+ErrorLog %(root)s/mod_fastcgi.error.log
+
+DocumentRoot "%(root)s"
+ServerName 127.0.0.1
+Listen %(port)s
+LoadModule fastcgi_module modules/mod_fastcgi.so
+LoadModule rewrite_module modules/mod_rewrite.so
+
+Options +ExecCGI
+SetHandler fastcgi-script
+RewriteEngine On
+RewriteRule ^(.*)$ /fastcgi.pyc [L]
+FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
+"""
+
+def erase_script_name(environ, start_response):
+ environ['SCRIPT_NAME'] = ''
+ return cherrypy.tree(environ, start_response)
+
+class ModFCGISupervisor(helper.LocalWSGISupervisor):
+
+ httpserver_class = "cherrypy.process.servers.FlupFCGIServer"
+ using_apache = True
+ using_wsgi = True
+ template = conf_fastcgi
+
+ def __str__(self):
+ return "FCGI Server on %s:%s" % (self.host, self.port)
+
+ def start(self, modulename):
+ cherrypy.server.httpserver = servers.FlupFCGIServer(
+ application=erase_script_name, bindAddress=('127.0.0.1', 4000))
+ cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000)
+ cherrypy.server.socket_port = 4000
+ # For FCGI, we both start apache...
+ self.start_apache()
+ # ...and our local server
+ cherrypy.engine.start()
+ self.sync_apps()
+
+ def start_apache(self):
+ fcgiconf = CONF_PATH
+ if not os.path.isabs(fcgiconf):
+ fcgiconf = os.path.join(curdir, fcgiconf)
+
+ # Write the Apache conf file.
+ f = open(fcgiconf, 'wb')
+ try:
+ server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
+ output = self.template % {'port': self.port, 'root': curdir,
+ 'server': server}
+ output = output.replace('\r\n', '\n')
+ f.write(output)
+ finally:
+ f.close()
+
+ result = read_process(APACHE_PATH, "-k start -f %s" % fcgiconf)
+ if result:
+ print(result)
+
+ def stop(self):
+ """Gracefully shutdown a server that is serving forever."""
+ read_process(APACHE_PATH, "-k stop")
+ helper.LocalWSGISupervisor.stop(self)
+
+ def sync_apps(self):
+ cherrypy.server.httpserver.fcgiserver.application = self.get_app(erase_script_name)
+
diff --git a/cherrypy/test/modfcgid.py b/cherrypy/test/modfcgid.py
new file mode 100755
index 0000000..736aa4c
--- /dev/null
+++ b/cherrypy/test/modfcgid.py
@@ -0,0 +1,125 @@
+"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing.
+
+To autostart fcgid, the "apache" executable or script must be
+on your system path, or you must override the global APACHE_PATH.
+On some platforms, "apache" may be called "apachectl", "apache2ctl",
+or "httpd"--create a symlink to them if needed.
+
+You'll also need the WSGIServer from flup.servers.
+See http://projects.amor.org/misc/wiki/ModPythonGateway
+
+
+KNOWN BUGS
+==========
+
+1. Apache processes Range headers automatically; CherryPy's truncated
+ output is then truncated again by Apache. See test_core.testRanges.
+ This was worked around in http://www.cherrypy.org/changeset/1319.
+2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
+ See test_core.testHTTPMethods.
+3. Max request header and body settings do not work with Apache.
+4. Apache replaces status "reason phrases" automatically. For example,
+ CherryPy may set "304 Not modified" but Apache will write out
+ "304 Not Modified" (capital "M").
+5. Apache does not allow custom error codes as per the spec.
+6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
+ Request-URI too early.
+7. mod_python will not read request bodies which use the "chunked"
+ transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
+ instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
+ mod_python's requestobject.c).
+8. Apache will output a "Content-Length: 0" response header even if there's
+ no response entity body. This isn't really a bug; it just differs from
+ the CherryPy default.
+"""
+
+import os
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+import re
+import sys
+import time
+
+import cherrypy
+from cherrypy._cpcompat import ntob
+from cherrypy.process import plugins, servers
+from cherrypy.test import helper
+
+
+def read_process(cmd, args=""):
+ pipein, pipeout = os.popen4("%s %s" % (cmd, args))
+ try:
+ firstline = pipeout.readline()
+ if (re.search(r"(not recognized|No such file|not found)", firstline,
+ re.IGNORECASE)):
+ raise IOError('%s must be on your system path.' % cmd)
+ output = firstline + pipeout.read()
+ finally:
+ pipeout.close()
+ return output
+
+
+APACHE_PATH = "httpd"
+CONF_PATH = "fcgi.conf"
+
+conf_fcgid = """
+# Apache2 server conf file for testing CherryPy with mod_fcgid.
+
+DocumentRoot "%(root)s"
+ServerName 127.0.0.1
+Listen %(port)s
+LoadModule fastcgi_module modules/mod_fastcgi.dll
+LoadModule rewrite_module modules/mod_rewrite.so
+
+Options ExecCGI
+SetHandler fastcgi-script
+RewriteEngine On
+RewriteRule ^(.*)$ /fastcgi.pyc [L]
+FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
+"""
+
+class ModFCGISupervisor(helper.LocalSupervisor):
+
+ using_apache = True
+ using_wsgi = True
+ template = conf_fcgid
+
+ def __str__(self):
+ return "FCGI Server on %s:%s" % (self.host, self.port)
+
+ def start(self, modulename):
+ cherrypy.server.httpserver = servers.FlupFCGIServer(
+ application=cherrypy.tree, bindAddress=('127.0.0.1', 4000))
+ cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000)
+ # For FCGI, we both start apache...
+ self.start_apache()
+ # ...and our local server
+ helper.LocalServer.start(self, modulename)
+
+ def start_apache(self):
+ fcgiconf = CONF_PATH
+ if not os.path.isabs(fcgiconf):
+ fcgiconf = os.path.join(curdir, fcgiconf)
+
+ # Write the Apache conf file.
+ f = open(fcgiconf, 'wb')
+ try:
+ server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
+ output = self.template % {'port': self.port, 'root': curdir,
+ 'server': server}
+ output = ntob(output.replace('\r\n', '\n'))
+ f.write(output)
+ finally:
+ f.close()
+
+ result = read_process(APACHE_PATH, "-k start -f %s" % fcgiconf)
+ if result:
+ print(result)
+
+ def stop(self):
+ """Gracefully shutdown a server that is serving forever."""
+ read_process(APACHE_PATH, "-k stop")
+ helper.LocalServer.stop(self)
+
+ def sync_apps(self):
+ cherrypy.server.httpserver.fcgiserver.application = self.get_app()
+
diff --git a/cherrypy/test/modpy.py b/cherrypy/test/modpy.py
new file mode 100755
index 0000000..519571f
--- /dev/null
+++ b/cherrypy/test/modpy.py
@@ -0,0 +1,163 @@
+"""Wrapper for mod_python, for use as a CherryPy HTTP server when testing.
+
+To autostart modpython, the "apache" executable or script must be
+on your system path, or you must override the global APACHE_PATH.
+On some platforms, "apache" may be called "apachectl" or "apache2ctl"--
+create a symlink to them if needed.
+
+If you wish to test the WSGI interface instead of our _cpmodpy interface,
+you also need the 'modpython_gateway' module at:
+http://projects.amor.org/misc/wiki/ModPythonGateway
+
+
+KNOWN BUGS
+==========
+
+1. Apache processes Range headers automatically; CherryPy's truncated
+ output is then truncated again by Apache. See test_core.testRanges.
+ This was worked around in http://www.cherrypy.org/changeset/1319.
+2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
+ See test_core.testHTTPMethods.
+3. Max request header and body settings do not work with Apache.
+4. Apache replaces status "reason phrases" automatically. For example,
+ CherryPy may set "304 Not modified" but Apache will write out
+ "304 Not Modified" (capital "M").
+5. Apache does not allow custom error codes as per the spec.
+6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
+ Request-URI too early.
+7. mod_python will not read request bodies which use the "chunked"
+ transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
+ instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
+ mod_python's requestobject.c).
+8. Apache will output a "Content-Length: 0" response header even if there's
+ no response entity body. This isn't really a bug; it just differs from
+ the CherryPy default.
+"""
+
+import os
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+import re
+import time
+
+from cherrypy.test import helper
+
+
+def read_process(cmd, args=""):
+ pipein, pipeout = os.popen4("%s %s" % (cmd, args))
+ try:
+ firstline = pipeout.readline()
+ if (re.search(r"(not recognized|No such file|not found)", firstline,
+ re.IGNORECASE)):
+ raise IOError('%s must be on your system path.' % cmd)
+ output = firstline + pipeout.read()
+ finally:
+ pipeout.close()
+ return output
+
+
+APACHE_PATH = "httpd"
+CONF_PATH = "test_mp.conf"
+
+conf_modpython_gateway = """
+# Apache2 server conf file for testing CherryPy with modpython_gateway.
+
+ServerName 127.0.0.1
+DocumentRoot "/"
+Listen %(port)s
+LoadModule python_module modules/mod_python.so
+
+SetHandler python-program
+PythonFixupHandler cherrypy.test.modpy::wsgisetup
+PythonOption testmod %(modulename)s
+PythonHandler modpython_gateway::handler
+PythonOption wsgi.application cherrypy::tree
+PythonOption socket_host %(host)s
+PythonDebug On
+"""
+
+conf_cpmodpy = """
+# Apache2 server conf file for testing CherryPy with _cpmodpy.
+
+ServerName 127.0.0.1
+DocumentRoot "/"
+Listen %(port)s
+LoadModule python_module modules/mod_python.so
+
+SetHandler python-program
+PythonFixupHandler cherrypy.test.modpy::cpmodpysetup
+PythonHandler cherrypy._cpmodpy::handler
+PythonOption cherrypy.setup cherrypy.test.%(modulename)s::setup_server
+PythonOption socket_host %(host)s
+PythonDebug On
+"""
+
+class ModPythonSupervisor(helper.Supervisor):
+
+ using_apache = True
+ using_wsgi = False
+ template = None
+
+ def __str__(self):
+ return "ModPython Server on %s:%s" % (self.host, self.port)
+
+ def start(self, modulename):
+ mpconf = CONF_PATH
+ if not os.path.isabs(mpconf):
+ mpconf = os.path.join(curdir, mpconf)
+
+ f = open(mpconf, 'wb')
+ try:
+ f.write(self.template %
+ {'port': self.port, 'modulename': modulename,
+ 'host': self.host})
+ finally:
+ f.close()
+
+ result = read_process(APACHE_PATH, "-k start -f %s" % mpconf)
+ if result:
+ print(result)
+
+ def stop(self):
+ """Gracefully shutdown a server that is serving forever."""
+ read_process(APACHE_PATH, "-k stop")
+
+
+loaded = False
+def wsgisetup(req):
+ global loaded
+ if not loaded:
+ loaded = True
+ options = req.get_options()
+
+ import cherrypy
+ cherrypy.config.update({
+ "log.error_file": os.path.join(curdir, "test.log"),
+ "environment": "test_suite",
+ "server.socket_host": options['socket_host'],
+ })
+
+ modname = options['testmod']
+ mod = __import__(modname, globals(), locals(), [''])
+ mod.setup_server()
+
+ cherrypy.server.unsubscribe()
+ cherrypy.engine.start()
+ from mod_python import apache
+ return apache.OK
+
+
+def cpmodpysetup(req):
+ global loaded
+ if not loaded:
+ loaded = True
+ options = req.get_options()
+
+ import cherrypy
+ cherrypy.config.update({
+ "log.error_file": os.path.join(curdir, "test.log"),
+ "environment": "test_suite",
+ "server.socket_host": options['socket_host'],
+ })
+ from mod_python import apache
+ return apache.OK
+
diff --git a/cherrypy/test/modwsgi.py b/cherrypy/test/modwsgi.py
new file mode 100755
index 0000000..309a541
--- /dev/null
+++ b/cherrypy/test/modwsgi.py
@@ -0,0 +1,148 @@
+"""Wrapper for mod_wsgi, for use as a CherryPy HTTP server.
+
+To autostart modwsgi, the "apache" executable or script must be
+on your system path, or you must override the global APACHE_PATH.
+On some platforms, "apache" may be called "apachectl" or "apache2ctl"--
+create a symlink to them if needed.
+
+
+KNOWN BUGS
+==========
+
+##1. Apache processes Range headers automatically; CherryPy's truncated
+## output is then truncated again by Apache. See test_core.testRanges.
+## This was worked around in http://www.cherrypy.org/changeset/1319.
+2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
+ See test_core.testHTTPMethods.
+3. Max request header and body settings do not work with Apache.
+##4. Apache replaces status "reason phrases" automatically. For example,
+## CherryPy may set "304 Not modified" but Apache will write out
+## "304 Not Modified" (capital "M").
+##5. Apache does not allow custom error codes as per the spec.
+##6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
+## Request-URI too early.
+7. mod_wsgi will not read request bodies which use the "chunked"
+ transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
+ instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
+ mod_python's requestobject.c).
+8. When responding with 204 No Content, mod_wsgi adds a Content-Length
+ header for you.
+9. When an error is raised, mod_wsgi has no facility for printing a
+ traceback as the response content (it's sent to the Apache log instead).
+10. Startup and shutdown of Apache when running mod_wsgi seems slow.
+"""
+
+import os
+curdir = os.path.abspath(os.path.dirname(__file__))
+import re
+import sys
+import time
+
+import cherrypy
+from cherrypy.test import helper, webtest
+
+
+def read_process(cmd, args=""):
+ pipein, pipeout = os.popen4("%s %s" % (cmd, args))
+ try:
+ firstline = pipeout.readline()
+ if (re.search(r"(not recognized|No such file|not found)", firstline,
+ re.IGNORECASE)):
+ raise IOError('%s must be on your system path.' % cmd)
+ output = firstline + pipeout.read()
+ finally:
+ pipeout.close()
+ return output
+
+
+if sys.platform == 'win32':
+ APACHE_PATH = "httpd"
+else:
+ APACHE_PATH = "apache"
+
+CONF_PATH = "test_mw.conf"
+
+conf_modwsgi = r"""
+# Apache2 server conf file for testing CherryPy with modpython_gateway.
+
+ServerName 127.0.0.1
+DocumentRoot "/"
+Listen %(port)s
+
+AllowEncodedSlashes On
+LoadModule rewrite_module modules/mod_rewrite.so
+RewriteEngine on
+RewriteMap escaping int:escape
+
+LoadModule log_config_module modules/mod_log_config.so
+LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined
+CustomLog "%(curdir)s/apache.access.log" combined
+ErrorLog "%(curdir)s/apache.error.log"
+LogLevel debug
+
+LoadModule wsgi_module modules/mod_wsgi.so
+LoadModule env_module modules/mod_env.so
+
+WSGIScriptAlias / "%(curdir)s/modwsgi.py"
+SetEnv testmod %(testmod)s
+"""
+
+
+class ModWSGISupervisor(helper.Supervisor):
+ """Server Controller for ModWSGI and CherryPy."""
+
+ using_apache = True
+ using_wsgi = True
+ template=conf_modwsgi
+
+ def __str__(self):
+ return "ModWSGI Server on %s:%s" % (self.host, self.port)
+
+ def start(self, modulename):
+ mpconf = CONF_PATH
+ if not os.path.isabs(mpconf):
+ mpconf = os.path.join(curdir, mpconf)
+
+ f = open(mpconf, 'wb')
+ try:
+ output = (self.template %
+ {'port': self.port, 'testmod': modulename,
+ 'curdir': curdir})
+ f.write(output)
+ finally:
+ f.close()
+
+ result = read_process(APACHE_PATH, "-k start -f %s" % mpconf)
+ if result:
+ print(result)
+
+ # Make a request so mod_wsgi starts up our app.
+ # If we don't, concurrent initial requests will 404.
+ cherrypy._cpserver.wait_for_occupied_port("127.0.0.1", self.port)
+ webtest.openURL('/ihopetheresnodefault', port=self.port)
+ time.sleep(1)
+
+ def stop(self):
+ """Gracefully shutdown a server that is serving forever."""
+ read_process(APACHE_PATH, "-k stop")
+
+
+loaded = False
+def application(environ, start_response):
+ import cherrypy
+ global loaded
+ if not loaded:
+ loaded = True
+ modname = "cherrypy.test." + environ['testmod']
+ mod = __import__(modname, globals(), locals(), [''])
+ mod.setup_server()
+
+ cherrypy.config.update({
+ "log.error_file": os.path.join(curdir, "test.error.log"),
+ "log.access_file": os.path.join(curdir, "test.access.log"),
+ "environment": "test_suite",
+ "engine.SIGHUP": None,
+ "engine.SIGTERM": None,
+ })
+ return cherrypy.tree(environ, start_response)
+
diff --git a/cherrypy/test/sessiondemo.py b/cherrypy/test/sessiondemo.py
new file mode 100755
index 0000000..342e5b5
--- /dev/null
+++ b/cherrypy/test/sessiondemo.py
@@ -0,0 +1,153 @@
+#!/usr/bin/python
+"""A session demonstration app."""
+
+import calendar
+from datetime import datetime
+import sys
+import cherrypy
+from cherrypy.lib import sessions
+from cherrypy._cpcompat import copyitems
+
+
+page = """
+<html>
+<head>
+<style type='text/css'>
+table { border-collapse: collapse; border: 1px solid #663333; }
+th { text-align: right; background-color: #663333; color: white; padding: 0.5em; }
+td { white-space: pre-wrap; font-family: monospace; padding: 0.5em;
+ border: 1px solid #663333; }
+.warn { font-family: serif; color: #990000; }
+</style>
+<script type="text/javascript">
+<!--
+function twodigit(d) { return d < 10 ? "0" + d : d; }
+function formattime(t) {
+ var month = t.getUTCMonth() + 1;
+ var day = t.getUTCDate();
+ var year = t.getUTCFullYear();
+ var hours = t.getUTCHours();
+ var minutes = t.getUTCMinutes();
+ return (year + "/" + twodigit(month) + "/" + twodigit(day) + " " +
+ hours + ":" + twodigit(minutes) + " UTC");
+}
+
+function interval(s) {
+ // Return the given interval (in seconds) as an English phrase
+ var seconds = s %% 60;
+ s = Math.floor(s / 60);
+ var minutes = s %% 60;
+ s = Math.floor(s / 60);
+ var hours = s %% 24;
+ var v = twodigit(hours) + ":" + twodigit(minutes) + ":" + twodigit(seconds);
+ var days = Math.floor(s / 24);
+ if (days != 0) v = days + ' days, ' + v;
+ return v;
+}
+
+var fudge_seconds = 5;
+
+function init() {
+ // Set the content of the 'btime' cell.
+ var currentTime = new Date();
+ var bunixtime = Math.floor(currentTime.getTime() / 1000);
+
+ var v = formattime(currentTime);
+ v += " (Unix time: " + bunixtime + ")";
+
+ var diff = Math.abs(%(serverunixtime)s - bunixtime);
+ if (diff > fudge_seconds) v += "<p class='warn'>Browser and Server times disagree.</p>";
+
+ document.getElementById('btime').innerHTML = v;
+
+ // Warn if response cookie expires is not close to one hour in the future.
+ // Yes, we want this to happen when wit hit the 'Expire' link, too.
+ var expires = Date.parse("%(expires)s") / 1000;
+ var onehour = (60 * 60);
+ if (Math.abs(expires - (bunixtime + onehour)) > fudge_seconds) {
+ diff = Math.floor(expires - bunixtime);
+ if (expires > (bunixtime + onehour)) {
+ var msg = "Response cookie 'expires' date is " + interval(diff) + " in the future.";
+ } else {
+ var msg = "Response cookie 'expires' date is " + interval(0 - diff) + " in the past.";
+ }
+ document.getElementById('respcookiewarn').innerHTML = msg;
+ }
+}
+//-->
+</script>
+</head>
+
+<body onload='init()'>
+<h2>Session Demo</h2>
+<p>Reload this page. The session ID should not change from one reload to the next</p>
+<p><a href='../'>Index</a> | <a href='expire'>Expire</a> | <a href='regen'>Regenerate</a></p>
+<table>
+ <tr><th>Session ID:</th><td>%(sessionid)s<p class='warn'>%(changemsg)s</p></td></tr>
+ <tr><th>Request Cookie</th><td>%(reqcookie)s</td></tr>
+ <tr><th>Response Cookie</th><td>%(respcookie)s<p id='respcookiewarn' class='warn'></p></td></tr>
+ <tr><th>Session Data</th><td>%(sessiondata)s</td></tr>
+ <tr><th>Server Time</th><td id='stime'>%(servertime)s (Unix time: %(serverunixtime)s)</td></tr>
+ <tr><th>Browser Time</th><td id='btime'>&nbsp;</td></tr>
+ <tr><th>Cherrypy Version:</th><td>%(cpversion)s</td></tr>
+ <tr><th>Python Version:</th><td>%(pyversion)s</td></tr>
+</table>
+</body></html>
+"""
+
+class Root(object):
+
+ def page(self):
+ changemsg = []
+ if cherrypy.session.id != cherrypy.session.originalid:
+ if cherrypy.session.originalid is None:
+ changemsg.append('Created new session because no session id was given.')
+ if cherrypy.session.missing:
+ changemsg.append('Created new session due to missing (expired or malicious) session.')
+ if cherrypy.session.regenerated:
+ changemsg.append('Application generated a new session.')
+
+ try:
+ expires = cherrypy.response.cookie['session_id']['expires']
+ except KeyError:
+ expires = ''
+
+ return page % {
+ 'sessionid': cherrypy.session.id,
+ 'changemsg': '<br>'.join(changemsg),
+ 'respcookie': cherrypy.response.cookie.output(),
+ 'reqcookie': cherrypy.request.cookie.output(),
+ 'sessiondata': copyitems(cherrypy.session),
+ 'servertime': datetime.utcnow().strftime("%Y/%m/%d %H:%M") + " UTC",
+ 'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()),
+ 'cpversion': cherrypy.__version__,
+ 'pyversion': sys.version,
+ 'expires': expires,
+ }
+
+ def index(self):
+ # Must modify data or the session will not be saved.
+ cherrypy.session['color'] = 'green'
+ return self.page()
+ index.exposed = True
+
+ def expire(self):
+ sessions.expire()
+ return self.page()
+ expire.exposed = True
+
+ def regen(self):
+ cherrypy.session.regenerate()
+ # Must modify data or the session will not be saved.
+ cherrypy.session['color'] = 'yellow'
+ return self.page()
+ regen.exposed = True
+
+if __name__ == '__main__':
+ cherrypy.config.update({
+ #'environment': 'production',
+ 'log.screen': True,
+ 'tools.sessions.on': True,
+ })
+ cherrypy.quickstart(Root())
+
diff --git a/cherrypy/test/static/dirback.jpg b/cherrypy/test/static/dirback.jpg
new file mode 100644
index 0000000..530e6d6
--- /dev/null
+++ b/cherrypy/test/static/dirback.jpg
Binary files differ
diff --git a/cherrypy/test/static/index.html b/cherrypy/test/static/index.html
new file mode 100644
index 0000000..b9f5f09
--- /dev/null
+++ b/cherrypy/test/static/index.html
@@ -0,0 +1 @@
+Hello, world
diff --git a/cherrypy/test/style.css b/cherrypy/test/style.css
new file mode 100644
index 0000000..b266e93
--- /dev/null
+++ b/cherrypy/test/style.css
@@ -0,0 +1 @@
+Dummy stylesheet
diff --git a/cherrypy/test/test.pem b/cherrypy/test/test.pem
new file mode 100644
index 0000000..47a4704
--- /dev/null
+++ b/cherrypy/test/test.pem
@@ -0,0 +1,38 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ
+R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn
+da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB
+AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj
+9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT
+enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18
+8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8
+tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i
+0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR
+MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB
+yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb
+8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5
+yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ=
+-----END RSA PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD
+VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv
+MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW
+MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy
+cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG
+A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn
+bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx
+FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl
+cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A
+ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M
+C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg
+KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ
+2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ
+/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
+YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0
+MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G
+CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME
+BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S
+8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2
+D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T
+NluCaWQys3MS
+-----END CERTIFICATE-----
diff --git a/cherrypy/test/test_auth_basic.py b/cherrypy/test/test_auth_basic.py
new file mode 100755
index 0000000..3a9781d
--- /dev/null
+++ b/cherrypy/test/test_auth_basic.py
@@ -0,0 +1,79 @@
+# This file is part of CherryPy <http://www.cherrypy.org/>
+# -*- coding: utf-8 -*-
+# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
+
+import cherrypy
+from cherrypy._cpcompat import md5, ntob
+from cherrypy.lib import auth_basic
+from cherrypy.test import helper
+
+
+class BasicAuthTest(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def index(self):
+ return "This is public."
+ index.exposed = True
+
+ class BasicProtected:
+ def index(self):
+ return "Hello %s, you've been authorized." % cherrypy.request.login
+ index.exposed = True
+
+ class BasicProtected2:
+ def index(self):
+ return "Hello %s, you've been authorized." % cherrypy.request.login
+ index.exposed = True
+
+ userpassdict = {'xuser' : 'xpassword'}
+ userhashdict = {'xuser' : md5(ntob('xpassword')).hexdigest()}
+
+ def checkpasshash(realm, user, password):
+ p = userhashdict.get(user)
+ return p and p == md5(ntob(password)).hexdigest() or False
+
+ conf = {'/basic': {'tools.auth_basic.on': True,
+ 'tools.auth_basic.realm': 'wonderland',
+ 'tools.auth_basic.checkpassword': auth_basic.checkpassword_dict(userpassdict)},
+ '/basic2': {'tools.auth_basic.on': True,
+ 'tools.auth_basic.realm': 'wonderland',
+ 'tools.auth_basic.checkpassword': checkpasshash},
+ }
+
+ root = Root()
+ root.basic = BasicProtected()
+ root.basic2 = BasicProtected2()
+ cherrypy.tree.mount(root, config=conf)
+ setup_server = staticmethod(setup_server)
+
+ def testPublic(self):
+ self.getPage("/")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'text/html;charset=utf-8')
+ self.assertBody('This is public.')
+
+ def testBasic(self):
+ self.getPage("/basic/")
+ self.assertStatus(401)
+ self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"')
+
+ self.getPage('/basic/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
+ self.assertStatus(401)
+
+ self.getPage('/basic/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
+ self.assertStatus('200 OK')
+ self.assertBody("Hello xuser, you've been authorized.")
+
+ def testBasic2(self):
+ self.getPage("/basic2/")
+ self.assertStatus(401)
+ self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"')
+
+ self.getPage('/basic2/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
+ self.assertStatus(401)
+
+ self.getPage('/basic2/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
+ self.assertStatus('200 OK')
+ self.assertBody("Hello xuser, you've been authorized.")
+
diff --git a/cherrypy/test/test_auth_digest.py b/cherrypy/test/test_auth_digest.py
new file mode 100755
index 0000000..1960fa8
--- /dev/null
+++ b/cherrypy/test/test_auth_digest.py
@@ -0,0 +1,115 @@
+# This file is part of CherryPy <http://www.cherrypy.org/>
+# -*- coding: utf-8 -*-
+# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
+
+
+import cherrypy
+from cherrypy.lib import auth_digest
+
+from cherrypy.test import helper
+
+class DigestAuthTest(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def index(self):
+ return "This is public."
+ index.exposed = True
+
+ class DigestProtected:
+ def index(self):
+ return "Hello %s, you've been authorized." % cherrypy.request.login
+ index.exposed = True
+
+ def fetch_users():
+ return {'test': 'test'}
+
+
+ get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(fetch_users())
+ conf = {'/digest': {'tools.auth_digest.on': True,
+ 'tools.auth_digest.realm': 'localhost',
+ 'tools.auth_digest.get_ha1': get_ha1,
+ 'tools.auth_digest.key': 'a565c27146791cfb',
+ 'tools.auth_digest.debug': 'True'}}
+
+ root = Root()
+ root.digest = DigestProtected()
+ cherrypy.tree.mount(root, config=conf)
+ setup_server = staticmethod(setup_server)
+
+ def testPublic(self):
+ self.getPage("/")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'text/html;charset=utf-8')
+ self.assertBody('This is public.')
+
+ def testDigest(self):
+ self.getPage("/digest/")
+ self.assertStatus(401)
+
+ value = None
+ for k, v in self.headers:
+ if k.lower() == "www-authenticate":
+ if v.startswith("Digest"):
+ value = v
+ break
+
+ if value is None:
+ self._handlewebError("Digest authentification scheme was not found")
+
+ value = value[7:]
+ items = value.split(', ')
+ tokens = {}
+ for item in items:
+ key, value = item.split('=')
+ tokens[key.lower()] = value
+
+ missing_msg = "%s is missing"
+ bad_value_msg = "'%s' was expecting '%s' but found '%s'"
+ nonce = None
+ if 'realm' not in tokens:
+ self._handlewebError(missing_msg % 'realm')
+ elif tokens['realm'] != '"localhost"':
+ self._handlewebError(bad_value_msg % ('realm', '"localhost"', tokens['realm']))
+ if 'nonce' not in tokens:
+ self._handlewebError(missing_msg % 'nonce')
+ else:
+ nonce = tokens['nonce'].strip('"')
+ if 'algorithm' not in tokens:
+ self._handlewebError(missing_msg % 'algorithm')
+ elif tokens['algorithm'] != '"MD5"':
+ self._handlewebError(bad_value_msg % ('algorithm', '"MD5"', tokens['algorithm']))
+ if 'qop' not in tokens:
+ self._handlewebError(missing_msg % 'qop')
+ elif tokens['qop'] != '"auth"':
+ self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop']))
+
+ get_ha1 = auth_digest.get_ha1_dict_plain({'test' : 'test'})
+
+ # Test user agent response with a wrong value for 'realm'
+ base_auth = 'Digest username="test", realm="wrong realm", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
+
+ auth_header = base_auth % (nonce, '11111111111111111111111111111111', '00000001')
+ auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
+ # calculate the response digest
+ ha1 = get_ha1(auth.realm, 'test')
+ response = auth.request_digest(ha1)
+ # send response with correct response digest, but wrong realm
+ auth_header = base_auth % (nonce, response, '00000001')
+ self.getPage('/digest/', [('Authorization', auth_header)])
+ self.assertStatus(401)
+
+ # Test that must pass
+ base_auth = 'Digest username="test", realm="localhost", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
+
+ auth_header = base_auth % (nonce, '11111111111111111111111111111111', '00000001')
+ auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
+ # calculate the response digest
+ ha1 = get_ha1('localhost', 'test')
+ response = auth.request_digest(ha1)
+ # send response with correct response digest
+ auth_header = base_auth % (nonce, response, '00000001')
+ self.getPage('/digest/', [('Authorization', auth_header)])
+ self.assertStatus('200 OK')
+ self.assertBody("Hello test, you've been authorized.")
+
diff --git a/cherrypy/test/test_bus.py b/cherrypy/test/test_bus.py
new file mode 100755
index 0000000..51c1022
--- /dev/null
+++ b/cherrypy/test/test_bus.py
@@ -0,0 +1,263 @@
+import threading
+import time
+import unittest
+
+import cherrypy
+from cherrypy._cpcompat import get_daemon, set
+from cherrypy.process import wspbus
+
+
+msg = "Listener %d on channel %s: %s."
+
+
+class PublishSubscribeTests(unittest.TestCase):
+
+ def get_listener(self, channel, index):
+ def listener(arg=None):
+ self.responses.append(msg % (index, channel, arg))
+ return listener
+
+ def test_builtin_channels(self):
+ b = wspbus.Bus()
+
+ self.responses, expected = [], []
+
+ for channel in b.listeners:
+ for index, priority in enumerate([100, 50, 0, 51]):
+ b.subscribe(channel, self.get_listener(channel, index), priority)
+
+ for channel in b.listeners:
+ b.publish(channel)
+ expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)])
+ b.publish(channel, arg=79347)
+ expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)])
+
+ self.assertEqual(self.responses, expected)
+
+ def test_custom_channels(self):
+ b = wspbus.Bus()
+
+ self.responses, expected = [], []
+
+ custom_listeners = ('hugh', 'louis', 'dewey')
+ for channel in custom_listeners:
+ for index, priority in enumerate([None, 10, 60, 40]):
+ b.subscribe(channel, self.get_listener(channel, index), priority)
+
+ for channel in custom_listeners:
+ b.publish(channel, 'ah so')
+ expected.extend([msg % (i, channel, 'ah so') for i in (1, 3, 0, 2)])
+ b.publish(channel)
+ expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)])
+
+ self.assertEqual(self.responses, expected)
+
+ def test_listener_errors(self):
+ b = wspbus.Bus()
+
+ self.responses, expected = [], []
+ channels = [c for c in b.listeners if c != 'log']
+
+ for channel in channels:
+ b.subscribe(channel, self.get_listener(channel, 1))
+ # This will break since the lambda takes no args.
+ b.subscribe(channel, lambda: None, priority=20)
+
+ for channel in channels:
+ self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123)
+ expected.append(msg % (1, channel, 123))
+
+ self.assertEqual(self.responses, expected)
+
+
+class BusMethodTests(unittest.TestCase):
+
+ def log(self, bus):
+ self._log_entries = []
+ def logit(msg, level):
+ self._log_entries.append(msg)
+ bus.subscribe('log', logit)
+
+ def assertLog(self, entries):
+ self.assertEqual(self._log_entries, entries)
+
+ def get_listener(self, channel, index):
+ def listener(arg=None):
+ self.responses.append(msg % (index, channel, arg))
+ return listener
+
+ def test_start(self):
+ b = wspbus.Bus()
+ self.log(b)
+
+ self.responses = []
+ num = 3
+ for index in range(num):
+ b.subscribe('start', self.get_listener('start', index))
+
+ b.start()
+ try:
+ # The start method MUST call all 'start' listeners.
+ self.assertEqual(set(self.responses),
+ set([msg % (i, 'start', None) for i in range(num)]))
+ # The start method MUST move the state to STARTED
+ # (or EXITING, if errors occur)
+ self.assertEqual(b.state, b.states.STARTED)
+ # The start method MUST log its states.
+ self.assertLog(['Bus STARTING', 'Bus STARTED'])
+ finally:
+ # Exit so the atexit handler doesn't complain.
+ b.exit()
+
+ def test_stop(self):
+ b = wspbus.Bus()
+ self.log(b)
+
+ self.responses = []
+ num = 3
+ for index in range(num):
+ b.subscribe('stop', self.get_listener('stop', index))
+
+ b.stop()
+
+ # The stop method MUST call all 'stop' listeners.
+ self.assertEqual(set(self.responses),
+ set([msg % (i, 'stop', None) for i in range(num)]))
+ # The stop method MUST move the state to STOPPED
+ self.assertEqual(b.state, b.states.STOPPED)
+ # The stop method MUST log its states.
+ self.assertLog(['Bus STOPPING', 'Bus STOPPED'])
+
+ def test_graceful(self):
+ b = wspbus.Bus()
+ self.log(b)
+
+ self.responses = []
+ num = 3
+ for index in range(num):
+ b.subscribe('graceful', self.get_listener('graceful', index))
+
+ b.graceful()
+
+ # The graceful method MUST call all 'graceful' listeners.
+ self.assertEqual(set(self.responses),
+ set([msg % (i, 'graceful', None) for i in range(num)]))
+ # The graceful method MUST log its states.
+ self.assertLog(['Bus graceful'])
+
+ def test_exit(self):
+ b = wspbus.Bus()
+ self.log(b)
+
+ self.responses = []
+ num = 3
+ for index in range(num):
+ b.subscribe('stop', self.get_listener('stop', index))
+ b.subscribe('exit', self.get_listener('exit', index))
+
+ b.exit()
+
+ # The exit method MUST call all 'stop' listeners,
+ # and then all 'exit' listeners.
+ self.assertEqual(set(self.responses),
+ set([msg % (i, 'stop', None) for i in range(num)] +
+ [msg % (i, 'exit', None) for i in range(num)]))
+ # The exit method MUST move the state to EXITING
+ self.assertEqual(b.state, b.states.EXITING)
+ # The exit method MUST log its states.
+ self.assertLog(['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED'])
+
+ def test_wait(self):
+ b = wspbus.Bus()
+
+ def f(method):
+ time.sleep(0.2)
+ getattr(b, method)()
+
+ for method, states in [('start', [b.states.STARTED]),
+ ('stop', [b.states.STOPPED]),
+ ('start', [b.states.STARTING, b.states.STARTED]),
+ ('exit', [b.states.EXITING]),
+ ]:
+ threading.Thread(target=f, args=(method,)).start()
+ b.wait(states)
+
+ # The wait method MUST wait for the given state(s).
+ if b.state not in states:
+ self.fail("State %r not in %r" % (b.state, states))
+
+ def test_block(self):
+ b = wspbus.Bus()
+ self.log(b)
+
+ def f():
+ time.sleep(0.2)
+ b.exit()
+ def g():
+ time.sleep(0.4)
+ threading.Thread(target=f).start()
+ threading.Thread(target=g).start()
+ threads = [t for t in threading.enumerate() if not get_daemon(t)]
+ self.assertEqual(len(threads), 3)
+
+ b.block()
+
+ # The block method MUST wait for the EXITING state.
+ self.assertEqual(b.state, b.states.EXITING)
+ # The block method MUST wait for ALL non-main, non-daemon threads to finish.
+ threads = [t for t in threading.enumerate() if not get_daemon(t)]
+ self.assertEqual(len(threads), 1)
+ # The last message will mention an indeterminable thread name; ignore it
+ self.assertEqual(self._log_entries[:-1],
+ ['Bus STOPPING', 'Bus STOPPED',
+ 'Bus EXITING', 'Bus EXITED',
+ 'Waiting for child threads to terminate...'])
+
+ def test_start_with_callback(self):
+ b = wspbus.Bus()
+ self.log(b)
+ try:
+ events = []
+ def f(*args, **kwargs):
+ events.append(("f", args, kwargs))
+ def g():
+ events.append("g")
+ b.subscribe("start", g)
+ b.start_with_callback(f, (1, 3, 5), {"foo": "bar"})
+ # Give wait() time to run f()
+ time.sleep(0.2)
+
+ # The callback method MUST wait for the STARTED state.
+ self.assertEqual(b.state, b.states.STARTED)
+ # The callback method MUST run after all start methods.
+ self.assertEqual(events, ["g", ("f", (1, 3, 5), {"foo": "bar"})])
+ finally:
+ b.exit()
+
+ def test_log(self):
+ b = wspbus.Bus()
+ self.log(b)
+ self.assertLog([])
+
+ # Try a normal message.
+ expected = []
+ for msg in ["O mah darlin'"] * 3 + ["Clementiiiiiiiine"]:
+ b.log(msg)
+ expected.append(msg)
+ self.assertLog(expected)
+
+ # Try an error message
+ try:
+ foo
+ except NameError:
+ b.log("You are lost and gone forever", traceback=True)
+ lastmsg = self._log_entries[-1]
+ if "Traceback" not in lastmsg or "NameError" not in lastmsg:
+ self.fail("Last log message %r did not contain "
+ "the expected traceback." % lastmsg)
+ else:
+ self.fail("NameError was not raised as expected.")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/cherrypy/test/test_caching.py b/cherrypy/test/test_caching.py
new file mode 100755
index 0000000..720a933
--- /dev/null
+++ b/cherrypy/test/test_caching.py
@@ -0,0 +1,329 @@
+import datetime
+import gzip
+from itertools import count
+import os
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+import sys
+import threading
+import time
+import urllib
+
+import cherrypy
+from cherrypy._cpcompat import next, ntob, quote, xrange
+from cherrypy.lib import httputil
+
+gif_bytes = ntob('GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00'
+ '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
+ '\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;')
+
+
+
+from cherrypy.test import helper
+
+class CacheTest(helper.CPWebCase):
+
+ def setup_server():
+
+ class Root:
+
+ _cp_config = {'tools.caching.on': True}
+
+ def __init__(self):
+ self.counter = 0
+ self.control_counter = 0
+ self.longlock = threading.Lock()
+
+ def index(self):
+ self.counter += 1
+ msg = "visit #%s" % self.counter
+ return msg
+ index.exposed = True
+
+ def control(self):
+ self.control_counter += 1
+ return "visit #%s" % self.control_counter
+ control.exposed = True
+
+ def a_gif(self):
+ cherrypy.response.headers['Last-Modified'] = httputil.HTTPDate()
+ return gif_bytes
+ a_gif.exposed = True
+
+ def long_process(self, seconds='1'):
+ try:
+ self.longlock.acquire()
+ time.sleep(float(seconds))
+ finally:
+ self.longlock.release()
+ return 'success!'
+ long_process.exposed = True
+
+ def clear_cache(self, path):
+ cherrypy._cache.store[cherrypy.request.base + path].clear()
+ clear_cache.exposed = True
+
+ class VaryHeaderCachingServer(object):
+
+ _cp_config = {'tools.caching.on': True,
+ 'tools.response_headers.on': True,
+ 'tools.response_headers.headers': [('Vary', 'Our-Varying-Header')],
+ }
+
+ def __init__(self):
+ self.counter = count(1)
+
+ def index(self):
+ return "visit #%s" % next(self.counter)
+ index.exposed = True
+
+ class UnCached(object):
+ _cp_config = {'tools.expires.on': True,
+ 'tools.expires.secs': 60,
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': 'static',
+ 'tools.staticdir.root': curdir,
+ }
+
+ def force(self):
+ cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
+ self._cp_config['tools.expires.force'] = True
+ self._cp_config['tools.expires.secs'] = 0
+ return "being forceful"
+ force.exposed = True
+ force._cp_config = {'tools.expires.secs': 0}
+
+ def dynamic(self):
+ cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
+ cherrypy.response.headers['Cache-Control'] = 'private'
+ return "D-d-d-dynamic!"
+ dynamic.exposed = True
+
+ def cacheable(self):
+ cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
+ return "Hi, I'm cacheable."
+ cacheable.exposed = True
+
+ def specific(self):
+ cherrypy.response.headers['Etag'] = 'need_this_to_make_me_cacheable'
+ return "I am being specific"
+ specific.exposed = True
+ specific._cp_config = {'tools.expires.secs': 86400}
+
+ class Foo(object):pass
+
+ def wrongtype(self):
+ cherrypy.response.headers['Etag'] = 'need_this_to_make_me_cacheable'
+ return "Woops"
+ wrongtype.exposed = True
+ wrongtype._cp_config = {'tools.expires.secs': Foo()}
+
+ cherrypy.tree.mount(Root())
+ cherrypy.tree.mount(UnCached(), "/expires")
+ cherrypy.tree.mount(VaryHeaderCachingServer(), "/varying_headers")
+ cherrypy.config.update({'tools.gzip.on': True})
+ setup_server = staticmethod(setup_server)
+
+ def testCaching(self):
+ elapsed = 0.0
+ for trial in range(10):
+ self.getPage("/")
+ # The response should be the same every time,
+ # except for the Age response header.
+ self.assertBody('visit #1')
+ if trial != 0:
+ age = int(self.assertHeader("Age"))
+ self.assert_(age >= elapsed)
+ elapsed = age
+
+ # POST, PUT, DELETE should not be cached.
+ self.getPage("/", method="POST")
+ self.assertBody('visit #2')
+ # Because gzip is turned on, the Vary header should always Vary for content-encoding
+ self.assertHeader('Vary', 'Accept-Encoding')
+ # The previous request should have invalidated the cache,
+ # so this request will recalc the response.
+ self.getPage("/", method="GET")
+ self.assertBody('visit #3')
+ # ...but this request should get the cached copy.
+ self.getPage("/", method="GET")
+ self.assertBody('visit #3')
+ self.getPage("/", method="DELETE")
+ self.assertBody('visit #4')
+
+ # The previous request should have invalidated the cache,
+ # so this request will recalc the response.
+ self.getPage("/", method="GET", headers=[('Accept-Encoding', 'gzip')])
+ self.assertHeader('Content-Encoding', 'gzip')
+ self.assertHeader('Vary')
+ self.assertEqual(cherrypy.lib.encoding.decompress(self.body), ntob("visit #5"))
+
+ # Now check that a second request gets the gzip header and gzipped body
+ # This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped
+ # response body was being gzipped a second time.
+ self.getPage("/", method="GET", headers=[('Accept-Encoding', 'gzip')])
+ self.assertHeader('Content-Encoding', 'gzip')
+ self.assertEqual(cherrypy.lib.encoding.decompress(self.body), ntob("visit #5"))
+
+ # Now check that a third request that doesn't accept gzip
+ # skips the cache (because the 'Vary' header denies it).
+ self.getPage("/", method="GET")
+ self.assertNoHeader('Content-Encoding')
+ self.assertBody('visit #6')
+
+ def testVaryHeader(self):
+ self.getPage("/varying_headers/")
+ self.assertStatus("200 OK")
+ self.assertHeaderItemValue('Vary', 'Our-Varying-Header')
+ self.assertBody('visit #1')
+
+ # Now check that different 'Vary'-fields don't evict each other.
+ # This test creates 2 requests with different 'Our-Varying-Header'
+ # and then tests if the first one still exists.
+ self.getPage("/varying_headers/", headers=[('Our-Varying-Header', 'request 2')])
+ self.assertStatus("200 OK")
+ self.assertBody('visit #2')
+
+ self.getPage("/varying_headers/", headers=[('Our-Varying-Header', 'request 2')])
+ self.assertStatus("200 OK")
+ self.assertBody('visit #2')
+
+ self.getPage("/varying_headers/")
+ self.assertStatus("200 OK")
+ self.assertBody('visit #1')
+
+ def testExpiresTool(self):
+ # test setting an expires header
+ self.getPage("/expires/specific")
+ self.assertStatus("200 OK")
+ self.assertHeader("Expires")
+
+ # test exceptions for bad time values
+ self.getPage("/expires/wrongtype")
+ self.assertStatus(500)
+ self.assertInBody("TypeError")
+
+ # static content should not have "cache prevention" headers
+ self.getPage("/expires/index.html")
+ self.assertStatus("200 OK")
+ self.assertNoHeader("Pragma")
+ self.assertNoHeader("Cache-Control")
+ self.assertHeader("Expires")
+
+ # dynamic content that sets indicators should not have
+ # "cache prevention" headers
+ self.getPage("/expires/cacheable")
+ self.assertStatus("200 OK")
+ self.assertNoHeader("Pragma")
+ self.assertNoHeader("Cache-Control")
+ self.assertHeader("Expires")
+
+ self.getPage('/expires/dynamic')
+ self.assertBody("D-d-d-dynamic!")
+ # the Cache-Control header should be untouched
+ self.assertHeader("Cache-Control", "private")
+ self.assertHeader("Expires")
+
+ # configure the tool to ignore indicators and replace existing headers
+ self.getPage("/expires/force")
+ self.assertStatus("200 OK")
+ # This also gives us a chance to test 0 expiry with no other headers
+ self.assertHeader("Pragma", "no-cache")
+ if cherrypy.server.protocol_version == "HTTP/1.1":
+ self.assertHeader("Cache-Control", "no-cache, must-revalidate")
+ self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT")
+
+ # static content should now have "cache prevention" headers
+ self.getPage("/expires/index.html")
+ self.assertStatus("200 OK")
+ self.assertHeader("Pragma", "no-cache")
+ if cherrypy.server.protocol_version == "HTTP/1.1":
+ self.assertHeader("Cache-Control", "no-cache, must-revalidate")
+ self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT")
+
+ # the cacheable handler should now have "cache prevention" headers
+ self.getPage("/expires/cacheable")
+ self.assertStatus("200 OK")
+ self.assertHeader("Pragma", "no-cache")
+ if cherrypy.server.protocol_version == "HTTP/1.1":
+ self.assertHeader("Cache-Control", "no-cache, must-revalidate")
+ self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT")
+
+ self.getPage('/expires/dynamic')
+ self.assertBody("D-d-d-dynamic!")
+ # dynamic sets Cache-Control to private but it should be
+ # overwritten here ...
+ self.assertHeader("Pragma", "no-cache")
+ if cherrypy.server.protocol_version == "HTTP/1.1":
+ self.assertHeader("Cache-Control", "no-cache, must-revalidate")
+ self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT")
+
+ def testLastModified(self):
+ self.getPage("/a.gif")
+ self.assertStatus(200)
+ self.assertBody(gif_bytes)
+ lm1 = self.assertHeader("Last-Modified")
+
+ # this request should get the cached copy.
+ self.getPage("/a.gif")
+ self.assertStatus(200)
+ self.assertBody(gif_bytes)
+ self.assertHeader("Age")
+ lm2 = self.assertHeader("Last-Modified")
+ self.assertEqual(lm1, lm2)
+
+ # this request should match the cached copy, but raise 304.
+ self.getPage("/a.gif", [('If-Modified-Since', lm1)])
+ self.assertStatus(304)
+ self.assertNoHeader("Last-Modified")
+ if not getattr(cherrypy.server, "using_apache", False):
+ self.assertHeader("Age")
+
+ def test_antistampede(self):
+ SECONDS = 4
+ # We MUST make an initial synchronous request in order to create the
+ # AntiStampedeCache object, and populate its selecting_headers,
+ # before the actual stampede.
+ self.getPage("/long_process?seconds=%d" % SECONDS)
+ self.assertBody('success!')
+ self.getPage("/clear_cache?path=" +
+ quote('/long_process?seconds=%d' % SECONDS, safe=''))
+ self.assertStatus(200)
+ sys.stdout.write("prepped... ")
+ sys.stdout.flush()
+
+ start = datetime.datetime.now()
+ def run():
+ self.getPage("/long_process?seconds=%d" % SECONDS)
+ # The response should be the same every time
+ self.assertBody('success!')
+ ts = [threading.Thread(target=run) for i in xrange(100)]
+ for t in ts:
+ t.start()
+ for t in ts:
+ t.join()
+ self.assertEqualDates(start, datetime.datetime.now(),
+ # Allow a second for our thread/TCP overhead etc.
+ seconds=SECONDS + 1.1)
+
+ def test_cache_control(self):
+ self.getPage("/control")
+ self.assertBody('visit #1')
+ self.getPage("/control")
+ self.assertBody('visit #1')
+
+ self.getPage("/control", headers=[('Cache-Control', 'no-cache')])
+ self.assertBody('visit #2')
+ self.getPage("/control")
+ self.assertBody('visit #2')
+
+ self.getPage("/control", headers=[('Pragma', 'no-cache')])
+ self.assertBody('visit #3')
+ self.getPage("/control")
+ self.assertBody('visit #3')
+
+ time.sleep(1)
+ self.getPage("/control", headers=[('Cache-Control', 'max-age=0')])
+ self.assertBody('visit #4')
+ self.getPage("/control")
+ self.assertBody('visit #4')
+
diff --git a/cherrypy/test/test_config.py b/cherrypy/test/test_config.py
new file mode 100755
index 0000000..a0bd8ab
--- /dev/null
+++ b/cherrypy/test/test_config.py
@@ -0,0 +1,249 @@
+"""Tests for the CherryPy configuration system."""
+
+import os, sys
+localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+from cherrypy._cpcompat import ntob, StringIO
+import unittest
+
+import cherrypy
+
+def setup_server():
+
+ class Root:
+
+ _cp_config = {'foo': 'this',
+ 'bar': 'that'}
+
+ def __init__(self):
+ cherrypy.config.namespaces['db'] = self.db_namespace
+
+ def db_namespace(self, k, v):
+ if k == "scheme":
+ self.db = v
+
+ # @cherrypy.expose(alias=('global_', 'xyz'))
+ def index(self, key):
+ return cherrypy.request.config.get(key, "None")
+ index = cherrypy.expose(index, alias=('global_', 'xyz'))
+
+ def repr(self, key):
+ return repr(cherrypy.request.config.get(key, None))
+ repr.exposed = True
+
+ def dbscheme(self):
+ return self.db
+ dbscheme.exposed = True
+
+ def plain(self, x):
+ return x
+ plain.exposed = True
+ plain._cp_config = {'request.body.attempt_charsets': ['utf-16']}
+
+ favicon_ico = cherrypy.tools.staticfile.handler(
+ filename=os.path.join(localDir, '../favicon.ico'))
+
+ class Foo:
+
+ _cp_config = {'foo': 'this2',
+ 'baz': 'that2'}
+
+ def index(self, key):
+ return cherrypy.request.config.get(key, "None")
+ index.exposed = True
+ nex = index
+
+ def silly(self):
+ return 'Hello world'
+ silly.exposed = True
+ silly._cp_config = {'response.headers.X-silly': 'sillyval'}
+
+ def bar(self, key):
+ return repr(cherrypy.request.config.get(key, None))
+ bar.exposed = True
+ bar._cp_config = {'foo': 'this3', 'bax': 'this4'}
+
+ class Another:
+
+ def index(self, key):
+ return str(cherrypy.request.config.get(key, "None"))
+ index.exposed = True
+
+
+ def raw_namespace(key, value):
+ if key == 'input.map':
+ handler = cherrypy.request.handler
+ def wrapper():
+ params = cherrypy.request.params
+ for name, coercer in list(value.items()):
+ try:
+ params[name] = coercer(params[name])
+ except KeyError:
+ pass
+ return handler()
+ cherrypy.request.handler = wrapper
+ elif key == 'output':
+ handler = cherrypy.request.handler
+ def wrapper():
+ # 'value' is a type (like int or str).
+ return value(handler())
+ cherrypy.request.handler = wrapper
+
+ class Raw:
+
+ _cp_config = {'raw.output': repr}
+
+ def incr(self, num):
+ return num + 1
+ incr.exposed = True
+ incr._cp_config = {'raw.input.map': {'num': int}}
+
+ ioconf = StringIO("""
+[/]
+neg: -1234
+filename: os.path.join(sys.prefix, "hello.py")
+thing1: cherrypy.lib.httputil.response_codes[404]
+thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2
+complex: 3+2j
+ones: "11"
+twos: "22"
+stradd: %%(ones)s + %%(twos)s + "33"
+
+[/favicon.ico]
+tools.staticfile.filename = %r
+""" % os.path.join(localDir, 'static/dirback.jpg'))
+
+ root = Root()
+ root.foo = Foo()
+ root.raw = Raw()
+ app = cherrypy.tree.mount(root, config=ioconf)
+ app.request_class.namespaces['raw'] = raw_namespace
+
+ cherrypy.tree.mount(Another(), "/another")
+ cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove',
+ 'db.scheme': r"sqlite///memory",
+ })
+
+
+# Client-side code #
+
+from cherrypy.test import helper
+
+class ConfigTests(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def testConfig(self):
+ tests = [
+ ('/', 'nex', 'None'),
+ ('/', 'foo', 'this'),
+ ('/', 'bar', 'that'),
+ ('/xyz', 'foo', 'this'),
+ ('/foo/', 'foo', 'this2'),
+ ('/foo/', 'bar', 'that'),
+ ('/foo/', 'bax', 'None'),
+ ('/foo/bar', 'baz', "'that2'"),
+ ('/foo/nex', 'baz', 'that2'),
+ # If 'foo' == 'this', then the mount point '/another' leaks into '/'.
+ ('/another/','foo', 'None'),
+ ]
+ for path, key, expected in tests:
+ self.getPage(path + "?key=" + key)
+ self.assertBody(expected)
+
+ expectedconf = {
+ # From CP defaults
+ 'tools.log_headers.on': False,
+ 'tools.log_tracebacks.on': True,
+ 'request.show_tracebacks': True,
+ 'log.screen': False,
+ 'environment': 'test_suite',
+ 'engine.autoreload_on': False,
+ # From global config
+ 'luxuryyacht': 'throatwobblermangrove',
+ # From Root._cp_config
+ 'bar': 'that',
+ # From Foo._cp_config
+ 'baz': 'that2',
+ # From Foo.bar._cp_config
+ 'foo': 'this3',
+ 'bax': 'this4',
+ }
+ for key, expected in expectedconf.items():
+ self.getPage("/foo/bar?key=" + key)
+ self.assertBody(repr(expected))
+
+ def testUnrepr(self):
+ self.getPage("/repr?key=neg")
+ self.assertBody("-1234")
+
+ self.getPage("/repr?key=filename")
+ self.assertBody(repr(os.path.join(sys.prefix, "hello.py")))
+
+ self.getPage("/repr?key=thing1")
+ self.assertBody(repr(cherrypy.lib.httputil.response_codes[404]))
+
+ if not getattr(cherrypy.server, "using_apache", False):
+ # The object ID's won't match up when using Apache, since the
+ # server and client are running in different processes.
+ self.getPage("/repr?key=thing2")
+ from cherrypy.tutorial import thing2
+ self.assertBody(repr(thing2))
+
+ self.getPage("/repr?key=complex")
+ self.assertBody("(3+2j)")
+
+ self.getPage("/repr?key=stradd")
+ self.assertBody(repr("112233"))
+
+ def testRespNamespaces(self):
+ self.getPage("/foo/silly")
+ self.assertHeader('X-silly', 'sillyval')
+ self.assertBody('Hello world')
+
+ def testCustomNamespaces(self):
+ self.getPage("/raw/incr?num=12")
+ self.assertBody("13")
+
+ self.getPage("/dbscheme")
+ self.assertBody(r"sqlite///memory")
+
+ def testHandlerToolConfigOverride(self):
+ # Assert that config overrides tool constructor args. Above, we set
+ # the favicon in the page handler to be '../favicon.ico',
+ # but then overrode it in config to be './static/dirback.jpg'.
+ self.getPage("/favicon.ico")
+ self.assertBody(open(os.path.join(localDir, "static/dirback.jpg"),
+ "rb").read())
+
+ def test_request_body_namespace(self):
+ self.getPage("/plain", method='POST', headers=[
+ ('Content-Type', 'application/x-www-form-urlencoded'),
+ ('Content-Length', '13')],
+ body=ntob('\xff\xfex\x00=\xff\xfea\x00b\x00c\x00'))
+ self.assertBody("abc")
+
+
+class VariableSubstitutionTests(unittest.TestCase):
+ setup_server = staticmethod(setup_server)
+
+ def test_config(self):
+ from textwrap import dedent
+
+ # variable substitution with [DEFAULT]
+ conf = dedent("""
+ [DEFAULT]
+ dir = "/some/dir"
+ my.dir = %(dir)s + "/sub"
+
+ [my]
+ my.dir = %(dir)s + "/my/dir"
+ my.dir2 = %(my.dir)s + '/dir2'
+
+ """)
+
+ fp = StringIO(conf)
+
+ cherrypy.config.update(fp)
+ self.assertEqual(cherrypy.config["my"]["my.dir"], "/some/dir/my/dir")
+ self.assertEqual(cherrypy.config["my"]["my.dir2"], "/some/dir/my/dir/dir2")
+
diff --git a/cherrypy/test/test_config_server.py b/cherrypy/test/test_config_server.py
new file mode 100755
index 0000000..0b9718d
--- /dev/null
+++ b/cherrypy/test/test_config_server.py
@@ -0,0 +1,121 @@
+"""Tests for the CherryPy configuration system."""
+
+import os, sys
+localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+import socket
+import time
+
+import cherrypy
+
+
+# Client-side code #
+
+from cherrypy.test import helper
+
+class ServerConfigTests(helper.CPWebCase):
+
+ def setup_server():
+
+ class Root:
+ def index(self):
+ return cherrypy.request.wsgi_environ['SERVER_PORT']
+ index.exposed = True
+
+ def upload(self, file):
+ return "Size: %s" % len(file.file.read())
+ upload.exposed = True
+
+ def tinyupload(self):
+ return cherrypy.request.body.read()
+ tinyupload.exposed = True
+ tinyupload._cp_config = {'request.body.maxbytes': 100}
+
+ cherrypy.tree.mount(Root())
+
+ cherrypy.config.update({
+ 'server.socket_host': '0.0.0.0',
+ 'server.socket_port': 9876,
+ 'server.max_request_body_size': 200,
+ 'server.max_request_header_size': 500,
+ 'server.socket_timeout': 0.5,
+
+ # Test explicit server.instance
+ 'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer',
+ 'server.2.socket_port': 9877,
+
+ # Test non-numeric <servername>
+ # Also test default server.instance = builtin server
+ 'server.yetanother.socket_port': 9878,
+ })
+ setup_server = staticmethod(setup_server)
+
+ PORT = 9876
+
+ def testBasicConfig(self):
+ self.getPage("/")
+ self.assertBody(str(self.PORT))
+
+ def testAdditionalServers(self):
+ if self.scheme == 'https':
+ return self.skip("not available under ssl")
+ self.PORT = 9877
+ self.getPage("/")
+ self.assertBody(str(self.PORT))
+ self.PORT = 9878
+ self.getPage("/")
+ self.assertBody(str(self.PORT))
+
+ def testMaxRequestSizePerHandler(self):
+ if getattr(cherrypy.server, "using_apache", False):
+ return self.skip("skipped due to known Apache differences... ")
+
+ self.getPage('/tinyupload', method="POST",
+ headers=[('Content-Type', 'text/plain'),
+ ('Content-Length', '100')],
+ body="x" * 100)
+ self.assertStatus(200)
+ self.assertBody("x" * 100)
+
+ self.getPage('/tinyupload', method="POST",
+ headers=[('Content-Type', 'text/plain'),
+ ('Content-Length', '101')],
+ body="x" * 101)
+ self.assertStatus(413)
+
+ def testMaxRequestSize(self):
+ if getattr(cherrypy.server, "using_apache", False):
+ return self.skip("skipped due to known Apache differences... ")
+
+ for size in (500, 5000, 50000):
+ self.getPage("/", headers=[('From', "x" * 500)])
+ self.assertStatus(413)
+
+ # Test for http://www.cherrypy.org/ticket/421
+ # (Incorrect border condition in readline of SizeCheckWrapper).
+ # This hangs in rev 891 and earlier.
+ lines256 = "x" * 248
+ self.getPage("/",
+ headers=[('Host', '%s:%s' % (self.HOST, self.PORT)),
+ ('From', lines256)])
+
+ # Test upload
+ body = '\r\n'.join([
+ '--x',
+ 'Content-Disposition: form-data; name="file"; filename="hello.txt"',
+ 'Content-Type: text/plain',
+ '',
+ '%s',
+ '--x--'])
+ partlen = 200 - len(body)
+ b = body % ("x" * partlen)
+ h = [("Content-type", "multipart/form-data; boundary=x"),
+ ("Content-Length", "%s" % len(b))]
+ self.getPage('/upload', h, "POST", b)
+ self.assertBody('Size: %d' % partlen)
+
+ b = body % ("x" * 200)
+ h = [("Content-type", "multipart/form-data; boundary=x"),
+ ("Content-Length", "%s" % len(b))]
+ self.getPage('/upload', h, "POST", b)
+ self.assertStatus(413)
+
diff --git a/cherrypy/test/test_conn.py b/cherrypy/test/test_conn.py
new file mode 100755
index 0000000..1346f59
--- /dev/null
+++ b/cherrypy/test/test_conn.py
@@ -0,0 +1,734 @@
+"""Tests for TCP connection handling, including proper and timely close."""
+
+import socket
+import sys
+import time
+timeout = 1
+
+
+import cherrypy
+from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, NotConnected, BadStatusLine
+from cherrypy._cpcompat import ntob, urlopen, unicodestr
+from cherrypy.test import webtest
+from cherrypy import _cperror
+
+
+pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN'
+
+def setup_server():
+
+ def raise500():
+ raise cherrypy.HTTPError(500)
+
+ class Root:
+
+ def index(self):
+ return pov
+ index.exposed = True
+ page1 = index
+ page2 = index
+ page3 = index
+
+ def hello(self):
+ return "Hello, world!"
+ hello.exposed = True
+
+ def timeout(self, t):
+ return str(cherrypy.server.httpserver.timeout)
+ timeout.exposed = True
+
+ def stream(self, set_cl=False):
+ if set_cl:
+ cherrypy.response.headers['Content-Length'] = 10
+
+ def content():
+ for x in range(10):
+ yield str(x)
+
+ return content()
+ stream.exposed = True
+ stream._cp_config = {'response.stream': True}
+
+ def error(self, code=500):
+ raise cherrypy.HTTPError(code)
+ error.exposed = True
+
+ def upload(self):
+ if not cherrypy.request.method == 'POST':
+ raise AssertionError("'POST' != request.method %r" %
+ cherrypy.request.method)
+ return "thanks for '%s'" % cherrypy.request.body.read()
+ upload.exposed = True
+
+ def custom(self, response_code):
+ cherrypy.response.status = response_code
+ return "Code = %s" % response_code
+ custom.exposed = True
+
+ def err_before_read(self):
+ return "ok"
+ err_before_read.exposed = True
+ err_before_read._cp_config = {'hooks.on_start_resource': raise500}
+
+ def one_megabyte_of_a(self):
+ return ["a" * 1024] * 1024
+ one_megabyte_of_a.exposed = True
+
+ def custom_cl(self, body, cl):
+ cherrypy.response.headers['Content-Length'] = cl
+ if not isinstance(body, list):
+ body = [body]
+ newbody = []
+ for chunk in body:
+ if isinstance(chunk, unicodestr):
+ chunk = chunk.encode('ISO-8859-1')
+ newbody.append(chunk)
+ return newbody
+ custom_cl.exposed = True
+ # Turn off the encoding tool so it doens't collapse
+ # our response body and reclaculate the Content-Length.
+ custom_cl._cp_config = {'tools.encode.on': False}
+
+ cherrypy.tree.mount(Root())
+ cherrypy.config.update({
+ 'server.max_request_body_size': 1001,
+ 'server.socket_timeout': timeout,
+ })
+
+
+from cherrypy.test import helper
+
+class ConnectionCloseTests(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def test_HTTP11(self):
+ if cherrypy.server.protocol_version != "HTTP/1.1":
+ return self.skip()
+
+ self.PROTOCOL = "HTTP/1.1"
+
+ self.persistent = True
+
+ # Make the first request and assert there's no "Connection: close".
+ self.getPage("/")
+ self.assertStatus('200 OK')
+ self.assertBody(pov)
+ self.assertNoHeader("Connection")
+
+ # Make another request on the same connection.
+ self.getPage("/page1")
+ self.assertStatus('200 OK')
+ self.assertBody(pov)
+ self.assertNoHeader("Connection")
+
+ # Test client-side close.
+ self.getPage("/page2", headers=[("Connection", "close")])
+ self.assertStatus('200 OK')
+ self.assertBody(pov)
+ self.assertHeader("Connection", "close")
+
+ # Make another request on the same connection, which should error.
+ self.assertRaises(NotConnected, self.getPage, "/")
+
+ def test_Streaming_no_len(self):
+ self._streaming(set_cl=False)
+
+ def test_Streaming_with_len(self):
+ self._streaming(set_cl=True)
+
+ def _streaming(self, set_cl):
+ if cherrypy.server.protocol_version == "HTTP/1.1":
+ self.PROTOCOL = "HTTP/1.1"
+
+ self.persistent = True
+
+ # Make the first request and assert there's no "Connection: close".
+ self.getPage("/")
+ self.assertStatus('200 OK')
+ self.assertBody(pov)
+ self.assertNoHeader("Connection")
+
+ # Make another, streamed request on the same connection.
+ if set_cl:
+ # When a Content-Length is provided, the content should stream
+ # without closing the connection.
+ self.getPage("/stream?set_cl=Yes")
+ self.assertHeader("Content-Length")
+ self.assertNoHeader("Connection", "close")
+ self.assertNoHeader("Transfer-Encoding")
+
+ self.assertStatus('200 OK')
+ self.assertBody('0123456789')
+ else:
+ # When no Content-Length response header is provided,
+ # streamed output will either close the connection, or use
+ # chunked encoding, to determine transfer-length.
+ self.getPage("/stream")
+ self.assertNoHeader("Content-Length")
+ self.assertStatus('200 OK')
+ self.assertBody('0123456789')
+
+ chunked_response = False
+ for k, v in self.headers:
+ if k.lower() == "transfer-encoding":
+ if str(v) == "chunked":
+ chunked_response = True
+
+ if chunked_response:
+ self.assertNoHeader("Connection", "close")
+ else:
+ self.assertHeader("Connection", "close")
+
+ # Make another request on the same connection, which should error.
+ self.assertRaises(NotConnected, self.getPage, "/")
+
+ # Try HEAD. See http://www.cherrypy.org/ticket/864.
+ self.getPage("/stream", method='HEAD')
+ self.assertStatus('200 OK')
+ self.assertBody('')
+ self.assertNoHeader("Transfer-Encoding")
+ else:
+ self.PROTOCOL = "HTTP/1.0"
+
+ self.persistent = True
+
+ # Make the first request and assert Keep-Alive.
+ self.getPage("/", headers=[("Connection", "Keep-Alive")])
+ self.assertStatus('200 OK')
+ self.assertBody(pov)
+ self.assertHeader("Connection", "Keep-Alive")
+
+ # Make another, streamed request on the same connection.
+ if set_cl:
+ # When a Content-Length is provided, the content should
+ # stream without closing the connection.
+ self.getPage("/stream?set_cl=Yes",
+ headers=[("Connection", "Keep-Alive")])
+ self.assertHeader("Content-Length")
+ self.assertHeader("Connection", "Keep-Alive")
+ self.assertNoHeader("Transfer-Encoding")
+ self.assertStatus('200 OK')
+ self.assertBody('0123456789')
+ else:
+ # When a Content-Length is not provided,
+ # the server should close the connection.
+ self.getPage("/stream", headers=[("Connection", "Keep-Alive")])
+ self.assertStatus('200 OK')
+ self.assertBody('0123456789')
+
+ self.assertNoHeader("Content-Length")
+ self.assertNoHeader("Connection", "Keep-Alive")
+ self.assertNoHeader("Transfer-Encoding")
+
+ # Make another request on the same connection, which should error.
+ self.assertRaises(NotConnected, self.getPage, "/")
+
+ def test_HTTP10_KeepAlive(self):
+ self.PROTOCOL = "HTTP/1.0"
+ if self.scheme == "https":
+ self.HTTP_CONN = HTTPSConnection
+ else:
+ self.HTTP_CONN = HTTPConnection
+
+ # Test a normal HTTP/1.0 request.
+ self.getPage("/page2")
+ self.assertStatus('200 OK')
+ self.assertBody(pov)
+ # Apache, for example, may emit a Connection header even for HTTP/1.0
+## self.assertNoHeader("Connection")
+
+ # Test a keep-alive HTTP/1.0 request.
+ self.persistent = True
+
+ self.getPage("/page3", headers=[("Connection", "Keep-Alive")])
+ self.assertStatus('200 OK')
+ self.assertBody(pov)
+ self.assertHeader("Connection", "Keep-Alive")
+
+ # Remove the keep-alive header again.
+ self.getPage("/page3")
+ self.assertStatus('200 OK')
+ self.assertBody(pov)
+ # Apache, for example, may emit a Connection header even for HTTP/1.0
+## self.assertNoHeader("Connection")
+
+
+class PipelineTests(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def test_HTTP11_Timeout(self):
+ # If we timeout without sending any data,
+ # the server will close the conn with a 408.
+ if cherrypy.server.protocol_version != "HTTP/1.1":
+ return self.skip()
+
+ self.PROTOCOL = "HTTP/1.1"
+
+ # Connect but send nothing.
+ self.persistent = True
+ conn = self.HTTP_CONN
+ conn.auto_open = False
+ conn.connect()
+
+ # Wait for our socket timeout
+ time.sleep(timeout * 2)
+
+ # The request should have returned 408 already.
+ response = conn.response_class(conn.sock, method="GET")
+ response.begin()
+ self.assertEqual(response.status, 408)
+ conn.close()
+
+ # Connect but send half the headers only.
+ self.persistent = True
+ conn = self.HTTP_CONN
+ conn.auto_open = False
+ conn.connect()
+ conn.send(ntob('GET /hello HTTP/1.1'))
+ conn.send(("Host: %s" % self.HOST).encode('ascii'))
+
+ # Wait for our socket timeout
+ time.sleep(timeout * 2)
+
+ # The conn should have already sent 408.
+ response = conn.response_class(conn.sock, method="GET")
+ response.begin()
+ self.assertEqual(response.status, 408)
+ conn.close()
+
+ def test_HTTP11_Timeout_after_request(self):
+ # If we timeout after at least one request has succeeded,
+ # the server will close the conn without 408.
+ if cherrypy.server.protocol_version != "HTTP/1.1":
+ return self.skip()
+
+ self.PROTOCOL = "HTTP/1.1"
+
+ # Make an initial request
+ self.persistent = True
+ conn = self.HTTP_CONN
+ conn.putrequest("GET", "/timeout?t=%s" % timeout, skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.endheaders()
+ response = conn.response_class(conn.sock, method="GET")
+ response.begin()
+ self.assertEqual(response.status, 200)
+ self.body = response.read()
+ self.assertBody(str(timeout))
+
+ # Make a second request on the same socket
+ conn._output(ntob('GET /hello HTTP/1.1'))
+ conn._output(ntob("Host: %s" % self.HOST, 'ascii'))
+ conn._send_output()
+ response = conn.response_class(conn.sock, method="GET")
+ response.begin()
+ self.assertEqual(response.status, 200)
+ self.body = response.read()
+ self.assertBody("Hello, world!")
+
+ # Wait for our socket timeout
+ time.sleep(timeout * 2)
+
+ # Make another request on the same socket, which should error
+ conn._output(ntob('GET /hello HTTP/1.1'))
+ conn._output(ntob("Host: %s" % self.HOST, 'ascii'))
+ conn._send_output()
+ response = conn.response_class(conn.sock, method="GET")
+ try:
+ response.begin()
+ except:
+ if not isinstance(sys.exc_info()[1],
+ (socket.error, BadStatusLine)):
+ self.fail("Writing to timed out socket didn't fail"
+ " as it should have: %s" % sys.exc_info()[1])
+ else:
+ if response.status != 408:
+ self.fail("Writing to timed out socket didn't fail"
+ " as it should have: %s" %
+ response.read())
+
+ conn.close()
+
+ # Make another request on a new socket, which should work
+ self.persistent = True
+ conn = self.HTTP_CONN
+ conn.putrequest("GET", "/", skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.endheaders()
+ response = conn.response_class(conn.sock, method="GET")
+ response.begin()
+ self.assertEqual(response.status, 200)
+ self.body = response.read()
+ self.assertBody(pov)
+
+
+ # Make another request on the same socket,
+ # but timeout on the headers
+ conn.send(ntob('GET /hello HTTP/1.1'))
+ # Wait for our socket timeout
+ time.sleep(timeout * 2)
+ response = conn.response_class(conn.sock, method="GET")
+ try:
+ response.begin()
+ except:
+ if not isinstance(sys.exc_info()[1],
+ (socket.error, BadStatusLine)):
+ self.fail("Writing to timed out socket didn't fail"
+ " as it should have: %s" % sys.exc_info()[1])
+ else:
+ self.fail("Writing to timed out socket didn't fail"
+ " as it should have: %s" %
+ response.read())
+
+ conn.close()
+
+ # Retry the request on a new connection, which should work
+ self.persistent = True
+ conn = self.HTTP_CONN
+ conn.putrequest("GET", "/", skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.endheaders()
+ response = conn.response_class(conn.sock, method="GET")
+ response.begin()
+ self.assertEqual(response.status, 200)
+ self.body = response.read()
+ self.assertBody(pov)
+ conn.close()
+
+ def test_HTTP11_pipelining(self):
+ if cherrypy.server.protocol_version != "HTTP/1.1":
+ return self.skip()
+
+ self.PROTOCOL = "HTTP/1.1"
+
+ # Test pipelining. httplib doesn't support this directly.
+ self.persistent = True
+ conn = self.HTTP_CONN
+
+ # Put request 1
+ conn.putrequest("GET", "/hello", skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.endheaders()
+
+ for trial in range(5):
+ # Put next request
+ conn._output(ntob('GET /hello HTTP/1.1'))
+ conn._output(ntob("Host: %s" % self.HOST, 'ascii'))
+ conn._send_output()
+
+ # Retrieve previous response
+ response = conn.response_class(conn.sock, method="GET")
+ response.begin()
+ body = response.read(13)
+ self.assertEqual(response.status, 200)
+ self.assertEqual(body, ntob("Hello, world!"))
+
+ # Retrieve final response
+ response = conn.response_class(conn.sock, method="GET")
+ response.begin()
+ body = response.read()
+ self.assertEqual(response.status, 200)
+ self.assertEqual(body, ntob("Hello, world!"))
+
+ conn.close()
+
+ def test_100_Continue(self):
+ if cherrypy.server.protocol_version != "HTTP/1.1":
+ return self.skip()
+
+ self.PROTOCOL = "HTTP/1.1"
+
+ self.persistent = True
+ conn = self.HTTP_CONN
+
+ # Try a page without an Expect request header first.
+ # Note that httplib's response.begin automatically ignores
+ # 100 Continue responses, so we must manually check for it.
+ conn.putrequest("POST", "/upload", skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.putheader("Content-Type", "text/plain")
+ conn.putheader("Content-Length", "4")
+ conn.endheaders()
+ conn.send(ntob("d'oh"))
+ response = conn.response_class(conn.sock, method="POST")
+ version, status, reason = response._read_status()
+ self.assertNotEqual(status, 100)
+ conn.close()
+
+ # Now try a page with an Expect header...
+ conn.connect()
+ conn.putrequest("POST", "/upload", skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.putheader("Content-Type", "text/plain")
+ conn.putheader("Content-Length", "17")
+ conn.putheader("Expect", "100-continue")
+ conn.endheaders()
+ response = conn.response_class(conn.sock, method="POST")
+
+ # ...assert and then skip the 100 response
+ version, status, reason = response._read_status()
+ self.assertEqual(status, 100)
+ while True:
+ line = response.fp.readline().strip()
+ if line:
+ self.fail("100 Continue should not output any headers. Got %r" % line)
+ else:
+ break
+
+ # ...send the body
+ body = ntob("I am a small file")
+ conn.send(body)
+
+ # ...get the final response
+ response.begin()
+ self.status, self.headers, self.body = webtest.shb(response)
+ self.assertStatus(200)
+ self.assertBody("thanks for '%s'" % body)
+ conn.close()
+
+
+class ConnectionTests(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def test_readall_or_close(self):
+ if cherrypy.server.protocol_version != "HTTP/1.1":
+ return self.skip()
+
+ self.PROTOCOL = "HTTP/1.1"
+
+ if self.scheme == "https":
+ self.HTTP_CONN = HTTPSConnection
+ else:
+ self.HTTP_CONN = HTTPConnection
+
+ # Test a max of 0 (the default) and then reset to what it was above.
+ old_max = cherrypy.server.max_request_body_size
+ for new_max in (0, old_max):
+ cherrypy.server.max_request_body_size = new_max
+
+ self.persistent = True
+ conn = self.HTTP_CONN
+
+ # Get a POST page with an error
+ conn.putrequest("POST", "/err_before_read", skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.putheader("Content-Type", "text/plain")
+ conn.putheader("Content-Length", "1000")
+ conn.putheader("Expect", "100-continue")
+ conn.endheaders()
+ response = conn.response_class(conn.sock, method="POST")
+
+ # ...assert and then skip the 100 response
+ version, status, reason = response._read_status()
+ self.assertEqual(status, 100)
+ while True:
+ skip = response.fp.readline().strip()
+ if not skip:
+ break
+
+ # ...send the body
+ conn.send(ntob("x" * 1000))
+
+ # ...get the final response
+ response.begin()
+ self.status, self.headers, self.body = webtest.shb(response)
+ self.assertStatus(500)
+
+ # Now try a working page with an Expect header...
+ conn._output(ntob('POST /upload HTTP/1.1'))
+ conn._output(ntob("Host: %s" % self.HOST, 'ascii'))
+ conn._output(ntob("Content-Type: text/plain"))
+ conn._output(ntob("Content-Length: 17"))
+ conn._output(ntob("Expect: 100-continue"))
+ conn._send_output()
+ response = conn.response_class(conn.sock, method="POST")
+
+ # ...assert and then skip the 100 response
+ version, status, reason = response._read_status()
+ self.assertEqual(status, 100)
+ while True:
+ skip = response.fp.readline().strip()
+ if not skip:
+ break
+
+ # ...send the body
+ body = ntob("I am a small file")
+ conn.send(body)
+
+ # ...get the final response
+ response.begin()
+ self.status, self.headers, self.body = webtest.shb(response)
+ self.assertStatus(200)
+ self.assertBody("thanks for '%s'" % body)
+ conn.close()
+
+ def test_No_Message_Body(self):
+ if cherrypy.server.protocol_version != "HTTP/1.1":
+ return self.skip()
+
+ self.PROTOCOL = "HTTP/1.1"
+
+ # Set our HTTP_CONN to an instance so it persists between requests.
+ self.persistent = True
+
+ # Make the first request and assert there's no "Connection: close".
+ self.getPage("/")
+ self.assertStatus('200 OK')
+ self.assertBody(pov)
+ self.assertNoHeader("Connection")
+
+ # Make a 204 request on the same connection.
+ self.getPage("/custom/204")
+ self.assertStatus(204)
+ self.assertNoHeader("Content-Length")
+ self.assertBody("")
+ self.assertNoHeader("Connection")
+
+ # Make a 304 request on the same connection.
+ self.getPage("/custom/304")
+ self.assertStatus(304)
+ self.assertNoHeader("Content-Length")
+ self.assertBody("")
+ self.assertNoHeader("Connection")
+
+ def test_Chunked_Encoding(self):
+ if cherrypy.server.protocol_version != "HTTP/1.1":
+ return self.skip()
+
+ if (hasattr(self, 'harness') and
+ "modpython" in self.harness.__class__.__name__.lower()):
+ # mod_python forbids chunked encoding
+ return self.skip()
+
+ self.PROTOCOL = "HTTP/1.1"
+
+ # Set our HTTP_CONN to an instance so it persists between requests.
+ self.persistent = True
+ conn = self.HTTP_CONN
+
+ # Try a normal chunked request (with extensions)
+ body = ntob("8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n"
+ "Content-Type: application/json\r\n"
+ "\r\n")
+ conn.putrequest("POST", "/upload", skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.putheader("Transfer-Encoding", "chunked")
+ conn.putheader("Trailer", "Content-Type")
+ # Note that this is somewhat malformed:
+ # we shouldn't be sending Content-Length.
+ # RFC 2616 says the server should ignore it.
+ conn.putheader("Content-Length", "3")
+ conn.endheaders()
+ conn.send(body)
+ response = conn.getresponse()
+ self.status, self.headers, self.body = webtest.shb(response)
+ self.assertStatus('200 OK')
+ self.assertBody("thanks for '%s'" % ntob('xx\r\nxxxxyyyyy'))
+
+ # Try a chunked request that exceeds server.max_request_body_size.
+ # Note that the delimiters and trailer are included.
+ body = ntob("3e3\r\n" + ("x" * 995) + "\r\n0\r\n\r\n")
+ conn.putrequest("POST", "/upload", skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.putheader("Transfer-Encoding", "chunked")
+ conn.putheader("Content-Type", "text/plain")
+ # Chunked requests don't need a content-length
+## conn.putheader("Content-Length", len(body))
+ conn.endheaders()
+ conn.send(body)
+ response = conn.getresponse()
+ self.status, self.headers, self.body = webtest.shb(response)
+ self.assertStatus(413)
+ conn.close()
+
+ def test_Content_Length_in(self):
+ # Try a non-chunked request where Content-Length exceeds
+ # server.max_request_body_size. Assert error before body send.
+ self.persistent = True
+ conn = self.HTTP_CONN
+ conn.putrequest("POST", "/upload", skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.putheader("Content-Type", "text/plain")
+ conn.putheader("Content-Length", "9999")
+ conn.endheaders()
+ response = conn.getresponse()
+ self.status, self.headers, self.body = webtest.shb(response)
+ self.assertStatus(413)
+ self.assertBody("The entity sent with the request exceeds "
+ "the maximum allowed bytes.")
+ conn.close()
+
+ def test_Content_Length_out_preheaders(self):
+ # Try a non-chunked response where Content-Length is less than
+ # the actual bytes in the response body.
+ self.persistent = True
+ conn = self.HTTP_CONN
+ conn.putrequest("GET", "/custom_cl?body=I+have+too+many+bytes&cl=5",
+ skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.endheaders()
+ response = conn.getresponse()
+ self.status, self.headers, self.body = webtest.shb(response)
+ self.assertStatus(500)
+ self.assertBody(
+ "The requested resource returned more bytes than the "
+ "declared Content-Length.")
+ conn.close()
+
+ def test_Content_Length_out_postheaders(self):
+ # Try a non-chunked response where Content-Length is less than
+ # the actual bytes in the response body.
+ self.persistent = True
+ conn = self.HTTP_CONN
+ conn.putrequest("GET", "/custom_cl?body=I+too&body=+have+too+many&cl=5",
+ skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.endheaders()
+ response = conn.getresponse()
+ self.status, self.headers, self.body = webtest.shb(response)
+ self.assertStatus(200)
+ self.assertBody("I too")
+ conn.close()
+
+ def test_598(self):
+ remote_data_conn = urlopen('%s://%s:%s/one_megabyte_of_a/' %
+ (self.scheme, self.HOST, self.PORT,))
+ buf = remote_data_conn.read(512)
+ time.sleep(timeout * 0.6)
+ remaining = (1024 * 1024) - 512
+ while remaining:
+ data = remote_data_conn.read(remaining)
+ if not data:
+ break
+ else:
+ buf += data
+ remaining -= len(data)
+
+ self.assertEqual(len(buf), 1024 * 1024)
+ self.assertEqual(buf, ntob("a" * 1024 * 1024))
+ self.assertEqual(remaining, 0)
+ remote_data_conn.close()
+
+
+class BadRequestTests(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def test_No_CRLF(self):
+ self.persistent = True
+
+ conn = self.HTTP_CONN
+ conn.send(ntob('GET /hello HTTP/1.1\n\n'))
+ response = conn.response_class(conn.sock, method="GET")
+ response.begin()
+ self.body = response.read()
+ self.assertBody("HTTP requires CRLF terminators")
+ conn.close()
+
+ conn.connect()
+ conn.send(ntob('GET /hello HTTP/1.1\r\n\n'))
+ response = conn.response_class(conn.sock, method="GET")
+ response.begin()
+ self.body = response.read()
+ self.assertBody("HTTP requires CRLF terminators")
+ conn.close()
+
diff --git a/cherrypy/test/test_core.py b/cherrypy/test/test_core.py
new file mode 100755
index 0000000..09544e3
--- /dev/null
+++ b/cherrypy/test/test_core.py
@@ -0,0 +1,617 @@
+"""Basic tests for the CherryPy core: request handling."""
+
+import os
+localDir = os.path.dirname(__file__)
+import sys
+import types
+
+import cherrypy
+from cherrypy._cpcompat import IncompleteRead, itervalues, ntob
+from cherrypy import _cptools, tools
+from cherrypy.lib import httputil, static
+
+
+favicon_path = os.path.join(os.getcwd(), localDir, "../favicon.ico")
+
+# Client-side code #
+
+from cherrypy.test import helper
+
+class CoreRequestHandlingTest(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+
+ def index(self):
+ return "hello"
+ index.exposed = True
+
+ favicon_ico = tools.staticfile.handler(filename=favicon_path)
+
+ def defct(self, newct):
+ newct = "text/%s" % newct
+ cherrypy.config.update({'tools.response_headers.on': True,
+ 'tools.response_headers.headers':
+ [('Content-Type', newct)]})
+ defct.exposed = True
+
+ def baseurl(self, path_info, relative=None):
+ return cherrypy.url(path_info, relative=bool(relative))
+ baseurl.exposed = True
+
+ root = Root()
+
+ if sys.version_info >= (2, 5):
+ from cherrypy.test._test_decorators import ExposeExamples
+ root.expose_dec = ExposeExamples()
+
+
+ class TestType(type):
+ """Metaclass which automatically exposes all functions in each subclass,
+ and adds an instance of the subclass as an attribute of root.
+ """
+ def __init__(cls, name, bases, dct):
+ type.__init__(cls, name, bases, dct)
+ for value in itervalues(dct):
+ if isinstance(value, types.FunctionType):
+ value.exposed = True
+ setattr(root, name.lower(), cls())
+ class Test(object):
+ __metaclass__ = TestType
+
+
+ class URL(Test):
+
+ _cp_config = {'tools.trailing_slash.on': False}
+
+ def index(self, path_info, relative=None):
+ if relative != 'server':
+ relative = bool(relative)
+ return cherrypy.url(path_info, relative=relative)
+
+ def leaf(self, path_info, relative=None):
+ if relative != 'server':
+ relative = bool(relative)
+ return cherrypy.url(path_info, relative=relative)
+
+
+ class Status(Test):
+
+ def index(self):
+ return "normal"
+
+ def blank(self):
+ cherrypy.response.status = ""
+
+ # According to RFC 2616, new status codes are OK as long as they
+ # are between 100 and 599.
+
+ # Here is an illegal code...
+ def illegal(self):
+ cherrypy.response.status = 781
+ return "oops"
+
+ # ...and here is an unknown but legal code.
+ def unknown(self):
+ cherrypy.response.status = "431 My custom error"
+ return "funky"
+
+ # Non-numeric code
+ def bad(self):
+ cherrypy.response.status = "error"
+ return "bad news"
+
+
+ class Redirect(Test):
+
+ class Error:
+ _cp_config = {"tools.err_redirect.on": True,
+ "tools.err_redirect.url": "/errpage",
+ "tools.err_redirect.internal": False,
+ }
+
+ def index(self):
+ raise NameError("redirect_test")
+ index.exposed = True
+ error = Error()
+
+ def index(self):
+ return "child"
+
+ def custom(self, url, code):
+ raise cherrypy.HTTPRedirect(url, code)
+
+ def by_code(self, code):
+ raise cherrypy.HTTPRedirect("somewhere%20else", code)
+ by_code._cp_config = {'tools.trailing_slash.extra': True}
+
+ def nomodify(self):
+ raise cherrypy.HTTPRedirect("", 304)
+
+ def proxy(self):
+ raise cherrypy.HTTPRedirect("proxy", 305)
+
+ def stringify(self):
+ return str(cherrypy.HTTPRedirect("/"))
+
+ def fragment(self, frag):
+ raise cherrypy.HTTPRedirect("/some/url#%s" % frag)
+
+ def login_redir():
+ if not getattr(cherrypy.request, "login", None):
+ raise cherrypy.InternalRedirect("/internalredirect/login")
+ tools.login_redir = _cptools.Tool('before_handler', login_redir)
+
+ def redir_custom():
+ raise cherrypy.InternalRedirect("/internalredirect/custom_err")
+
+ class InternalRedirect(Test):
+
+ def index(self):
+ raise cherrypy.InternalRedirect("/")
+
+ def choke(self):
+ return 3 / 0
+ choke.exposed = True
+ choke._cp_config = {'hooks.before_error_response': redir_custom}
+
+ def relative(self, a, b):
+ raise cherrypy.InternalRedirect("cousin?t=6")
+
+ def cousin(self, t):
+ assert cherrypy.request.prev.closed
+ return cherrypy.request.prev.query_string
+
+ def petshop(self, user_id):
+ if user_id == "parrot":
+ # Trade it for a slug when redirecting
+ raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=slug')
+ elif user_id == "terrier":
+ # Trade it for a fish when redirecting
+ raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=fish')
+ else:
+ # This should pass the user_id through to getImagesByUser
+ raise cherrypy.InternalRedirect(
+ '/image/getImagesByUser?user_id=%s' % str(user_id))
+
+ # We support Python 2.3, but the @-deco syntax would look like this:
+ # @tools.login_redir()
+ def secure(self):
+ return "Welcome!"
+ secure = tools.login_redir()(secure)
+ # Since calling the tool returns the same function you pass in,
+ # you could skip binding the return value, and just write:
+ # tools.login_redir()(secure)
+
+ def login(self):
+ return "Please log in"
+
+ def custom_err(self):
+ return "Something went horribly wrong."
+
+ def early_ir(self, arg):
+ return "whatever"
+ early_ir._cp_config = {'hooks.before_request_body': redir_custom}
+
+
+ class Image(Test):
+
+ def getImagesByUser(self, user_id):
+ return "0 images for %s" % user_id
+
+
+ class Flatten(Test):
+
+ def as_string(self):
+ return "content"
+
+ def as_list(self):
+ return ["con", "tent"]
+
+ def as_yield(self):
+ yield ntob("content")
+
+ def as_dblyield(self):
+ yield self.as_yield()
+ as_dblyield._cp_config = {'tools.flatten.on': True}
+
+ def as_refyield(self):
+ for chunk in self.as_yield():
+ yield chunk
+
+
+ class Ranges(Test):
+
+ def get_ranges(self, bytes):
+ return repr(httputil.get_ranges('bytes=%s' % bytes, 8))
+
+ def slice_file(self):
+ path = os.path.join(os.getcwd(), os.path.dirname(__file__))
+ return static.serve_file(os.path.join(path, "static/index.html"))
+
+
+ class Cookies(Test):
+
+ def single(self, name):
+ cookie = cherrypy.request.cookie[name]
+ # Python2's SimpleCookie.__setitem__ won't take unicode keys.
+ cherrypy.response.cookie[str(name)] = cookie.value
+
+ def multiple(self, names):
+ for name in names:
+ cookie = cherrypy.request.cookie[name]
+ # Python2's SimpleCookie.__setitem__ won't take unicode keys.
+ cherrypy.response.cookie[str(name)] = cookie.value
+
+
+ cherrypy.tree.mount(root)
+ setup_server = staticmethod(setup_server)
+
+
+ def testStatus(self):
+ self.getPage("/status/")
+ self.assertBody('normal')
+ self.assertStatus(200)
+
+ self.getPage("/status/blank")
+ self.assertBody('')
+ self.assertStatus(200)
+
+ self.getPage("/status/illegal")
+ self.assertStatus(500)
+ msg = "Illegal response status from server (781 is out of range)."
+ self.assertErrorPage(500, msg)
+
+ if not getattr(cherrypy.server, 'using_apache', False):
+ self.getPage("/status/unknown")
+ self.assertBody('funky')
+ self.assertStatus(431)
+
+ self.getPage("/status/bad")
+ self.assertStatus(500)
+ msg = "Illegal response status from server ('error' is non-numeric)."
+ self.assertErrorPage(500, msg)
+
+ def testSlashes(self):
+ # Test that requests for index methods without a trailing slash
+ # get redirected to the same URI path with a trailing slash.
+ # Make sure GET params are preserved.
+ self.getPage("/redirect?id=3")
+ self.assertStatus(301)
+ self.assertInBody("<a href='%s/redirect/?id=3'>"
+ "%s/redirect/?id=3</a>" % (self.base(), self.base()))
+
+ if self.prefix():
+ # Corner case: the "trailing slash" redirect could be tricky if
+ # we're using a virtual root and the URI is "/vroot" (no slash).
+ self.getPage("")
+ self.assertStatus(301)
+ self.assertInBody("<a href='%s/'>%s/</a>" %
+ (self.base(), self.base()))
+
+ # Test that requests for NON-index methods WITH a trailing slash
+ # get redirected to the same URI path WITHOUT a trailing slash.
+ # Make sure GET params are preserved.
+ self.getPage("/redirect/by_code/?code=307")
+ self.assertStatus(301)
+ self.assertInBody("<a href='%s/redirect/by_code?code=307'>"
+ "%s/redirect/by_code?code=307</a>"
+ % (self.base(), self.base()))
+
+ # If the trailing_slash tool is off, CP should just continue
+ # as if the slashes were correct. But it needs some help
+ # inside cherrypy.url to form correct output.
+ self.getPage('/url?path_info=page1')
+ self.assertBody('%s/url/page1' % self.base())
+ self.getPage('/url/leaf/?path_info=page1')
+ self.assertBody('%s/url/page1' % self.base())
+
+ def testRedirect(self):
+ self.getPage("/redirect/")
+ self.assertBody('child')
+ self.assertStatus(200)
+
+ self.getPage("/redirect/by_code?code=300")
+ self.assertMatchesBody(r"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
+ self.assertStatus(300)
+
+ self.getPage("/redirect/by_code?code=301")
+ self.assertMatchesBody(r"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
+ self.assertStatus(301)
+
+ self.getPage("/redirect/by_code?code=302")
+ self.assertMatchesBody(r"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
+ self.assertStatus(302)
+
+ self.getPage("/redirect/by_code?code=303")
+ self.assertMatchesBody(r"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
+ self.assertStatus(303)
+
+ self.getPage("/redirect/by_code?code=307")
+ self.assertMatchesBody(r"<a href='(.*)somewhere%20else'>\1somewhere%20else</a>")
+ self.assertStatus(307)
+
+ self.getPage("/redirect/nomodify")
+ self.assertBody('')
+ self.assertStatus(304)
+
+ self.getPage("/redirect/proxy")
+ self.assertBody('')
+ self.assertStatus(305)
+
+ # HTTPRedirect on error
+ self.getPage("/redirect/error/")
+ self.assertStatus(('302 Found', '303 See Other'))
+ self.assertInBody('/errpage')
+
+ # Make sure str(HTTPRedirect()) works.
+ self.getPage("/redirect/stringify", protocol="HTTP/1.0")
+ self.assertStatus(200)
+ self.assertBody("(['%s/'], 302)" % self.base())
+ if cherrypy.server.protocol_version == "HTTP/1.1":
+ self.getPage("/redirect/stringify", protocol="HTTP/1.1")
+ self.assertStatus(200)
+ self.assertBody("(['%s/'], 303)" % self.base())
+
+ # check that #fragments are handled properly
+ # http://skrb.org/ietf/http_errata.html#location-fragments
+ frag = "foo"
+ self.getPage("/redirect/fragment/%s" % frag)
+ self.assertMatchesBody(r"<a href='(.*)\/some\/url\#%s'>\1\/some\/url\#%s</a>" % (frag, frag))
+ loc = self.assertHeader('Location')
+ assert loc.endswith("#%s" % frag)
+ self.assertStatus(('302 Found', '303 See Other'))
+
+ # check injection protection
+ # See http://www.cherrypy.org/ticket/1003
+ self.getPage("/redirect/custom?code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval")
+ self.assertStatus(303)
+ loc = self.assertHeader('Location')
+ assert 'Set-Cookie' in loc
+ self.assertNoHeader('Set-Cookie')
+
+ def test_InternalRedirect(self):
+ # InternalRedirect
+ self.getPage("/internalredirect/")
+ self.assertBody('hello')
+ self.assertStatus(200)
+
+ # Test passthrough
+ self.getPage("/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film")
+ self.assertBody('0 images for Sir-not-appearing-in-this-film')
+ self.assertStatus(200)
+
+ # Test args
+ self.getPage("/internalredirect/petshop?user_id=parrot")
+ self.assertBody('0 images for slug')
+ self.assertStatus(200)
+
+ # Test POST
+ self.getPage("/internalredirect/petshop", method="POST",
+ body="user_id=terrier")
+ self.assertBody('0 images for fish')
+ self.assertStatus(200)
+
+ # Test ir before body read
+ self.getPage("/internalredirect/early_ir", method="POST",
+ body="arg=aha!")
+ self.assertBody("Something went horribly wrong.")
+ self.assertStatus(200)
+
+ self.getPage("/internalredirect/secure")
+ self.assertBody('Please log in')
+ self.assertStatus(200)
+
+ # Relative path in InternalRedirect.
+ # Also tests request.prev.
+ self.getPage("/internalredirect/relative?a=3&b=5")
+ self.assertBody("a=3&b=5")
+ self.assertStatus(200)
+
+ # InternalRedirect on error
+ self.getPage("/internalredirect/choke")
+ self.assertStatus(200)
+ self.assertBody("Something went horribly wrong.")
+
+ def testFlatten(self):
+ for url in ["/flatten/as_string", "/flatten/as_list",
+ "/flatten/as_yield", "/flatten/as_dblyield",
+ "/flatten/as_refyield"]:
+ self.getPage(url)
+ self.assertBody('content')
+
+ def testRanges(self):
+ self.getPage("/ranges/get_ranges?bytes=3-6")
+ self.assertBody("[(3, 7)]")
+
+ # Test multiple ranges and a suffix-byte-range-spec, for good measure.
+ self.getPage("/ranges/get_ranges?bytes=2-4,-1")
+ self.assertBody("[(2, 5), (7, 8)]")
+
+ # Get a partial file.
+ if cherrypy.server.protocol_version == "HTTP/1.1":
+ self.getPage("/ranges/slice_file", [('Range', 'bytes=2-5')])
+ self.assertStatus(206)
+ self.assertHeader("Content-Type", "text/html;charset=utf-8")
+ self.assertHeader("Content-Range", "bytes 2-5/14")
+ self.assertBody("llo,")
+
+ # What happens with overlapping ranges (and out of order, too)?
+ self.getPage("/ranges/slice_file", [('Range', 'bytes=4-6,2-5')])
+ self.assertStatus(206)
+ ct = self.assertHeader("Content-Type")
+ expected_type = "multipart/byteranges; boundary="
+ self.assert_(ct.startswith(expected_type))
+ boundary = ct[len(expected_type):]
+ expected_body = ("\r\n--%s\r\n"
+ "Content-type: text/html\r\n"
+ "Content-range: bytes 4-6/14\r\n"
+ "\r\n"
+ "o, \r\n"
+ "--%s\r\n"
+ "Content-type: text/html\r\n"
+ "Content-range: bytes 2-5/14\r\n"
+ "\r\n"
+ "llo,\r\n"
+ "--%s--\r\n" % (boundary, boundary, boundary))
+ self.assertBody(expected_body)
+ self.assertHeader("Content-Length")
+
+ # Test "416 Requested Range Not Satisfiable"
+ self.getPage("/ranges/slice_file", [('Range', 'bytes=2300-2900')])
+ self.assertStatus(416)
+ # "When this status code is returned for a byte-range request,
+ # the response SHOULD include a Content-Range entity-header
+ # field specifying the current length of the selected resource"
+ self.assertHeader("Content-Range", "bytes */14")
+ elif cherrypy.server.protocol_version == "HTTP/1.0":
+ # Test Range behavior with HTTP/1.0 request
+ self.getPage("/ranges/slice_file", [('Range', 'bytes=2-5')])
+ self.assertStatus(200)
+ self.assertBody("Hello, world\r\n")
+
+ def testFavicon(self):
+ # favicon.ico is served by staticfile.
+ icofilename = os.path.join(localDir, "../favicon.ico")
+ icofile = open(icofilename, "rb")
+ data = icofile.read()
+ icofile.close()
+
+ self.getPage("/favicon.ico")
+ self.assertBody(data)
+
+ def testCookies(self):
+ if sys.version_info >= (2, 5):
+ header_value = lambda x: x
+ else:
+ header_value = lambda x: x+';'
+
+ self.getPage("/cookies/single?name=First",
+ [('Cookie', 'First=Dinsdale;')])
+ self.assertHeader('Set-Cookie', header_value('First=Dinsdale'))
+
+ self.getPage("/cookies/multiple?names=First&names=Last",
+ [('Cookie', 'First=Dinsdale; Last=Piranha;'),
+ ])
+ self.assertHeader('Set-Cookie', header_value('First=Dinsdale'))
+ self.assertHeader('Set-Cookie', header_value('Last=Piranha'))
+
+ self.getPage("/cookies/single?name=Something-With:Colon",
+ [('Cookie', 'Something-With:Colon=some-value')])
+ self.assertStatus(400)
+
+ def testDefaultContentType(self):
+ self.getPage('/')
+ self.assertHeader('Content-Type', 'text/html;charset=utf-8')
+ self.getPage('/defct/plain')
+ self.getPage('/')
+ self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
+ self.getPage('/defct/html')
+
+ def test_cherrypy_url(self):
+ # Input relative to current
+ self.getPage('/url/leaf?path_info=page1')
+ self.assertBody('%s/url/page1' % self.base())
+ self.getPage('/url/?path_info=page1')
+ self.assertBody('%s/url/page1' % self.base())
+ # Other host header
+ host = 'www.mydomain.example'
+ self.getPage('/url/leaf?path_info=page1',
+ headers=[('Host', host)])
+ self.assertBody('%s://%s/url/page1' % (self.scheme, host))
+
+ # Input is 'absolute'; that is, relative to script_name
+ self.getPage('/url/leaf?path_info=/page1')
+ self.assertBody('%s/page1' % self.base())
+ self.getPage('/url/?path_info=/page1')
+ self.assertBody('%s/page1' % self.base())
+
+ # Single dots
+ self.getPage('/url/leaf?path_info=./page1')
+ self.assertBody('%s/url/page1' % self.base())
+ self.getPage('/url/leaf?path_info=other/./page1')
+ self.assertBody('%s/url/other/page1' % self.base())
+ self.getPage('/url/?path_info=/other/./page1')
+ self.assertBody('%s/other/page1' % self.base())
+
+ # Double dots
+ self.getPage('/url/leaf?path_info=../page1')
+ self.assertBody('%s/page1' % self.base())
+ self.getPage('/url/leaf?path_info=other/../page1')
+ self.assertBody('%s/url/page1' % self.base())
+ self.getPage('/url/leaf?path_info=/other/../page1')
+ self.assertBody('%s/page1' % self.base())
+
+ # Output relative to current path or script_name
+ self.getPage('/url/?path_info=page1&relative=True')
+ self.assertBody('page1')
+ self.getPage('/url/leaf?path_info=/page1&relative=True')
+ self.assertBody('../page1')
+ self.getPage('/url/leaf?path_info=page1&relative=True')
+ self.assertBody('page1')
+ self.getPage('/url/leaf?path_info=leaf/page1&relative=True')
+ self.assertBody('leaf/page1')
+ self.getPage('/url/leaf?path_info=../page1&relative=True')
+ self.assertBody('../page1')
+ self.getPage('/url/?path_info=other/../page1&relative=True')
+ self.assertBody('page1')
+
+ # Output relative to /
+ self.getPage('/baseurl?path_info=ab&relative=True')
+ self.assertBody('ab')
+ # Output relative to /
+ self.getPage('/baseurl?path_info=/ab&relative=True')
+ self.assertBody('ab')
+
+ # absolute-path references ("server-relative")
+ # Input relative to current
+ self.getPage('/url/leaf?path_info=page1&relative=server')
+ self.assertBody('/url/page1')
+ self.getPage('/url/?path_info=page1&relative=server')
+ self.assertBody('/url/page1')
+ # Input is 'absolute'; that is, relative to script_name
+ self.getPage('/url/leaf?path_info=/page1&relative=server')
+ self.assertBody('/page1')
+ self.getPage('/url/?path_info=/page1&relative=server')
+ self.assertBody('/page1')
+
+ def test_expose_decorator(self):
+ if not sys.version_info >= (2, 5):
+ return self.skip("skipped (Python 2.5+ only) ")
+
+ # Test @expose
+ self.getPage("/expose_dec/no_call")
+ self.assertStatus(200)
+ self.assertBody("Mr E. R. Bradshaw")
+
+ # Test @expose()
+ self.getPage("/expose_dec/call_empty")
+ self.assertStatus(200)
+ self.assertBody("Mrs. B.J. Smegma")
+
+ # Test @expose("alias")
+ self.getPage("/expose_dec/call_alias")
+ self.assertStatus(200)
+ self.assertBody("Mr Nesbitt")
+ # Does the original name work?
+ self.getPage("/expose_dec/nesbitt")
+ self.assertStatus(200)
+ self.assertBody("Mr Nesbitt")
+
+ # Test @expose(["alias1", "alias2"])
+ self.getPage("/expose_dec/alias1")
+ self.assertStatus(200)
+ self.assertBody("Mr Ken Andrews")
+ self.getPage("/expose_dec/alias2")
+ self.assertStatus(200)
+ self.assertBody("Mr Ken Andrews")
+ # Does the original name work?
+ self.getPage("/expose_dec/andrews")
+ self.assertStatus(200)
+ self.assertBody("Mr Ken Andrews")
+
+ # Test @expose(alias="alias")
+ self.getPage("/expose_dec/alias3")
+ self.assertStatus(200)
+ self.assertBody("Mr. and Mrs. Watson")
+
diff --git a/cherrypy/test/test_dynamicobjectmapping.py b/cherrypy/test/test_dynamicobjectmapping.py
new file mode 100755
index 0000000..1e04d08
--- /dev/null
+++ b/cherrypy/test/test_dynamicobjectmapping.py
@@ -0,0 +1,403 @@
+import cherrypy
+from cherrypy._cptree import Application
+from cherrypy.test import helper
+
+script_names = ["", "/foo", "/users/fred/blog", "/corp/blog"]
+
+
+
+def setup_server():
+ class SubSubRoot:
+ def index(self):
+ return "SubSubRoot index"
+ index.exposed = True
+
+ def default(self, *args):
+ return "SubSubRoot default"
+ default.exposed = True
+
+ def handler(self):
+ return "SubSubRoot handler"
+ handler.exposed = True
+
+ def dispatch(self):
+ return "SubSubRoot dispatch"
+ dispatch.exposed = True
+
+ subsubnodes = {
+ '1': SubSubRoot(),
+ '2': SubSubRoot(),
+ }
+
+ class SubRoot:
+ def index(self):
+ return "SubRoot index"
+ index.exposed = True
+
+ def default(self, *args):
+ return "SubRoot %s" % (args,)
+ default.exposed = True
+
+ def handler(self):
+ return "SubRoot handler"
+ handler.exposed = True
+
+ def _cp_dispatch(self, vpath):
+ return subsubnodes.get(vpath[0], None)
+
+ subnodes = {
+ '1': SubRoot(),
+ '2': SubRoot(),
+ }
+ class Root:
+ def index(self):
+ return "index"
+ index.exposed = True
+
+ def default(self, *args):
+ return "default %s" % (args,)
+ default.exposed = True
+
+ def handler(self):
+ return "handler"
+ handler.exposed = True
+
+ def _cp_dispatch(self, vpath):
+ return subnodes.get(vpath[0])
+
+ #--------------------------------------------------------------------------
+ # DynamicNodeAndMethodDispatcher example.
+ # This example exposes a fairly naive HTTP api
+ class User(object):
+ def __init__(self, id, name):
+ self.id = id
+ self.name = name
+
+ def __unicode__(self):
+ return unicode(self.name)
+
+ user_lookup = {
+ 1: User(1, 'foo'),
+ 2: User(2, 'bar'),
+ }
+
+ def make_user(name, id=None):
+ if not id:
+ id = max(*user_lookup.keys()) + 1
+ user_lookup[id] = User(id, name)
+ return id
+
+ class UserContainerNode(object):
+ exposed = True
+
+ def POST(self, name):
+ """
+ Allow the creation of a new Object
+ """
+ return "POST %d" % make_user(name)
+
+ def GET(self):
+ keys = user_lookup.keys()
+ keys.sort()
+ return unicode(keys)
+
+ def dynamic_dispatch(self, vpath):
+ try:
+ id = int(vpath[0])
+ except (ValueError, IndexError):
+ return None
+ return UserInstanceNode(id)
+
+ class UserInstanceNode(object):
+ exposed = True
+ def __init__(self, id):
+ self.id = id
+ self.user = user_lookup.get(id, None)
+
+ # For all but PUT methods there MUST be a valid user identified
+ # by self.id
+ if not self.user and cherrypy.request.method != 'PUT':
+ raise cherrypy.HTTPError(404)
+
+ def GET(self, *args, **kwargs):
+ """
+ Return the appropriate representation of the instance.
+ """
+ return unicode(self.user)
+
+ def POST(self, name):
+ """
+ Update the fields of the user instance.
+ """
+ self.user.name = name
+ return "POST %d" % self.user.id
+
+ def PUT(self, name):
+ """
+ Create a new user with the specified id, or edit it if it already exists
+ """
+ if self.user:
+ # Edit the current user
+ self.user.name = name
+ return "PUT %d" % self.user.id
+ else:
+ # Make a new user with said attributes.
+ return "PUT %d" % make_user(name, self.id)
+
+ def DELETE(self):
+ """
+ Delete the user specified at the id.
+ """
+ id = self.user.id
+ del user_lookup[self.user.id]
+ del self.user
+ return "DELETE %d" % id
+
+
+ class ABHandler:
+ class CustomDispatch:
+ def index(self, a, b):
+ return "custom"
+ index.exposed = True
+
+ def _cp_dispatch(self, vpath):
+ """Make sure that if we don't pop anything from vpath,
+ processing still works.
+ """
+ return self.CustomDispatch()
+
+ def index(self, a, b=None):
+ body = [ 'a:' + str(a) ]
+ if b is not None:
+ body.append(',b:' + str(b))
+ return ''.join(body)
+ index.exposed = True
+
+ def delete(self, a, b):
+ return 'deleting ' + str(a) + ' and ' + str(b)
+ delete.exposed = True
+
+ class IndexOnly:
+ def _cp_dispatch(self, vpath):
+ """Make sure that popping ALL of vpath still shows the index
+ handler.
+ """
+ while vpath:
+ vpath.pop()
+ return self
+
+ def index(self):
+ return "IndexOnly index"
+ index.exposed = True
+
+ class DecoratedPopArgs:
+ """Test _cp_dispatch with @cherrypy.popargs."""
+ def index(self):
+ return "no params"
+ index.exposed = True
+
+ def hi(self):
+ return "hi was not interpreted as 'a' param"
+ hi.exposed = True
+ DecoratedPopArgs = cherrypy.popargs('a', 'b', handler=ABHandler())(DecoratedPopArgs)
+
+ class NonDecoratedPopArgs:
+ """Test _cp_dispatch = cherrypy.popargs()"""
+
+ _cp_dispatch = cherrypy.popargs('a')
+
+ def index(self, a):
+ return "index: " + str(a)
+ index.exposed = True
+
+ class ParameterizedHandler:
+ """Special handler created for each request"""
+
+ def __init__(self, a):
+ self.a = a
+
+ def index(self):
+ if 'a' in cherrypy.request.params:
+ raise Exception("Parameterized handler argument ended up in request.params")
+ return self.a
+ index.exposed = True
+
+ class ParameterizedPopArgs:
+ """Test cherrypy.popargs() with a function call handler"""
+ ParameterizedPopArgs = cherrypy.popargs('a', handler=ParameterizedHandler)(ParameterizedPopArgs)
+
+ Root.decorated = DecoratedPopArgs()
+ Root.undecorated = NonDecoratedPopArgs()
+ Root.index_only = IndexOnly()
+ Root.parameter_test = ParameterizedPopArgs()
+
+ Root.users = UserContainerNode()
+
+ md = cherrypy.dispatch.MethodDispatcher('dynamic_dispatch')
+ for url in script_names:
+ conf = {'/': {
+ 'user': (url or "/").split("/")[-2],
+ },
+ '/users': {
+ 'request.dispatch': md
+ },
+ }
+ cherrypy.tree.mount(Root(), url, conf)
+
+class DynamicObjectMappingTest(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def testObjectMapping(self):
+ for url in script_names:
+ prefix = self.script_name = url
+
+ self.getPage('/')
+ self.assertBody('index')
+
+ self.getPage('/handler')
+ self.assertBody('handler')
+
+ # Dynamic dispatch will succeed here for the subnodes
+ # so the subroot gets called
+ self.getPage('/1/')
+ self.assertBody('SubRoot index')
+
+ self.getPage('/2/')
+ self.assertBody('SubRoot index')
+
+ self.getPage('/1/handler')
+ self.assertBody('SubRoot handler')
+
+ self.getPage('/2/handler')
+ self.assertBody('SubRoot handler')
+
+ # Dynamic dispatch will fail here for the subnodes
+ # so the default gets called
+ self.getPage('/asdf/')
+ self.assertBody("default ('asdf',)")
+
+ self.getPage('/asdf/asdf')
+ self.assertBody("default ('asdf', 'asdf')")
+
+ self.getPage('/asdf/handler')
+ self.assertBody("default ('asdf', 'handler')")
+
+ # Dynamic dispatch will succeed here for the subsubnodes
+ # so the subsubroot gets called
+ self.getPage('/1/1/')
+ self.assertBody('SubSubRoot index')
+
+ self.getPage('/2/2/')
+ self.assertBody('SubSubRoot index')
+
+ self.getPage('/1/1/handler')
+ self.assertBody('SubSubRoot handler')
+
+ self.getPage('/2/2/handler')
+ self.assertBody('SubSubRoot handler')
+
+ self.getPage('/2/2/dispatch')
+ self.assertBody('SubSubRoot dispatch')
+
+ # The exposed dispatch will not be called as a dispatch
+ # method.
+ self.getPage('/2/2/foo/foo')
+ self.assertBody("SubSubRoot default")
+
+ # Dynamic dispatch will fail here for the subsubnodes
+ # so the SubRoot gets called
+ self.getPage('/1/asdf/')
+ self.assertBody("SubRoot ('asdf',)")
+
+ self.getPage('/1/asdf/asdf')
+ self.assertBody("SubRoot ('asdf', 'asdf')")
+
+ self.getPage('/1/asdf/handler')
+ self.assertBody("SubRoot ('asdf', 'handler')")
+
+ def testMethodDispatch(self):
+ # GET acts like a container
+ self.getPage("/users")
+ self.assertBody("[1, 2]")
+ self.assertHeader('Allow', 'GET, HEAD, POST')
+
+ # POST to the container URI allows creation
+ self.getPage("/users", method="POST", body="name=baz")
+ self.assertBody("POST 3")
+ self.assertHeader('Allow', 'GET, HEAD, POST')
+
+ # POST to a specific instanct URI results in a 404
+ # as the resource does not exit.
+ self.getPage("/users/5", method="POST", body="name=baz")
+ self.assertStatus(404)
+
+ # PUT to a specific instanct URI results in creation
+ self.getPage("/users/5", method="PUT", body="name=boris")
+ self.assertBody("PUT 5")
+ self.assertHeader('Allow', 'DELETE, GET, HEAD, POST, PUT')
+
+ # GET acts like a container
+ self.getPage("/users")
+ self.assertBody("[1, 2, 3, 5]")
+ self.assertHeader('Allow', 'GET, HEAD, POST')
+
+ test_cases = (
+ (1, 'foo', 'fooupdated', 'DELETE, GET, HEAD, POST, PUT'),
+ (2, 'bar', 'barupdated', 'DELETE, GET, HEAD, POST, PUT'),
+ (3, 'baz', 'bazupdated', 'DELETE, GET, HEAD, POST, PUT'),
+ (5, 'boris', 'borisupdated', 'DELETE, GET, HEAD, POST, PUT'),
+ )
+ for id, name, updatedname, headers in test_cases:
+ self.getPage("/users/%d" % id)
+ self.assertBody(name)
+ self.assertHeader('Allow', headers)
+
+ # Make sure POSTs update already existings resources
+ self.getPage("/users/%d" % id, method='POST', body="name=%s" % updatedname)
+ self.assertBody("POST %d" % id)
+ self.assertHeader('Allow', headers)
+
+ # Make sure PUTs Update already existing resources.
+ self.getPage("/users/%d" % id, method='PUT', body="name=%s" % updatedname)
+ self.assertBody("PUT %d" % id)
+ self.assertHeader('Allow', headers)
+
+ # Make sure DELETES Remove already existing resources.
+ self.getPage("/users/%d" % id, method='DELETE')
+ self.assertBody("DELETE %d" % id)
+ self.assertHeader('Allow', headers)
+
+
+ # GET acts like a container
+ self.getPage("/users")
+ self.assertBody("[]")
+ self.assertHeader('Allow', 'GET, HEAD, POST')
+
+ def testVpathDispatch(self):
+ self.getPage("/decorated/")
+ self.assertBody("no params")
+
+ self.getPage("/decorated/hi")
+ self.assertBody("hi was not interpreted as 'a' param")
+
+ self.getPage("/decorated/yo/")
+ self.assertBody("a:yo")
+
+ self.getPage("/decorated/yo/there/")
+ self.assertBody("a:yo,b:there")
+
+ self.getPage("/decorated/yo/there/delete")
+ self.assertBody("deleting yo and there")
+
+ self.getPage("/decorated/yo/there/handled_by_dispatch/")
+ self.assertBody("custom")
+
+ self.getPage("/undecorated/blah/")
+ self.assertBody("index: blah")
+
+ self.getPage("/index_only/a/b/c/d/e/f/g/")
+ self.assertBody("IndexOnly index")
+
+ self.getPage("/parameter_test/argument2/")
+ self.assertBody("argument2")
+
diff --git a/cherrypy/test/test_encoding.py b/cherrypy/test/test_encoding.py
new file mode 100755
index 0000000..67b28ed
--- /dev/null
+++ b/cherrypy/test/test_encoding.py
@@ -0,0 +1,363 @@
+
+import gzip
+import sys
+
+import cherrypy
+from cherrypy._cpcompat import BytesIO, IncompleteRead, ntob, ntou
+
+europoundUnicode = ntou('\x80\xa3')
+sing = u"\u6bdb\u6cfd\u4e1c: Sing, Little Birdie?"
+sing8 = sing.encode('utf-8')
+sing16 = sing.encode('utf-16')
+
+
+from cherrypy.test import helper
+
+
+class EncodingTests(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def index(self, param):
+ assert param == europoundUnicode, "%r != %r" % (param, europoundUnicode)
+ yield europoundUnicode
+ index.exposed = True
+
+ def mao_zedong(self):
+ return sing
+ mao_zedong.exposed = True
+
+ def utf8(self):
+ return sing8
+ utf8.exposed = True
+ utf8._cp_config = {'tools.encode.encoding': 'utf-8'}
+
+ def cookies_and_headers(self):
+ # if the headers have non-ascii characters and a cookie has
+ # any part which is unicode (even ascii), the response
+ # should not fail.
+ cherrypy.response.cookie['candy'] = 'bar'
+ cherrypy.response.cookie['candy']['domain'] = 'cherrypy.org'
+ cherrypy.response.headers['Some-Header'] = 'My d\xc3\xb6g has fleas'
+ return 'Any content'
+ cookies_and_headers.exposed = True
+
+ def reqparams(self, *args, **kwargs):
+ return ntob(', ').join([": ".join((k, v)).encode('utf8')
+ for k, v in cherrypy.request.params.items()])
+ reqparams.exposed = True
+
+ def nontext(self, *args, **kwargs):
+ cherrypy.response.headers['Content-Type'] = 'application/binary'
+ return '\x00\x01\x02\x03'
+ nontext.exposed = True
+ nontext._cp_config = {'tools.encode.text_only': False,
+ 'tools.encode.add_charset': True,
+ }
+
+ class GZIP:
+ def index(self):
+ yield "Hello, world"
+ index.exposed = True
+
+ def noshow(self):
+ # Test for ticket #147, where yield showed no exceptions (content-
+ # encoding was still gzip even though traceback wasn't zipped).
+ raise IndexError()
+ yield "Here be dragons"
+ noshow.exposed = True
+ # Turn encoding off so the gzip tool is the one doing the collapse.
+ noshow._cp_config = {'tools.encode.on': False}
+
+ def noshow_stream(self):
+ # Test for ticket #147, where yield showed no exceptions (content-
+ # encoding was still gzip even though traceback wasn't zipped).
+ raise IndexError()
+ yield "Here be dragons"
+ noshow_stream.exposed = True
+ noshow_stream._cp_config = {'response.stream': True}
+
+ class Decode:
+ def extra_charset(self, *args, **kwargs):
+ return ', '.join([": ".join((k, v))
+ for k, v in cherrypy.request.params.items()])
+ extra_charset.exposed = True
+ extra_charset._cp_config = {
+ 'tools.decode.on': True,
+ 'tools.decode.default_encoding': ['utf-16'],
+ }
+
+ def force_charset(self, *args, **kwargs):
+ return ', '.join([": ".join((k, v))
+ for k, v in cherrypy.request.params.items()])
+ force_charset.exposed = True
+ force_charset._cp_config = {
+ 'tools.decode.on': True,
+ 'tools.decode.encoding': 'utf-16',
+ }
+
+ root = Root()
+ root.gzip = GZIP()
+ root.decode = Decode()
+ cherrypy.tree.mount(root, config={'/gzip': {'tools.gzip.on': True}})
+ setup_server = staticmethod(setup_server)
+
+ def test_query_string_decoding(self):
+ europoundUtf8 = europoundUnicode.encode('utf-8')
+ self.getPage(ntob('/?param=') + europoundUtf8)
+ self.assertBody(europoundUtf8)
+
+ # Encoded utf8 query strings MUST be parsed correctly.
+ # Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX
+ self.getPage("/reqparams?q=%C2%A3")
+ # The return value will be encoded as utf8.
+ self.assertBody(ntob("q: \xc2\xa3"))
+
+ # Query strings that are incorrectly encoded MUST raise 404.
+ # Here, q is the POUND SIGN U+00A3 encoded in latin1 and then %HEX
+ self.getPage("/reqparams?q=%A3")
+ self.assertStatus(404)
+ self.assertErrorPage(404,
+ "The given query string could not be processed. Query "
+ "strings for this resource must be encoded with 'utf8'.")
+
+ def test_urlencoded_decoding(self):
+ # Test the decoding of an application/x-www-form-urlencoded entity.
+ europoundUtf8 = europoundUnicode.encode('utf-8')
+ body=ntob("param=") + europoundUtf8
+ self.getPage('/', method='POST',
+ headers=[("Content-Type", "application/x-www-form-urlencoded"),
+ ("Content-Length", str(len(body))),
+ ],
+ body=body),
+ self.assertBody(europoundUtf8)
+
+ # Encoded utf8 entities MUST be parsed and decoded correctly.
+ # Here, q is the POUND SIGN U+00A3 encoded in utf8
+ body = ntob("q=\xc2\xa3")
+ self.getPage('/reqparams', method='POST',
+ headers=[("Content-Type", "application/x-www-form-urlencoded"),
+ ("Content-Length", str(len(body))),
+ ],
+ body=body),
+ self.assertBody(ntob("q: \xc2\xa3"))
+
+ # ...and in utf16, which is not in the default attempt_charsets list:
+ body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00")
+ self.getPage('/reqparams', method='POST',
+ headers=[("Content-Type", "application/x-www-form-urlencoded;charset=utf-16"),
+ ("Content-Length", str(len(body))),
+ ],
+ body=body),
+ self.assertBody(ntob("q: \xc2\xa3"))
+
+ # Entities that are incorrectly encoded MUST raise 400.
+ # Here, q is the POUND SIGN U+00A3 encoded in utf16, but
+ # the Content-Type incorrectly labels it utf-8.
+ body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00")
+ self.getPage('/reqparams', method='POST',
+ headers=[("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"),
+ ("Content-Length", str(len(body))),
+ ],
+ body=body),
+ self.assertStatus(400)
+ self.assertErrorPage(400,
+ "The request entity could not be decoded. The following charsets "
+ "were attempted: ['utf-8']")
+
+ def test_decode_tool(self):
+ # An extra charset should be tried first, and succeed if it matches.
+ # Here, we add utf-16 as a charset and pass a utf-16 body.
+ body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00")
+ self.getPage('/decode/extra_charset', method='POST',
+ headers=[("Content-Type", "application/x-www-form-urlencoded"),
+ ("Content-Length", str(len(body))),
+ ],
+ body=body),
+ self.assertBody(ntob("q: \xc2\xa3"))
+
+ # An extra charset should be tried first, and continue to other default
+ # charsets if it doesn't match.
+ # Here, we add utf-16 as a charset but still pass a utf-8 body.
+ body = ntob("q=\xc2\xa3")
+ self.getPage('/decode/extra_charset', method='POST',
+ headers=[("Content-Type", "application/x-www-form-urlencoded"),
+ ("Content-Length", str(len(body))),
+ ],
+ body=body),
+ self.assertBody(ntob("q: \xc2\xa3"))
+
+ # An extra charset should error if force is True and it doesn't match.
+ # Here, we force utf-16 as a charset but still pass a utf-8 body.
+ body = ntob("q=\xc2\xa3")
+ self.getPage('/decode/force_charset', method='POST',
+ headers=[("Content-Type", "application/x-www-form-urlencoded"),
+ ("Content-Length", str(len(body))),
+ ],
+ body=body),
+ self.assertErrorPage(400,
+ "The request entity could not be decoded. The following charsets "
+ "were attempted: ['utf-16']")
+
+ def test_multipart_decoding(self):
+ # Test the decoding of a multipart entity when the charset (utf16) is
+ # explicitly given.
+ body=ntob('\r\n'.join(['--X',
+ 'Content-Type: text/plain;charset=utf-16',
+ 'Content-Disposition: form-data; name="text"',
+ '',
+ '\xff\xfea\x00b\x00\x1c c\x00',
+ '--X',
+ 'Content-Type: text/plain;charset=utf-16',
+ 'Content-Disposition: form-data; name="submit"',
+ '',
+ '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
+ '--X--']))
+ self.getPage('/reqparams', method='POST',
+ headers=[("Content-Type", "multipart/form-data;boundary=X"),
+ ("Content-Length", str(len(body))),
+ ],
+ body=body),
+ self.assertBody(ntob("text: ab\xe2\x80\x9cc, submit: Create"))
+
+ def test_multipart_decoding_no_charset(self):
+ # Test the decoding of a multipart entity when the charset (utf8) is
+ # NOT explicitly given, but is in the list of charsets to attempt.
+ body=ntob('\r\n'.join(['--X',
+ 'Content-Disposition: form-data; name="text"',
+ '',
+ '\xe2\x80\x9c',
+ '--X',
+ 'Content-Disposition: form-data; name="submit"',
+ '',
+ 'Create',
+ '--X--']))
+ self.getPage('/reqparams', method='POST',
+ headers=[("Content-Type", "multipart/form-data;boundary=X"),
+ ("Content-Length", str(len(body))),
+ ],
+ body=body),
+ self.assertBody(ntob("text: \xe2\x80\x9c, submit: Create"))
+
+ def test_multipart_decoding_no_successful_charset(self):
+ # Test the decoding of a multipart entity when the charset (utf16) is
+ # NOT explicitly given, and is NOT in the list of charsets to attempt.
+ body=ntob('\r\n'.join(['--X',
+ 'Content-Disposition: form-data; name="text"',
+ '',
+ '\xff\xfea\x00b\x00\x1c c\x00',
+ '--X',
+ 'Content-Disposition: form-data; name="submit"',
+ '',
+ '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
+ '--X--']))
+ self.getPage('/reqparams', method='POST',
+ headers=[("Content-Type", "multipart/form-data;boundary=X"),
+ ("Content-Length", str(len(body))),
+ ],
+ body=body),
+ self.assertStatus(400)
+ self.assertErrorPage(400,
+ "The request entity could not be decoded. The following charsets "
+ "were attempted: ['us-ascii', 'utf-8']")
+
+ def test_nontext(self):
+ self.getPage('/nontext')
+ self.assertHeader('Content-Type', 'application/binary;charset=utf-8')
+ self.assertBody('\x00\x01\x02\x03')
+
+ def testEncoding(self):
+ # Default encoding should be utf-8
+ self.getPage('/mao_zedong')
+ self.assertBody(sing8)
+
+ # Ask for utf-16.
+ self.getPage('/mao_zedong', [('Accept-Charset', 'utf-16')])
+ self.assertHeader('Content-Type', 'text/html;charset=utf-16')
+ self.assertBody(sing16)
+
+ # Ask for multiple encodings. ISO-8859-1 should fail, and utf-16
+ # should be produced.
+ self.getPage('/mao_zedong', [('Accept-Charset',
+ 'iso-8859-1;q=1, utf-16;q=0.5')])
+ self.assertBody(sing16)
+
+ # The "*" value should default to our default_encoding, utf-8
+ self.getPage('/mao_zedong', [('Accept-Charset', '*;q=1, utf-7;q=.2')])
+ self.assertBody(sing8)
+
+ # Only allow iso-8859-1, which should fail and raise 406.
+ self.getPage('/mao_zedong', [('Accept-Charset', 'iso-8859-1, *;q=0')])
+ self.assertStatus("406 Not Acceptable")
+ self.assertInBody("Your client sent this Accept-Charset header: "
+ "iso-8859-1, *;q=0. We tried these charsets: "
+ "iso-8859-1.")
+
+ # Ask for x-mac-ce, which should be unknown. See ticket #569.
+ self.getPage('/mao_zedong', [('Accept-Charset',
+ 'us-ascii, ISO-8859-1, x-mac-ce')])
+ self.assertStatus("406 Not Acceptable")
+ self.assertInBody("Your client sent this Accept-Charset header: "
+ "us-ascii, ISO-8859-1, x-mac-ce. We tried these "
+ "charsets: ISO-8859-1, us-ascii, x-mac-ce.")
+
+ # Test the 'encoding' arg to encode.
+ self.getPage('/utf8')
+ self.assertBody(sing8)
+ self.getPage('/utf8', [('Accept-Charset', 'us-ascii, ISO-8859-1')])
+ self.assertStatus("406 Not Acceptable")
+
+ def testGzip(self):
+ zbuf = BytesIO()
+ zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
+ zfile.write(ntob("Hello, world"))
+ zfile.close()
+
+ self.getPage('/gzip/', headers=[("Accept-Encoding", "gzip")])
+ self.assertInBody(zbuf.getvalue()[:3])
+ self.assertHeader("Vary", "Accept-Encoding")
+ self.assertHeader("Content-Encoding", "gzip")
+
+ # Test when gzip is denied.
+ self.getPage('/gzip/', headers=[("Accept-Encoding", "identity")])
+ self.assertHeader("Vary", "Accept-Encoding")
+ self.assertNoHeader("Content-Encoding")
+ self.assertBody("Hello, world")
+
+ self.getPage('/gzip/', headers=[("Accept-Encoding", "gzip;q=0")])
+ self.assertHeader("Vary", "Accept-Encoding")
+ self.assertNoHeader("Content-Encoding")
+ self.assertBody("Hello, world")
+
+ self.getPage('/gzip/', headers=[("Accept-Encoding", "*;q=0")])
+ self.assertStatus(406)
+ self.assertNoHeader("Content-Encoding")
+ self.assertErrorPage(406, "identity, gzip")
+
+ # Test for ticket #147
+ self.getPage('/gzip/noshow', headers=[("Accept-Encoding", "gzip")])
+ self.assertNoHeader('Content-Encoding')
+ self.assertStatus(500)
+ self.assertErrorPage(500, pattern="IndexError\n")
+
+ # In this case, there's nothing we can do to deliver a
+ # readable page, since 1) the gzip header is already set,
+ # and 2) we may have already written some of the body.
+ # The fix is to never stream yields when using gzip.
+ if (cherrypy.server.protocol_version == "HTTP/1.0" or
+ getattr(cherrypy.server, "using_apache", False)):
+ self.getPage('/gzip/noshow_stream',
+ headers=[("Accept-Encoding", "gzip")])
+ self.assertHeader('Content-Encoding', 'gzip')
+ self.assertInBody('\x1f\x8b\x08\x00')
+ else:
+ # The wsgiserver will simply stop sending data, and the HTTP client
+ # will error due to an incomplete chunk-encoded stream.
+ self.assertRaises((ValueError, IncompleteRead), self.getPage,
+ '/gzip/noshow_stream',
+ headers=[("Accept-Encoding", "gzip")])
+
+ def test_UnicodeHeaders(self):
+ self.getPage('/cookies_and_headers')
+ self.assertBody('Any content')
+
diff --git a/cherrypy/test/test_etags.py b/cherrypy/test/test_etags.py
new file mode 100755
index 0000000..026f9d6
--- /dev/null
+++ b/cherrypy/test/test_etags.py
@@ -0,0 +1,81 @@
+import cherrypy
+from cherrypy.test import helper
+
+
+class ETagTest(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def resource(self):
+ return "Oh wah ta goo Siam."
+ resource.exposed = True
+
+ def fail(self, code):
+ code = int(code)
+ if 300 <= code <= 399:
+ raise cherrypy.HTTPRedirect([], code)
+ else:
+ raise cherrypy.HTTPError(code)
+ fail.exposed = True
+
+ def unicoded(self):
+ return u'I am a \u1ee4nicode string.'
+ unicoded.exposed = True
+ unicoded._cp_config = {'tools.encode.on': True}
+
+ conf = {'/': {'tools.etags.on': True,
+ 'tools.etags.autotags': True,
+ }}
+ cherrypy.tree.mount(Root(), config=conf)
+ setup_server = staticmethod(setup_server)
+
+ def test_etags(self):
+ self.getPage("/resource")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'text/html;charset=utf-8')
+ self.assertBody('Oh wah ta goo Siam.')
+ etag = self.assertHeader('ETag')
+
+ # Test If-Match (both valid and invalid)
+ self.getPage("/resource", headers=[('If-Match', etag)])
+ self.assertStatus("200 OK")
+ self.getPage("/resource", headers=[('If-Match', "*")])
+ self.assertStatus("200 OK")
+ self.getPage("/resource", headers=[('If-Match', "*")], method="POST")
+ self.assertStatus("200 OK")
+ self.getPage("/resource", headers=[('If-Match', "a bogus tag")])
+ self.assertStatus("412 Precondition Failed")
+
+ # Test If-None-Match (both valid and invalid)
+ self.getPage("/resource", headers=[('If-None-Match', etag)])
+ self.assertStatus(304)
+ self.getPage("/resource", method='POST', headers=[('If-None-Match', etag)])
+ self.assertStatus("412 Precondition Failed")
+ self.getPage("/resource", headers=[('If-None-Match', "*")])
+ self.assertStatus(304)
+ self.getPage("/resource", headers=[('If-None-Match', "a bogus tag")])
+ self.assertStatus("200 OK")
+
+ def test_errors(self):
+ self.getPage("/resource")
+ self.assertStatus(200)
+ etag = self.assertHeader('ETag')
+
+ # Test raising errors in page handler
+ self.getPage("/fail/412", headers=[('If-Match', etag)])
+ self.assertStatus(412)
+ self.getPage("/fail/304", headers=[('If-Match', etag)])
+ self.assertStatus(304)
+ self.getPage("/fail/412", headers=[('If-None-Match', "*")])
+ self.assertStatus(412)
+ self.getPage("/fail/304", headers=[('If-None-Match', "*")])
+ self.assertStatus(304)
+
+ def test_unicode_body(self):
+ self.getPage("/unicoded")
+ self.assertStatus(200)
+ etag1 = self.assertHeader('ETag')
+ self.getPage("/unicoded", headers=[('If-Match', etag1)])
+ self.assertStatus(200)
+ self.assertHeader('ETag', etag1)
+
diff --git a/cherrypy/test/test_http.py b/cherrypy/test/test_http.py
new file mode 100755
index 0000000..eb72b5b
--- /dev/null
+++ b/cherrypy/test/test_http.py
@@ -0,0 +1,168 @@
+"""Tests for managing HTTP issues (malformed requests, etc)."""
+
+import mimetypes
+
+import cherrypy
+from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob
+
+
+def encode_multipart_formdata(files):
+ """Return (content_type, body) ready for httplib.HTTP instance.
+
+ files: a sequence of (name, filename, value) tuples for multipart uploads.
+ """
+ BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$'
+ L = []
+ for key, filename, value in files:
+ L.append('--' + BOUNDARY)
+ L.append('Content-Disposition: form-data; name="%s"; filename="%s"' %
+ (key, filename))
+ ct = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
+ L.append('Content-Type: %s' % ct)
+ L.append('')
+ L.append(value)
+ L.append('--' + BOUNDARY + '--')
+ L.append('')
+ body = '\r\n'.join(L)
+ content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
+ return content_type, body
+
+
+
+
+from cherrypy.test import helper
+
+class HTTPTests(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def index(self, *args, **kwargs):
+ return "Hello world!"
+ index.exposed = True
+
+ def no_body(self, *args, **kwargs):
+ return "Hello world!"
+ no_body.exposed = True
+ no_body._cp_config = {'request.process_request_body': False}
+
+ def post_multipart(self, file):
+ """Return a summary ("a * 65536\nb * 65536") of the uploaded file."""
+ contents = file.file.read()
+ summary = []
+ curchar = ""
+ count = 0
+ for c in contents:
+ if c == curchar:
+ count += 1
+ else:
+ if count:
+ summary.append("%s * %d" % (curchar, count))
+ count = 1
+ curchar = c
+ if count:
+ summary.append("%s * %d" % (curchar, count))
+ return ", ".join(summary)
+ post_multipart.exposed = True
+
+ cherrypy.tree.mount(Root())
+ cherrypy.config.update({'server.max_request_body_size': 30000000})
+ setup_server = staticmethod(setup_server)
+
+ def test_no_content_length(self):
+ # "The presence of a message-body in a request is signaled by the
+ # inclusion of a Content-Length or Transfer-Encoding header field in
+ # the request's message-headers."
+ #
+ # Send a message with neither header and no body. Even though
+ # the request is of method POST, this should be OK because we set
+ # request.process_request_body to False for our handler.
+ if self.scheme == "https":
+ c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
+ else:
+ c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+ c.request("POST", "/no_body")
+ response = c.getresponse()
+ self.body = response.fp.read()
+ self.status = str(response.status)
+ self.assertStatus(200)
+ self.assertBody(ntob('Hello world!'))
+
+ # Now send a message that has no Content-Length, but does send a body.
+ # Verify that CP times out the socket and responds
+ # with 411 Length Required.
+ if self.scheme == "https":
+ c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
+ else:
+ c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+ c.request("POST", "/")
+ response = c.getresponse()
+ self.body = response.fp.read()
+ self.status = str(response.status)
+ self.assertStatus(411)
+
+ def test_post_multipart(self):
+ alphabet = "abcdefghijklmnopqrstuvwxyz"
+ # generate file contents for a large post
+ contents = "".join([c * 65536 for c in alphabet])
+
+ # encode as multipart form data
+ files=[('file', 'file.txt', contents)]
+ content_type, body = encode_multipart_formdata(files)
+ body = body.encode('Latin-1')
+
+ # post file
+ if self.scheme == 'https':
+ c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
+ else:
+ c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+ c.putrequest('POST', '/post_multipart')
+ c.putheader('Content-Type', content_type)
+ c.putheader('Content-Length', str(len(body)))
+ c.endheaders()
+ c.send(body)
+
+ response = c.getresponse()
+ self.body = response.fp.read()
+ self.status = str(response.status)
+ self.assertStatus(200)
+ self.assertBody(", ".join(["%s * 65536" % c for c in alphabet]))
+
+ def test_malformed_request_line(self):
+ if getattr(cherrypy.server, "using_apache", False):
+ return self.skip("skipped due to known Apache differences...")
+
+ # Test missing version in Request-Line
+ if self.scheme == 'https':
+ c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
+ else:
+ c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+ c._output(ntob('GET /'))
+ c._send_output()
+ if hasattr(c, 'strict'):
+ response = c.response_class(c.sock, strict=c.strict, method='GET')
+ else:
+ # Python 3.2 removed the 'strict' feature, saying:
+ # "http.client now always assumes HTTP/1.x compliant servers."
+ response = c.response_class(c.sock, method='GET')
+ response.begin()
+ self.assertEqual(response.status, 400)
+ self.assertEqual(response.fp.read(22), ntob("Malformed Request-Line"))
+ c.close()
+
+ def test_malformed_header(self):
+ if self.scheme == 'https':
+ c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
+ else:
+ c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+ c.putrequest('GET', '/')
+ c.putheader('Content-Type', 'text/plain')
+ # See http://www.cherrypy.org/ticket/941
+ c._output(ntob('Re, 1.2.3.4#015#012'))
+ c.endheaders()
+
+ response = c.getresponse()
+ self.status = str(response.status)
+ self.assertStatus(400)
+ self.body = response.fp.read(20)
+ self.assertBody("Illegal header line.")
+
diff --git a/cherrypy/test/test_httpauth.py b/cherrypy/test/test_httpauth.py
new file mode 100755
index 0000000..9d0eecb
--- /dev/null
+++ b/cherrypy/test/test_httpauth.py
@@ -0,0 +1,151 @@
+import cherrypy
+from cherrypy._cpcompat import md5, sha, ntob
+from cherrypy.lib import httpauth
+
+from cherrypy.test import helper
+
+class HTTPAuthTest(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def index(self):
+ return "This is public."
+ index.exposed = True
+
+ class DigestProtected:
+ def index(self):
+ return "Hello %s, you've been authorized." % cherrypy.request.login
+ index.exposed = True
+
+ class BasicProtected:
+ def index(self):
+ return "Hello %s, you've been authorized." % cherrypy.request.login
+ index.exposed = True
+
+ class BasicProtected2:
+ def index(self):
+ return "Hello %s, you've been authorized." % cherrypy.request.login
+ index.exposed = True
+
+ def fetch_users():
+ return {'test': 'test'}
+
+ def sha_password_encrypter(password):
+ return sha(ntob(password)).hexdigest()
+
+ def fetch_password(username):
+ return sha(ntob('test')).hexdigest()
+
+ conf = {'/digest': {'tools.digest_auth.on': True,
+ 'tools.digest_auth.realm': 'localhost',
+ 'tools.digest_auth.users': fetch_users},
+ '/basic': {'tools.basic_auth.on': True,
+ 'tools.basic_auth.realm': 'localhost',
+ 'tools.basic_auth.users': {'test': md5(ntob('test')).hexdigest()}},
+ '/basic2': {'tools.basic_auth.on': True,
+ 'tools.basic_auth.realm': 'localhost',
+ 'tools.basic_auth.users': fetch_password,
+ 'tools.basic_auth.encrypt': sha_password_encrypter}}
+
+ root = Root()
+ root.digest = DigestProtected()
+ root.basic = BasicProtected()
+ root.basic2 = BasicProtected2()
+ cherrypy.tree.mount(root, config=conf)
+ setup_server = staticmethod(setup_server)
+
+
+ def testPublic(self):
+ self.getPage("/")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'text/html;charset=utf-8')
+ self.assertBody('This is public.')
+
+ def testBasic(self):
+ self.getPage("/basic/")
+ self.assertStatus(401)
+ self.assertHeader('WWW-Authenticate', 'Basic realm="localhost"')
+
+ self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZX60')])
+ self.assertStatus(401)
+
+ self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZXN0')])
+ self.assertStatus('200 OK')
+ self.assertBody("Hello test, you've been authorized.")
+
+ def testBasic2(self):
+ self.getPage("/basic2/")
+ self.assertStatus(401)
+ self.assertHeader('WWW-Authenticate', 'Basic realm="localhost"')
+
+ self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZX60')])
+ self.assertStatus(401)
+
+ self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZXN0')])
+ self.assertStatus('200 OK')
+ self.assertBody("Hello test, you've been authorized.")
+
+ def testDigest(self):
+ self.getPage("/digest/")
+ self.assertStatus(401)
+
+ value = None
+ for k, v in self.headers:
+ if k.lower() == "www-authenticate":
+ if v.startswith("Digest"):
+ value = v
+ break
+
+ if value is None:
+ self._handlewebError("Digest authentification scheme was not found")
+
+ value = value[7:]
+ items = value.split(', ')
+ tokens = {}
+ for item in items:
+ key, value = item.split('=')
+ tokens[key.lower()] = value
+
+ missing_msg = "%s is missing"
+ bad_value_msg = "'%s' was expecting '%s' but found '%s'"
+ nonce = None
+ if 'realm' not in tokens:
+ self._handlewebError(missing_msg % 'realm')
+ elif tokens['realm'] != '"localhost"':
+ self._handlewebError(bad_value_msg % ('realm', '"localhost"', tokens['realm']))
+ if 'nonce' not in tokens:
+ self._handlewebError(missing_msg % 'nonce')
+ else:
+ nonce = tokens['nonce'].strip('"')
+ if 'algorithm' not in tokens:
+ self._handlewebError(missing_msg % 'algorithm')
+ elif tokens['algorithm'] != '"MD5"':
+ self._handlewebError(bad_value_msg % ('algorithm', '"MD5"', tokens['algorithm']))
+ if 'qop' not in tokens:
+ self._handlewebError(missing_msg % 'qop')
+ elif tokens['qop'] != '"auth"':
+ self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop']))
+
+ # Test a wrong 'realm' value
+ base_auth = 'Digest username="test", realm="wrong realm", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
+
+ auth = base_auth % (nonce, '', '00000001')
+ params = httpauth.parseAuthorization(auth)
+ response = httpauth._computeDigestResponse(params, 'test')
+
+ auth = base_auth % (nonce, response, '00000001')
+ self.getPage('/digest/', [('Authorization', auth)])
+ self.assertStatus(401)
+
+ # Test that must pass
+ base_auth = 'Digest username="test", realm="localhost", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
+
+ auth = base_auth % (nonce, '', '00000001')
+ params = httpauth.parseAuthorization(auth)
+ response = httpauth._computeDigestResponse(params, 'test')
+
+ auth = base_auth % (nonce, response, '00000001')
+ self.getPage('/digest/', [('Authorization', auth)])
+ self.assertStatus('200 OK')
+ self.assertBody("Hello test, you've been authorized.")
+
diff --git a/cherrypy/test/test_httplib.py b/cherrypy/test/test_httplib.py
new file mode 100755
index 0000000..5dc40fd
--- /dev/null
+++ b/cherrypy/test/test_httplib.py
@@ -0,0 +1,29 @@
+"""Tests for cherrypy/lib/httputil.py."""
+
+import unittest
+from cherrypy.lib import httputil
+
+
+class UtilityTests(unittest.TestCase):
+
+ def test_urljoin(self):
+ # Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO
+ self.assertEqual(httputil.urljoin("/sn/", "/pi/"), "/sn/pi/")
+ self.assertEqual(httputil.urljoin("/sn/", "/pi"), "/sn/pi")
+ self.assertEqual(httputil.urljoin("/sn/", "/"), "/sn/")
+ self.assertEqual(httputil.urljoin("/sn/", ""), "/sn/")
+ self.assertEqual(httputil.urljoin("/sn", "/pi/"), "/sn/pi/")
+ self.assertEqual(httputil.urljoin("/sn", "/pi"), "/sn/pi")
+ self.assertEqual(httputil.urljoin("/sn", "/"), "/sn/")
+ self.assertEqual(httputil.urljoin("/sn", ""), "/sn")
+ self.assertEqual(httputil.urljoin("/", "/pi/"), "/pi/")
+ self.assertEqual(httputil.urljoin("/", "/pi"), "/pi")
+ self.assertEqual(httputil.urljoin("/", "/"), "/")
+ self.assertEqual(httputil.urljoin("/", ""), "/")
+ self.assertEqual(httputil.urljoin("", "/pi/"), "/pi/")
+ self.assertEqual(httputil.urljoin("", "/pi"), "/pi")
+ self.assertEqual(httputil.urljoin("", "/"), "/")
+ self.assertEqual(httputil.urljoin("", ""), "/")
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/cherrypy/test/test_json.py b/cherrypy/test/test_json.py
new file mode 100755
index 0000000..a02c076
--- /dev/null
+++ b/cherrypy/test/test_json.py
@@ -0,0 +1,79 @@
+import cherrypy
+from cherrypy.test import helper
+
+from cherrypy._cpcompat import json
+
+class JsonTest(helper.CPWebCase):
+ def setup_server():
+ class Root(object):
+ def plain(self):
+ return 'hello'
+ plain.exposed = True
+
+ def json_string(self):
+ return 'hello'
+ json_string.exposed = True
+ json_string._cp_config = {'tools.json_out.on': True}
+
+ def json_list(self):
+ return ['a', 'b', 42]
+ json_list.exposed = True
+ json_list._cp_config = {'tools.json_out.on': True}
+
+ def json_dict(self):
+ return {'answer': 42}
+ json_dict.exposed = True
+ json_dict._cp_config = {'tools.json_out.on': True}
+
+ def json_post(self):
+ if cherrypy.request.json == [13, 'c']:
+ return 'ok'
+ else:
+ return 'nok'
+ json_post.exposed = True
+ json_post._cp_config = {'tools.json_in.on': True}
+
+ root = Root()
+ cherrypy.tree.mount(root)
+ setup_server = staticmethod(setup_server)
+
+ def test_json_output(self):
+ if json is None:
+ self.skip("json not found ")
+ return
+
+ self.getPage("/plain")
+ self.assertBody("hello")
+
+ self.getPage("/json_string")
+ self.assertBody('"hello"')
+
+ self.getPage("/json_list")
+ self.assertBody('["a", "b", 42]')
+
+ self.getPage("/json_dict")
+ self.assertBody('{"answer": 42}')
+
+ def test_json_input(self):
+ if json is None:
+ self.skip("json not found ")
+ return
+
+ body = '[13, "c"]'
+ headers = [('Content-Type', 'application/json'),
+ ('Content-Length', str(len(body)))]
+ self.getPage("/json_post", method="POST", headers=headers, body=body)
+ self.assertBody('ok')
+
+ body = '[13, "c"]'
+ headers = [('Content-Type', 'text/plain'),
+ ('Content-Length', str(len(body)))]
+ self.getPage("/json_post", method="POST", headers=headers, body=body)
+ self.assertStatus(415, 'Expected an application/json content type')
+
+ body = '[13, -]'
+ headers = [('Content-Type', 'application/json'),
+ ('Content-Length', str(len(body)))]
+ self.getPage("/json_post", method="POST", headers=headers, body=body)
+ self.assertStatus(400, 'Invalid JSON document')
+
diff --git a/cherrypy/test/test_logging.py b/cherrypy/test/test_logging.py
new file mode 100755
index 0000000..5a13cd4
--- /dev/null
+++ b/cherrypy/test/test_logging.py
@@ -0,0 +1,149 @@
+"""Basic tests for the CherryPy core: request handling."""
+
+import os
+localDir = os.path.dirname(__file__)
+
+import cherrypy
+
+access_log = os.path.join(localDir, "access.log")
+error_log = os.path.join(localDir, "error.log")
+
+# Some unicode strings.
+tartaros = u'\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2'
+erebos = u'\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com'
+
+
+def setup_server():
+ class Root:
+
+ def index(self):
+ return "hello"
+ index.exposed = True
+
+ def uni_code(self):
+ cherrypy.request.login = tartaros
+ cherrypy.request.remote.name = erebos
+ uni_code.exposed = True
+
+ def slashes(self):
+ cherrypy.request.request_line = r'GET /slashed\path HTTP/1.1'
+ slashes.exposed = True
+
+ def whitespace(self):
+ # User-Agent = "User-Agent" ":" 1*( product | comment )
+ # comment = "(" *( ctext | quoted-pair | comment ) ")"
+ # ctext = <any TEXT excluding "(" and ")">
+ # TEXT = <any OCTET except CTLs, but including LWS>
+ # LWS = [CRLF] 1*( SP | HT )
+ cherrypy.request.headers['User-Agent'] = 'Browzuh (1.0\r\n\t\t.3)'
+ whitespace.exposed = True
+
+ def as_string(self):
+ return "content"
+ as_string.exposed = True
+
+ def as_yield(self):
+ yield "content"
+ as_yield.exposed = True
+
+ def error(self):
+ raise ValueError()
+ error.exposed = True
+ error._cp_config = {'tools.log_tracebacks.on': True}
+
+ root = Root()
+
+
+ cherrypy.config.update({'log.error_file': error_log,
+ 'log.access_file': access_log,
+ })
+ cherrypy.tree.mount(root)
+
+
+
+from cherrypy.test import helper, logtest
+
+class AccessLogTests(helper.CPWebCase, logtest.LogCase):
+ setup_server = staticmethod(setup_server)
+
+ logfile = access_log
+
+ def testNormalReturn(self):
+ self.markLog()
+ self.getPage("/as_string",
+ headers=[('Referer', 'http://www.cherrypy.org/'),
+ ('User-Agent', 'Mozilla/5.0')])
+ self.assertBody('content')
+ self.assertStatus(200)
+
+ intro = '%s - - [' % self.interface()
+
+ self.assertLog(-1, intro)
+
+ if [k for k, v in self.headers if k.lower() == 'content-length']:
+ self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 7 '
+ '"http://www.cherrypy.org/" "Mozilla/5.0"'
+ % self.prefix())
+ else:
+ self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 - '
+ '"http://www.cherrypy.org/" "Mozilla/5.0"'
+ % self.prefix())
+
+ def testNormalYield(self):
+ self.markLog()
+ self.getPage("/as_yield")
+ self.assertBody('content')
+ self.assertStatus(200)
+
+ intro = '%s - - [' % self.interface()
+
+ self.assertLog(-1, intro)
+ if [k for k, v in self.headers if k.lower() == 'content-length']:
+ self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 7 "" ""' %
+ self.prefix())
+ else:
+ self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 - "" ""'
+ % self.prefix())
+
+ def testEscapedOutput(self):
+ # Test unicode in access log pieces.
+ self.markLog()
+ self.getPage("/uni_code")
+ self.assertStatus(200)
+ self.assertLog(-1, repr(tartaros.encode('utf8'))[1:-1])
+ # Test the erebos value. Included inline for your enlightenment.
+ # Note the 'r' prefix--those backslashes are literals.
+ self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82')
+
+ # Test backslashes in output.
+ self.markLog()
+ self.getPage("/slashes")
+ self.assertStatus(200)
+ self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"')
+
+ # Test whitespace in output.
+ self.markLog()
+ self.getPage("/whitespace")
+ self.assertStatus(200)
+ # Again, note the 'r' prefix.
+ self.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"')
+
+
+class ErrorLogTests(helper.CPWebCase, logtest.LogCase):
+ setup_server = staticmethod(setup_server)
+
+ logfile = error_log
+
+ def testTracebacks(self):
+ # Test that tracebacks get written to the error log.
+ self.markLog()
+ ignore = helper.webtest.ignored_exceptions
+ ignore.append(ValueError)
+ try:
+ self.getPage("/error")
+ self.assertInBody("raise ValueError()")
+ self.assertLog(0, 'HTTP Traceback (most recent call last):')
+ self.assertLog(-3, 'raise ValueError()')
+ finally:
+ ignore.pop()
+
diff --git a/cherrypy/test/test_mime.py b/cherrypy/test/test_mime.py
new file mode 100755
index 0000000..605071b
--- /dev/null
+++ b/cherrypy/test/test_mime.py
@@ -0,0 +1,128 @@
+"""Tests for various MIME issues, including the safe_multipart Tool."""
+
+import cherrypy
+from cherrypy._cpcompat import ntob, ntou, sorted
+
+def setup_server():
+
+ class Root:
+
+ def multipart(self, parts):
+ return repr(parts)
+ multipart.exposed = True
+
+ def multipart_form_data(self, **kwargs):
+ return repr(list(sorted(kwargs.items())))
+ multipart_form_data.exposed = True
+
+ def flashupload(self, Filedata, Upload, Filename):
+ return ("Upload: %r, Filename: %r, Filedata: %r" %
+ (Upload, Filename, Filedata.file.read()))
+ flashupload.exposed = True
+
+ cherrypy.config.update({'server.max_request_body_size': 0})
+ cherrypy.tree.mount(Root())
+
+
+# Client-side code #
+
+from cherrypy.test import helper
+
+class MultipartTest(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def test_multipart(self):
+ text_part = ntou("This is the text version")
+ html_part = ntou("""<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+ <meta content="text/html;charset=ISO-8859-1" http-equiv="Content-Type">
+</head>
+<body bgcolor="#ffffff" text="#000000">
+
+This is the <strong>HTML</strong> version
+</body>
+</html>
+""")
+ body = '\r\n'.join([
+ "--123456789",
+ "Content-Type: text/plain; charset='ISO-8859-1'",
+ "Content-Transfer-Encoding: 7bit",
+ "",
+ text_part,
+ "--123456789",
+ "Content-Type: text/html; charset='ISO-8859-1'",
+ "",
+ html_part,
+ "--123456789--"])
+ headers = [
+ ('Content-Type', 'multipart/mixed; boundary=123456789'),
+ ('Content-Length', str(len(body))),
+ ]
+ self.getPage('/multipart', headers, "POST", body)
+ self.assertBody(repr([text_part, html_part]))
+
+ def test_multipart_form_data(self):
+ body='\r\n'.join(['--X',
+ 'Content-Disposition: form-data; name="foo"',
+ '',
+ 'bar',
+ '--X',
+ # Test a param with more than one value.
+ # See http://www.cherrypy.org/ticket/1028
+ 'Content-Disposition: form-data; name="baz"',
+ '',
+ '111',
+ '--X',
+ 'Content-Disposition: form-data; name="baz"',
+ '',
+ '333',
+ '--X--'])
+ self.getPage('/multipart_form_data', method='POST',
+ headers=[("Content-Type", "multipart/form-data;boundary=X"),
+ ("Content-Length", str(len(body))),
+ ],
+ body=body),
+ self.assertBody(repr([('baz', [u'111', u'333']), ('foo', u'bar')]))
+
+
+class SafeMultipartHandlingTest(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def test_Flash_Upload(self):
+ headers = [
+ ('Accept', 'text/*'),
+ ('Content-Type', 'multipart/form-data; '
+ 'boundary=----------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6'),
+ ('User-Agent', 'Shockwave Flash'),
+ ('Host', 'www.example.com:8080'),
+ ('Content-Length', '499'),
+ ('Connection', 'Keep-Alive'),
+ ('Cache-Control', 'no-cache'),
+ ]
+ filedata = ntob('<?xml version="1.0" encoding="UTF-8"?>\r\n'
+ '<projectDescription>\r\n'
+ '</projectDescription>\r\n')
+ body = (ntob(
+ '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
+ 'Content-Disposition: form-data; name="Filename"\r\n'
+ '\r\n'
+ '.project\r\n'
+ '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
+ 'Content-Disposition: form-data; '
+ 'name="Filedata"; filename=".project"\r\n'
+ 'Content-Type: application/octet-stream\r\n'
+ '\r\n')
+ + filedata +
+ ntob('\r\n'
+ '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
+ 'Content-Disposition: form-data; name="Upload"\r\n'
+ '\r\n'
+ 'Submit Query\r\n'
+ # Flash apps omit the trailing \r\n on the last line:
+ '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--'
+ ))
+ self.getPage('/flashupload', headers, "POST", body)
+ self.assertBody("Upload: u'Submit Query', Filename: u'.project', "
+ "Filedata: %r" % filedata)
+
diff --git a/cherrypy/test/test_misc_tools.py b/cherrypy/test/test_misc_tools.py
new file mode 100755
index 0000000..fb94e86
--- /dev/null
+++ b/cherrypy/test/test_misc_tools.py
@@ -0,0 +1,202 @@
+import os
+localDir = os.path.dirname(__file__)
+logfile = os.path.join(localDir, "test_misc_tools.log")
+
+import cherrypy
+from cherrypy import tools
+
+
+def setup_server():
+ class Root:
+ def index(self):
+ yield "Hello, world"
+ index.exposed = True
+ h = [("Content-Language", "en-GB"), ('Content-Type', 'text/plain')]
+ tools.response_headers(headers=h)(index)
+
+ def other(self):
+ return "salut"
+ other.exposed = True
+ other._cp_config = {
+ 'tools.response_headers.on': True,
+ 'tools.response_headers.headers': [("Content-Language", "fr"),
+ ('Content-Type', 'text/plain')],
+ 'tools.log_hooks.on': True,
+ }
+
+
+ class Accept:
+ _cp_config = {'tools.accept.on': True}
+
+ def index(self):
+ return '<a href="feed">Atom feed</a>'
+ index.exposed = True
+
+ # In Python 2.4+, we could use a decorator instead:
+ # @tools.accept('application/atom+xml')
+ def feed(self):
+ return """<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <title>Unknown Blog</title>
+</feed>"""
+ feed.exposed = True
+ feed._cp_config = {'tools.accept.media': 'application/atom+xml'}
+
+ def select(self):
+ # We could also write this: mtype = cherrypy.lib.accept.accept(...)
+ mtype = tools.accept.callable(['text/html', 'text/plain'])
+ if mtype == 'text/html':
+ return "<h2>Page Title</h2>"
+ else:
+ return "PAGE TITLE"
+ select.exposed = True
+
+ class Referer:
+ def accept(self):
+ return "Accepted!"
+ accept.exposed = True
+ reject = accept
+
+ class AutoVary:
+ def index(self):
+ # Read a header directly with 'get'
+ ae = cherrypy.request.headers.get('Accept-Encoding')
+ # Read a header directly with '__getitem__'
+ cl = cherrypy.request.headers['Host']
+ # Read a header directly with '__contains__'
+ hasif = 'If-Modified-Since' in cherrypy.request.headers
+ # Read a header directly with 'has_key'
+ has = cherrypy.request.headers.has_key('Range')
+ # Call a lib function
+ mtype = tools.accept.callable(['text/html', 'text/plain'])
+ return "Hello, world!"
+ index.exposed = True
+
+ conf = {'/referer': {'tools.referer.on': True,
+ 'tools.referer.pattern': r'http://[^/]*example\.com',
+ },
+ '/referer/reject': {'tools.referer.accept': False,
+ 'tools.referer.accept_missing': True,
+ },
+ '/autovary': {'tools.autovary.on': True},
+ }
+
+ root = Root()
+ root.referer = Referer()
+ root.accept = Accept()
+ root.autovary = AutoVary()
+ cherrypy.tree.mount(root, config=conf)
+ cherrypy.config.update({'log.error_file': logfile})
+
+
+from cherrypy.test import helper
+
+class ResponseHeadersTest(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def testResponseHeadersDecorator(self):
+ self.getPage('/')
+ self.assertHeader("Content-Language", "en-GB")
+ self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
+
+ def testResponseHeaders(self):
+ self.getPage('/other')
+ self.assertHeader("Content-Language", "fr")
+ self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
+
+
+class RefererTest(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def testReferer(self):
+ self.getPage('/referer/accept')
+ self.assertErrorPage(403, 'Forbidden Referer header.')
+
+ self.getPage('/referer/accept',
+ headers=[('Referer', 'http://www.example.com/')])
+ self.assertStatus(200)
+ self.assertBody('Accepted!')
+
+ # Reject
+ self.getPage('/referer/reject')
+ self.assertStatus(200)
+ self.assertBody('Accepted!')
+
+ self.getPage('/referer/reject',
+ headers=[('Referer', 'http://www.example.com/')])
+ self.assertErrorPage(403, 'Forbidden Referer header.')
+
+
+class AcceptTest(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def test_Accept_Tool(self):
+ # Test with no header provided
+ self.getPage('/accept/feed')
+ self.assertStatus(200)
+ self.assertInBody('<title>Unknown Blog</title>')
+
+ # Specify exact media type
+ self.getPage('/accept/feed', headers=[('Accept', 'application/atom+xml')])
+ self.assertStatus(200)
+ self.assertInBody('<title>Unknown Blog</title>')
+
+ # Specify matching media range
+ self.getPage('/accept/feed', headers=[('Accept', 'application/*')])
+ self.assertStatus(200)
+ self.assertInBody('<title>Unknown Blog</title>')
+
+ # Specify all media ranges
+ self.getPage('/accept/feed', headers=[('Accept', '*/*')])
+ self.assertStatus(200)
+ self.assertInBody('<title>Unknown Blog</title>')
+
+ # Specify unacceptable media types
+ self.getPage('/accept/feed', headers=[('Accept', 'text/html')])
+ self.assertErrorPage(406,
+ "Your client sent this Accept header: text/html. "
+ "But this resource only emits these media types: "
+ "application/atom+xml.")
+
+ # Test resource where tool is 'on' but media is None (not set).
+ self.getPage('/accept/')
+ self.assertStatus(200)
+ self.assertBody('<a href="feed">Atom feed</a>')
+
+ def test_accept_selection(self):
+ # Try both our expected media types
+ self.getPage('/accept/select', [('Accept', 'text/html')])
+ self.assertStatus(200)
+ self.assertBody('<h2>Page Title</h2>')
+ self.getPage('/accept/select', [('Accept', 'text/plain')])
+ self.assertStatus(200)
+ self.assertBody('PAGE TITLE')
+ self.getPage('/accept/select', [('Accept', 'text/plain, text/*;q=0.5')])
+ self.assertStatus(200)
+ self.assertBody('PAGE TITLE')
+
+ # text/* and */* should prefer text/html since it comes first
+ # in our 'media' argument to tools.accept
+ self.getPage('/accept/select', [('Accept', 'text/*')])
+ self.assertStatus(200)
+ self.assertBody('<h2>Page Title</h2>')
+ self.getPage('/accept/select', [('Accept', '*/*')])
+ self.assertStatus(200)
+ self.assertBody('<h2>Page Title</h2>')
+
+ # Try unacceptable media types
+ self.getPage('/accept/select', [('Accept', 'application/xml')])
+ self.assertErrorPage(406,
+ "Your client sent this Accept header: application/xml. "
+ "But this resource only emits these media types: "
+ "text/html, text/plain.")
+
+
+class AutoVaryTest(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def testAutoVary(self):
+ self.getPage('/autovary/')
+ self.assertHeader(
+ "Vary", 'Accept, Accept-Charset, Accept-Encoding, Host, If-Modified-Since, Range')
+
diff --git a/cherrypy/test/test_objectmapping.py b/cherrypy/test/test_objectmapping.py
new file mode 100755
index 0000000..46816fc
--- /dev/null
+++ b/cherrypy/test/test_objectmapping.py
@@ -0,0 +1,403 @@
+import cherrypy
+from cherrypy._cptree import Application
+from cherrypy.test import helper
+
+script_names = ["", "/foo", "/users/fred/blog", "/corp/blog"]
+
+
+class ObjectMappingTest(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def index(self, name="world"):
+ return name
+ index.exposed = True
+
+ def foobar(self):
+ return "bar"
+ foobar.exposed = True
+
+ def default(self, *params, **kwargs):
+ return "default:" + repr(params)
+ default.exposed = True
+
+ def other(self):
+ return "other"
+ other.exposed = True
+
+ def extra(self, *p):
+ return repr(p)
+ extra.exposed = True
+
+ def redirect(self):
+ raise cherrypy.HTTPRedirect('dir1/', 302)
+ redirect.exposed = True
+
+ def notExposed(self):
+ return "not exposed"
+
+ def confvalue(self):
+ return cherrypy.request.config.get("user")
+ confvalue.exposed = True
+
+ def redirect_via_url(self, path):
+ raise cherrypy.HTTPRedirect(cherrypy.url(path))
+ redirect_via_url.exposed = True
+
+ def translate_html(self):
+ return "OK"
+ translate_html.exposed = True
+
+ def mapped_func(self, ID=None):
+ return "ID is %s" % ID
+ mapped_func.exposed = True
+ setattr(Root, "Von B\xfclow", mapped_func)
+
+
+ class Exposing:
+ def base(self):
+ return "expose works!"
+ cherrypy.expose(base)
+ cherrypy.expose(base, "1")
+ cherrypy.expose(base, "2")
+
+ class ExposingNewStyle(object):
+ def base(self):
+ return "expose works!"
+ cherrypy.expose(base)
+ cherrypy.expose(base, "1")
+ cherrypy.expose(base, "2")
+
+
+ class Dir1:
+ def index(self):
+ return "index for dir1"
+ index.exposed = True
+
+ def myMethod(self):
+ return "myMethod from dir1, path_info is:" + repr(cherrypy.request.path_info)
+ myMethod.exposed = True
+ myMethod._cp_config = {'tools.trailing_slash.extra': True}
+
+ def default(self, *params):
+ return "default for dir1, param is:" + repr(params)
+ default.exposed = True
+
+
+ class Dir2:
+ def index(self):
+ return "index for dir2, path is:" + cherrypy.request.path_info
+ index.exposed = True
+
+ def script_name(self):
+ return cherrypy.tree.script_name()
+ script_name.exposed = True
+
+ def cherrypy_url(self):
+ return cherrypy.url("/extra")
+ cherrypy_url.exposed = True
+
+ def posparam(self, *vpath):
+ return "/".join(vpath)
+ posparam.exposed = True
+
+
+ class Dir3:
+ def default(self):
+ return "default for dir3, not exposed"
+
+ class Dir4:
+ def index(self):
+ return "index for dir4, not exposed"
+
+ class DefNoIndex:
+ def default(self, *args):
+ raise cherrypy.HTTPRedirect("contact")
+ default.exposed = True
+
+ # MethodDispatcher code
+ class ByMethod:
+ exposed = True
+
+ def __init__(self, *things):
+ self.things = list(things)
+
+ def GET(self):
+ return repr(self.things)
+
+ def POST(self, thing):
+ self.things.append(thing)
+
+ class Collection:
+ default = ByMethod('a', 'bit')
+
+ Root.exposing = Exposing()
+ Root.exposingnew = ExposingNewStyle()
+ Root.dir1 = Dir1()
+ Root.dir1.dir2 = Dir2()
+ Root.dir1.dir2.dir3 = Dir3()
+ Root.dir1.dir2.dir3.dir4 = Dir4()
+ Root.defnoindex = DefNoIndex()
+ Root.bymethod = ByMethod('another')
+ Root.collection = Collection()
+
+ d = cherrypy.dispatch.MethodDispatcher()
+ for url in script_names:
+ conf = {'/': {'user': (url or "/").split("/")[-2]},
+ '/bymethod': {'request.dispatch': d},
+ '/collection': {'request.dispatch': d},
+ }
+ cherrypy.tree.mount(Root(), url, conf)
+
+
+ class Isolated:
+ def index(self):
+ return "made it!"
+ index.exposed = True
+
+ cherrypy.tree.mount(Isolated(), "/isolated")
+
+ class AnotherApp:
+
+ exposed = True
+
+ def GET(self):
+ return "milk"
+
+ cherrypy.tree.mount(AnotherApp(), "/app", {'/': {'request.dispatch': d}})
+ setup_server = staticmethod(setup_server)
+
+
+ def testObjectMapping(self):
+ for url in script_names:
+ prefix = self.script_name = url
+
+ self.getPage('/')
+ self.assertBody('world')
+
+ self.getPage("/dir1/myMethod")
+ self.assertBody("myMethod from dir1, path_info is:'/dir1/myMethod'")
+
+ self.getPage("/this/method/does/not/exist")
+ self.assertBody("default:('this', 'method', 'does', 'not', 'exist')")
+
+ self.getPage("/extra/too/much")
+ self.assertBody("('too', 'much')")
+
+ self.getPage("/other")
+ self.assertBody('other')
+
+ self.getPage("/notExposed")
+ self.assertBody("default:('notExposed',)")
+
+ self.getPage("/dir1/dir2/")
+ self.assertBody('index for dir2, path is:/dir1/dir2/')
+
+ # Test omitted trailing slash (should be redirected by default).
+ self.getPage("/dir1/dir2")
+ self.assertStatus(301)
+ self.assertHeader('Location', '%s/dir1/dir2/' % self.base())
+
+ # Test extra trailing slash (should be redirected if configured).
+ self.getPage("/dir1/myMethod/")
+ self.assertStatus(301)
+ self.assertHeader('Location', '%s/dir1/myMethod' % self.base())
+
+ # Test that default method must be exposed in order to match.
+ self.getPage("/dir1/dir2/dir3/dir4/index")
+ self.assertBody("default for dir1, param is:('dir2', 'dir3', 'dir4', 'index')")
+
+ # Test *vpath when default() is defined but not index()
+ # This also tests HTTPRedirect with default.
+ self.getPage("/defnoindex")
+ self.assertStatus((302, 303))
+ self.assertHeader('Location', '%s/contact' % self.base())
+ self.getPage("/defnoindex/")
+ self.assertStatus((302, 303))
+ self.assertHeader('Location', '%s/defnoindex/contact' % self.base())
+ self.getPage("/defnoindex/page")
+ self.assertStatus((302, 303))
+ self.assertHeader('Location', '%s/defnoindex/contact' % self.base())
+
+ self.getPage("/redirect")
+ self.assertStatus('302 Found')
+ self.assertHeader('Location', '%s/dir1/' % self.base())
+
+ if not getattr(cherrypy.server, "using_apache", False):
+ # Test that we can use URL's which aren't all valid Python identifiers
+ # This should also test the %XX-unquoting of URL's.
+ self.getPage("/Von%20B%fclow?ID=14")
+ self.assertBody("ID is 14")
+
+ # Test that %2F in the path doesn't get unquoted too early;
+ # that is, it should not be used to separate path components.
+ # See ticket #393.
+ self.getPage("/page%2Fname")
+ self.assertBody("default:('page/name',)")
+
+ self.getPage("/dir1/dir2/script_name")
+ self.assertBody(url)
+ self.getPage("/dir1/dir2/cherrypy_url")
+ self.assertBody("%s/extra" % self.base())
+
+ # Test that configs don't overwrite each other from diferent apps
+ self.getPage("/confvalue")
+ self.assertBody((url or "/").split("/")[-2])
+
+ self.script_name = ""
+
+ # Test absoluteURI's in the Request-Line
+ self.getPage('http://%s:%s/' % (self.interface(), self.PORT))
+ self.assertBody('world')
+
+ self.getPage('http://%s:%s/abs/?service=http://192.168.0.1/x/y/z' %
+ (self.interface(), self.PORT))
+ self.assertBody("default:('abs',)")
+
+ self.getPage('/rel/?service=http://192.168.120.121:8000/x/y/z')
+ self.assertBody("default:('rel',)")
+
+ # Test that the "isolated" app doesn't leak url's into the root app.
+ # If it did leak, Root.default() would answer with
+ # "default:('isolated', 'doesnt', 'exist')".
+ self.getPage("/isolated/")
+ self.assertStatus("200 OK")
+ self.assertBody("made it!")
+ self.getPage("/isolated/doesnt/exist")
+ self.assertStatus("404 Not Found")
+
+ # Make sure /foobar maps to Root.foobar and not to the app
+ # mounted at /foo. See http://www.cherrypy.org/ticket/573
+ self.getPage("/foobar")
+ self.assertBody("bar")
+
+ def test_translate(self):
+ self.getPage("/translate_html")
+ self.assertStatus("200 OK")
+ self.assertBody("OK")
+
+ self.getPage("/translate.html")
+ self.assertStatus("200 OK")
+ self.assertBody("OK")
+
+ self.getPage("/translate-html")
+ self.assertStatus("200 OK")
+ self.assertBody("OK")
+
+ def test_redir_using_url(self):
+ for url in script_names:
+ prefix = self.script_name = url
+
+ # Test the absolute path to the parent (leading slash)
+ self.getPage('/redirect_via_url?path=./')
+ self.assertStatus(('302 Found', '303 See Other'))
+ self.assertHeader('Location', '%s/' % self.base())
+
+ # Test the relative path to the parent (no leading slash)
+ self.getPage('/redirect_via_url?path=./')
+ self.assertStatus(('302 Found', '303 See Other'))
+ self.assertHeader('Location', '%s/' % self.base())
+
+ # Test the absolute path to the parent (leading slash)
+ self.getPage('/redirect_via_url/?path=./')
+ self.assertStatus(('302 Found', '303 See Other'))
+ self.assertHeader('Location', '%s/' % self.base())
+
+ # Test the relative path to the parent (no leading slash)
+ self.getPage('/redirect_via_url/?path=./')
+ self.assertStatus(('302 Found', '303 See Other'))
+ self.assertHeader('Location', '%s/' % self.base())
+
+ def testPositionalParams(self):
+ self.getPage("/dir1/dir2/posparam/18/24/hut/hike")
+ self.assertBody("18/24/hut/hike")
+
+ # intermediate index methods should not receive posparams;
+ # only the "final" index method should do so.
+ self.getPage("/dir1/dir2/5/3/sir")
+ self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')")
+
+ # test that extra positional args raises an 404 Not Found
+ # See http://www.cherrypy.org/ticket/733.
+ self.getPage("/dir1/dir2/script_name/extra/stuff")
+ self.assertStatus(404)
+
+ def testExpose(self):
+ # Test the cherrypy.expose function/decorator
+ self.getPage("/exposing/base")
+ self.assertBody("expose works!")
+
+ self.getPage("/exposing/1")
+ self.assertBody("expose works!")
+
+ self.getPage("/exposing/2")
+ self.assertBody("expose works!")
+
+ self.getPage("/exposingnew/base")
+ self.assertBody("expose works!")
+
+ self.getPage("/exposingnew/1")
+ self.assertBody("expose works!")
+
+ self.getPage("/exposingnew/2")
+ self.assertBody("expose works!")
+
+ def testMethodDispatch(self):
+ self.getPage("/bymethod")
+ self.assertBody("['another']")
+ self.assertHeader('Allow', 'GET, HEAD, POST')
+
+ self.getPage("/bymethod", method="HEAD")
+ self.assertBody("")
+ self.assertHeader('Allow', 'GET, HEAD, POST')
+
+ self.getPage("/bymethod", method="POST", body="thing=one")
+ self.assertBody("")
+ self.assertHeader('Allow', 'GET, HEAD, POST')
+
+ self.getPage("/bymethod")
+ self.assertBody("['another', u'one']")
+ self.assertHeader('Allow', 'GET, HEAD, POST')
+
+ self.getPage("/bymethod", method="PUT")
+ self.assertErrorPage(405)
+ self.assertHeader('Allow', 'GET, HEAD, POST')
+
+ # Test default with posparams
+ self.getPage("/collection/silly", method="POST")
+ self.getPage("/collection", method="GET")
+ self.assertBody("['a', 'bit', 'silly']")
+
+ # Test custom dispatcher set on app root (see #737).
+ self.getPage("/app")
+ self.assertBody("milk")
+
+ def testTreeMounting(self):
+ class Root(object):
+ def hello(self):
+ return "Hello world!"
+ hello.exposed = True
+
+ # When mounting an application instance,
+ # we can't specify a different script name in the call to mount.
+ a = Application(Root(), '/somewhere')
+ self.assertRaises(ValueError, cherrypy.tree.mount, a, '/somewhereelse')
+
+ # When mounting an application instance...
+ a = Application(Root(), '/somewhere')
+ # ...we MUST allow in identical script name in the call to mount...
+ cherrypy.tree.mount(a, '/somewhere')
+ self.getPage('/somewhere/hello')
+ self.assertStatus(200)
+ # ...and MUST allow a missing script_name.
+ del cherrypy.tree.apps['/somewhere']
+ cherrypy.tree.mount(a)
+ self.getPage('/somewhere/hello')
+ self.assertStatus(200)
+
+ # In addition, we MUST be able to create an Application using
+ # script_name == None for access to the wsgi_environ.
+ a = Application(Root(), script_name=None)
+ # However, this does not apply to tree.mount
+ self.assertRaises(TypeError, cherrypy.tree.mount, a, None)
+
diff --git a/cherrypy/test/test_proxy.py b/cherrypy/test/test_proxy.py
new file mode 100755
index 0000000..2fbb619
--- /dev/null
+++ b/cherrypy/test/test_proxy.py
@@ -0,0 +1,129 @@
+import cherrypy
+from cherrypy.test import helper
+
+script_names = ["", "/path/to/myapp"]
+
+
+class ProxyTest(helper.CPWebCase):
+
+ def setup_server():
+
+ # Set up site
+ cherrypy.config.update({
+ 'tools.proxy.on': True,
+ 'tools.proxy.base': 'www.mydomain.test',
+ })
+
+ # Set up application
+
+ class Root:
+
+ def __init__(self, sn):
+ # Calculate a URL outside of any requests.
+ self.thisnewpage = cherrypy.url("/this/new/page", script_name=sn)
+
+ def pageurl(self):
+ return self.thisnewpage
+ pageurl.exposed = True
+
+ def index(self):
+ raise cherrypy.HTTPRedirect('dummy')
+ index.exposed = True
+
+ def remoteip(self):
+ return cherrypy.request.remote.ip
+ remoteip.exposed = True
+
+ def xhost(self):
+ raise cherrypy.HTTPRedirect('blah')
+ xhost.exposed = True
+ xhost._cp_config = {'tools.proxy.local': 'X-Host',
+ 'tools.trailing_slash.extra': True,
+ }
+
+ def base(self):
+ return cherrypy.request.base
+ base.exposed = True
+
+ def ssl(self):
+ return cherrypy.request.base
+ ssl.exposed = True
+ ssl._cp_config = {'tools.proxy.scheme': 'X-Forwarded-Ssl'}
+
+ def newurl(self):
+ return ("Browse to <a href='%s'>this page</a>."
+ % cherrypy.url("/this/new/page"))
+ newurl.exposed = True
+
+ for sn in script_names:
+ cherrypy.tree.mount(Root(sn), sn)
+ setup_server = staticmethod(setup_server)
+
+ def testProxy(self):
+ self.getPage("/")
+ self.assertHeader('Location',
+ "%s://www.mydomain.test%s/dummy" %
+ (self.scheme, self.prefix()))
+
+ # Test X-Forwarded-Host (Apache 1.3.33+ and Apache 2)
+ self.getPage("/", headers=[('X-Forwarded-Host', 'http://www.example.test')])
+ self.assertHeader('Location', "http://www.example.test/dummy")
+ self.getPage("/", headers=[('X-Forwarded-Host', 'www.example.test')])
+ self.assertHeader('Location', "%s://www.example.test/dummy" % self.scheme)
+ # Test multiple X-Forwarded-Host headers
+ self.getPage("/", headers=[
+ ('X-Forwarded-Host', 'http://www.example.test, www.cherrypy.test'),
+ ])
+ self.assertHeader('Location', "http://www.example.test/dummy")
+
+ # Test X-Forwarded-For (Apache2)
+ self.getPage("/remoteip",
+ headers=[('X-Forwarded-For', '192.168.0.20')])
+ self.assertBody("192.168.0.20")
+ self.getPage("/remoteip",
+ headers=[('X-Forwarded-For', '67.15.36.43, 192.168.0.20')])
+ self.assertBody("192.168.0.20")
+
+ # Test X-Host (lighttpd; see https://trac.lighttpd.net/trac/ticket/418)
+ self.getPage("/xhost", headers=[('X-Host', 'www.example.test')])
+ self.assertHeader('Location', "%s://www.example.test/blah" % self.scheme)
+
+ # Test X-Forwarded-Proto (lighttpd)
+ self.getPage("/base", headers=[('X-Forwarded-Proto', 'https')])
+ self.assertBody("https://www.mydomain.test")
+
+ # Test X-Forwarded-Ssl (webfaction?)
+ self.getPage("/ssl", headers=[('X-Forwarded-Ssl', 'on')])
+ self.assertBody("https://www.mydomain.test")
+
+ # Test cherrypy.url()
+ for sn in script_names:
+ # Test the value inside requests
+ self.getPage(sn + "/newurl")
+ self.assertBody("Browse to <a href='%s://www.mydomain.test" % self.scheme
+ + sn + "/this/new/page'>this page</a>.")
+ self.getPage(sn + "/newurl", headers=[('X-Forwarded-Host',
+ 'http://www.example.test')])
+ self.assertBody("Browse to <a href='http://www.example.test"
+ + sn + "/this/new/page'>this page</a>.")
+
+ # Test the value outside requests
+ port = ""
+ if self.scheme == "http" and self.PORT != 80:
+ port = ":%s" % self.PORT
+ elif self.scheme == "https" and self.PORT != 443:
+ port = ":%s" % self.PORT
+ host = self.HOST
+ if host in ('0.0.0.0', '::'):
+ import socket
+ host = socket.gethostname()
+ expected = ("%s://%s%s%s/this/new/page"
+ % (self.scheme, host, port, sn))
+ self.getPage(sn + "/pageurl")
+ self.assertBody(expected)
+
+ # Test trailing slash (see http://www.cherrypy.org/ticket/562).
+ self.getPage("/xhost/", headers=[('X-Host', 'www.example.test')])
+ self.assertHeader('Location', "%s://www.example.test/xhost"
+ % self.scheme)
+
diff --git a/cherrypy/test/test_refleaks.py b/cherrypy/test/test_refleaks.py
new file mode 100755
index 0000000..4df1f08
--- /dev/null
+++ b/cherrypy/test/test_refleaks.py
@@ -0,0 +1,119 @@
+"""Tests for refleaks."""
+
+import gc
+from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob
+import threading
+
+import cherrypy
+from cherrypy import _cprequest
+
+
+data = object()
+
+def get_instances(cls):
+ return [x for x in gc.get_objects() if isinstance(x, cls)]
+
+
+from cherrypy.test import helper
+
+
+class ReferenceTests(helper.CPWebCase):
+
+ def setup_server():
+
+ class Root:
+ def index(self, *args, **kwargs):
+ cherrypy.request.thing = data
+ return "Hello world!"
+ index.exposed = True
+
+ def gc_stats(self):
+ output = ["Statistics:"]
+
+ # Uncollectable garbage
+
+ # gc_collect isn't perfectly synchronous, because it may
+ # break reference cycles that then take time to fully
+ # finalize. Call it twice and hope for the best.
+ gc.collect()
+ unreachable = gc.collect()
+ if unreachable:
+ output.append("\n%s unreachable objects:" % unreachable)
+ trash = {}
+ for x in gc.garbage:
+ trash[type(x)] = trash.get(type(x), 0) + 1
+ trash = [(v, k) for k, v in trash.items()]
+ trash.sort()
+ for pair in trash:
+ output.append(" " + repr(pair))
+
+ # Request references
+ reqs = get_instances(_cprequest.Request)
+ lenreqs = len(reqs)
+ if lenreqs < 2:
+ output.append("\nMissing Request reference. Should be 1 in "
+ "this request thread and 1 in the main thread.")
+ elif lenreqs > 2:
+ output.append("\nToo many Request references (%r)." % lenreqs)
+ for req in reqs:
+ output.append("Referrers for %s:" % repr(req))
+ for ref in gc.get_referrers(req):
+ if ref is not reqs:
+ output.append(" %s" % repr(ref))
+
+ # Response references
+ resps = get_instances(_cprequest.Response)
+ lenresps = len(resps)
+ if lenresps < 2:
+ output.append("\nMissing Response reference. Should be 1 in "
+ "this request thread and 1 in the main thread.")
+ elif lenresps > 2:
+ output.append("\nToo many Response references (%r)." % lenresps)
+ for resp in resps:
+ output.append("Referrers for %s:" % repr(resp))
+ for ref in gc.get_referrers(resp):
+ if ref is not resps:
+ output.append(" %s" % repr(ref))
+
+ return "\n".join(output)
+ gc_stats.exposed = True
+
+ cherrypy.tree.mount(Root())
+ setup_server = staticmethod(setup_server)
+
+
+ def test_threadlocal_garbage(self):
+ success = []
+
+ def getpage():
+ host = '%s:%s' % (self.interface(), self.PORT)
+ if self.scheme == 'https':
+ c = HTTPSConnection(host)
+ else:
+ c = HTTPConnection(host)
+ try:
+ c.putrequest('GET', '/')
+ c.endheaders()
+ response = c.getresponse()
+ body = response.read()
+ self.assertEqual(response.status, 200)
+ self.assertEqual(body, ntob("Hello world!"))
+ finally:
+ c.close()
+ success.append(True)
+
+ ITERATIONS = 25
+ ts = []
+ for _ in range(ITERATIONS):
+ t = threading.Thread(target=getpage)
+ ts.append(t)
+ t.start()
+
+ for t in ts:
+ t.join()
+
+ self.assertEqual(len(success), ITERATIONS)
+
+ self.getPage("/gc_stats")
+ self.assertBody("Statistics:")
+
diff --git a/cherrypy/test/test_request_obj.py b/cherrypy/test/test_request_obj.py
new file mode 100755
index 0000000..91ee4fd
--- /dev/null
+++ b/cherrypy/test/test_request_obj.py
@@ -0,0 +1,722 @@
+"""Basic tests for the cherrypy.Request object."""
+
+import os
+localDir = os.path.dirname(__file__)
+import sys
+import types
+from cherrypy._cpcompat import IncompleteRead, ntob, unicodestr
+
+import cherrypy
+from cherrypy import _cptools, tools
+from cherrypy.lib import httputil
+
+defined_http_methods = ("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE",
+ "TRACE", "PROPFIND")
+
+
+# Client-side code #
+
+from cherrypy.test import helper
+
+class RequestObjectTests(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+
+ def index(self):
+ return "hello"
+ index.exposed = True
+
+ def scheme(self):
+ return cherrypy.request.scheme
+ scheme.exposed = True
+
+ root = Root()
+
+
+ class TestType(type):
+ """Metaclass which automatically exposes all functions in each subclass,
+ and adds an instance of the subclass as an attribute of root.
+ """
+ def __init__(cls, name, bases, dct):
+ type.__init__(cls, name, bases, dct)
+ for value in dct.values():
+ if isinstance(value, types.FunctionType):
+ value.exposed = True
+ setattr(root, name.lower(), cls())
+ class Test(object):
+ __metaclass__ = TestType
+
+
+ class Params(Test):
+
+ def index(self, thing):
+ return repr(thing)
+
+ def ismap(self, x, y):
+ return "Coordinates: %s, %s" % (x, y)
+
+ def default(self, *args, **kwargs):
+ return "args: %s kwargs: %s" % (args, kwargs)
+ default._cp_config = {'request.query_string_encoding': 'latin1'}
+
+
+ class ParamErrorsCallable(object):
+ exposed = True
+ def __call__(self):
+ return "data"
+
+ class ParamErrors(Test):
+
+ def one_positional(self, param1):
+ return "data"
+ one_positional.exposed = True
+
+ def one_positional_args(self, param1, *args):
+ return "data"
+ one_positional_args.exposed = True
+
+ def one_positional_args_kwargs(self, param1, *args, **kwargs):
+ return "data"
+ one_positional_args_kwargs.exposed = True
+
+ def one_positional_kwargs(self, param1, **kwargs):
+ return "data"
+ one_positional_kwargs.exposed = True
+
+ def no_positional(self):
+ return "data"
+ no_positional.exposed = True
+
+ def no_positional_args(self, *args):
+ return "data"
+ no_positional_args.exposed = True
+
+ def no_positional_args_kwargs(self, *args, **kwargs):
+ return "data"
+ no_positional_args_kwargs.exposed = True
+
+ def no_positional_kwargs(self, **kwargs):
+ return "data"
+ no_positional_kwargs.exposed = True
+
+ callable_object = ParamErrorsCallable()
+
+ def raise_type_error(self, **kwargs):
+ raise TypeError("Client Error")
+ raise_type_error.exposed = True
+
+ def raise_type_error_with_default_param(self, x, y=None):
+ return '%d' % 'a' # throw an exception
+ raise_type_error_with_default_param.exposed = True
+
+ def callable_error_page(status, **kwargs):
+ return "Error %s - Well, I'm very sorry but you haven't paid!" % status
+
+
+ class Error(Test):
+
+ _cp_config = {'tools.log_tracebacks.on': True,
+ }
+
+ def reason_phrase(self):
+ raise cherrypy.HTTPError("410 Gone fishin'")
+
+ def custom(self, err='404'):
+ raise cherrypy.HTTPError(int(err), "No, <b>really</b>, not found!")
+ custom._cp_config = {'error_page.404': os.path.join(localDir, "static/index.html"),
+ 'error_page.401': callable_error_page,
+ }
+
+ def custom_default(self):
+ return 1 + 'a' # raise an unexpected error
+ custom_default._cp_config = {'error_page.default': callable_error_page}
+
+ def noexist(self):
+ raise cherrypy.HTTPError(404, "No, <b>really</b>, not found!")
+ noexist._cp_config = {'error_page.404': "nonexistent.html"}
+
+ def page_method(self):
+ raise ValueError()
+
+ def page_yield(self):
+ yield "howdy"
+ raise ValueError()
+
+ def page_streamed(self):
+ yield "word up"
+ raise ValueError()
+ yield "very oops"
+ page_streamed._cp_config = {"response.stream": True}
+
+ def cause_err_in_finalize(self):
+ # Since status must start with an int, this should error.
+ cherrypy.response.status = "ZOO OK"
+ cause_err_in_finalize._cp_config = {'request.show_tracebacks': False}
+
+ def rethrow(self):
+ """Test that an error raised here will be thrown out to the server."""
+ raise ValueError()
+ rethrow._cp_config = {'request.throw_errors': True}
+
+
+ class Expect(Test):
+
+ def expectation_failed(self):
+ expect = cherrypy.request.headers.elements("Expect")
+ if expect and expect[0].value != '100-continue':
+ raise cherrypy.HTTPError(400)
+ raise cherrypy.HTTPError(417, 'Expectation Failed')
+
+ class Headers(Test):
+
+ def default(self, headername):
+ """Spit back out the value for the requested header."""
+ return cherrypy.request.headers[headername]
+
+ def doubledheaders(self):
+ # From http://www.cherrypy.org/ticket/165:
+ # "header field names should not be case sensitive sayes the rfc.
+ # if i set a headerfield in complete lowercase i end up with two
+ # header fields, one in lowercase, the other in mixed-case."
+
+ # Set the most common headers
+ hMap = cherrypy.response.headers
+ hMap['content-type'] = "text/html"
+ hMap['content-length'] = 18
+ hMap['server'] = 'CherryPy headertest'
+ hMap['location'] = ('%s://%s:%s/headers/'
+ % (cherrypy.request.local.ip,
+ cherrypy.request.local.port,
+ cherrypy.request.scheme))
+
+ # Set a rare header for fun
+ hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT'
+
+ return "double header test"
+
+ def ifmatch(self):
+ val = cherrypy.request.headers['If-Match']
+ assert isinstance(val, unicodestr)
+ cherrypy.response.headers['ETag'] = val
+ return val
+
+
+ class HeaderElements(Test):
+
+ def get_elements(self, headername):
+ e = cherrypy.request.headers.elements(headername)
+ return "\n".join([unicodestr(x) for x in e])
+
+
+ class Method(Test):
+
+ def index(self):
+ m = cherrypy.request.method
+ if m in defined_http_methods or m == "CONNECT":
+ return m
+
+ if m == "LINK":
+ raise cherrypy.HTTPError(405)
+ else:
+ raise cherrypy.HTTPError(501)
+
+ def parameterized(self, data):
+ return data
+
+ def request_body(self):
+ # This should be a file object (temp file),
+ # which CP will just pipe back out if we tell it to.
+ return cherrypy.request.body
+
+ def reachable(self):
+ return "success"
+
+ class Divorce:
+ """HTTP Method handlers shouldn't collide with normal method names.
+ For example, a GET-handler shouldn't collide with a method named 'get'.
+
+ If you build HTTP method dispatching into CherryPy, rewrite this class
+ to use your new dispatch mechanism and make sure that:
+ "GET /divorce HTTP/1.1" maps to divorce.index() and
+ "GET /divorce/get?ID=13 HTTP/1.1" maps to divorce.get()
+ """
+
+ documents = {}
+
+ def index(self):
+ yield "<h1>Choose your document</h1>\n"
+ yield "<ul>\n"
+ for id, contents in self.documents.items():
+ yield (" <li><a href='/divorce/get?ID=%s'>%s</a>: %s</li>\n"
+ % (id, id, contents))
+ yield "</ul>"
+ index.exposed = True
+
+ def get(self, ID):
+ return ("Divorce document %s: %s" %
+ (ID, self.documents.get(ID, "empty")))
+ get.exposed = True
+
+ root.divorce = Divorce()
+
+
+ class ThreadLocal(Test):
+
+ def index(self):
+ existing = repr(getattr(cherrypy.request, "asdf", None))
+ cherrypy.request.asdf = "rassfrassin"
+ return existing
+
+ appconf = {
+ '/method': {'request.methods_with_bodies': ("POST", "PUT", "PROPFIND")},
+ }
+ cherrypy.tree.mount(root, config=appconf)
+ setup_server = staticmethod(setup_server)
+
+ def test_scheme(self):
+ self.getPage("/scheme")
+ self.assertBody(self.scheme)
+
+ def testParams(self):
+ self.getPage("/params/?thing=a")
+ self.assertBody("u'a'")
+
+ self.getPage("/params/?thing=a&thing=b&thing=c")
+ self.assertBody("[u'a', u'b', u'c']")
+
+ # Test friendly error message when given params are not accepted.
+ cherrypy.config.update({"request.show_mismatched_params": True})
+ self.getPage("/params/?notathing=meeting")
+ self.assertInBody("Missing parameters: thing")
+ self.getPage("/params/?thing=meeting&notathing=meeting")
+ self.assertInBody("Unexpected query string parameters: notathing")
+
+ # Test ability to turn off friendly error messages
+ cherrypy.config.update({"request.show_mismatched_params": False})
+ self.getPage("/params/?notathing=meeting")
+ self.assertInBody("Not Found")
+ self.getPage("/params/?thing=meeting&notathing=meeting")
+ self.assertInBody("Not Found")
+
+ # Test "% HEX HEX"-encoded URL, param keys, and values
+ self.getPage("/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville")
+ self.assertBody(r"args: ('\xd4 \xe3', 'cheese') "
+ r"kwargs: {'Gruy\xe8re': u'Bulgn\xe9ville'}")
+
+ # Make sure that encoded = and & get parsed correctly
+ self.getPage("/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2")
+ self.assertBody(r"args: ('code',) "
+ r"kwargs: {'url': u'http://cherrypy.org/index?a=1&b=2'}")
+
+ # Test coordinates sent by <img ismap>
+ self.getPage("/params/ismap?223,114")
+ self.assertBody("Coordinates: 223, 114")
+
+ # Test "name[key]" dict-like params
+ self.getPage("/params/dictlike?a[1]=1&a[2]=2&b=foo&b[bar]=baz")
+ self.assertBody(
+ "args: ('dictlike',) "
+ "kwargs: {'a[1]': u'1', 'b[bar]': u'baz', 'b': u'foo', 'a[2]': u'2'}")
+
+ def testParamErrors(self):
+
+ # test that all of the handlers work when given
+ # the correct parameters in order to ensure that the
+ # errors below aren't coming from some other source.
+ for uri in (
+ '/paramerrors/one_positional?param1=foo',
+ '/paramerrors/one_positional_args?param1=foo',
+ '/paramerrors/one_positional_args/foo',
+ '/paramerrors/one_positional_args/foo/bar/baz',
+ '/paramerrors/one_positional_args_kwargs?param1=foo&param2=bar',
+ '/paramerrors/one_positional_args_kwargs/foo?param2=bar&param3=baz',
+ '/paramerrors/one_positional_args_kwargs/foo/bar/baz?param2=bar&param3=baz',
+ '/paramerrors/one_positional_kwargs?param1=foo&param2=bar&param3=baz',
+ '/paramerrors/one_positional_kwargs/foo?param4=foo&param2=bar&param3=baz',
+ '/paramerrors/no_positional',
+ '/paramerrors/no_positional_args/foo',
+ '/paramerrors/no_positional_args/foo/bar/baz',
+ '/paramerrors/no_positional_args_kwargs?param1=foo&param2=bar',
+ '/paramerrors/no_positional_args_kwargs/foo?param2=bar',
+ '/paramerrors/no_positional_args_kwargs/foo/bar/baz?param2=bar&param3=baz',
+ '/paramerrors/no_positional_kwargs?param1=foo&param2=bar',
+ '/paramerrors/callable_object',
+ ):
+ self.getPage(uri)
+ self.assertStatus(200)
+
+ # query string parameters are part of the URI, so if they are wrong
+ # for a particular handler, the status MUST be a 404.
+ error_msgs = [
+ 'Missing parameters',
+ 'Nothing matches the given URI',
+ 'Multiple values for parameters',
+ 'Unexpected query string parameters',
+ 'Unexpected body parameters',
+ ]
+ for uri, msg in (
+ ('/paramerrors/one_positional', error_msgs[0]),
+ ('/paramerrors/one_positional?foo=foo', error_msgs[0]),
+ ('/paramerrors/one_positional/foo/bar/baz', error_msgs[1]),
+ ('/paramerrors/one_positional/foo?param1=foo', error_msgs[2]),
+ ('/paramerrors/one_positional/foo?param1=foo&param2=foo', error_msgs[2]),
+ ('/paramerrors/one_positional_args/foo?param1=foo&param2=foo', error_msgs[2]),
+ ('/paramerrors/one_positional_args/foo/bar/baz?param2=foo', error_msgs[3]),
+ ('/paramerrors/one_positional_args_kwargs/foo/bar/baz?param1=bar&param3=baz', error_msgs[2]),
+ ('/paramerrors/one_positional_kwargs/foo?param1=foo&param2=bar&param3=baz', error_msgs[2]),
+ ('/paramerrors/no_positional/boo', error_msgs[1]),
+ ('/paramerrors/no_positional?param1=foo', error_msgs[3]),
+ ('/paramerrors/no_positional_args/boo?param1=foo', error_msgs[3]),
+ ('/paramerrors/no_positional_kwargs/boo?param1=foo', error_msgs[1]),
+ ('/paramerrors/callable_object?param1=foo', error_msgs[3]),
+ ('/paramerrors/callable_object/boo', error_msgs[1]),
+ ):
+ for show_mismatched_params in (True, False):
+ cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params})
+ self.getPage(uri)
+ self.assertStatus(404)
+ if show_mismatched_params:
+ self.assertInBody(msg)
+ else:
+ self.assertInBody("Not Found")
+
+ # if body parameters are wrong, a 400 must be returned.
+ for uri, body, msg in (
+ ('/paramerrors/one_positional/foo', 'param1=foo', error_msgs[2]),
+ ('/paramerrors/one_positional/foo', 'param1=foo&param2=foo', error_msgs[2]),
+ ('/paramerrors/one_positional_args/foo', 'param1=foo&param2=foo', error_msgs[2]),
+ ('/paramerrors/one_positional_args/foo/bar/baz', 'param2=foo', error_msgs[4]),
+ ('/paramerrors/one_positional_args_kwargs/foo/bar/baz', 'param1=bar&param3=baz', error_msgs[2]),
+ ('/paramerrors/one_positional_kwargs/foo', 'param1=foo&param2=bar&param3=baz', error_msgs[2]),
+ ('/paramerrors/no_positional', 'param1=foo', error_msgs[4]),
+ ('/paramerrors/no_positional_args/boo', 'param1=foo', error_msgs[4]),
+ ('/paramerrors/callable_object', 'param1=foo', error_msgs[4]),
+ ):
+ for show_mismatched_params in (True, False):
+ cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params})
+ self.getPage(uri, method='POST', body=body)
+ self.assertStatus(400)
+ if show_mismatched_params:
+ self.assertInBody(msg)
+ else:
+ self.assertInBody("Bad Request")
+
+
+ # even if body parameters are wrong, if we get the uri wrong, then
+ # it's a 404
+ for uri, body, msg in (
+ ('/paramerrors/one_positional?param2=foo', 'param1=foo', error_msgs[3]),
+ ('/paramerrors/one_positional/foo/bar', 'param2=foo', error_msgs[1]),
+ ('/paramerrors/one_positional_args/foo/bar?param2=foo', 'param3=foo', error_msgs[3]),
+ ('/paramerrors/one_positional_kwargs/foo/bar', 'param2=bar&param3=baz', error_msgs[1]),
+ ('/paramerrors/no_positional?param1=foo', 'param2=foo', error_msgs[3]),
+ ('/paramerrors/no_positional_args/boo?param2=foo', 'param1=foo', error_msgs[3]),
+ ('/paramerrors/callable_object?param2=bar', 'param1=foo', error_msgs[3]),
+ ):
+ for show_mismatched_params in (True, False):
+ cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params})
+ self.getPage(uri, method='POST', body=body)
+ self.assertStatus(404)
+ if show_mismatched_params:
+ self.assertInBody(msg)
+ else:
+ self.assertInBody("Not Found")
+
+ # In the case that a handler raises a TypeError we should
+ # let that type error through.
+ for uri in (
+ '/paramerrors/raise_type_error',
+ '/paramerrors/raise_type_error_with_default_param?x=0',
+ '/paramerrors/raise_type_error_with_default_param?x=0&y=0',
+ ):
+ self.getPage(uri, method='GET')
+ self.assertStatus(500)
+ self.assertTrue('Client Error', self.body)
+
+ def testErrorHandling(self):
+ self.getPage("/error/missing")
+ self.assertStatus(404)
+ self.assertErrorPage(404, "The path '/error/missing' was not found.")
+
+ ignore = helper.webtest.ignored_exceptions
+ ignore.append(ValueError)
+ try:
+ valerr = '\n raise ValueError()\nValueError'
+ self.getPage("/error/page_method")
+ self.assertErrorPage(500, pattern=valerr)
+
+ self.getPage("/error/page_yield")
+ self.assertErrorPage(500, pattern=valerr)
+
+ if (cherrypy.server.protocol_version == "HTTP/1.0" or
+ getattr(cherrypy.server, "using_apache", False)):
+ self.getPage("/error/page_streamed")
+ # Because this error is raised after the response body has
+ # started, the status should not change to an error status.
+ self.assertStatus(200)
+ self.assertBody("word up")
+ else:
+ # Under HTTP/1.1, the chunked transfer-coding is used.
+ # The HTTP client will choke when the output is incomplete.
+ self.assertRaises((ValueError, IncompleteRead), self.getPage,
+ "/error/page_streamed")
+
+ # No traceback should be present
+ self.getPage("/error/cause_err_in_finalize")
+ msg = "Illegal response status from server ('ZOO' is non-numeric)."
+ self.assertErrorPage(500, msg, None)
+ finally:
+ ignore.pop()
+
+ # Test HTTPError with a reason-phrase in the status arg.
+ self.getPage('/error/reason_phrase')
+ self.assertStatus("410 Gone fishin'")
+
+ # Test custom error page for a specific error.
+ self.getPage("/error/custom")
+ self.assertStatus(404)
+ self.assertBody("Hello, world\r\n" + (" " * 499))
+
+ # Test custom error page for a specific error.
+ self.getPage("/error/custom?err=401")
+ self.assertStatus(401)
+ self.assertBody("Error 401 Unauthorized - Well, I'm very sorry but you haven't paid!")
+
+ # Test default custom error page.
+ self.getPage("/error/custom_default")
+ self.assertStatus(500)
+ self.assertBody("Error 500 Internal Server Error - Well, I'm very sorry but you haven't paid!".ljust(513))
+
+ # Test error in custom error page (ticket #305).
+ # Note that the message is escaped for HTML (ticket #310).
+ self.getPage("/error/noexist")
+ self.assertStatus(404)
+ msg = ("No, &lt;b&gt;really&lt;/b&gt;, not found!<br />"
+ "In addition, the custom error page failed:\n<br />"
+ "IOError: [Errno 2] No such file or directory: 'nonexistent.html'")
+ self.assertInBody(msg)
+
+ if getattr(cherrypy.server, "using_apache", False):
+ pass
+ else:
+ # Test throw_errors (ticket #186).
+ self.getPage("/error/rethrow")
+ self.assertInBody("raise ValueError()")
+
+ def testExpect(self):
+ e = ('Expect', '100-continue')
+ self.getPage("/headerelements/get_elements?headername=Expect", [e])
+ self.assertBody('100-continue')
+
+ self.getPage("/expect/expectation_failed", [e])
+ self.assertStatus(417)
+
+ def testHeaderElements(self):
+ # Accept-* header elements should be sorted, with most preferred first.
+ h = [('Accept', 'audio/*; q=0.2, audio/basic')]
+ self.getPage("/headerelements/get_elements?headername=Accept", h)
+ self.assertStatus(200)
+ self.assertBody("audio/basic\n"
+ "audio/*;q=0.2")
+
+ h = [('Accept', 'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c')]
+ self.getPage("/headerelements/get_elements?headername=Accept", h)
+ self.assertStatus(200)
+ self.assertBody("text/x-c\n"
+ "text/html\n"
+ "text/x-dvi;q=0.8\n"
+ "text/plain;q=0.5")
+
+ # Test that more specific media ranges get priority.
+ h = [('Accept', 'text/*, text/html, text/html;level=1, */*')]
+ self.getPage("/headerelements/get_elements?headername=Accept", h)
+ self.assertStatus(200)
+ self.assertBody("text/html;level=1\n"
+ "text/html\n"
+ "text/*\n"
+ "*/*")
+
+ # Test Accept-Charset
+ h = [('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8')]
+ self.getPage("/headerelements/get_elements?headername=Accept-Charset", h)
+ self.assertStatus("200 OK")
+ self.assertBody("iso-8859-5\n"
+ "unicode-1-1;q=0.8")
+
+ # Test Accept-Encoding
+ h = [('Accept-Encoding', 'gzip;q=1.0, identity; q=0.5, *;q=0')]
+ self.getPage("/headerelements/get_elements?headername=Accept-Encoding", h)
+ self.assertStatus("200 OK")
+ self.assertBody("gzip;q=1.0\n"
+ "identity;q=0.5\n"
+ "*;q=0")
+
+ # Test Accept-Language
+ h = [('Accept-Language', 'da, en-gb;q=0.8, en;q=0.7')]
+ self.getPage("/headerelements/get_elements?headername=Accept-Language", h)
+ self.assertStatus("200 OK")
+ self.assertBody("da\n"
+ "en-gb;q=0.8\n"
+ "en;q=0.7")
+
+ # Test malformed header parsing. See http://www.cherrypy.org/ticket/763.
+ self.getPage("/headerelements/get_elements?headername=Content-Type",
+ # Note the illegal trailing ";"
+ headers=[('Content-Type', 'text/html; charset=utf-8;')])
+ self.assertStatus(200)
+ self.assertBody("text/html;charset=utf-8")
+
+ def test_repeated_headers(self):
+ # Test that two request headers are collapsed into one.
+ # See http://www.cherrypy.org/ticket/542.
+ self.getPage("/headers/Accept-Charset",
+ headers=[("Accept-Charset", "iso-8859-5"),
+ ("Accept-Charset", "unicode-1-1;q=0.8")])
+ self.assertBody("iso-8859-5, unicode-1-1;q=0.8")
+
+ # Tests that each header only appears once, regardless of case.
+ self.getPage("/headers/doubledheaders")
+ self.assertBody("double header test")
+ hnames = [name.title() for name, val in self.headers]
+ for key in ['Content-Length', 'Content-Type', 'Date',
+ 'Expires', 'Location', 'Server']:
+ self.assertEqual(hnames.count(key), 1, self.headers)
+
+ def test_encoded_headers(self):
+ # First, make sure the innards work like expected.
+ self.assertEqual(httputil.decode_TEXT(u"=?utf-8?q?f=C3=BCr?="), u"f\xfcr")
+
+ if cherrypy.server.protocol_version == "HTTP/1.1":
+ # Test RFC-2047-encoded request and response header values
+ u = u'\u212bngstr\xf6m'
+ c = u"=E2=84=ABngstr=C3=B6m"
+ self.getPage("/headers/ifmatch", [('If-Match', u'=?utf-8?q?%s?=' % c)])
+ # The body should be utf-8 encoded.
+ self.assertBody("\xe2\x84\xabngstr\xc3\xb6m")
+ # But the Etag header should be RFC-2047 encoded (binary)
+ self.assertHeader("ETag", u'=?utf-8?b?4oSrbmdzdHLDtm0=?=')
+
+ # Test a *LONG* RFC-2047-encoded request and response header value
+ self.getPage("/headers/ifmatch",
+ [('If-Match', u'=?utf-8?q?%s?=' % (c * 10))])
+ self.assertBody("\xe2\x84\xabngstr\xc3\xb6m" * 10)
+ # Note: this is different output for Python3, but it decodes fine.
+ etag = self.assertHeader("ETag",
+ '=?utf-8?b?4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
+ '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
+ '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
+ '4oSrbmdzdHLDtm0=?=')
+ self.assertEqual(httputil.decode_TEXT(etag), u * 10)
+
+ def test_header_presence(self):
+ # If we don't pass a Content-Type header, it should not be present
+ # in cherrypy.request.headers
+ self.getPage("/headers/Content-Type",
+ headers=[])
+ self.assertStatus(500)
+
+ # If Content-Type is present in the request, it should be present in
+ # cherrypy.request.headers
+ self.getPage("/headers/Content-Type",
+ headers=[("Content-type", "application/json")])
+ self.assertBody("application/json")
+
+ def test_basic_HTTPMethods(self):
+ helper.webtest.methods_with_bodies = ("POST", "PUT", "PROPFIND")
+
+ # Test that all defined HTTP methods work.
+ for m in defined_http_methods:
+ self.getPage("/method/", method=m)
+
+ # HEAD requests should not return any body.
+ if m == "HEAD":
+ self.assertBody("")
+ elif m == "TRACE":
+ # Some HTTP servers (like modpy) have their own TRACE support
+ self.assertEqual(self.body[:5], ntob("TRACE"))
+ else:
+ self.assertBody(m)
+
+ # Request a PUT method with a form-urlencoded body
+ self.getPage("/method/parameterized", method="PUT",
+ body="data=on+top+of+other+things")
+ self.assertBody("on top of other things")
+
+ # Request a PUT method with a file body
+ b = "one thing on top of another"
+ h = [("Content-Type", "text/plain"),
+ ("Content-Length", str(len(b)))]
+ self.getPage("/method/request_body", headers=h, method="PUT", body=b)
+ self.assertStatus(200)
+ self.assertBody(b)
+
+ # Request a PUT method with a file body but no Content-Type.
+ # See http://www.cherrypy.org/ticket/790.
+ b = ntob("one thing on top of another")
+ self.persistent = True
+ try:
+ conn = self.HTTP_CONN
+ conn.putrequest("PUT", "/method/request_body", skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.putheader('Content-Length', str(len(b)))
+ conn.endheaders()
+ conn.send(b)
+ response = conn.response_class(conn.sock, method="PUT")
+ response.begin()
+ self.assertEqual(response.status, 200)
+ self.body = response.read()
+ self.assertBody(b)
+ finally:
+ self.persistent = False
+
+ # Request a PUT method with no body whatsoever (not an empty one).
+ # See http://www.cherrypy.org/ticket/650.
+ # Provide a C-T or webtest will provide one (and a C-L) for us.
+ h = [("Content-Type", "text/plain")]
+ self.getPage("/method/reachable", headers=h, method="PUT")
+ self.assertStatus(411)
+
+ # Request a custom method with a request body
+ b = ('<?xml version="1.0" encoding="utf-8" ?>\n\n'
+ '<propfind xmlns="DAV:"><prop><getlastmodified/>'
+ '</prop></propfind>')
+ h = [('Content-Type', 'text/xml'),
+ ('Content-Length', str(len(b)))]
+ self.getPage("/method/request_body", headers=h, method="PROPFIND", body=b)
+ self.assertStatus(200)
+ self.assertBody(b)
+
+ # Request a disallowed method
+ self.getPage("/method/", method="LINK")
+ self.assertStatus(405)
+
+ # Request an unknown method
+ self.getPage("/method/", method="SEARCH")
+ self.assertStatus(501)
+
+ # For method dispatchers: make sure that an HTTP method doesn't
+ # collide with a virtual path atom. If you build HTTP-method
+ # dispatching into the core, rewrite these handlers to use
+ # your dispatch idioms.
+ self.getPage("/divorce/get?ID=13")
+ self.assertBody('Divorce document 13: empty')
+ self.assertStatus(200)
+ self.getPage("/divorce/", method="GET")
+ self.assertBody('<h1>Choose your document</h1>\n<ul>\n</ul>')
+ self.assertStatus(200)
+
+ def test_CONNECT_method(self):
+ if getattr(cherrypy.server, "using_apache", False):
+ return self.skip("skipped due to known Apache differences... ")
+
+ self.getPage("/method/", method="CONNECT")
+ self.assertBody("CONNECT")
+
+ def testEmptyThreadlocals(self):
+ results = []
+ for x in range(20):
+ self.getPage("/threadlocal/")
+ results.append(self.body)
+ self.assertEqual(results, [ntob("None")] * 20)
+
diff --git a/cherrypy/test/test_routes.py b/cherrypy/test/test_routes.py
new file mode 100755
index 0000000..a8062f8
--- /dev/null
+++ b/cherrypy/test/test_routes.py
@@ -0,0 +1,69 @@
+import os
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+import cherrypy
+
+from cherrypy.test import helper
+import nose
+
+class RoutesDispatchTest(helper.CPWebCase):
+
+ def setup_server():
+
+ try:
+ import routes
+ except ImportError:
+ raise nose.SkipTest('Install routes to test RoutesDispatcher code')
+
+ class Dummy:
+ def index(self):
+ return "I said good day!"
+
+ class City:
+
+ def __init__(self, name):
+ self.name = name
+ self.population = 10000
+
+ def index(self, **kwargs):
+ return "Welcome to %s, pop. %s" % (self.name, self.population)
+ index._cp_config = {'tools.response_headers.on': True,
+ 'tools.response_headers.headers': [('Content-Language', 'en-GB')]}
+
+ def update(self, **kwargs):
+ self.population = kwargs['pop']
+ return "OK"
+
+ d = cherrypy.dispatch.RoutesDispatcher()
+ d.connect(action='index', name='hounslow', route='/hounslow',
+ controller=City('Hounslow'))
+ d.connect(name='surbiton', route='/surbiton', controller=City('Surbiton'),
+ action='index', conditions=dict(method=['GET']))
+ d.mapper.connect('/surbiton', controller='surbiton',
+ action='update', conditions=dict(method=['POST']))
+ d.connect('main', ':action', controller=Dummy())
+
+ conf = {'/': {'request.dispatch': d}}
+ cherrypy.tree.mount(root=None, config=conf)
+ setup_server = staticmethod(setup_server)
+
+ def test_Routes_Dispatch(self):
+ self.getPage("/hounslow")
+ self.assertStatus("200 OK")
+ self.assertBody("Welcome to Hounslow, pop. 10000")
+
+ self.getPage("/foo")
+ self.assertStatus("404 Not Found")
+
+ self.getPage("/surbiton")
+ self.assertStatus("200 OK")
+ self.assertBody("Welcome to Surbiton, pop. 10000")
+
+ self.getPage("/surbiton", method="POST", body="pop=1327")
+ self.assertStatus("200 OK")
+ self.assertBody("OK")
+ self.getPage("/surbiton")
+ self.assertStatus("200 OK")
+ self.assertHeader("Content-Language", "en-GB")
+ self.assertBody("Welcome to Surbiton, pop. 1327")
+
diff --git a/cherrypy/test/test_session.py b/cherrypy/test/test_session.py
new file mode 100755
index 0000000..874023e
--- /dev/null
+++ b/cherrypy/test/test_session.py
@@ -0,0 +1,464 @@
+import os
+localDir = os.path.dirname(__file__)
+import sys
+import threading
+import time
+
+import cherrypy
+from cherrypy._cpcompat import copykeys, HTTPConnection, HTTPSConnection
+from cherrypy.lib import sessions
+from cherrypy.lib.httputil import response_codes
+
+def http_methods_allowed(methods=['GET', 'HEAD']):
+ method = cherrypy.request.method.upper()
+ if method not in methods:
+ cherrypy.response.headers['Allow'] = ", ".join(methods)
+ raise cherrypy.HTTPError(405)
+
+cherrypy.tools.allow = cherrypy.Tool('on_start_resource', http_methods_allowed)
+
+
+def setup_server():
+
+ class Root:
+
+ _cp_config = {'tools.sessions.on': True,
+ 'tools.sessions.storage_type' : 'ram',
+ 'tools.sessions.storage_path' : localDir,
+ 'tools.sessions.timeout': (1.0 / 60),
+ 'tools.sessions.clean_freq': (1.0 / 60),
+ }
+
+ def clear(self):
+ cherrypy.session.cache.clear()
+ clear.exposed = True
+
+ def data(self):
+ cherrypy.session['aha'] = 'foo'
+ return repr(cherrypy.session._data)
+ data.exposed = True
+
+ def testGen(self):
+ counter = cherrypy.session.get('counter', 0) + 1
+ cherrypy.session['counter'] = counter
+ yield str(counter)
+ testGen.exposed = True
+
+ def testStr(self):
+ counter = cherrypy.session.get('counter', 0) + 1
+ cherrypy.session['counter'] = counter
+ return str(counter)
+ testStr.exposed = True
+
+ def setsessiontype(self, newtype):
+ self.__class__._cp_config.update({'tools.sessions.storage_type': newtype})
+ if hasattr(cherrypy, "session"):
+ del cherrypy.session
+ cls = getattr(sessions, newtype.title() + 'Session')
+ if cls.clean_thread:
+ cls.clean_thread.stop()
+ cls.clean_thread.unsubscribe()
+ del cls.clean_thread
+ setsessiontype.exposed = True
+ setsessiontype._cp_config = {'tools.sessions.on': False}
+
+ def index(self):
+ sess = cherrypy.session
+ c = sess.get('counter', 0) + 1
+ time.sleep(0.01)
+ sess['counter'] = c
+ return str(c)
+ index.exposed = True
+
+ def keyin(self, key):
+ return str(key in cherrypy.session)
+ keyin.exposed = True
+
+ def delete(self):
+ cherrypy.session.delete()
+ sessions.expire()
+ return "done"
+ delete.exposed = True
+
+ def delkey(self, key):
+ del cherrypy.session[key]
+ return "OK"
+ delkey.exposed = True
+
+ def blah(self):
+ return self._cp_config['tools.sessions.storage_type']
+ blah.exposed = True
+
+ def iredir(self):
+ raise cherrypy.InternalRedirect('/blah')
+ iredir.exposed = True
+
+ def restricted(self):
+ return cherrypy.request.method
+ restricted.exposed = True
+ restricted._cp_config = {'tools.allow.on': True,
+ 'tools.allow.methods': ['GET']}
+
+ def regen(self):
+ cherrypy.tools.sessions.regenerate()
+ return "logged in"
+ regen.exposed = True
+
+ def length(self):
+ return str(len(cherrypy.session))
+ length.exposed = True
+
+ def session_cookie(self):
+ # Must load() to start the clean thread.
+ cherrypy.session.load()
+ return cherrypy.session.id
+ session_cookie.exposed = True
+ session_cookie._cp_config = {
+ 'tools.sessions.path': '/session_cookie',
+ 'tools.sessions.name': 'temp',
+ 'tools.sessions.persistent': False}
+
+ cherrypy.tree.mount(Root())
+
+
+from cherrypy.test import helper
+
+class SessionTest(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def tearDown(self):
+ # Clean up sessions.
+ for fname in os.listdir(localDir):
+ if fname.startswith(sessions.FileSession.SESSION_PREFIX):
+ os.unlink(os.path.join(localDir, fname))
+
+ def test_0_Session(self):
+ self.getPage('/setsessiontype/ram')
+ self.getPage('/clear')
+
+ # Test that a normal request gets the same id in the cookies.
+ # Note: this wouldn't work if /data didn't load the session.
+ self.getPage('/data')
+ self.assertBody("{'aha': 'foo'}")
+ c = self.cookies[0]
+ self.getPage('/data', self.cookies)
+ self.assertEqual(self.cookies[0], c)
+
+ self.getPage('/testStr')
+ self.assertBody('1')
+ cookie_parts = dict([p.strip().split('=')
+ for p in self.cookies[0][1].split(";")])
+ # Assert there is an 'expires' param
+ self.assertEqual(set(cookie_parts.keys()),
+ set(['session_id', 'expires', 'Path']))
+ self.getPage('/testGen', self.cookies)
+ self.assertBody('2')
+ self.getPage('/testStr', self.cookies)
+ self.assertBody('3')
+ self.getPage('/data', self.cookies)
+ self.assertBody("{'aha': 'foo', 'counter': 3}")
+ self.getPage('/length', self.cookies)
+ self.assertBody('2')
+ self.getPage('/delkey?key=counter', self.cookies)
+ self.assertStatus(200)
+
+ self.getPage('/setsessiontype/file')
+ self.getPage('/testStr')
+ self.assertBody('1')
+ self.getPage('/testGen', self.cookies)
+ self.assertBody('2')
+ self.getPage('/testStr', self.cookies)
+ self.assertBody('3')
+ self.getPage('/delkey?key=counter', self.cookies)
+ self.assertStatus(200)
+
+ # Wait for the session.timeout (1 second)
+ time.sleep(2)
+ self.getPage('/')
+ self.assertBody('1')
+ self.getPage('/length', self.cookies)
+ self.assertBody('1')
+
+ # Test session __contains__
+ self.getPage('/keyin?key=counter', self.cookies)
+ self.assertBody("True")
+ cookieset1 = self.cookies
+
+ # Make a new session and test __len__ again
+ self.getPage('/')
+ self.getPage('/length', self.cookies)
+ self.assertBody('2')
+
+ # Test session delete
+ self.getPage('/delete', self.cookies)
+ self.assertBody("done")
+ self.getPage('/delete', cookieset1)
+ self.assertBody("done")
+ f = lambda: [x for x in os.listdir(localDir) if x.startswith('session-')]
+ self.assertEqual(f(), [])
+
+ # Wait for the cleanup thread to delete remaining session files
+ self.getPage('/')
+ f = lambda: [x for x in os.listdir(localDir) if x.startswith('session-')]
+ self.assertNotEqual(f(), [])
+ time.sleep(2)
+ self.assertEqual(f(), [])
+
+ def test_1_Ram_Concurrency(self):
+ self.getPage('/setsessiontype/ram')
+ self._test_Concurrency()
+
+ def test_2_File_Concurrency(self):
+ self.getPage('/setsessiontype/file')
+ self._test_Concurrency()
+
+ def _test_Concurrency(self):
+ client_thread_count = 5
+ request_count = 30
+
+ # Get initial cookie
+ self.getPage("/")
+ self.assertBody("1")
+ cookies = self.cookies
+
+ data_dict = {}
+ errors = []
+
+ def request(index):
+ if self.scheme == 'https':
+ c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
+ else:
+ c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+ for i in range(request_count):
+ c.putrequest('GET', '/')
+ for k, v in cookies:
+ c.putheader(k, v)
+ c.endheaders()
+ response = c.getresponse()
+ body = response.read()
+ if response.status != 200 or not body.isdigit():
+ errors.append((response.status, body))
+ else:
+ data_dict[index] = max(data_dict[index], int(body))
+ # Uncomment the following line to prove threads overlap.
+## sys.stdout.write("%d " % index)
+
+ # Start <request_count> requests from each of
+ # <client_thread_count> concurrent clients
+ ts = []
+ for c in range(client_thread_count):
+ data_dict[c] = 0
+ t = threading.Thread(target=request, args=(c,))
+ ts.append(t)
+ t.start()
+
+ for t in ts:
+ t.join()
+
+ hitcount = max(data_dict.values())
+ expected = 1 + (client_thread_count * request_count)
+
+ for e in errors:
+ print(e)
+ self.assertEqual(hitcount, expected)
+
+ def test_3_Redirect(self):
+ # Start a new session
+ self.getPage('/testStr')
+ self.getPage('/iredir', self.cookies)
+ self.assertBody("file")
+
+ def test_4_File_deletion(self):
+ # Start a new session
+ self.getPage('/testStr')
+ # Delete the session file manually and retry.
+ id = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1]
+ path = os.path.join(localDir, "session-" + id)
+ os.unlink(path)
+ self.getPage('/testStr', self.cookies)
+
+ def test_5_Error_paths(self):
+ self.getPage('/unknown/page')
+ self.assertErrorPage(404, "The path '/unknown/page' was not found.")
+
+ # Note: this path is *not* the same as above. The above
+ # takes a normal route through the session code; this one
+ # skips the session code's before_handler and only calls
+ # before_finalize (save) and on_end (close). So the session
+ # code has to survive calling save/close without init.
+ self.getPage('/restricted', self.cookies, method='POST')
+ self.assertErrorPage(405, response_codes[405])
+
+ def test_6_regenerate(self):
+ self.getPage('/testStr')
+ # grab the cookie ID
+ id1 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1]
+ self.getPage('/regen')
+ self.assertBody('logged in')
+ id2 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1]
+ self.assertNotEqual(id1, id2)
+
+ self.getPage('/testStr')
+ # grab the cookie ID
+ id1 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1]
+ self.getPage('/testStr',
+ headers=[('Cookie',
+ 'session_id=maliciousid; '
+ 'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')])
+ id2 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1]
+ self.assertNotEqual(id1, id2)
+ self.assertNotEqual(id2, 'maliciousid')
+
+ def test_7_session_cookies(self):
+ self.getPage('/setsessiontype/ram')
+ self.getPage('/clear')
+ self.getPage('/session_cookie')
+ # grab the cookie ID
+ cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")])
+ # Assert there is no 'expires' param
+ self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
+ id1 = cookie_parts['temp']
+ self.assertEqual(copykeys(sessions.RamSession.cache), [id1])
+
+ # Send another request in the same "browser session".
+ self.getPage('/session_cookie', self.cookies)
+ cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")])
+ # Assert there is no 'expires' param
+ self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
+ self.assertBody(id1)
+ self.assertEqual(copykeys(sessions.RamSession.cache), [id1])
+
+ # Simulate a browser close by just not sending the cookies
+ self.getPage('/session_cookie')
+ # grab the cookie ID
+ cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")])
+ # Assert there is no 'expires' param
+ self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
+ # Assert a new id has been generated...
+ id2 = cookie_parts['temp']
+ self.assertNotEqual(id1, id2)
+ self.assertEqual(set(sessions.RamSession.cache.keys()), set([id1, id2]))
+
+ # Wait for the session.timeout on both sessions
+ time.sleep(2.5)
+ cache = copykeys(sessions.RamSession.cache)
+ if cache:
+ if cache == [id2]:
+ self.fail("The second session did not time out.")
+ else:
+ self.fail("Unknown session id in cache: %r", cache)
+
+
+import socket
+try:
+ import memcache
+
+ host, port = '127.0.0.1', 11211
+ for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
+ socket.SOCK_STREAM):
+ af, socktype, proto, canonname, sa = res
+ s = None
+ try:
+ s = socket.socket(af, socktype, proto)
+ # See http://groups.google.com/group/cherrypy-users/
+ # browse_frm/thread/bbfe5eb39c904fe0
+ s.settimeout(1.0)
+ s.connect((host, port))
+ s.close()
+ except socket.error:
+ if s:
+ s.close()
+ raise
+ break
+except (ImportError, socket.error):
+ class MemcachedSessionTest(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def test(self):
+ return self.skip("memcached not reachable ")
+else:
+ class MemcachedSessionTest(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def test_0_Session(self):
+ self.getPage('/setsessiontype/memcached')
+
+ self.getPage('/testStr')
+ self.assertBody('1')
+ self.getPage('/testGen', self.cookies)
+ self.assertBody('2')
+ self.getPage('/testStr', self.cookies)
+ self.assertBody('3')
+ self.getPage('/length', self.cookies)
+ self.assertErrorPage(500)
+ self.assertInBody("NotImplementedError")
+ self.getPage('/delkey?key=counter', self.cookies)
+ self.assertStatus(200)
+
+ # Wait for the session.timeout (1 second)
+ time.sleep(1.25)
+ self.getPage('/')
+ self.assertBody('1')
+
+ # Test session __contains__
+ self.getPage('/keyin?key=counter', self.cookies)
+ self.assertBody("True")
+
+ # Test session delete
+ self.getPage('/delete', self.cookies)
+ self.assertBody("done")
+
+ def test_1_Concurrency(self):
+ client_thread_count = 5
+ request_count = 30
+
+ # Get initial cookie
+ self.getPage("/")
+ self.assertBody("1")
+ cookies = self.cookies
+
+ data_dict = {}
+
+ def request(index):
+ for i in range(request_count):
+ self.getPage("/", cookies)
+ # Uncomment the following line to prove threads overlap.
+## sys.stdout.write("%d " % index)
+ if not self.body.isdigit():
+ self.fail(self.body)
+ data_dict[index] = v = int(self.body)
+
+ # Start <request_count> concurrent requests from
+ # each of <client_thread_count> clients
+ ts = []
+ for c in range(client_thread_count):
+ data_dict[c] = 0
+ t = threading.Thread(target=request, args=(c,))
+ ts.append(t)
+ t.start()
+
+ for t in ts:
+ t.join()
+
+ hitcount = max(data_dict.values())
+ expected = 1 + (client_thread_count * request_count)
+ self.assertEqual(hitcount, expected)
+
+ def test_3_Redirect(self):
+ # Start a new session
+ self.getPage('/testStr')
+ self.getPage('/iredir', self.cookies)
+ self.assertBody("memcached")
+
+ def test_5_Error_paths(self):
+ self.getPage('/unknown/page')
+ self.assertErrorPage(404, "The path '/unknown/page' was not found.")
+
+ # Note: this path is *not* the same as above. The above
+ # takes a normal route through the session code; this one
+ # skips the session code's before_handler and only calls
+ # before_finalize (save) and on_end (close). So the session
+ # code has to survive calling save/close without init.
+ self.getPage('/restricted', self.cookies, method='POST')
+ self.assertErrorPage(405, response_codes[405])
+
diff --git a/cherrypy/test/test_sessionauthenticate.py b/cherrypy/test/test_sessionauthenticate.py
new file mode 100755
index 0000000..ab1fe51
--- /dev/null
+++ b/cherrypy/test/test_sessionauthenticate.py
@@ -0,0 +1,62 @@
+import cherrypy
+from cherrypy.test import helper
+
+
+class SessionAuthenticateTest(helper.CPWebCase):
+
+ def setup_server():
+
+ def check(username, password):
+ # Dummy check_username_and_password function
+ if username != 'test' or password != 'password':
+ return 'Wrong login/password'
+
+ def augment_params():
+ # A simple tool to add some things to request.params
+ # This is to check to make sure that session_auth can handle request
+ # params (ticket #780)
+ cherrypy.request.params["test"] = "test"
+
+ cherrypy.tools.augment_params = cherrypy.Tool('before_handler',
+ augment_params, None, priority=30)
+
+ class Test:
+
+ _cp_config = {'tools.sessions.on': True,
+ 'tools.session_auth.on': True,
+ 'tools.session_auth.check_username_and_password': check,
+ 'tools.augment_params.on': True,
+ }
+
+ def index(self, **kwargs):
+ return "Hi %s, you are logged in" % cherrypy.request.login
+ index.exposed = True
+
+ cherrypy.tree.mount(Test())
+ setup_server = staticmethod(setup_server)
+
+
+ def testSessionAuthenticate(self):
+ # request a page and check for login form
+ self.getPage('/')
+ self.assertInBody('<form method="post" action="do_login">')
+
+ # setup credentials
+ login_body = 'username=test&password=password&from_page=/'
+
+ # attempt a login
+ self.getPage('/do_login', method='POST', body=login_body)
+ self.assertStatus((302, 303))
+
+ # get the page now that we are logged in
+ self.getPage('/', self.cookies)
+ self.assertBody('Hi test, you are logged in')
+
+ # do a logout
+ self.getPage('/do_logout', self.cookies, method='POST')
+ self.assertStatus((302, 303))
+
+ # verify we are logged out
+ self.getPage('/', self.cookies)
+ self.assertInBody('<form method="post" action="do_login">')
+
diff --git a/cherrypy/test/test_states.py b/cherrypy/test/test_states.py
new file mode 100755
index 0000000..0f97337
--- /dev/null
+++ b/cherrypy/test/test_states.py
@@ -0,0 +1,436 @@
+from cherrypy._cpcompat import BadStatusLine, ntob
+import os
+import sys
+import threading
+import time
+
+import cherrypy
+engine = cherrypy.engine
+thisdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+
+class Dependency:
+
+ def __init__(self, bus):
+ self.bus = bus
+ self.running = False
+ self.startcount = 0
+ self.gracecount = 0
+ self.threads = {}
+
+ def subscribe(self):
+ self.bus.subscribe('start', self.start)
+ self.bus.subscribe('stop', self.stop)
+ self.bus.subscribe('graceful', self.graceful)
+ self.bus.subscribe('start_thread', self.startthread)
+ self.bus.subscribe('stop_thread', self.stopthread)
+
+ def start(self):
+ self.running = True
+ self.startcount += 1
+
+ def stop(self):
+ self.running = False
+
+ def graceful(self):
+ self.gracecount += 1
+
+ def startthread(self, thread_id):
+ self.threads[thread_id] = None
+
+ def stopthread(self, thread_id):
+ del self.threads[thread_id]
+
+db_connection = Dependency(engine)
+
+def setup_server():
+ class Root:
+ def index(self):
+ return "Hello World"
+ index.exposed = True
+
+ def ctrlc(self):
+ raise KeyboardInterrupt()
+ ctrlc.exposed = True
+
+ def graceful(self):
+ engine.graceful()
+ return "app was (gracefully) restarted succesfully"
+ graceful.exposed = True
+
+ def block_explicit(self):
+ while True:
+ if cherrypy.response.timed_out:
+ cherrypy.response.timed_out = False
+ return "broken!"
+ time.sleep(0.01)
+ block_explicit.exposed = True
+
+ def block_implicit(self):
+ time.sleep(0.5)
+ return "response.timeout = %s" % cherrypy.response.timeout
+ block_implicit.exposed = True
+
+ cherrypy.tree.mount(Root())
+ cherrypy.config.update({
+ 'environment': 'test_suite',
+ 'engine.deadlock_poll_freq': 0.1,
+ })
+
+ db_connection.subscribe()
+
+
+
+# ------------ Enough helpers. Time for real live test cases. ------------ #
+
+
+from cherrypy.test import helper
+
+class ServerStateTests(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+
+ def setUp(self):
+ cherrypy.server.socket_timeout = 0.1
+
+ def test_0_NormalStateFlow(self):
+ engine.stop()
+ # Our db_connection should not be running
+ self.assertEqual(db_connection.running, False)
+ self.assertEqual(db_connection.startcount, 1)
+ self.assertEqual(len(db_connection.threads), 0)
+
+ # Test server start
+ engine.start()
+ self.assertEqual(engine.state, engine.states.STARTED)
+
+ host = cherrypy.server.socket_host
+ port = cherrypy.server.socket_port
+ self.assertRaises(IOError, cherrypy._cpserver.check_port, host, port)
+
+ # The db_connection should be running now
+ self.assertEqual(db_connection.running, True)
+ self.assertEqual(db_connection.startcount, 2)
+ self.assertEqual(len(db_connection.threads), 0)
+
+ self.getPage("/")
+ self.assertBody("Hello World")
+ self.assertEqual(len(db_connection.threads), 1)
+
+ # Test engine stop. This will also stop the HTTP server.
+ engine.stop()
+ self.assertEqual(engine.state, engine.states.STOPPED)
+
+ # Verify that our custom stop function was called
+ self.assertEqual(db_connection.running, False)
+ self.assertEqual(len(db_connection.threads), 0)
+
+ # Block the main thread now and verify that exit() works.
+ def exittest():
+ self.getPage("/")
+ self.assertBody("Hello World")
+ engine.exit()
+ cherrypy.server.start()
+ engine.start_with_callback(exittest)
+ engine.block()
+ self.assertEqual(engine.state, engine.states.EXITING)
+
+ def test_1_Restart(self):
+ cherrypy.server.start()
+ engine.start()
+
+ # The db_connection should be running now
+ self.assertEqual(db_connection.running, True)
+ grace = db_connection.gracecount
+
+ self.getPage("/")
+ self.assertBody("Hello World")
+ self.assertEqual(len(db_connection.threads), 1)
+
+ # Test server restart from this thread
+ engine.graceful()
+ self.assertEqual(engine.state, engine.states.STARTED)
+ self.getPage("/")
+ self.assertBody("Hello World")
+ self.assertEqual(db_connection.running, True)
+ self.assertEqual(db_connection.gracecount, grace + 1)
+ self.assertEqual(len(db_connection.threads), 1)
+
+ # Test server restart from inside a page handler
+ self.getPage("/graceful")
+ self.assertEqual(engine.state, engine.states.STARTED)
+ self.assertBody("app was (gracefully) restarted succesfully")
+ self.assertEqual(db_connection.running, True)
+ self.assertEqual(db_connection.gracecount, grace + 2)
+ # Since we are requesting synchronously, is only one thread used?
+ # Note that the "/graceful" request has been flushed.
+ self.assertEqual(len(db_connection.threads), 0)
+
+ engine.stop()
+ self.assertEqual(engine.state, engine.states.STOPPED)
+ self.assertEqual(db_connection.running, False)
+ self.assertEqual(len(db_connection.threads), 0)
+
+ def test_2_KeyboardInterrupt(self):
+ # Raise a keyboard interrupt in the HTTP server's main thread.
+ # We must start the server in this, the main thread
+ engine.start()
+ cherrypy.server.start()
+
+ self.persistent = True
+ try:
+ # Make the first request and assert there's no "Connection: close".
+ self.getPage("/")
+ self.assertStatus('200 OK')
+ self.assertBody("Hello World")
+ self.assertNoHeader("Connection")
+
+ cherrypy.server.httpserver.interrupt = KeyboardInterrupt
+ engine.block()
+
+ self.assertEqual(db_connection.running, False)
+ self.assertEqual(len(db_connection.threads), 0)
+ self.assertEqual(engine.state, engine.states.EXITING)
+ finally:
+ self.persistent = False
+
+ # Raise a keyboard interrupt in a page handler; on multithreaded
+ # servers, this should occur in one of the worker threads.
+ # This should raise a BadStatusLine error, since the worker
+ # thread will just die without writing a response.
+ engine.start()
+ cherrypy.server.start()
+
+ try:
+ self.getPage("/ctrlc")
+ except BadStatusLine:
+ pass
+ else:
+ print(self.body)
+ self.fail("AssertionError: BadStatusLine not raised")
+
+ engine.block()
+ self.assertEqual(db_connection.running, False)
+ self.assertEqual(len(db_connection.threads), 0)
+
+ def test_3_Deadlocks(self):
+ cherrypy.config.update({'response.timeout': 0.2})
+
+ engine.start()
+ cherrypy.server.start()
+ try:
+ self.assertNotEqual(engine.timeout_monitor.thread, None)
+
+ # Request a "normal" page.
+ self.assertEqual(engine.timeout_monitor.servings, [])
+ self.getPage("/")
+ self.assertBody("Hello World")
+ # request.close is called async.
+ while engine.timeout_monitor.servings:
+ sys.stdout.write(".")
+ time.sleep(0.01)
+
+ # Request a page that explicitly checks itself for deadlock.
+ # The deadlock_timeout should be 2 secs.
+ self.getPage("/block_explicit")
+ self.assertBody("broken!")
+
+ # Request a page that implicitly breaks deadlock.
+ # If we deadlock, we want to touch as little code as possible,
+ # so we won't even call handle_error, just bail ASAP.
+ self.getPage("/block_implicit")
+ self.assertStatus(500)
+ self.assertInBody("raise cherrypy.TimeoutError()")
+ finally:
+ engine.exit()
+
+ def test_4_Autoreload(self):
+ # Start the demo script in a new process
+ p = helper.CPProcess(ssl=(self.scheme.lower()=='https'))
+ p.write_conf(
+ extra='test_case_name: "test_4_Autoreload"')
+ p.start(imports='cherrypy.test._test_states_demo')
+ try:
+ self.getPage("/start")
+ start = float(self.body)
+
+ # Give the autoreloader time to cache the file time.
+ time.sleep(2)
+
+ # Touch the file
+ os.utime(os.path.join(thisdir, "_test_states_demo.py"), None)
+
+ # Give the autoreloader time to re-exec the process
+ time.sleep(2)
+ host = cherrypy.server.socket_host
+ port = cherrypy.server.socket_port
+ cherrypy._cpserver.wait_for_occupied_port(host, port)
+
+ self.getPage("/start")
+ self.assert_(float(self.body) > start)
+ finally:
+ # Shut down the spawned process
+ self.getPage("/exit")
+ p.join()
+
+ def test_5_Start_Error(self):
+ # If a process errors during start, it should stop the engine
+ # and exit with a non-zero exit code.
+ p = helper.CPProcess(ssl=(self.scheme.lower()=='https'),
+ wait=True)
+ p.write_conf(
+ extra="""starterror: True
+test_case_name: "test_5_Start_Error"
+"""
+ )
+ p.start(imports='cherrypy.test._test_states_demo')
+ if p.exit_code == 0:
+ self.fail("Process failed to return nonzero exit code.")
+
+
+class PluginTests(helper.CPWebCase):
+ def test_daemonize(self):
+ if os.name not in ['posix']:
+ return self.skip("skipped (not on posix) ")
+ self.HOST = '127.0.0.1'
+ self.PORT = 8081
+ # Spawn the process and wait, when this returns, the original process
+ # is finished. If it daemonized properly, we should still be able
+ # to access pages.
+ p = helper.CPProcess(ssl=(self.scheme.lower()=='https'),
+ wait=True, daemonize=True,
+ socket_host='127.0.0.1',
+ socket_port=8081)
+ p.write_conf(
+ extra='test_case_name: "test_daemonize"')
+ p.start(imports='cherrypy.test._test_states_demo')
+ try:
+ # Just get the pid of the daemonization process.
+ self.getPage("/pid")
+ self.assertStatus(200)
+ page_pid = int(self.body)
+ self.assertEqual(page_pid, p.get_pid())
+ finally:
+ # Shut down the spawned process
+ self.getPage("/exit")
+ p.join()
+
+ # Wait until here to test the exit code because we want to ensure
+ # that we wait for the daemon to finish running before we fail.
+ if p.exit_code != 0:
+ self.fail("Daemonized parent process failed to exit cleanly.")
+
+
+class SignalHandlingTests(helper.CPWebCase):
+ def test_SIGHUP_tty(self):
+ # When not daemonized, SIGHUP should shut down the server.
+ try:
+ from signal import SIGHUP
+ except ImportError:
+ return self.skip("skipped (no SIGHUP) ")
+
+ # Spawn the process.
+ p = helper.CPProcess(ssl=(self.scheme.lower()=='https'))
+ p.write_conf(
+ extra='test_case_name: "test_SIGHUP_tty"')
+ p.start(imports='cherrypy.test._test_states_demo')
+ # Send a SIGHUP
+ os.kill(p.get_pid(), SIGHUP)
+ # This might hang if things aren't working right, but meh.
+ p.join()
+
+ def test_SIGHUP_daemonized(self):
+ # When daemonized, SIGHUP should restart the server.
+ try:
+ from signal import SIGHUP
+ except ImportError:
+ return self.skip("skipped (no SIGHUP) ")
+
+ if os.name not in ['posix']:
+ return self.skip("skipped (not on posix) ")
+
+ # Spawn the process and wait, when this returns, the original process
+ # is finished. If it daemonized properly, we should still be able
+ # to access pages.
+ p = helper.CPProcess(ssl=(self.scheme.lower()=='https'),
+ wait=True, daemonize=True)
+ p.write_conf(
+ extra='test_case_name: "test_SIGHUP_daemonized"')
+ p.start(imports='cherrypy.test._test_states_demo')
+
+ pid = p.get_pid()
+ try:
+ # Send a SIGHUP
+ os.kill(pid, SIGHUP)
+ # Give the server some time to restart
+ time.sleep(2)
+ self.getPage("/pid")
+ self.assertStatus(200)
+ new_pid = int(self.body)
+ self.assertNotEqual(new_pid, pid)
+ finally:
+ # Shut down the spawned process
+ self.getPage("/exit")
+ p.join()
+
+ def test_SIGTERM(self):
+ # SIGTERM should shut down the server whether daemonized or not.
+ try:
+ from signal import SIGTERM
+ except ImportError:
+ return self.skip("skipped (no SIGTERM) ")
+
+ try:
+ from os import kill
+ except ImportError:
+ return self.skip("skipped (no os.kill) ")
+
+ # Spawn a normal, undaemonized process.
+ p = helper.CPProcess(ssl=(self.scheme.lower()=='https'))
+ p.write_conf(
+ extra='test_case_name: "test_SIGTERM"')
+ p.start(imports='cherrypy.test._test_states_demo')
+ # Send a SIGTERM
+ os.kill(p.get_pid(), SIGTERM)
+ # This might hang if things aren't working right, but meh.
+ p.join()
+
+ if os.name in ['posix']:
+ # Spawn a daemonized process and test again.
+ p = helper.CPProcess(ssl=(self.scheme.lower()=='https'),
+ wait=True, daemonize=True)
+ p.write_conf(
+ extra='test_case_name: "test_SIGTERM_2"')
+ p.start(imports='cherrypy.test._test_states_demo')
+ # Send a SIGTERM
+ os.kill(p.get_pid(), SIGTERM)
+ # This might hang if things aren't working right, but meh.
+ p.join()
+
+ def test_signal_handler_unsubscribe(self):
+ try:
+ from signal import SIGTERM
+ except ImportError:
+ return self.skip("skipped (no SIGTERM) ")
+
+ try:
+ from os import kill
+ except ImportError:
+ return self.skip("skipped (no os.kill) ")
+
+ # Spawn a normal, undaemonized process.
+ p = helper.CPProcess(ssl=(self.scheme.lower()=='https'))
+ p.write_conf(
+ extra="""unsubsig: True
+test_case_name: "test_signal_handler_unsubscribe"
+""")
+ p.start(imports='cherrypy.test._test_states_demo')
+ # Send a SIGTERM
+ os.kill(p.get_pid(), SIGTERM)
+ # This might hang if things aren't working right, but meh.
+ p.join()
+
+ # Assert the old handler ran.
+ target_line = open(p.error_log, 'rb').readlines()[-10]
+ if not ntob("I am an old SIGTERM handler.") in target_line:
+ self.fail("Old SIGTERM handler did not run.\n%r" % target_line)
+
diff --git a/cherrypy/test/test_static.py b/cherrypy/test/test_static.py
new file mode 100755
index 0000000..871420b
--- /dev/null
+++ b/cherrypy/test/test_static.py
@@ -0,0 +1,300 @@
+from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob
+from cherrypy._cpcompat import BytesIO
+
+import os
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+has_space_filepath = os.path.join(curdir, 'static', 'has space.html')
+bigfile_filepath = os.path.join(curdir, "static", "bigfile.log")
+BIGFILE_SIZE = 1024 * 1024
+import threading
+
+import cherrypy
+from cherrypy.lib import static
+from cherrypy.test import helper
+
+
+class StaticTest(helper.CPWebCase):
+
+ def setup_server():
+ if not os.path.exists(has_space_filepath):
+ open(has_space_filepath, 'wb').write(ntob('Hello, world\r\n'))
+ if not os.path.exists(bigfile_filepath):
+ open(bigfile_filepath, 'wb').write(ntob("x" * BIGFILE_SIZE))
+
+ class Root:
+
+ def bigfile(self):
+ from cherrypy.lib import static
+ self.f = static.serve_file(bigfile_filepath)
+ return self.f
+ bigfile.exposed = True
+ bigfile._cp_config = {'response.stream': True}
+
+ def tell(self):
+ if self.f.input.closed:
+ return ''
+ return repr(self.f.input.tell()).rstrip('L')
+ tell.exposed = True
+
+ def fileobj(self):
+ f = open(os.path.join(curdir, 'style.css'), 'rb')
+ return static.serve_fileobj(f, content_type='text/css')
+ fileobj.exposed = True
+
+ def bytesio(self):
+ f = BytesIO(ntob('Fee\nfie\nfo\nfum'))
+ return static.serve_fileobj(f, content_type='text/plain')
+ bytesio.exposed = True
+
+ class Static:
+
+ def index(self):
+ return 'You want the Baron? You can have the Baron!'
+ index.exposed = True
+
+ def dynamic(self):
+ return "This is a DYNAMIC page"
+ dynamic.exposed = True
+
+
+ root = Root()
+ root.static = Static()
+
+ rootconf = {
+ '/static': {
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.dir': 'static',
+ 'tools.staticdir.root': curdir,
+ },
+ '/style.css': {
+ 'tools.staticfile.on': True,
+ 'tools.staticfile.filename': os.path.join(curdir, 'style.css'),
+ },
+ '/docroot': {
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.root': curdir,
+ 'tools.staticdir.dir': 'static',
+ 'tools.staticdir.index': 'index.html',
+ },
+ '/error': {
+ 'tools.staticdir.on': True,
+ 'request.show_tracebacks': True,
+ },
+ }
+ rootApp = cherrypy.Application(root)
+ rootApp.merge(rootconf)
+
+ test_app_conf = {
+ '/test': {
+ 'tools.staticdir.index': 'index.html',
+ 'tools.staticdir.on': True,
+ 'tools.staticdir.root': curdir,
+ 'tools.staticdir.dir': 'static',
+ },
+ }
+ testApp = cherrypy.Application(Static())
+ testApp.merge(test_app_conf)
+
+ vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp})
+ cherrypy.tree.graft(vhost)
+ setup_server = staticmethod(setup_server)
+
+
+ def teardown_server():
+ for f in (has_space_filepath, bigfile_filepath):
+ if os.path.exists(f):
+ try:
+ os.unlink(f)
+ except:
+ pass
+ teardown_server = staticmethod(teardown_server)
+
+
+ def testStatic(self):
+ self.getPage("/static/index.html")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'text/html')
+ self.assertBody('Hello, world\r\n')
+
+ # Using a staticdir.root value in a subdir...
+ self.getPage("/docroot/index.html")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'text/html')
+ self.assertBody('Hello, world\r\n')
+
+ # Check a filename with spaces in it
+ self.getPage("/static/has%20space.html")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'text/html')
+ self.assertBody('Hello, world\r\n')
+
+ self.getPage("/style.css")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'text/css')
+ # Note: The body should be exactly 'Dummy stylesheet\n', but
+ # unfortunately some tools such as WinZip sometimes turn \n
+ # into \r\n on Windows when extracting the CherryPy tarball so
+ # we just check the content
+ self.assertMatchesBody('^Dummy stylesheet')
+
+ def test_fallthrough(self):
+ # Test that NotFound will then try dynamic handlers (see [878]).
+ self.getPage("/static/dynamic")
+ self.assertBody("This is a DYNAMIC page")
+
+ # Check a directory via fall-through to dynamic handler.
+ self.getPage("/static/")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'text/html;charset=utf-8')
+ self.assertBody('You want the Baron? You can have the Baron!')
+
+ def test_index(self):
+ # Check a directory via "staticdir.index".
+ self.getPage("/docroot/")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'text/html')
+ self.assertBody('Hello, world\r\n')
+ # The same page should be returned even if redirected.
+ self.getPage("/docroot")
+ self.assertStatus(301)
+ self.assertHeader('Location', '%s/docroot/' % self.base())
+ self.assertMatchesBody("This resource .* <a href='%s/docroot/'>"
+ "%s/docroot/</a>." % (self.base(), self.base()))
+
+ def test_config_errors(self):
+ # Check that we get an error if no .file or .dir
+ self.getPage("/error/thing.html")
+ self.assertErrorPage(500)
+ self.assertMatchesBody(ntob("TypeError: staticdir\(\) takes at least 2 "
+ "(positional )?arguments \(0 given\)"))
+
+ def test_security(self):
+ # Test up-level security
+ self.getPage("/static/../../test/style.css")
+ self.assertStatus((400, 403))
+
+ def test_modif(self):
+ # Test modified-since on a reasonably-large file
+ self.getPage("/static/dirback.jpg")
+ self.assertStatus("200 OK")
+ lastmod = ""
+ for k, v in self.headers:
+ if k == 'Last-Modified':
+ lastmod = v
+ ims = ("If-Modified-Since", lastmod)
+ self.getPage("/static/dirback.jpg", headers=[ims])
+ self.assertStatus(304)
+ self.assertNoHeader("Content-Type")
+ self.assertNoHeader("Content-Length")
+ self.assertNoHeader("Content-Disposition")
+ self.assertBody("")
+
+ def test_755_vhost(self):
+ self.getPage("/test/", [('Host', 'virt.net')])
+ self.assertStatus(200)
+ self.getPage("/test", [('Host', 'virt.net')])
+ self.assertStatus(301)
+ self.assertHeader('Location', self.scheme + '://virt.net/test/')
+
+ def test_serve_fileobj(self):
+ self.getPage("/fileobj")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'text/css;charset=utf-8')
+ self.assertMatchesBody('^Dummy stylesheet')
+
+ def test_serve_bytesio(self):
+ self.getPage("/bytesio")
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
+ self.assertHeader('Content-Length', 14)
+ self.assertMatchesBody('Fee\nfie\nfo\nfum')
+
+ def test_file_stream(self):
+ if cherrypy.server.protocol_version != "HTTP/1.1":
+ return self.skip()
+
+ self.PROTOCOL = "HTTP/1.1"
+
+ # Make an initial request
+ self.persistent = True
+ conn = self.HTTP_CONN
+ conn.putrequest("GET", "/bigfile", skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.endheaders()
+ response = conn.response_class(conn.sock, method="GET")
+ response.begin()
+ self.assertEqual(response.status, 200)
+
+ body = ntob('')
+ remaining = BIGFILE_SIZE
+ while remaining > 0:
+ data = response.fp.read(65536)
+ if not data:
+ break
+ body += data
+ remaining -= len(data)
+
+ if self.scheme == "https":
+ newconn = HTTPSConnection
+ else:
+ newconn = HTTPConnection
+ s, h, b = helper.webtest.openURL(
+ ntob("/tell"), headers=[], host=self.HOST, port=self.PORT,
+ http_conn=newconn)
+ if not b:
+ # The file was closed on the server.
+ tell_position = BIGFILE_SIZE
+ else:
+ tell_position = int(b)
+
+ expected = len(body)
+ if tell_position >= BIGFILE_SIZE:
+ # We can't exactly control how much content the server asks for.
+ # Fudge it by only checking the first half of the reads.
+ if expected < (BIGFILE_SIZE / 2):
+ self.fail(
+ "The file should have advanced to position %r, but has "
+ "already advanced to the end of the file. It may not be "
+ "streamed as intended, or at the wrong chunk size (64k)" %
+ expected)
+ elif tell_position < expected:
+ self.fail(
+ "The file should have advanced to position %r, but has "
+ "only advanced to position %r. It may not be streamed "
+ "as intended, or at the wrong chunk size (65536)" %
+ (expected, tell_position))
+
+ if body != ntob("x" * BIGFILE_SIZE):
+ self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
+ (BIGFILE_SIZE, body[:50], len(body)))
+ conn.close()
+
+ def test_file_stream_deadlock(self):
+ if cherrypy.server.protocol_version != "HTTP/1.1":
+ return self.skip()
+
+ self.PROTOCOL = "HTTP/1.1"
+
+ # Make an initial request but abort early.
+ self.persistent = True
+ conn = self.HTTP_CONN
+ conn.putrequest("GET", "/bigfile", skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.endheaders()
+ response = conn.response_class(conn.sock, method="GET")
+ response.begin()
+ self.assertEqual(response.status, 200)
+ body = response.fp.read(65536)
+ if body != ntob("x" * len(body)):
+ self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
+ (65536, body[:50], len(body)))
+ response.close()
+ conn.close()
+
+ # Make a second request, which should fetch the whole file.
+ self.persistent = False
+ self.getPage("/bigfile")
+ if self.body != ntob("x" * BIGFILE_SIZE):
+ self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
+ (BIGFILE_SIZE, self.body[:50], len(body)))
+
diff --git a/cherrypy/test/test_tools.py b/cherrypy/test/test_tools.py
new file mode 100755
index 0000000..bc8579f
--- /dev/null
+++ b/cherrypy/test/test_tools.py
@@ -0,0 +1,393 @@
+"""Test the various means of instantiating and invoking tools."""
+
+import gzip
+import sys
+from cherrypy._cpcompat import BytesIO, copyitems, itervalues, IncompleteRead, ntob, ntou, xrange
+import time
+timeout = 0.2
+import types
+
+import cherrypy
+from cherrypy import tools
+
+
+europoundUnicode = ntou('\x80\xa3')
+
+
+# Client-side code #
+
+from cherrypy.test import helper
+
+
+class ToolTests(helper.CPWebCase):
+ def setup_server():
+
+ # Put check_access in a custom toolbox with its own namespace
+ myauthtools = cherrypy._cptools.Toolbox("myauth")
+
+ def check_access(default=False):
+ if not getattr(cherrypy.request, "userid", default):
+ raise cherrypy.HTTPError(401)
+ myauthtools.check_access = cherrypy.Tool('before_request_body', check_access)
+
+ def numerify():
+ def number_it(body):
+ for chunk in body:
+ for k, v in cherrypy.request.numerify_map:
+ chunk = chunk.replace(k, v)
+ yield chunk
+ cherrypy.response.body = number_it(cherrypy.response.body)
+
+ class NumTool(cherrypy.Tool):
+ def _setup(self):
+ def makemap():
+ m = self._merged_args().get("map", {})
+ cherrypy.request.numerify_map = copyitems(m)
+ cherrypy.request.hooks.attach('on_start_resource', makemap)
+
+ def critical():
+ cherrypy.request.error_response = cherrypy.HTTPError(502).set_response
+ critical.failsafe = True
+
+ cherrypy.request.hooks.attach('on_start_resource', critical)
+ cherrypy.request.hooks.attach(self._point, self.callable)
+
+ tools.numerify = NumTool('before_finalize', numerify)
+
+ # It's not mandatory to inherit from cherrypy.Tool.
+ class NadsatTool:
+
+ def __init__(self):
+ self.ended = {}
+ self._name = "nadsat"
+
+ def nadsat(self):
+ def nadsat_it_up(body):
+ for chunk in body:
+ chunk = chunk.replace(ntob("good"), ntob("horrorshow"))
+ chunk = chunk.replace(ntob("piece"), ntob("lomtick"))
+ yield chunk
+ cherrypy.response.body = nadsat_it_up(cherrypy.response.body)
+ nadsat.priority = 0
+
+ def cleanup(self):
+ # This runs after the request has been completely written out.
+ cherrypy.response.body = [ntob("razdrez")]
+ id = cherrypy.request.params.get("id")
+ if id:
+ self.ended[id] = True
+ cleanup.failsafe = True
+
+ def _setup(self):
+ cherrypy.request.hooks.attach('before_finalize', self.nadsat)
+ cherrypy.request.hooks.attach('on_end_request', self.cleanup)
+ tools.nadsat = NadsatTool()
+
+ def pipe_body():
+ cherrypy.request.process_request_body = False
+ clen = int(cherrypy.request.headers['Content-Length'])
+ cherrypy.request.body = cherrypy.request.rfile.read(clen)
+
+ # Assert that we can use a callable object instead of a function.
+ class Rotator(object):
+ def __call__(self, scale):
+ r = cherrypy.response
+ r.collapse_body()
+ r.body = [chr((ord(x) + scale) % 256) for x in r.body[0]]
+ cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator())
+
+ def stream_handler(next_handler, *args, **kwargs):
+ cherrypy.response.output = o = BytesIO()
+ try:
+ response = next_handler(*args, **kwargs)
+ # Ignore the response and return our accumulated output instead.
+ return o.getvalue()
+ finally:
+ o.close()
+ cherrypy.tools.streamer = cherrypy._cptools.HandlerWrapperTool(stream_handler)
+
+ class Root:
+ def index(self):
+ return "Howdy earth!"
+ index.exposed = True
+
+ def tarfile(self):
+ cherrypy.response.output.write(ntob('I am '))
+ cherrypy.response.output.write(ntob('a tarfile'))
+ tarfile.exposed = True
+ tarfile._cp_config = {'tools.streamer.on': True}
+
+ def euro(self):
+ hooks = list(cherrypy.request.hooks['before_finalize'])
+ hooks.sort()
+ cbnames = [x.callback.__name__ for x in hooks]
+ assert cbnames == ['gzip'], cbnames
+ priorities = [x.priority for x in hooks]
+ assert priorities == [80], priorities
+ yield ntou("Hello,")
+ yield ntou("world")
+ yield europoundUnicode
+ euro.exposed = True
+
+ # Bare hooks
+ def pipe(self):
+ return cherrypy.request.body
+ pipe.exposed = True
+ pipe._cp_config = {'hooks.before_request_body': pipe_body}
+
+ # Multiple decorators; include kwargs just for fun.
+ # Note that rotator must run before gzip.
+ def decorated_euro(self, *vpath):
+ yield ntou("Hello,")
+ yield ntou("world")
+ yield europoundUnicode
+ decorated_euro.exposed = True
+ decorated_euro = tools.gzip(compress_level=6)(decorated_euro)
+ decorated_euro = tools.rotator(scale=3)(decorated_euro)
+
+ root = Root()
+
+
+ class TestType(type):
+ """Metaclass which automatically exposes all functions in each subclass,
+ and adds an instance of the subclass as an attribute of root.
+ """
+ def __init__(cls, name, bases, dct):
+ type.__init__(cls, name, bases, dct)
+ for value in itervalues(dct):
+ if isinstance(value, types.FunctionType):
+ value.exposed = True
+ setattr(root, name.lower(), cls())
+ class Test(object):
+ __metaclass__ = TestType
+
+
+ # METHOD ONE:
+ # Declare Tools in _cp_config
+ class Demo(Test):
+
+ _cp_config = {"tools.nadsat.on": True}
+
+ def index(self, id=None):
+ return "A good piece of cherry pie"
+
+ def ended(self, id):
+ return repr(tools.nadsat.ended[id])
+
+ def err(self, id=None):
+ raise ValueError()
+
+ def errinstream(self, id=None):
+ yield "nonconfidential"
+ raise ValueError()
+ yield "confidential"
+
+ # METHOD TWO: decorator using Tool()
+ # We support Python 2.3, but the @-deco syntax would look like this:
+ # @tools.check_access()
+ def restricted(self):
+ return "Welcome!"
+ restricted = myauthtools.check_access()(restricted)
+ userid = restricted
+
+ def err_in_onstart(self):
+ return "success!"
+
+ def stream(self, id=None):
+ for x in xrange(100000000):
+ yield str(x)
+ stream._cp_config = {'response.stream': True}
+
+
+ conf = {
+ # METHOD THREE:
+ # Declare Tools in detached config
+ '/demo': {
+ 'tools.numerify.on': True,
+ 'tools.numerify.map': {ntob("pie"): ntob("3.14159")},
+ },
+ '/demo/restricted': {
+ 'request.show_tracebacks': False,
+ },
+ '/demo/userid': {
+ 'request.show_tracebacks': False,
+ 'myauth.check_access.default': True,
+ },
+ '/demo/errinstream': {
+ 'response.stream': True,
+ },
+ '/demo/err_in_onstart': {
+ # Because this isn't a dict, on_start_resource will error.
+ 'tools.numerify.map': "pie->3.14159"
+ },
+ # Combined tools
+ '/euro': {
+ 'tools.gzip.on': True,
+ 'tools.encode.on': True,
+ },
+ # Priority specified in config
+ '/decorated_euro/subpath': {
+ 'tools.gzip.priority': 10,
+ },
+ # Handler wrappers
+ '/tarfile': {'tools.streamer.on': True}
+ }
+ app = cherrypy.tree.mount(root, config=conf)
+ app.request_class.namespaces['myauth'] = myauthtools
+
+ if sys.version_info >= (2, 5):
+ from cherrypy.test import _test_decorators
+ root.tooldecs = _test_decorators.ToolExamples()
+ setup_server = staticmethod(setup_server)
+
+ def testHookErrors(self):
+ self.getPage("/demo/?id=1")
+ # If body is "razdrez", then on_end_request is being called too early.
+ self.assertBody("A horrorshow lomtick of cherry 3.14159")
+ # If this fails, then on_end_request isn't being called at all.
+ time.sleep(0.1)
+ self.getPage("/demo/ended/1")
+ self.assertBody("True")
+
+ valerr = '\n raise ValueError()\nValueError'
+ self.getPage("/demo/err?id=3")
+ # If body is "razdrez", then on_end_request is being called too early.
+ self.assertErrorPage(502, pattern=valerr)
+ # If this fails, then on_end_request isn't being called at all.
+ time.sleep(0.1)
+ self.getPage("/demo/ended/3")
+ self.assertBody("True")
+
+ # If body is "razdrez", then on_end_request is being called too early.
+ if (cherrypy.server.protocol_version == "HTTP/1.0" or
+ getattr(cherrypy.server, "using_apache", False)):
+ self.getPage("/demo/errinstream?id=5")
+ # Because this error is raised after the response body has
+ # started, the status should not change to an error status.
+ self.assertStatus("200 OK")
+ self.assertBody("nonconfidential")
+ else:
+ # Because this error is raised after the response body has
+ # started, and because it's chunked output, an error is raised by
+ # the HTTP client when it encounters incomplete output.
+ self.assertRaises((ValueError, IncompleteRead), self.getPage,
+ "/demo/errinstream?id=5")
+ # If this fails, then on_end_request isn't being called at all.
+ time.sleep(0.1)
+ self.getPage("/demo/ended/5")
+ self.assertBody("True")
+
+ # Test the "__call__" technique (compile-time decorator).
+ self.getPage("/demo/restricted")
+ self.assertErrorPage(401)
+
+ # Test compile-time decorator with kwargs from config.
+ self.getPage("/demo/userid")
+ self.assertBody("Welcome!")
+
+ def testEndRequestOnDrop(self):
+ old_timeout = None
+ try:
+ httpserver = cherrypy.server.httpserver
+ old_timeout = httpserver.timeout
+ except (AttributeError, IndexError):
+ return self.skip()
+
+ try:
+ httpserver.timeout = timeout
+
+ # Test that on_end_request is called even if the client drops.
+ self.persistent = True
+ try:
+ conn = self.HTTP_CONN
+ conn.putrequest("GET", "/demo/stream?id=9", skip_host=True)
+ conn.putheader("Host", self.HOST)
+ conn.endheaders()
+ # Skip the rest of the request and close the conn. This will
+ # cause the server's active socket to error, which *should*
+ # result in the request being aborted, and request.close being
+ # called all the way up the stack (including WSGI middleware),
+ # eventually calling our on_end_request hook.
+ finally:
+ self.persistent = False
+ time.sleep(timeout * 2)
+ # Test that the on_end_request hook was called.
+ self.getPage("/demo/ended/9")
+ self.assertBody("True")
+ finally:
+ if old_timeout is not None:
+ httpserver.timeout = old_timeout
+
+ def testGuaranteedHooks(self):
+ # The 'critical' on_start_resource hook is 'failsafe' (guaranteed
+ # to run even if there are failures in other on_start methods).
+ # This is NOT true of the other hooks.
+ # Here, we have set up a failure in NumerifyTool.numerify_map,
+ # but our 'critical' hook should run and set the error to 502.
+ self.getPage("/demo/err_in_onstart")
+ self.assertErrorPage(502)
+ self.assertInBody("AttributeError: 'str' object has no attribute 'items'")
+
+ def testCombinedTools(self):
+ expectedResult = (ntou("Hello,world") + europoundUnicode).encode('utf-8')
+ zbuf = BytesIO()
+ zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
+ zfile.write(expectedResult)
+ zfile.close()
+
+ self.getPage("/euro", headers=[("Accept-Encoding", "gzip"),
+ ("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7")])
+ self.assertInBody(zbuf.getvalue()[:3])
+
+ zbuf = BytesIO()
+ zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=6)
+ zfile.write(expectedResult)
+ zfile.close()
+
+ self.getPage("/decorated_euro", headers=[("Accept-Encoding", "gzip")])
+ self.assertInBody(zbuf.getvalue()[:3])
+
+ # This returns a different value because gzip's priority was
+ # lowered in conf, allowing the rotator to run after gzip.
+ # Of course, we don't want breakage in production apps,
+ # but it proves the priority was changed.
+ self.getPage("/decorated_euro/subpath",
+ headers=[("Accept-Encoding", "gzip")])
+ self.assertInBody(''.join([chr((ord(x) + 3) % 256) for x in zbuf.getvalue()]))
+
+ def testBareHooks(self):
+ content = "bit of a pain in me gulliver"
+ self.getPage("/pipe",
+ headers=[("Content-Length", str(len(content))),
+ ("Content-Type", "text/plain")],
+ method="POST", body=content)
+ self.assertBody(content)
+
+ def testHandlerWrapperTool(self):
+ self.getPage("/tarfile")
+ self.assertBody("I am a tarfile")
+
+ def testToolWithConfig(self):
+ if not sys.version_info >= (2, 5):
+ return self.skip("skipped (Python 2.5+ only)")
+
+ self.getPage('/tooldecs/blah')
+ self.assertHeader('Content-Type', 'application/data')
+
+ def testWarnToolOn(self):
+ # get
+ try:
+ numon = cherrypy.tools.numerify.on
+ except AttributeError:
+ pass
+ else:
+ raise AssertionError("Tool.on did not error as it should have.")
+
+ # set
+ try:
+ cherrypy.tools.numerify.on = True
+ except AttributeError:
+ pass
+ else:
+ raise AssertionError("Tool.on did not error as it should have.")
+
diff --git a/cherrypy/test/test_tutorials.py b/cherrypy/test/test_tutorials.py
new file mode 100755
index 0000000..aab2786
--- /dev/null
+++ b/cherrypy/test/test_tutorials.py
@@ -0,0 +1,201 @@
+import sys
+
+import cherrypy
+from cherrypy.test import helper
+
+
+class TutorialTest(helper.CPWebCase):
+
+ def setup_server(cls):
+
+ conf = cherrypy.config.copy()
+
+ def load_tut_module(name):
+ """Import or reload tutorial module as needed."""
+ cherrypy.config.reset()
+ cherrypy.config.update(conf)
+
+ target = "cherrypy.tutorial." + name
+ if target in sys.modules:
+ module = reload(sys.modules[target])
+ else:
+ module = __import__(target, globals(), locals(), [''])
+ # The above import will probably mount a new app at "".
+ app = cherrypy.tree.apps[""]
+
+ app.root.load_tut_module = load_tut_module
+ app.root.sessions = sessions
+ app.root.traceback_setting = traceback_setting
+
+ cls.supervisor.sync_apps()
+ load_tut_module.exposed = True
+
+ def sessions():
+ cherrypy.config.update({"tools.sessions.on": True})
+ sessions.exposed = True
+
+ def traceback_setting():
+ return repr(cherrypy.request.show_tracebacks)
+ traceback_setting.exposed = True
+
+ class Dummy:
+ pass
+ root = Dummy()
+ root.load_tut_module = load_tut_module
+ cherrypy.tree.mount(root)
+ setup_server = classmethod(setup_server)
+
+
+ def test01HelloWorld(self):
+ self.getPage("/load_tut_module/tut01_helloworld")
+ self.getPage("/")
+ self.assertBody('Hello world!')
+
+ def test02ExposeMethods(self):
+ self.getPage("/load_tut_module/tut02_expose_methods")
+ self.getPage("/showMessage")
+ self.assertBody('Hello world!')
+
+ def test03GetAndPost(self):
+ self.getPage("/load_tut_module/tut03_get_and_post")
+
+ # Try different GET queries
+ self.getPage("/greetUser?name=Bob")
+ self.assertBody("Hey Bob, what's up?")
+
+ self.getPage("/greetUser")
+ self.assertBody('Please enter your name <a href="./">here</a>.')
+
+ self.getPage("/greetUser?name=")
+ self.assertBody('No, really, enter your name <a href="./">here</a>.')
+
+ # Try the same with POST
+ self.getPage("/greetUser", method="POST", body="name=Bob")
+ self.assertBody("Hey Bob, what's up?")
+
+ self.getPage("/greetUser", method="POST", body="name=")
+ self.assertBody('No, really, enter your name <a href="./">here</a>.')
+
+ def test04ComplexSite(self):
+ self.getPage("/load_tut_module/tut04_complex_site")
+ msg = '''
+ <p>Here are some extra useful links:</p>
+
+ <ul>
+ <li><a href="http://del.icio.us">del.icio.us</a></li>
+ <li><a href="http://www.mornography.de">Hendrik's weblog</a></li>
+ </ul>
+
+ <p>[<a href="../">Return to links page</a>]</p>'''
+ self.getPage("/links/extra/")
+ self.assertBody(msg)
+
+ def test05DerivedObjects(self):
+ self.getPage("/load_tut_module/tut05_derived_objects")
+ msg = '''
+ <html>
+ <head>
+ <title>Another Page</title>
+ <head>
+ <body>
+ <h2>Another Page</h2>
+
+ <p>
+ And this is the amazing second page!
+ </p>
+
+ </body>
+ </html>
+ '''
+ self.getPage("/another/")
+ self.assertBody(msg)
+
+ def test06DefaultMethod(self):
+ self.getPage("/load_tut_module/tut06_default_method")
+ self.getPage('/hendrik')
+ self.assertBody('Hendrik Mans, CherryPy co-developer & crazy German '
+ '(<a href="./">back</a>)')
+
+ def test07Sessions(self):
+ self.getPage("/load_tut_module/tut07_sessions")
+ self.getPage("/sessions")
+
+ self.getPage('/')
+ self.assertBody("\n During your current session, you've viewed this"
+ "\n page 1 times! Your life is a patio of fun!"
+ "\n ")
+
+ self.getPage('/', self.cookies)
+ self.assertBody("\n During your current session, you've viewed this"
+ "\n page 2 times! Your life is a patio of fun!"
+ "\n ")
+
+ def test08GeneratorsAndYield(self):
+ self.getPage("/load_tut_module/tut08_generators_and_yield")
+ self.getPage('/')
+ self.assertBody('<html><body><h2>Generators rule!</h2>'
+ '<h3>List of users:</h3>'
+ 'Remi<br/>Carlos<br/>Hendrik<br/>Lorenzo Lamas<br/>'
+ '</body></html>')
+
+ def test09Files(self):
+ self.getPage("/load_tut_module/tut09_files")
+
+ # Test upload
+ filesize = 5
+ h = [("Content-type", "multipart/form-data; boundary=x"),
+ ("Content-Length", str(105 + filesize))]
+ b = '--x\n' + \
+ 'Content-Disposition: form-data; name="myFile"; filename="hello.txt"\r\n' + \
+ 'Content-Type: text/plain\r\n' + \
+ '\r\n' + \
+ 'a' * filesize + '\n' + \
+ '--x--\n'
+ self.getPage('/upload', h, "POST", b)
+ self.assertBody('''<html>
+ <body>
+ myFile length: %d<br />
+ myFile filename: hello.txt<br />
+ myFile mime-type: text/plain
+ </body>
+ </html>''' % filesize)
+
+ # Test download
+ self.getPage('/download')
+ self.assertStatus("200 OK")
+ self.assertHeader("Content-Type", "application/x-download")
+ self.assertHeader("Content-Disposition",
+ # Make sure the filename is quoted.
+ 'attachment; filename="pdf_file.pdf"')
+ self.assertEqual(len(self.body), 85698)
+
+ def test10HTTPErrors(self):
+ self.getPage("/load_tut_module/tut10_http_errors")
+
+ self.getPage("/")
+ self.assertInBody("""<a href="toggleTracebacks">""")
+ self.assertInBody("""<a href="/doesNotExist">""")
+ self.assertInBody("""<a href="/error?code=403">""")
+ self.assertInBody("""<a href="/error?code=500">""")
+ self.assertInBody("""<a href="/messageArg">""")
+
+ self.getPage("/traceback_setting")
+ setting = self.body
+ self.getPage("/toggleTracebacks")
+ self.assertStatus((302, 303))
+ self.getPage("/traceback_setting")
+ self.assertBody(str(not eval(setting)))
+
+ self.getPage("/error?code=500")
+ self.assertStatus(500)
+ self.assertInBody("The server encountered an unexpected condition "
+ "which prevented it from fulfilling the request.")
+
+ self.getPage("/error?code=403")
+ self.assertStatus(403)
+ self.assertInBody("<h2>You can't do that!</h2>")
+
+ self.getPage("/messageArg")
+ self.assertStatus(500)
+ self.assertInBody("If you construct an HTTPError with a 'message'")
+
diff --git a/cherrypy/test/test_virtualhost.py b/cherrypy/test/test_virtualhost.py
new file mode 100755
index 0000000..d6eed0e
--- /dev/null
+++ b/cherrypy/test/test_virtualhost.py
@@ -0,0 +1,107 @@
+import os
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+import cherrypy
+from cherrypy.test import helper
+
+
+class VirtualHostTest(helper.CPWebCase):
+
+ def setup_server():
+ class Root:
+ def index(self):
+ return "Hello, world"
+ index.exposed = True
+
+ def dom4(self):
+ return "Under construction"
+ dom4.exposed = True
+
+ def method(self, value):
+ return "You sent %s" % repr(value)
+ method.exposed = True
+
+ class VHost:
+ def __init__(self, sitename):
+ self.sitename = sitename
+
+ def index(self):
+ return "Welcome to %s" % self.sitename
+ index.exposed = True
+
+ def vmethod(self, value):
+ return "You sent %s" % repr(value)
+ vmethod.exposed = True
+
+ def url(self):
+ return cherrypy.url("nextpage")
+ url.exposed = True
+
+ # Test static as a handler (section must NOT include vhost prefix)
+ static = cherrypy.tools.staticdir.handler(section='/static', dir=curdir)
+
+ root = Root()
+ root.mydom2 = VHost("Domain 2")
+ root.mydom3 = VHost("Domain 3")
+ hostmap = {'www.mydom2.com': '/mydom2',
+ 'www.mydom3.com': '/mydom3',
+ 'www.mydom4.com': '/dom4',
+ }
+ cherrypy.tree.mount(root, config={
+ '/': {'request.dispatch': cherrypy.dispatch.VirtualHost(**hostmap)},
+ # Test static in config (section must include vhost prefix)
+ '/mydom2/static2': {'tools.staticdir.on': True,
+ 'tools.staticdir.root': curdir,
+ 'tools.staticdir.dir': 'static',
+ 'tools.staticdir.index': 'index.html',
+ },
+ })
+ setup_server = staticmethod(setup_server)
+
+ def testVirtualHost(self):
+ self.getPage("/", [('Host', 'www.mydom1.com')])
+ self.assertBody('Hello, world')
+ self.getPage("/mydom2/", [('Host', 'www.mydom1.com')])
+ self.assertBody('Welcome to Domain 2')
+
+ self.getPage("/", [('Host', 'www.mydom2.com')])
+ self.assertBody('Welcome to Domain 2')
+ self.getPage("/", [('Host', 'www.mydom3.com')])
+ self.assertBody('Welcome to Domain 3')
+ self.getPage("/", [('Host', 'www.mydom4.com')])
+ self.assertBody('Under construction')
+
+ # Test GET, POST, and positional params
+ self.getPage("/method?value=root")
+ self.assertBody("You sent u'root'")
+ self.getPage("/vmethod?value=dom2+GET", [('Host', 'www.mydom2.com')])
+ self.assertBody("You sent u'dom2 GET'")
+ self.getPage("/vmethod", [('Host', 'www.mydom3.com')], method="POST",
+ body="value=dom3+POST")
+ self.assertBody("You sent u'dom3 POST'")
+ self.getPage("/vmethod/pos", [('Host', 'www.mydom3.com')])
+ self.assertBody("You sent 'pos'")
+
+ # Test that cherrypy.url uses the browser url, not the virtual url
+ self.getPage("/url", [('Host', 'www.mydom2.com')])
+ self.assertBody("%s://www.mydom2.com/nextpage" % self.scheme)
+
+ def test_VHost_plus_Static(self):
+ # Test static as a handler
+ self.getPage("/static/style.css", [('Host', 'www.mydom2.com')])
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'text/css;charset=utf-8')
+
+ # Test static in config
+ self.getPage("/static2/dirback.jpg", [('Host', 'www.mydom2.com')])
+ self.assertStatus('200 OK')
+ self.assertHeader('Content-Type', 'image/jpeg')
+
+ # Test static config with "index" arg
+ self.getPage("/static2/", [('Host', 'www.mydom2.com')])
+ self.assertStatus('200 OK')
+ self.assertBody('Hello, world\r\n')
+ # Since tools.trailing_slash is on by default, this should redirect
+ self.getPage("/static2", [('Host', 'www.mydom2.com')])
+ self.assertStatus(301)
+
diff --git a/cherrypy/test/test_wsgi_ns.py b/cherrypy/test/test_wsgi_ns.py
new file mode 100755
index 0000000..d57013c
--- /dev/null
+++ b/cherrypy/test/test_wsgi_ns.py
@@ -0,0 +1,80 @@
+import cherrypy
+from cherrypy.test import helper
+
+
+class WSGI_Namespace_Test(helper.CPWebCase):
+
+ def setup_server():
+
+ class WSGIResponse(object):
+
+ def __init__(self, appresults):
+ self.appresults = appresults
+ self.iter = iter(appresults)
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ return self.iter.next()
+
+ def close(self):
+ if hasattr(self.appresults, "close"):
+ self.appresults.close()
+
+
+ class ChangeCase(object):
+
+ def __init__(self, app, to=None):
+ self.app = app
+ self.to = to
+
+ def __call__(self, environ, start_response):
+ res = self.app(environ, start_response)
+ class CaseResults(WSGIResponse):
+ def next(this):
+ return getattr(this.iter.next(), self.to)()
+ return CaseResults(res)
+
+ class Replacer(object):
+
+ def __init__(self, app, map={}):
+ self.app = app
+ self.map = map
+
+ def __call__(self, environ, start_response):
+ res = self.app(environ, start_response)
+ class ReplaceResults(WSGIResponse):
+ def next(this):
+ line = this.iter.next()
+ for k, v in self.map.iteritems():
+ line = line.replace(k, v)
+ return line
+ return ReplaceResults(res)
+
+ class Root(object):
+
+ def index(self):
+ return "HellO WoRlD!"
+ index.exposed = True
+
+
+ root_conf = {'wsgi.pipeline': [('replace', Replacer)],
+ 'wsgi.replace.map': {'L': 'X', 'l': 'r'},
+ }
+
+ app = cherrypy.Application(Root())
+ app.wsgiapp.pipeline.append(('changecase', ChangeCase))
+ app.wsgiapp.config['changecase'] = {'to': 'upper'}
+ cherrypy.tree.mount(app, config={'/': root_conf})
+ setup_server = staticmethod(setup_server)
+
+
+ def test_pipeline(self):
+ if not cherrypy.server.httpserver:
+ return self.skip()
+
+ self.getPage("/")
+ # If body is "HEXXO WORXD!", the middleware was applied out of order.
+ self.assertBody("HERRO WORRD!")
+
diff --git a/cherrypy/test/test_wsgi_vhost.py b/cherrypy/test/test_wsgi_vhost.py
new file mode 100755
index 0000000..abb1a91
--- /dev/null
+++ b/cherrypy/test/test_wsgi_vhost.py
@@ -0,0 +1,36 @@
+import cherrypy
+from cherrypy.test import helper
+
+
+class WSGI_VirtualHost_Test(helper.CPWebCase):
+
+ def setup_server():
+
+ class ClassOfRoot(object):
+
+ def __init__(self, name):
+ self.name = name
+
+ def index(self):
+ return "Welcome to the %s website!" % self.name
+ index.exposed = True
+
+
+ default = cherrypy.Application(None)
+
+ domains = {}
+ for year in range(1997, 2008):
+ app = cherrypy.Application(ClassOfRoot('Class of %s' % year))
+ domains['www.classof%s.example' % year] = app
+
+ cherrypy.tree.graft(cherrypy._cpwsgi.VirtualHost(default, domains))
+ setup_server = staticmethod(setup_server)
+
+ def test_welcome(self):
+ if not cherrypy.server.using_wsgi:
+ return self.skip("skipped (not using WSGI)... ")
+
+ for year in range(1997, 2008):
+ self.getPage("/", headers=[('Host', 'www.classof%s.example' % year)])
+ self.assertBody("Welcome to the Class of %s website!" % year)
+
diff --git a/cherrypy/test/test_wsgiapps.py b/cherrypy/test/test_wsgiapps.py
new file mode 100755
index 0000000..fa5420c
--- /dev/null
+++ b/cherrypy/test/test_wsgiapps.py
@@ -0,0 +1,111 @@
+from cherrypy.test import helper
+
+
+class WSGIGraftTests(helper.CPWebCase):
+
+ def setup_server():
+ import os
+ curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+ import cherrypy
+
+ def test_app(environ, start_response):
+ status = '200 OK'
+ response_headers = [('Content-type', 'text/plain')]
+ start_response(status, response_headers)
+ output = ['Hello, world!\n',
+ 'This is a wsgi app running within CherryPy!\n\n']
+ keys = list(environ.keys())
+ keys.sort()
+ for k in keys:
+ output.append('%s: %s\n' % (k,environ[k]))
+ return output
+
+ def test_empty_string_app(environ, start_response):
+ status = '200 OK'
+ response_headers = [('Content-type', 'text/plain')]
+ start_response(status, response_headers)
+ return ['Hello', '', ' ', '', 'world']
+
+
+ class WSGIResponse(object):
+
+ def __init__(self, appresults):
+ self.appresults = appresults
+ self.iter = iter(appresults)
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ return self.iter.next()
+
+ def close(self):
+ if hasattr(self.appresults, "close"):
+ self.appresults.close()
+
+
+ class ReversingMiddleware(object):
+
+ def __init__(self, app):
+ self.app = app
+
+ def __call__(self, environ, start_response):
+ results = app(environ, start_response)
+ class Reverser(WSGIResponse):
+ def next(this):
+ line = list(this.iter.next())
+ line.reverse()
+ return "".join(line)
+ return Reverser(results)
+
+ class Root:
+ def index(self):
+ return "I'm a regular CherryPy page handler!"
+ index.exposed = True
+
+
+ cherrypy.tree.mount(Root())
+
+ cherrypy.tree.graft(test_app, '/hosted/app1')
+ cherrypy.tree.graft(test_empty_string_app, '/hosted/app3')
+
+ # Set script_name explicitly to None to signal CP that it should
+ # be pulled from the WSGI environ each time.
+ app = cherrypy.Application(Root(), script_name=None)
+ cherrypy.tree.graft(ReversingMiddleware(app), '/hosted/app2')
+ setup_server = staticmethod(setup_server)
+
+ wsgi_output = '''Hello, world!
+This is a wsgi app running within CherryPy!'''
+
+ def test_01_standard_app(self):
+ self.getPage("/")
+ self.assertBody("I'm a regular CherryPy page handler!")
+
+ def test_04_pure_wsgi(self):
+ import cherrypy
+ if not cherrypy.server.using_wsgi:
+ return self.skip("skipped (not using WSGI)... ")
+ self.getPage("/hosted/app1")
+ self.assertHeader("Content-Type", "text/plain")
+ self.assertInBody(self.wsgi_output)
+
+ def test_05_wrapped_cp_app(self):
+ import cherrypy
+ if not cherrypy.server.using_wsgi:
+ return self.skip("skipped (not using WSGI)... ")
+ self.getPage("/hosted/app2/")
+ body = list("I'm a regular CherryPy page handler!")
+ body.reverse()
+ body = "".join(body)
+ self.assertInBody(body)
+
+ def test_06_empty_string_app(self):
+ import cherrypy
+ if not cherrypy.server.using_wsgi:
+ return self.skip("skipped (not using WSGI)... ")
+ self.getPage("/hosted/app3")
+ self.assertHeader("Content-Type", "text/plain")
+ self.assertInBody('Hello world')
+
diff --git a/cherrypy/test/test_xmlrpc.py b/cherrypy/test/test_xmlrpc.py
new file mode 100755
index 0000000..c4bf61e
--- /dev/null
+++ b/cherrypy/test/test_xmlrpc.py
@@ -0,0 +1,172 @@
+import sys
+from xmlrpclib import DateTime, Fault, ServerProxy, SafeTransport
+
+class HTTPSTransport(SafeTransport):
+ """Subclass of SafeTransport to fix sock.recv errors (by using file)."""
+
+ def request(self, host, handler, request_body, verbose=0):
+ # issue XML-RPC request
+ h = self.make_connection(host)
+ if verbose:
+ h.set_debuglevel(1)
+
+ self.send_request(h, handler, request_body)
+ self.send_host(h, host)
+ self.send_user_agent(h)
+ self.send_content(h, request_body)
+
+ errcode, errmsg, headers = h.getreply()
+ if errcode != 200:
+ raise xmlrpclib.ProtocolError(host + handler, errcode, errmsg,
+ headers)
+
+ self.verbose = verbose
+
+ # Here's where we differ from the superclass. It says:
+ # try:
+ # sock = h._conn.sock
+ # except AttributeError:
+ # sock = None
+ # return self._parse_response(h.getfile(), sock)
+
+ return self.parse_response(h.getfile())
+
+import cherrypy
+
+
+def setup_server():
+ from cherrypy import _cptools
+
+ class Root:
+ def index(self):
+ return "I'm a standard index!"
+ index.exposed = True
+
+
+ class XmlRpc(_cptools.XMLRPCController):
+
+ def foo(self):
+ return "Hello world!"
+ foo.exposed = True
+
+ def return_single_item_list(self):
+ return [42]
+ return_single_item_list.exposed = True
+
+ def return_string(self):
+ return "here is a string"
+ return_string.exposed = True
+
+ def return_tuple(self):
+ return ('here', 'is', 1, 'tuple')
+ return_tuple.exposed = True
+
+ def return_dict(self):
+ return dict(a=1, b=2, c=3)
+ return_dict.exposed = True
+
+ def return_composite(self):
+ return dict(a=1,z=26), 'hi', ['welcome', 'friend']
+ return_composite.exposed = True
+
+ def return_int(self):
+ return 42
+ return_int.exposed = True
+
+ def return_float(self):
+ return 3.14
+ return_float.exposed = True
+
+ def return_datetime(self):
+ return DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1))
+ return_datetime.exposed = True
+
+ def return_boolean(self):
+ return True
+ return_boolean.exposed = True
+
+ def test_argument_passing(self, num):
+ return num * 2
+ test_argument_passing.exposed = True
+
+ def test_returning_Fault(self):
+ return Fault(1, "custom Fault response")
+ test_returning_Fault.exposed = True
+
+ root = Root()
+ root.xmlrpc = XmlRpc()
+ cherrypy.tree.mount(root, config={'/': {
+ 'request.dispatch': cherrypy.dispatch.XMLRPCDispatcher(),
+ 'tools.xmlrpc.allow_none': 0,
+ }})
+
+
+from cherrypy.test import helper
+
+class XmlRpcTest(helper.CPWebCase):
+ setup_server = staticmethod(setup_server)
+ def testXmlRpc(self):
+
+ scheme = "http"
+ try:
+ scheme = self.harness.scheme
+ except AttributeError:
+ pass
+
+ if scheme == "https":
+ url = 'https://%s:%s/xmlrpc/' % (self.interface(), self.PORT)
+ proxy = ServerProxy(url, transport=HTTPSTransport())
+ else:
+ url = 'http://%s:%s/xmlrpc/' % (self.interface(), self.PORT)
+ proxy = ServerProxy(url)
+
+ # begin the tests ...
+ self.getPage("/xmlrpc/foo")
+ self.assertBody("Hello world!")
+
+ self.assertEqual(proxy.return_single_item_list(), [42])
+ self.assertNotEqual(proxy.return_single_item_list(), 'one bazillion')
+ self.assertEqual(proxy.return_string(), "here is a string")
+ self.assertEqual(proxy.return_tuple(), list(('here', 'is', 1, 'tuple')))
+ self.assertEqual(proxy.return_dict(), {'a': 1, 'c': 3, 'b': 2})
+ self.assertEqual(proxy.return_composite(),
+ [{'a': 1, 'z': 26}, 'hi', ['welcome', 'friend']])
+ self.assertEqual(proxy.return_int(), 42)
+ self.assertEqual(proxy.return_float(), 3.14)
+ self.assertEqual(proxy.return_datetime(),
+ DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1)))
+ self.assertEqual(proxy.return_boolean(), True)
+ self.assertEqual(proxy.test_argument_passing(22), 22 * 2)
+
+ # Test an error in the page handler (should raise an xmlrpclib.Fault)
+ try:
+ proxy.test_argument_passing({})
+ except Exception:
+ x = sys.exc_info()[1]
+ self.assertEqual(x.__class__, Fault)
+ self.assertEqual(x.faultString, ("unsupported operand type(s) "
+ "for *: 'dict' and 'int'"))
+ else:
+ self.fail("Expected xmlrpclib.Fault")
+
+ # http://www.cherrypy.org/ticket/533
+ # if a method is not found, an xmlrpclib.Fault should be raised
+ try:
+ proxy.non_method()
+ except Exception:
+ x = sys.exc_info()[1]
+ self.assertEqual(x.__class__, Fault)
+ self.assertEqual(x.faultString, 'method "non_method" is not supported')
+ else:
+ self.fail("Expected xmlrpclib.Fault")
+
+ # Test returning a Fault from the page handler.
+ try:
+ proxy.test_returning_Fault()
+ except Exception:
+ x = sys.exc_info()[1]
+ self.assertEqual(x.__class__, Fault)
+ self.assertEqual(x.faultString, ("custom Fault response"))
+ else:
+ self.fail("Expected xmlrpclib.Fault")
+
diff --git a/cherrypy/test/webtest.py b/cherrypy/test/webtest.py
new file mode 100755
index 0000000..969eab0
--- /dev/null
+++ b/cherrypy/test/webtest.py
@@ -0,0 +1,535 @@
+"""Extensions to unittest for web frameworks.
+
+Use the WebCase.getPage method to request a page from your HTTP server.
+
+Framework Integration
+=====================
+
+If you have control over your server process, you can handle errors
+in the server-side of the HTTP conversation a bit better. You must run
+both the client (your WebCase tests) and the server in the same process
+(but in separate threads, obviously).
+
+When an error occurs in the framework, call server_error. It will print
+the traceback to stdout, and keep any assertions you have from running
+(the assumption is that, if the server errors, the page output will not
+be of further significance to your tests).
+"""
+
+import os
+import pprint
+import re
+import socket
+import sys
+import time
+import traceback
+import types
+
+from unittest import *
+from unittest import _TextTestResult
+
+from cherrypy._cpcompat import basestring, HTTPConnection, HTTPSConnection, unicodestr
+
+
+
+def interface(host):
+ """Return an IP address for a client connection given the server host.
+
+ If the server is listening on '0.0.0.0' (INADDR_ANY)
+ or '::' (IN6ADDR_ANY), this will return the proper localhost."""
+ if host == '0.0.0.0':
+ # INADDR_ANY, which should respond on localhost.
+ return "127.0.0.1"
+ if host == '::':
+ # IN6ADDR_ANY, which should respond on localhost.
+ return "::1"
+ return host
+
+
+class TerseTestResult(_TextTestResult):
+
+ def printErrors(self):
+ # Overridden to avoid unnecessary empty line
+ if self.errors or self.failures:
+ if self.dots or self.showAll:
+ self.stream.writeln()
+ self.printErrorList('ERROR', self.errors)
+ self.printErrorList('FAIL', self.failures)
+
+
+class TerseTestRunner(TextTestRunner):
+ """A test runner class that displays results in textual form."""
+
+ def _makeResult(self):
+ return TerseTestResult(self.stream, self.descriptions, self.verbosity)
+
+ def run(self, test):
+ "Run the given test case or test suite."
+ # Overridden to remove unnecessary empty lines and separators
+ result = self._makeResult()
+ test(result)
+ result.printErrors()
+ if not result.wasSuccessful():
+ self.stream.write("FAILED (")
+ failed, errored = list(map(len, (result.failures, result.errors)))
+ if failed:
+ self.stream.write("failures=%d" % failed)
+ if errored:
+ if failed: self.stream.write(", ")
+ self.stream.write("errors=%d" % errored)
+ self.stream.writeln(")")
+ return result
+
+
+class ReloadingTestLoader(TestLoader):
+
+ def loadTestsFromName(self, name, module=None):
+ """Return a suite of all tests cases given a string specifier.
+
+ The name may resolve either to a module, a test case class, a
+ test method within a test case class, or a callable object which
+ returns a TestCase or TestSuite instance.
+
+ The method optionally resolves the names relative to a given module.
+ """
+ parts = name.split('.')
+ unused_parts = []
+ if module is None:
+ if not parts:
+ raise ValueError("incomplete test name: %s" % name)
+ else:
+ parts_copy = parts[:]
+ while parts_copy:
+ target = ".".join(parts_copy)
+ if target in sys.modules:
+ module = reload(sys.modules[target])
+ parts = unused_parts
+ break
+ else:
+ try:
+ module = __import__(target)
+ parts = unused_parts
+ break
+ except ImportError:
+ unused_parts.insert(0,parts_copy[-1])
+ del parts_copy[-1]
+ if not parts_copy:
+ raise
+ parts = parts[1:]
+ obj = module
+ for part in parts:
+ obj = getattr(obj, part)
+
+ if type(obj) == types.ModuleType:
+ return self.loadTestsFromModule(obj)
+ elif (isinstance(obj, (type, types.ClassType)) and
+ issubclass(obj, TestCase)):
+ return self.loadTestsFromTestCase(obj)
+ elif type(obj) == types.UnboundMethodType:
+ return obj.im_class(obj.__name__)
+ elif hasattr(obj, '__call__'):
+ test = obj()
+ if not isinstance(test, TestCase) and \
+ not isinstance(test, TestSuite):
+ raise ValueError("calling %s returned %s, "
+ "not a test" % (obj,test))
+ return test
+ else:
+ raise ValueError("do not know how to make test from: %s" % obj)
+
+
+try:
+ # Jython support
+ if sys.platform[:4] == 'java':
+ def getchar():
+ # Hopefully this is enough
+ return sys.stdin.read(1)
+ else:
+ # On Windows, msvcrt.getch reads a single char without output.
+ import msvcrt
+ def getchar():
+ return msvcrt.getch()
+except ImportError:
+ # Unix getchr
+ import tty, termios
+ def getchar():
+ fd = sys.stdin.fileno()
+ old_settings = termios.tcgetattr(fd)
+ try:
+ tty.setraw(sys.stdin.fileno())
+ ch = sys.stdin.read(1)
+ finally:
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
+ return ch
+
+
+class WebCase(TestCase):
+ HOST = "127.0.0.1"
+ PORT = 8000
+ HTTP_CONN = HTTPConnection
+ PROTOCOL = "HTTP/1.1"
+
+ scheme = "http"
+ url = None
+
+ status = None
+ headers = None
+ body = None
+
+ encoding = 'utf-8'
+
+ time = None
+
+ def get_conn(self, auto_open=False):
+ """Return a connection to our HTTP server."""
+ if self.scheme == "https":
+ cls = HTTPSConnection
+ else:
+ cls = HTTPConnection
+ conn = cls(self.interface(), self.PORT)
+ # Automatically re-connect?
+ conn.auto_open = auto_open
+ conn.connect()
+ return conn
+
+ def set_persistent(self, on=True, auto_open=False):
+ """Make our HTTP_CONN persistent (or not).
+
+ If the 'on' argument is True (the default), then self.HTTP_CONN
+ will be set to an instance of HTTPConnection (or HTTPS
+ if self.scheme is "https"). This will then persist across requests.
+
+ We only allow for a single open connection, so if you call this
+ and we currently have an open connection, it will be closed.
+ """
+ try:
+ self.HTTP_CONN.close()
+ except (TypeError, AttributeError):
+ pass
+
+ if on:
+ self.HTTP_CONN = self.get_conn(auto_open=auto_open)
+ else:
+ if self.scheme == "https":
+ self.HTTP_CONN = HTTPSConnection
+ else:
+ self.HTTP_CONN = HTTPConnection
+
+ def _get_persistent(self):
+ return hasattr(self.HTTP_CONN, "__class__")
+ def _set_persistent(self, on):
+ self.set_persistent(on)
+ persistent = property(_get_persistent, _set_persistent)
+
+ def interface(self):
+ """Return an IP address for a client connection.
+
+ If the server is listening on '0.0.0.0' (INADDR_ANY)
+ or '::' (IN6ADDR_ANY), this will return the proper localhost."""
+ return interface(self.HOST)
+
+ def getPage(self, url, headers=None, method="GET", body=None, protocol=None):
+ """Open the url with debugging support. Return status, headers, body."""
+ ServerError.on = False
+
+ if isinstance(url, unicodestr):
+ url = url.encode('utf-8')
+ if isinstance(body, unicodestr):
+ body = body.encode('utf-8')
+
+ self.url = url
+ self.time = None
+ start = time.time()
+ result = openURL(url, headers, method, body, self.HOST, self.PORT,
+ self.HTTP_CONN, protocol or self.PROTOCOL)
+ self.time = time.time() - start
+ self.status, self.headers, self.body = result
+
+ # Build a list of request cookies from the previous response cookies.
+ self.cookies = [('Cookie', v) for k, v in self.headers
+ if k.lower() == 'set-cookie']
+
+ if ServerError.on:
+ raise ServerError()
+ return result
+
+ interactive = True
+ console_height = 30
+
+ def _handlewebError(self, msg):
+ print("")
+ print(" ERROR: %s" % msg)
+
+ if not self.interactive:
+ raise self.failureException(msg)
+
+ p = " Show: [B]ody [H]eaders [S]tatus [U]RL; [I]gnore, [R]aise, or sys.e[X]it >> "
+ sys.stdout.write(p)
+ sys.stdout.flush()
+ while True:
+ i = getchar().upper()
+ if i not in "BHSUIRX":
+ continue
+ print(i.upper()) # Also prints new line
+ if i == "B":
+ for x, line in enumerate(self.body.splitlines()):
+ if (x + 1) % self.console_height == 0:
+ # The \r and comma should make the next line overwrite
+ sys.stdout.write("<-- More -->\r")
+ m = getchar().lower()
+ # Erase our "More" prompt
+ sys.stdout.write(" \r")
+ if m == "q":
+ break
+ print(line)
+ elif i == "H":
+ pprint.pprint(self.headers)
+ elif i == "S":
+ print(self.status)
+ elif i == "U":
+ print(self.url)
+ elif i == "I":
+ # return without raising the normal exception
+ return
+ elif i == "R":
+ raise self.failureException(msg)
+ elif i == "X":
+ self.exit()
+ sys.stdout.write(p)
+ sys.stdout.flush()
+
+ def exit(self):
+ sys.exit()
+
+ def assertStatus(self, status, msg=None):
+ """Fail if self.status != status."""
+ if isinstance(status, basestring):
+ if not self.status == status:
+ if msg is None:
+ msg = 'Status (%r) != %r' % (self.status, status)
+ self._handlewebError(msg)
+ elif isinstance(status, int):
+ code = int(self.status[:3])
+ if code != status:
+ if msg is None:
+ msg = 'Status (%r) != %r' % (self.status, status)
+ self._handlewebError(msg)
+ else:
+ # status is a tuple or list.
+ match = False
+ for s in status:
+ if isinstance(s, basestring):
+ if self.status == s:
+ match = True
+ break
+ elif int(self.status[:3]) == s:
+ match = True
+ break
+ if not match:
+ if msg is None:
+ msg = 'Status (%r) not in %r' % (self.status, status)
+ self._handlewebError(msg)
+
+ def assertHeader(self, key, value=None, msg=None):
+ """Fail if (key, [value]) not in self.headers."""
+ lowkey = key.lower()
+ for k, v in self.headers:
+ if k.lower() == lowkey:
+ if value is None or str(value) == v:
+ return v
+
+ if msg is None:
+ if value is None:
+ msg = '%r not in headers' % key
+ else:
+ msg = '%r:%r not in headers' % (key, value)
+ self._handlewebError(msg)
+
+ def assertHeaderItemValue(self, key, value, msg=None):
+ """Fail if the header does not contain the specified value"""
+ actual_value = self.assertHeader(key, msg=msg)
+ header_values = map(str.strip, actual_value.split(','))
+ if value in header_values:
+ return value
+
+ if msg is None:
+ msg = "%r not in %r" % (value, header_values)
+ self._handlewebError(msg)
+
+ def assertNoHeader(self, key, msg=None):
+ """Fail if key in self.headers."""
+ lowkey = key.lower()
+ matches = [k for k, v in self.headers if k.lower() == lowkey]
+ if matches:
+ if msg is None:
+ msg = '%r in headers' % key
+ self._handlewebError(msg)
+
+ def assertBody(self, value, msg=None):
+ """Fail if value != self.body."""
+ if value != self.body:
+ if msg is None:
+ msg = 'expected body:\n%r\n\nactual body:\n%r' % (value, self.body)
+ self._handlewebError(msg)
+
+ def assertInBody(self, value, msg=None):
+ """Fail if value not in self.body."""
+ if value not in self.body:
+ if msg is None:
+ msg = '%r not in body: %s' % (value, self.body)
+ self._handlewebError(msg)
+
+ def assertNotInBody(self, value, msg=None):
+ """Fail if value in self.body."""
+ if value in self.body:
+ if msg is None:
+ msg = '%r found in body' % value
+ self._handlewebError(msg)
+
+ def assertMatchesBody(self, pattern, msg=None, flags=0):
+ """Fail if value (a regex pattern) is not in self.body."""
+ if re.search(pattern, self.body, flags) is None:
+ if msg is None:
+ msg = 'No match for %r in body' % pattern
+ self._handlewebError(msg)
+
+
+methods_with_bodies = ("POST", "PUT")
+
+def cleanHeaders(headers, method, body, host, port):
+ """Return request headers, with required headers added (if missing)."""
+ if headers is None:
+ headers = []
+
+ # Add the required Host request header if not present.
+ # [This specifies the host:port of the server, not the client.]
+ found = False
+ for k, v in headers:
+ if k.lower() == 'host':
+ found = True
+ break
+ if not found:
+ if port == 80:
+ headers.append(("Host", host))
+ else:
+ headers.append(("Host", "%s:%s" % (host, port)))
+
+ if method in methods_with_bodies:
+ # Stick in default type and length headers if not present
+ found = False
+ for k, v in headers:
+ if k.lower() == 'content-type':
+ found = True
+ break
+ if not found:
+ headers.append(("Content-Type", "application/x-www-form-urlencoded"))
+ headers.append(("Content-Length", str(len(body or ""))))
+
+ return headers
+
+
+def shb(response):
+ """Return status, headers, body the way we like from a response."""
+ h = []
+ key, value = None, None
+ for line in response.msg.headers:
+ if line:
+ if line[0] in " \t":
+ value += line.strip()
+ else:
+ if key and value:
+ h.append((key, value))
+ key, value = line.split(":", 1)
+ key = key.strip()
+ value = value.strip()
+ if key and value:
+ h.append((key, value))
+
+ return "%s %s" % (response.status, response.reason), h, response.read()
+
+
+def openURL(url, headers=None, method="GET", body=None,
+ host="127.0.0.1", port=8000, http_conn=HTTPConnection,
+ protocol="HTTP/1.1"):
+ """Open the given HTTP resource and return status, headers, and body."""
+
+ headers = cleanHeaders(headers, method, body, host, port)
+
+ # Trying 10 times is simply in case of socket errors.
+ # Normal case--it should run once.
+ for trial in range(10):
+ try:
+ # Allow http_conn to be a class or an instance
+ if hasattr(http_conn, "host"):
+ conn = http_conn
+ else:
+ conn = http_conn(interface(host), port)
+
+ conn._http_vsn_str = protocol
+ conn._http_vsn = int("".join([x for x in protocol if x.isdigit()]))
+
+ # skip_accept_encoding argument added in python version 2.4
+ if sys.version_info < (2, 4):
+ def putheader(self, header, value):
+ if header == 'Accept-Encoding' and value == 'identity':
+ return
+ self.__class__.putheader(self, header, value)
+ import new
+ conn.putheader = new.instancemethod(putheader, conn, conn.__class__)
+ conn.putrequest(method.upper(), url, skip_host=True)
+ else:
+ conn.putrequest(method.upper(), url, skip_host=True,
+ skip_accept_encoding=True)
+
+ for key, value in headers:
+ conn.putheader(key, value)
+ conn.endheaders()
+
+ if body is not None:
+ conn.send(body)
+
+ # Handle response
+ response = conn.getresponse()
+
+ s, h, b = shb(response)
+
+ if not hasattr(http_conn, "host"):
+ # We made our own conn instance. Close it.
+ conn.close()
+
+ return s, h, b
+ except socket.error:
+ time.sleep(0.5)
+ raise
+
+
+# Add any exceptions which your web framework handles
+# normally (that you don't want server_error to trap).
+ignored_exceptions = []
+
+# You'll want set this to True when you can't guarantee
+# that each response will immediately follow each request;
+# for example, when handling requests via multiple threads.
+ignore_all = False
+
+class ServerError(Exception):
+ on = False
+
+
+def server_error(exc=None):
+ """Server debug hook. Return True if exception handled, False if ignored.
+
+ You probably want to wrap this, so you can still handle an error using
+ your framework when it's ignored.
+ """
+ if exc is None:
+ exc = sys.exc_info()
+
+ if ignore_all or exc[0] in ignored_exceptions:
+ return False
+ else:
+ ServerError.on = True
+ print("")
+ print("".join(traceback.format_exception(*exc)))
+ return True
+
diff --git a/cherrypy/tutorial/README.txt b/cherrypy/tutorial/README.txt
new file mode 100644
index 0000000..2b877e1
--- /dev/null
+++ b/cherrypy/tutorial/README.txt
@@ -0,0 +1,16 @@
+CherryPy Tutorials
+------------------------------------------------------------------------
+
+This is a series of tutorials explaining how to develop dynamic web
+applications using CherryPy. A couple of notes:
+
+ - Each of these tutorials builds on the ones before it. If you're
+ new to CherryPy, we recommend you start with 01_helloworld.py and
+ work your way upwards. :)
+
+ - In most of these tutorials, you will notice that all output is done
+ by returning normal Python strings, often using simple Python
+ variable substitution. In most real-world applications, you will
+ probably want to use a separate template package (like Cheetah,
+ CherryTemplate or XML/XSL).
+
diff --git a/cherrypy/tutorial/__init__.py b/cherrypy/tutorial/__init__.py
new file mode 100755
index 0000000..c4e2c55
--- /dev/null
+++ b/cherrypy/tutorial/__init__.py
@@ -0,0 +1,3 @@
+
+# This is used in test_config to test unrepr of "from A import B"
+thing2 = object() \ No newline at end of file
diff --git a/cherrypy/tutorial/bonus-sqlobject.py b/cherrypy/tutorial/bonus-sqlobject.py
new file mode 100755
index 0000000..c43feb4
--- /dev/null
+++ b/cherrypy/tutorial/bonus-sqlobject.py
@@ -0,0 +1,168 @@
+'''
+Bonus Tutorial: Using SQLObject
+
+This is a silly little contacts manager application intended to
+demonstrate how to use SQLObject from within a CherryPy2 project. It
+also shows how to use inline Cheetah templates.
+
+SQLObject is an Object/Relational Mapper that allows you to access
+data stored in an RDBMS in a pythonic fashion. You create data objects
+as Python classes and let SQLObject take care of all the nasty details.
+
+This code depends on the latest development version (0.6+) of SQLObject.
+You can get it from the SQLObject Subversion server. You can find all
+necessary information at <http://www.sqlobject.org>. This code will NOT
+work with the 0.5.x version advertised on their website!
+
+This code also depends on a recent version of Cheetah. You can find
+Cheetah at <http://www.cheetahtemplate.org>.
+
+After starting this application for the first time, you will need to
+access the /reset URI in order to create the database table and some
+sample data. Accessing /reset again will drop and re-create the table,
+so you may want to be careful. :-)
+
+This application isn't supposed to be fool-proof, it's not even supposed
+to be very GOOD. Play around with it some, browse the source code, smile.
+
+:)
+
+-- Hendrik Mans <hendrik@mans.de>
+'''
+
+import cherrypy
+from Cheetah.Template import Template
+from sqlobject import *
+
+# configure your database connection here
+__connection__ = 'mysql://root:@localhost/test'
+
+# this is our (only) data class.
+class Contact(SQLObject):
+ lastName = StringCol(length = 50, notNone = True)
+ firstName = StringCol(length = 50, notNone = True)
+ phone = StringCol(length = 30, notNone = True, default = '')
+ email = StringCol(length = 30, notNone = True, default = '')
+ url = StringCol(length = 100, notNone = True, default = '')
+
+
+class ContactManager:
+ def index(self):
+ # Let's display a list of all stored contacts.
+ contacts = Contact.select()
+
+ template = Template('''
+ <h2>All Contacts</h2>
+
+ #for $contact in $contacts
+ <a href="mailto:$contact.email">$contact.lastName, $contact.firstName</a>
+ [<a href="./edit?id=$contact.id">Edit</a>]
+ [<a href="./delete?id=$contact.id">Delete</a>]
+ <br/>
+ #end for
+
+ <p>[<a href="./edit">Add new contact</a>]</p>
+ ''', [locals(), globals()])
+
+ return template.respond()
+
+ index.exposed = True
+
+
+ def edit(self, id = 0):
+ # we really want id as an integer. Since GET/POST parameters
+ # are always passed as strings, let's convert it.
+ id = int(id)
+
+ if id > 0:
+ # if an id is specified, we're editing an existing contact.
+ contact = Contact.get(id)
+ title = "Edit Contact"
+ else:
+ # if no id is specified, we're entering a new contact.
+ contact = None
+ title = "New Contact"
+
+
+ # In the following template code, please note that we use
+ # Cheetah's $getVar() construct for the form values. We have
+ # to do this because contact may be set to None (see above).
+ template = Template('''
+ <h2>$title</h2>
+
+ <form action="./store" method="POST">
+ <input type="hidden" name="id" value="$id" />
+ Last Name: <input name="lastName" value="$getVar('contact.lastName', '')" /><br/>
+ First Name: <input name="firstName" value="$getVar('contact.firstName', '')" /><br/>
+ Phone: <input name="phone" value="$getVar('contact.phone', '')" /><br/>
+ Email: <input name="email" value="$getVar('contact.email', '')" /><br/>
+ URL: <input name="url" value="$getVar('contact.url', '')" /><br/>
+ <input type="submit" value="Store" />
+ </form>
+ ''', [locals(), globals()])
+
+ return template.respond()
+
+ edit.exposed = True
+
+
+ def delete(self, id):
+ # Delete the specified contact
+ contact = Contact.get(int(id))
+ contact.destroySelf()
+ return 'Deleted. <a href="./">Return to Index</a>'
+
+ delete.exposed = True
+
+
+ def store(self, lastName, firstName, phone, email, url, id = None):
+ if id and int(id) > 0:
+ # If an id was specified, update an existing contact.
+ contact = Contact.get(int(id))
+
+ # We could set one field after another, but that would
+ # cause multiple UPDATE clauses. So we'll just do it all
+ # in a single pass through the set() method.
+ contact.set(
+ lastName = lastName,
+ firstName = firstName,
+ phone = phone,
+ email = email,
+ url = url)
+ else:
+ # Otherwise, add a new contact.
+ contact = Contact(
+ lastName = lastName,
+ firstName = firstName,
+ phone = phone,
+ email = email,
+ url = url)
+
+ return 'Stored. <a href="./">Return to Index</a>'
+
+ store.exposed = True
+
+
+ def reset(self):
+ # Drop existing table
+ Contact.dropTable(True)
+
+ # Create new table
+ Contact.createTable()
+
+ # Create some sample data
+ Contact(
+ firstName = 'Hendrik',
+ lastName = 'Mans',
+ email = 'hendrik@mans.de',
+ phone = '++49 89 12345678',
+ url = 'http://www.mornography.de')
+
+ return "reset completed!"
+
+ reset.exposed = True
+
+
+print("If you're running this application for the first time, please go to http://localhost:8080/reset once in order to create the database!")
+
+cherrypy.quickstart(ContactManager())
diff --git a/cherrypy/tutorial/custom_error.html b/cherrypy/tutorial/custom_error.html
new file mode 100644
index 0000000..d0f30c8
--- /dev/null
+++ b/cherrypy/tutorial/custom_error.html
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<head>
+ <title>403 Unauthorized</title>
+</head>
+ <body>
+ <h2>You can't do that!</h2>
+ <p>%(message)s</p>
+ <p>This is a custom error page that is read from a file.<p>
+ <pre>%(traceback)s</pre>
+ </body>
+</html>
diff --git a/cherrypy/tutorial/pdf_file.pdf b/cherrypy/tutorial/pdf_file.pdf
new file mode 100644
index 0000000..38b4f15
--- /dev/null
+++ b/cherrypy/tutorial/pdf_file.pdf
Binary files differ
diff --git a/cherrypy/tutorial/tut01_helloworld.py b/cherrypy/tutorial/tut01_helloworld.py
new file mode 100755
index 0000000..ef94760
--- /dev/null
+++ b/cherrypy/tutorial/tut01_helloworld.py
@@ -0,0 +1,35 @@
+"""
+Tutorial - Hello World
+
+The most basic (working) CherryPy application possible.
+"""
+
+# Import CherryPy global namespace
+import cherrypy
+
+class HelloWorld:
+ """ Sample request handler class. """
+
+ def index(self):
+ # CherryPy will call this method for the root URI ("/") and send
+ # its return value to the client. Because this is tutorial
+ # lesson number 01, we'll just send something really simple.
+ # How about...
+ return "Hello world!"
+
+ # Expose the index method through the web. CherryPy will never
+ # publish methods that don't have the exposed attribute set to True.
+ index.exposed = True
+
+
+import os.path
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+ # CherryPy always starts with app.root when trying to map request URIs
+ # to objects, so we need to mount a request handler root. A request
+ # to '/' will be mapped to HelloWorld().index().
+ cherrypy.quickstart(HelloWorld(), config=tutconf)
+else:
+ # This branch is for the test suite; you can ignore it.
+ cherrypy.tree.mount(HelloWorld(), config=tutconf)
diff --git a/cherrypy/tutorial/tut02_expose_methods.py b/cherrypy/tutorial/tut02_expose_methods.py
new file mode 100755
index 0000000..600fca3
--- /dev/null
+++ b/cherrypy/tutorial/tut02_expose_methods.py
@@ -0,0 +1,32 @@
+"""
+Tutorial - Multiple methods
+
+This tutorial shows you how to link to other methods of your request
+handler.
+"""
+
+import cherrypy
+
+class HelloWorld:
+
+ def index(self):
+ # Let's link to another method here.
+ return 'We have an <a href="showMessage">important message</a> for you!'
+ index.exposed = True
+
+ def showMessage(self):
+ # Here's the important message!
+ return "Hello world!"
+ showMessage.exposed = True
+
+import os.path
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+ # CherryPy always starts with app.root when trying to map request URIs
+ # to objects, so we need to mount a request handler root. A request
+ # to '/' will be mapped to HelloWorld().index().
+ cherrypy.quickstart(HelloWorld(), config=tutconf)
+else:
+ # This branch is for the test suite; you can ignore it.
+ cherrypy.tree.mount(HelloWorld(), config=tutconf)
diff --git a/cherrypy/tutorial/tut03_get_and_post.py b/cherrypy/tutorial/tut03_get_and_post.py
new file mode 100755
index 0000000..283477d
--- /dev/null
+++ b/cherrypy/tutorial/tut03_get_and_post.py
@@ -0,0 +1,53 @@
+"""
+Tutorial - Passing variables
+
+This tutorial shows you how to pass GET/POST variables to methods.
+"""
+
+import cherrypy
+
+
+class WelcomePage:
+
+ def index(self):
+ # Ask for the user's name.
+ return '''
+ <form action="greetUser" method="GET">
+ What is your name?
+ <input type="text" name="name" />
+ <input type="submit" />
+ </form>'''
+ index.exposed = True
+
+ def greetUser(self, name = None):
+ # CherryPy passes all GET and POST variables as method parameters.
+ # It doesn't make a difference where the variables come from, how
+ # large their contents are, and so on.
+ #
+ # You can define default parameter values as usual. In this
+ # example, the "name" parameter defaults to None so we can check
+ # if a name was actually specified.
+
+ if name:
+ # Greet the user!
+ return "Hey %s, what's up?" % name
+ else:
+ if name is None:
+ # No name was specified
+ return 'Please enter your name <a href="./">here</a>.'
+ else:
+ return 'No, really, enter your name <a href="./">here</a>.'
+ greetUser.exposed = True
+
+
+import os.path
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+ # CherryPy always starts with app.root when trying to map request URIs
+ # to objects, so we need to mount a request handler root. A request
+ # to '/' will be mapped to HelloWorld().index().
+ cherrypy.quickstart(WelcomePage(), config=tutconf)
+else:
+ # This branch is for the test suite; you can ignore it.
+ cherrypy.tree.mount(WelcomePage(), config=tutconf)
diff --git a/cherrypy/tutorial/tut04_complex_site.py b/cherrypy/tutorial/tut04_complex_site.py
new file mode 100755
index 0000000..b4d820e
--- /dev/null
+++ b/cherrypy/tutorial/tut04_complex_site.py
@@ -0,0 +1,98 @@
+"""
+Tutorial - Multiple objects
+
+This tutorial shows you how to create a site structure through multiple
+possibly nested request handler objects.
+"""
+
+import cherrypy
+
+
+class HomePage:
+ def index(self):
+ return '''
+ <p>Hi, this is the home page! Check out the other
+ fun stuff on this site:</p>
+
+ <ul>
+ <li><a href="/joke/">A silly joke</a></li>
+ <li><a href="/links/">Useful links</a></li>
+ </ul>'''
+ index.exposed = True
+
+
+class JokePage:
+ def index(self):
+ return '''
+ <p>"In Python, how do you create a string of random
+ characters?" -- "Read a Perl file!"</p>
+ <p>[<a href="../">Return</a>]</p>'''
+ index.exposed = True
+
+
+class LinksPage:
+ def __init__(self):
+ # Request handler objects can create their own nested request
+ # handler objects. Simply create them inside their __init__
+ # methods!
+ self.extra = ExtraLinksPage()
+
+ def index(self):
+ # Note the way we link to the extra links page (and back).
+ # As you can see, this object doesn't really care about its
+ # absolute position in the site tree, since we use relative
+ # links exclusively.
+ return '''
+ <p>Here are some useful links:</p>
+
+ <ul>
+ <li><a href="http://www.cherrypy.org">The CherryPy Homepage</a></li>
+ <li><a href="http://www.python.org">The Python Homepage</a></li>
+ </ul>
+
+ <p>You can check out some extra useful
+ links <a href="./extra/">here</a>.</p>
+
+ <p>[<a href="../">Return</a>]</p>
+ '''
+ index.exposed = True
+
+
+class ExtraLinksPage:
+ def index(self):
+ # Note the relative link back to the Links page!
+ return '''
+ <p>Here are some extra useful links:</p>
+
+ <ul>
+ <li><a href="http://del.icio.us">del.icio.us</a></li>
+ <li><a href="http://www.mornography.de">Hendrik's weblog</a></li>
+ </ul>
+
+ <p>[<a href="../">Return to links page</a>]</p>'''
+ index.exposed = True
+
+
+# Of course we can also mount request handler objects right here!
+root = HomePage()
+root.joke = JokePage()
+root.links = LinksPage()
+
+# Remember, we don't need to mount ExtraLinksPage here, because
+# LinksPage does that itself on initialization. In fact, there is
+# no reason why you shouldn't let your root object take care of
+# creating all contained request handler objects.
+
+
+import os.path
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+ # CherryPy always starts with app.root when trying to map request URIs
+ # to objects, so we need to mount a request handler root. A request
+ # to '/' will be mapped to HelloWorld().index().
+ cherrypy.quickstart(root, config=tutconf)
+else:
+ # This branch is for the test suite; you can ignore it.
+ cherrypy.tree.mount(root, config=tutconf)
+
diff --git a/cherrypy/tutorial/tut05_derived_objects.py b/cherrypy/tutorial/tut05_derived_objects.py
new file mode 100755
index 0000000..3d4ec9b
--- /dev/null
+++ b/cherrypy/tutorial/tut05_derived_objects.py
@@ -0,0 +1,83 @@
+"""
+Tutorial - Object inheritance
+
+You are free to derive your request handler classes from any base
+class you wish. In most real-world applications, you will probably
+want to create a central base class used for all your pages, which takes
+care of things like printing a common page header and footer.
+"""
+
+import cherrypy
+
+
+class Page:
+ # Store the page title in a class attribute
+ title = 'Untitled Page'
+
+ def header(self):
+ return '''
+ <html>
+ <head>
+ <title>%s</title>
+ <head>
+ <body>
+ <h2>%s</h2>
+ ''' % (self.title, self.title)
+
+ def footer(self):
+ return '''
+ </body>
+ </html>
+ '''
+
+ # Note that header and footer don't get their exposed attributes
+ # set to True. This isn't necessary since the user isn't supposed
+ # to call header or footer directly; instead, we'll call them from
+ # within the actually exposed handler methods defined in this
+ # class' subclasses.
+
+
+class HomePage(Page):
+ # Different title for this page
+ title = 'Tutorial 5'
+
+ def __init__(self):
+ # create a subpage
+ self.another = AnotherPage()
+
+ def index(self):
+ # Note that we call the header and footer methods inherited
+ # from the Page class!
+ return self.header() + '''
+ <p>
+ Isn't this exciting? There's
+ <a href="./another/">another page</a>, too!
+ </p>
+ ''' + self.footer()
+ index.exposed = True
+
+
+class AnotherPage(Page):
+ title = 'Another Page'
+
+ def index(self):
+ return self.header() + '''
+ <p>
+ And this is the amazing second page!
+ </p>
+ ''' + self.footer()
+ index.exposed = True
+
+
+import os.path
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+ # CherryPy always starts with app.root when trying to map request URIs
+ # to objects, so we need to mount a request handler root. A request
+ # to '/' will be mapped to HelloWorld().index().
+ cherrypy.quickstart(HomePage(), config=tutconf)
+else:
+ # This branch is for the test suite; you can ignore it.
+ cherrypy.tree.mount(HomePage(), config=tutconf)
+
diff --git a/cherrypy/tutorial/tut06_default_method.py b/cherrypy/tutorial/tut06_default_method.py
new file mode 100755
index 0000000..fe24f38
--- /dev/null
+++ b/cherrypy/tutorial/tut06_default_method.py
@@ -0,0 +1,64 @@
+"""
+Tutorial - The default method
+
+Request handler objects can implement a method called "default" that
+is called when no other suitable method/object could be found.
+Essentially, if CherryPy2 can't find a matching request handler object
+for the given request URI, it will use the default method of the object
+located deepest on the URI path.
+
+Using this mechanism you can easily simulate virtual URI structures
+by parsing the extra URI string, which you can access through
+cherrypy.request.virtualPath.
+
+The application in this tutorial simulates an URI structure looking
+like /users/<username>. Since the <username> bit will not be found (as
+there are no matching methods), it is handled by the default method.
+"""
+
+import cherrypy
+
+
+class UsersPage:
+
+ def index(self):
+ # Since this is just a stupid little example, we'll simply
+ # display a list of links to random, made-up users. In a real
+ # application, this could be generated from a database result set.
+ return '''
+ <a href="./remi">Remi Delon</a><br/>
+ <a href="./hendrik">Hendrik Mans</a><br/>
+ <a href="./lorenzo">Lorenzo Lamas</a><br/>
+ '''
+ index.exposed = True
+
+ def default(self, user):
+ # Here we react depending on the virtualPath -- the part of the
+ # path that could not be mapped to an object method. In a real
+ # application, we would probably do some database lookups here
+ # instead of the silly if/elif/else construct.
+ if user == 'remi':
+ out = "Remi Delon, CherryPy lead developer"
+ elif user == 'hendrik':
+ out = "Hendrik Mans, CherryPy co-developer & crazy German"
+ elif user == 'lorenzo':
+ out = "Lorenzo Lamas, famous actor and singer!"
+ else:
+ out = "Unknown user. :-("
+
+ return '%s (<a href="./">back</a>)' % out
+ default.exposed = True
+
+
+import os.path
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+ # CherryPy always starts with app.root when trying to map request URIs
+ # to objects, so we need to mount a request handler root. A request
+ # to '/' will be mapped to HelloWorld().index().
+ cherrypy.quickstart(UsersPage(), config=tutconf)
+else:
+ # This branch is for the test suite; you can ignore it.
+ cherrypy.tree.mount(UsersPage(), config=tutconf)
+
diff --git a/cherrypy/tutorial/tut07_sessions.py b/cherrypy/tutorial/tut07_sessions.py
new file mode 100755
index 0000000..4b1386b
--- /dev/null
+++ b/cherrypy/tutorial/tut07_sessions.py
@@ -0,0 +1,44 @@
+"""
+Tutorial - Sessions
+
+Storing session data in CherryPy applications is very easy: cherrypy
+provides a dictionary called "session" that represents the session
+data for the current user. If you use RAM based sessions, you can store
+any kind of object into that dictionary; otherwise, you are limited to
+objects that can be pickled.
+"""
+
+import cherrypy
+
+
+class HitCounter:
+
+ _cp_config = {'tools.sessions.on': True}
+
+ def index(self):
+ # Increase the silly hit counter
+ count = cherrypy.session.get('count', 0) + 1
+
+ # Store the new value in the session dictionary
+ cherrypy.session['count'] = count
+
+ # And display a silly hit count message!
+ return '''
+ During your current session, you've viewed this
+ page %s times! Your life is a patio of fun!
+ ''' % count
+ index.exposed = True
+
+
+import os.path
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+ # CherryPy always starts with app.root when trying to map request URIs
+ # to objects, so we need to mount a request handler root. A request
+ # to '/' will be mapped to HelloWorld().index().
+ cherrypy.quickstart(HitCounter(), config=tutconf)
+else:
+ # This branch is for the test suite; you can ignore it.
+ cherrypy.tree.mount(HitCounter(), config=tutconf)
+
diff --git a/cherrypy/tutorial/tut08_generators_and_yield.py b/cherrypy/tutorial/tut08_generators_and_yield.py
new file mode 100755
index 0000000..a6fbdc2
--- /dev/null
+++ b/cherrypy/tutorial/tut08_generators_and_yield.py
@@ -0,0 +1,47 @@
+"""
+Bonus Tutorial: Using generators to return result bodies
+
+Instead of returning a complete result string, you can use the yield
+statement to return one result part after another. This may be convenient
+in situations where using a template package like CherryPy or Cheetah
+would be overkill, and messy string concatenation too uncool. ;-)
+"""
+
+import cherrypy
+
+
+class GeneratorDemo:
+
+ def header(self):
+ return "<html><body><h2>Generators rule!</h2>"
+
+ def footer(self):
+ return "</body></html>"
+
+ def index(self):
+ # Let's make up a list of users for presentation purposes
+ users = ['Remi', 'Carlos', 'Hendrik', 'Lorenzo Lamas']
+
+ # Every yield line adds one part to the total result body.
+ yield self.header()
+ yield "<h3>List of users:</h3>"
+
+ for user in users:
+ yield "%s<br/>" % user
+
+ yield self.footer()
+ index.exposed = True
+
+
+import os.path
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+ # CherryPy always starts with app.root when trying to map request URIs
+ # to objects, so we need to mount a request handler root. A request
+ # to '/' will be mapped to HelloWorld().index().
+ cherrypy.quickstart(GeneratorDemo(), config=tutconf)
+else:
+ # This branch is for the test suite; you can ignore it.
+ cherrypy.tree.mount(GeneratorDemo(), config=tutconf)
+
diff --git a/cherrypy/tutorial/tut09_files.py b/cherrypy/tutorial/tut09_files.py
new file mode 100755
index 0000000..4c8e581
--- /dev/null
+++ b/cherrypy/tutorial/tut09_files.py
@@ -0,0 +1,107 @@
+"""
+
+Tutorial: File upload and download
+
+Uploads
+-------
+
+When a client uploads a file to a CherryPy application, it's placed
+on disk immediately. CherryPy will pass it to your exposed method
+as an argument (see "myFile" below); that arg will have a "file"
+attribute, which is a handle to the temporary uploaded file.
+If you wish to permanently save the file, you need to read()
+from myFile.file and write() somewhere else.
+
+Note the use of 'enctype="multipart/form-data"' and 'input type="file"'
+in the HTML which the client uses to upload the file.
+
+
+Downloads
+---------
+
+If you wish to send a file to the client, you have two options:
+First, you can simply return a file-like object from your page handler.
+CherryPy will read the file and serve it as the content (HTTP body)
+of the response. However, that doesn't tell the client that
+the response is a file to be saved, rather than displayed.
+Use cherrypy.lib.static.serve_file for that; it takes four
+arguments:
+
+serve_file(path, content_type=None, disposition=None, name=None)
+
+Set "name" to the filename that you expect clients to use when they save
+your file. Note that the "name" argument is ignored if you don't also
+provide a "disposition" (usually "attachement"). You can manually set
+"content_type", but be aware that if you also use the encoding tool, it
+may choke if the file extension is not recognized as belonging to a known
+Content-Type. Setting the content_type to "application/x-download" works
+in most cases, and should prompt the user with an Open/Save dialog in
+popular browsers.
+
+"""
+
+import os
+localDir = os.path.dirname(__file__)
+absDir = os.path.join(os.getcwd(), localDir)
+
+import cherrypy
+from cherrypy.lib import static
+
+
+class FileDemo(object):
+
+ def index(self):
+ return """
+ <html><body>
+ <h2>Upload a file</h2>
+ <form action="upload" method="post" enctype="multipart/form-data">
+ filename: <input type="file" name="myFile" /><br />
+ <input type="submit" />
+ </form>
+ <h2>Download a file</h2>
+ <a href='download'>This one</a>
+ </body></html>
+ """
+ index.exposed = True
+
+ def upload(self, myFile):
+ out = """<html>
+ <body>
+ myFile length: %s<br />
+ myFile filename: %s<br />
+ myFile mime-type: %s
+ </body>
+ </html>"""
+
+ # Although this just counts the file length, it demonstrates
+ # how to read large files in chunks instead of all at once.
+ # CherryPy reads the uploaded file into a temporary file;
+ # myFile.file.read reads from that.
+ size = 0
+ while True:
+ data = myFile.file.read(8192)
+ if not data:
+ break
+ size += len(data)
+
+ return out % (size, myFile.filename, myFile.content_type)
+ upload.exposed = True
+
+ def download(self):
+ path = os.path.join(absDir, "pdf_file.pdf")
+ return static.serve_file(path, "application/x-download",
+ "attachment", os.path.basename(path))
+ download.exposed = True
+
+
+import os.path
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+ # CherryPy always starts with app.root when trying to map request URIs
+ # to objects, so we need to mount a request handler root. A request
+ # to '/' will be mapped to HelloWorld().index().
+ cherrypy.quickstart(FileDemo(), config=tutconf)
+else:
+ # This branch is for the test suite; you can ignore it.
+ cherrypy.tree.mount(FileDemo(), config=tutconf)
diff --git a/cherrypy/tutorial/tut10_http_errors.py b/cherrypy/tutorial/tut10_http_errors.py
new file mode 100755
index 0000000..dfa5733
--- /dev/null
+++ b/cherrypy/tutorial/tut10_http_errors.py
@@ -0,0 +1,81 @@
+"""
+
+Tutorial: HTTP errors
+
+HTTPError is used to return an error response to the client.
+CherryPy has lots of options regarding how such errors are
+logged, displayed, and formatted.
+
+"""
+
+import os
+localDir = os.path.dirname(__file__)
+curpath = os.path.normpath(os.path.join(os.getcwd(), localDir))
+
+import cherrypy
+
+
+class HTTPErrorDemo(object):
+
+ # Set a custom response for 403 errors.
+ _cp_config = {'error_page.403' : os.path.join(curpath, "custom_error.html")}
+
+ def index(self):
+ # display some links that will result in errors
+ tracebacks = cherrypy.request.show_tracebacks
+ if tracebacks:
+ trace = 'off'
+ else:
+ trace = 'on'
+
+ return """
+ <html><body>
+ <p>Toggle tracebacks <a href="toggleTracebacks">%s</a></p>
+ <p><a href="/doesNotExist">Click me; I'm a broken link!</a></p>
+ <p><a href="/error?code=403">Use a custom error page from a file.</a></p>
+ <p>These errors are explicitly raised by the application:</p>
+ <ul>
+ <li><a href="/error?code=400">400</a></li>
+ <li><a href="/error?code=401">401</a></li>
+ <li><a href="/error?code=402">402</a></li>
+ <li><a href="/error?code=500">500</a></li>
+ </ul>
+ <p><a href="/messageArg">You can also set the response body
+ when you raise an error.</a></p>
+ </body></html>
+ """ % trace
+ index.exposed = True
+
+ def toggleTracebacks(self):
+ # simple function to toggle tracebacks on and off
+ tracebacks = cherrypy.request.show_tracebacks
+ cherrypy.config.update({'request.show_tracebacks': not tracebacks})
+
+ # redirect back to the index
+ raise cherrypy.HTTPRedirect('/')
+ toggleTracebacks.exposed = True
+
+ def error(self, code):
+ # raise an error based on the get query
+ raise cherrypy.HTTPError(status = code)
+ error.exposed = True
+
+ def messageArg(self):
+ message = ("If you construct an HTTPError with a 'message' "
+ "argument, it wil be placed on the error page "
+ "(underneath the status line by default).")
+ raise cherrypy.HTTPError(500, message=message)
+ messageArg.exposed = True
+
+
+import os.path
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+ # CherryPy always starts with app.root when trying to map request URIs
+ # to objects, so we need to mount a request handler root. A request
+ # to '/' will be mapped to HelloWorld().index().
+ cherrypy.quickstart(HTTPErrorDemo(), config=tutconf)
+else:
+ # This branch is for the test suite; you can ignore it.
+ cherrypy.tree.mount(HTTPErrorDemo(), config=tutconf)
diff --git a/cherrypy/tutorial/tutorial.conf b/cherrypy/tutorial/tutorial.conf
new file mode 100644
index 0000000..6537fd3
--- /dev/null
+++ b/cherrypy/tutorial/tutorial.conf
@@ -0,0 +1,4 @@
+[global]
+server.socket_host = "127.0.0.1"
+server.socket_port = 8080
+server.thread_pool = 10
diff --git a/cherrypy/wsgiserver/__init__.py b/cherrypy/wsgiserver/__init__.py
new file mode 100755
index 0000000..55d1dd9
--- /dev/null
+++ b/cherrypy/wsgiserver/__init__.py
@@ -0,0 +1,2219 @@
+"""A high-speed, production ready, thread pooled, generic HTTP server.
+
+Simplest example on how to use this module directly
+(without using CherryPy's application machinery)::
+
+ from cherrypy import wsgiserver
+
+ def my_crazy_app(environ, start_response):
+ status = '200 OK'
+ response_headers = [('Content-type','text/plain')]
+ start_response(status, response_headers)
+ return ['Hello world!']
+
+ server = wsgiserver.CherryPyWSGIServer(
+ ('0.0.0.0', 8070), my_crazy_app,
+ server_name='www.cherrypy.example')
+ server.start()
+
+The CherryPy WSGI server can serve as many WSGI applications
+as you want in one instance by using a WSGIPathInfoDispatcher::
+
+ d = WSGIPathInfoDispatcher({'/': my_crazy_app, '/blog': my_blog_app})
+ server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 80), d)
+
+Want SSL support? Just set server.ssl_adapter to an SSLAdapter instance.
+
+This won't call the CherryPy engine (application side) at all, only the
+HTTP server, which is independent from the rest of CherryPy. Don't
+let the name "CherryPyWSGIServer" throw you; the name merely reflects
+its origin, not its coupling.
+
+For those of you wanting to understand internals of this module, here's the
+basic call flow. The server's listening thread runs a very tight loop,
+sticking incoming connections onto a Queue::
+
+ server = CherryPyWSGIServer(...)
+ server.start()
+ while True:
+ tick()
+ # This blocks until a request comes in:
+ child = socket.accept()
+ conn = HTTPConnection(child, ...)
+ server.requests.put(conn)
+
+Worker threads are kept in a pool and poll the Queue, popping off and then
+handling each connection in turn. Each connection can consist of an arbitrary
+number of requests and their responses, so we run a nested loop::
+
+ while True:
+ conn = server.requests.get()
+ conn.communicate()
+ -> while True:
+ req = HTTPRequest(...)
+ req.parse_request()
+ -> # Read the Request-Line, e.g. "GET /page HTTP/1.1"
+ req.rfile.readline()
+ read_headers(req.rfile, req.inheaders)
+ req.respond()
+ -> response = app(...)
+ try:
+ for chunk in response:
+ if chunk:
+ req.write(chunk)
+ finally:
+ if hasattr(response, "close"):
+ response.close()
+ if req.close_connection:
+ return
+"""
+
+CRLF = '\r\n'
+import os
+import Queue
+import re
+quoted_slash = re.compile("(?i)%2F")
+import rfc822
+import socket
+import sys
+if 'win' in sys.platform and not hasattr(socket, 'IPPROTO_IPV6'):
+ socket.IPPROTO_IPV6 = 41
+try:
+ import cStringIO as StringIO
+except ImportError:
+ import StringIO
+DEFAULT_BUFFER_SIZE = -1
+
+_fileobject_uses_str_type = isinstance(socket._fileobject(None)._rbuf, basestring)
+
+import threading
+import time
+import traceback
+def format_exc(limit=None):
+ """Like print_exc() but return a string. Backport for Python 2.3."""
+ try:
+ etype, value, tb = sys.exc_info()
+ return ''.join(traceback.format_exception(etype, value, tb, limit))
+ finally:
+ etype = value = tb = None
+
+
+from urllib import unquote
+from urlparse import urlparse
+import warnings
+
+import errno
+
+def plat_specific_errors(*errnames):
+ """Return error numbers for all errors in errnames on this platform.
+
+ The 'errno' module contains different global constants depending on
+ the specific platform (OS). This function will return the list of
+ numeric values for a given list of potential names.
+ """
+ errno_names = dir(errno)
+ nums = [getattr(errno, k) for k in errnames if k in errno_names]
+ # de-dupe the list
+ return dict.fromkeys(nums).keys()
+
+socket_error_eintr = plat_specific_errors("EINTR", "WSAEINTR")
+
+socket_errors_to_ignore = plat_specific_errors(
+ "EPIPE",
+ "EBADF", "WSAEBADF",
+ "ENOTSOCK", "WSAENOTSOCK",
+ "ETIMEDOUT", "WSAETIMEDOUT",
+ "ECONNREFUSED", "WSAECONNREFUSED",
+ "ECONNRESET", "WSAECONNRESET",
+ "ECONNABORTED", "WSAECONNABORTED",
+ "ENETRESET", "WSAENETRESET",
+ "EHOSTDOWN", "EHOSTUNREACH",
+ )
+socket_errors_to_ignore.append("timed out")
+socket_errors_to_ignore.append("The read operation timed out")
+
+socket_errors_nonblocking = plat_specific_errors(
+ 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK')
+
+comma_separated_headers = ['Accept', 'Accept-Charset', 'Accept-Encoding',
+ 'Accept-Language', 'Accept-Ranges', 'Allow', 'Cache-Control',
+ 'Connection', 'Content-Encoding', 'Content-Language', 'Expect',
+ 'If-Match', 'If-None-Match', 'Pragma', 'Proxy-Authenticate', 'TE',
+ 'Trailer', 'Transfer-Encoding', 'Upgrade', 'Vary', 'Via', 'Warning',
+ 'WWW-Authenticate']
+
+
+import logging
+if not hasattr(logging, 'statistics'): logging.statistics = {}
+
+
+def read_headers(rfile, hdict=None):
+ """Read headers from the given stream into the given header dict.
+
+ If hdict is None, a new header dict is created. Returns the populated
+ header dict.
+
+ Headers which are repeated are folded together using a comma if their
+ specification so dictates.
+
+ This function raises ValueError when the read bytes violate the HTTP spec.
+ You should probably return "400 Bad Request" if this happens.
+ """
+ if hdict is None:
+ hdict = {}
+
+ while True:
+ line = rfile.readline()
+ if not line:
+ # No more data--illegal end of headers
+ raise ValueError("Illegal end of headers.")
+
+ if line == CRLF:
+ # Normal end of headers
+ break
+ if not line.endswith(CRLF):
+ raise ValueError("HTTP requires CRLF terminators")
+
+ if line[0] in ' \t':
+ # It's a continuation line.
+ v = line.strip()
+ else:
+ try:
+ k, v = line.split(":", 1)
+ except ValueError:
+ raise ValueError("Illegal header line.")
+ # TODO: what about TE and WWW-Authenticate?
+ k = k.strip().title()
+ v = v.strip()
+ hname = k
+
+ if k in comma_separated_headers:
+ existing = hdict.get(hname)
+ if existing:
+ v = ", ".join((existing, v))
+ hdict[hname] = v
+
+ return hdict
+
+
+class MaxSizeExceeded(Exception):
+ pass
+
+class SizeCheckWrapper(object):
+ """Wraps a file-like object, raising MaxSizeExceeded if too large."""
+
+ def __init__(self, rfile, maxlen):
+ self.rfile = rfile
+ self.maxlen = maxlen
+ self.bytes_read = 0
+
+ def _check_length(self):
+ if self.maxlen and self.bytes_read > self.maxlen:
+ raise MaxSizeExceeded()
+
+ def read(self, size=None):
+ data = self.rfile.read(size)
+ self.bytes_read += len(data)
+ self._check_length()
+ return data
+
+ def readline(self, size=None):
+ if size is not None:
+ data = self.rfile.readline(size)
+ self.bytes_read += len(data)
+ self._check_length()
+ return data
+
+ # User didn't specify a size ...
+ # We read the line in chunks to make sure it's not a 100MB line !
+ res = []
+ while True:
+ data = self.rfile.readline(256)
+ self.bytes_read += len(data)
+ self._check_length()
+ res.append(data)
+ # See http://www.cherrypy.org/ticket/421
+ if len(data) < 256 or data[-1:] == "\n":
+ return ''.join(res)
+
+ def readlines(self, sizehint=0):
+ # Shamelessly stolen from StringIO
+ total = 0
+ lines = []
+ line = self.readline()
+ while line:
+ lines.append(line)
+ total += len(line)
+ if 0 < sizehint <= total:
+ break
+ line = self.readline()
+ return lines
+
+ def close(self):
+ self.rfile.close()
+
+ def __iter__(self):
+ return self
+
+ def next(self):
+ data = self.rfile.next()
+ self.bytes_read += len(data)
+ self._check_length()
+ return data
+
+
+class KnownLengthRFile(object):
+ """Wraps a file-like object, returning an empty string when exhausted."""
+
+ def __init__(self, rfile, content_length):
+ self.rfile = rfile
+ self.remaining = content_length
+
+ def read(self, size=None):
+ if self.remaining == 0:
+ return ''
+ if size is None:
+ size = self.remaining
+ else:
+ size = min(size, self.remaining)
+
+ data = self.rfile.read(size)
+ self.remaining -= len(data)
+ return data
+
+ def readline(self, size=None):
+ if self.remaining == 0:
+ return ''
+ if size is None:
+ size = self.remaining
+ else:
+ size = min(size, self.remaining)
+
+ data = self.rfile.readline(size)
+ self.remaining -= len(data)
+ return data
+
+ def readlines(self, sizehint=0):
+ # Shamelessly stolen from StringIO
+ total = 0
+ lines = []
+ line = self.readline(sizehint)
+ while line:
+ lines.append(line)
+ total += len(line)
+ if 0 < sizehint <= total:
+ break
+ line = self.readline(sizehint)
+ return lines
+
+ def close(self):
+ self.rfile.close()
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ data = next(self.rfile)
+ self.remaining -= len(data)
+ return data
+
+
+class ChunkedRFile(object):
+ """Wraps a file-like object, returning an empty string when exhausted.
+
+ This class is intended to provide a conforming wsgi.input value for
+ request entities that have been encoded with the 'chunked' transfer
+ encoding.
+ """
+
+ def __init__(self, rfile, maxlen, bufsize=8192):
+ self.rfile = rfile
+ self.maxlen = maxlen
+ self.bytes_read = 0
+ self.buffer = ''
+ self.bufsize = bufsize
+ self.closed = False
+
+ def _fetch(self):
+ if self.closed:
+ return
+
+ line = self.rfile.readline()
+ self.bytes_read += len(line)
+
+ if self.maxlen and self.bytes_read > self.maxlen:
+ raise MaxSizeExceeded("Request Entity Too Large", self.maxlen)
+
+ line = line.strip().split(";", 1)
+
+ try:
+ chunk_size = line.pop(0)
+ chunk_size = int(chunk_size, 16)
+ except ValueError:
+ raise ValueError("Bad chunked transfer size: " + repr(chunk_size))
+
+ if chunk_size <= 0:
+ self.closed = True
+ return
+
+## if line: chunk_extension = line[0]
+
+ if self.maxlen and self.bytes_read + chunk_size > self.maxlen:
+ raise IOError("Request Entity Too Large")
+
+ chunk = self.rfile.read(chunk_size)
+ self.bytes_read += len(chunk)
+ self.buffer += chunk
+
+ crlf = self.rfile.read(2)
+ if crlf != CRLF:
+ raise ValueError(
+ "Bad chunked transfer coding (expected '\\r\\n', "
+ "got " + repr(crlf) + ")")
+
+ def read(self, size=None):
+ data = ''
+ while True:
+ if size and len(data) >= size:
+ return data
+
+ if not self.buffer:
+ self._fetch()
+ if not self.buffer:
+ # EOF
+ return data
+
+ if size:
+ remaining = size - len(data)
+ data += self.buffer[:remaining]
+ self.buffer = self.buffer[remaining:]
+ else:
+ data += self.buffer
+
+ def readline(self, size=None):
+ data = ''
+ while True:
+ if size and len(data) >= size:
+ return data
+
+ if not self.buffer:
+ self._fetch()
+ if not self.buffer:
+ # EOF
+ return data
+
+ newline_pos = self.buffer.find('\n')
+ if size:
+ if newline_pos == -1:
+ remaining = size - len(data)
+ data += self.buffer[:remaining]
+ self.buffer = self.buffer[remaining:]
+ else:
+ remaining = min(size - len(data), newline_pos)
+ data += self.buffer[:remaining]
+ self.buffer = self.buffer[remaining:]
+ else:
+ if newline_pos == -1:
+ data += self.buffer
+ else:
+ data += self.buffer[:newline_pos]
+ self.buffer = self.buffer[newline_pos:]
+
+ def readlines(self, sizehint=0):
+ # Shamelessly stolen from StringIO
+ total = 0
+ lines = []
+ line = self.readline(sizehint)
+ while line:
+ lines.append(line)
+ total += len(line)
+ if 0 < sizehint <= total:
+ break
+ line = self.readline(sizehint)
+ return lines
+
+ def read_trailer_lines(self):
+ if not self.closed:
+ raise ValueError(
+ "Cannot read trailers until the request body has been read.")
+
+ while True:
+ line = self.rfile.readline()
+ if not line:
+ # No more data--illegal end of headers
+ raise ValueError("Illegal end of headers.")
+
+ self.bytes_read += len(line)
+ if self.maxlen and self.bytes_read > self.maxlen:
+ raise IOError("Request Entity Too Large")
+
+ if line == CRLF:
+ # Normal end of headers
+ break
+ if not line.endswith(CRLF):
+ raise ValueError("HTTP requires CRLF terminators")
+
+ yield line
+
+ def close(self):
+ self.rfile.close()
+
+ def __iter__(self):
+ # Shamelessly stolen from StringIO
+ total = 0
+ line = self.readline(sizehint)
+ while line:
+ yield line
+ total += len(line)
+ if 0 < sizehint <= total:
+ break
+ line = self.readline(sizehint)
+
+
+class HTTPRequest(object):
+ """An HTTP Request (and response).
+
+ A single HTTP connection may consist of multiple request/response pairs.
+ """
+
+ server = None
+ """The HTTPServer object which is receiving this request."""
+
+ conn = None
+ """The HTTPConnection object on which this request connected."""
+
+ inheaders = {}
+ """A dict of request headers."""
+
+ outheaders = []
+ """A list of header tuples to write in the response."""
+
+ ready = False
+ """When True, the request has been parsed and is ready to begin generating
+ the response. When False, signals the calling Connection that the response
+ should not be generated and the connection should close."""
+
+ close_connection = False
+ """Signals the calling Connection that the request should close. This does
+ not imply an error! The client and/or server may each request that the
+ connection be closed."""
+
+ chunked_write = False
+ """If True, output will be encoded with the "chunked" transfer-coding.
+
+ This value is set automatically inside send_headers."""
+
+ def __init__(self, server, conn):
+ self.server= server
+ self.conn = conn
+
+ self.ready = False
+ self.started_request = False
+ self.scheme = "http"
+ if self.server.ssl_adapter is not None:
+ self.scheme = "https"
+ # Use the lowest-common protocol in case read_request_line errors.
+ self.response_protocol = 'HTTP/1.0'
+ self.inheaders = {}
+
+ self.status = ""
+ self.outheaders = []
+ self.sent_headers = False
+ self.close_connection = self.__class__.close_connection
+ self.chunked_read = False
+ self.chunked_write = self.__class__.chunked_write
+
+ def parse_request(self):
+ """Parse the next HTTP request start-line and message-headers."""
+ self.rfile = SizeCheckWrapper(self.conn.rfile,
+ self.server.max_request_header_size)
+ try:
+ self.read_request_line()
+ except MaxSizeExceeded:
+ self.simple_response("414 Request-URI Too Long",
+ "The Request-URI sent with the request exceeds the maximum "
+ "allowed bytes.")
+ return
+
+ try:
+ success = self.read_request_headers()
+ except MaxSizeExceeded:
+ self.simple_response("413 Request Entity Too Large",
+ "The headers sent with the request exceed the maximum "
+ "allowed bytes.")
+ return
+ else:
+ if not success:
+ return
+
+ self.ready = True
+
+ def read_request_line(self):
+ # HTTP/1.1 connections are persistent by default. If a client
+ # requests a page, then idles (leaves the connection open),
+ # then rfile.readline() will raise socket.error("timed out").
+ # Note that it does this based on the value given to settimeout(),
+ # and doesn't need the client to request or acknowledge the close
+ # (although your TCP stack might suffer for it: cf Apache's history
+ # with FIN_WAIT_2).
+ request_line = self.rfile.readline()
+
+ # Set started_request to True so communicate() knows to send 408
+ # from here on out.
+ self.started_request = True
+ if not request_line:
+ # Force self.ready = False so the connection will close.
+ self.ready = False
+ return
+
+ if request_line == CRLF:
+ # RFC 2616 sec 4.1: "...if the server is reading the protocol
+ # stream at the beginning of a message and receives a CRLF
+ # first, it should ignore the CRLF."
+ # But only ignore one leading line! else we enable a DoS.
+ request_line = self.rfile.readline()
+ if not request_line:
+ self.ready = False
+ return
+
+ if not request_line.endswith(CRLF):
+ self.simple_response("400 Bad Request", "HTTP requires CRLF terminators")
+ return
+
+ try:
+ method, uri, req_protocol = request_line.strip().split(" ", 2)
+ rp = int(req_protocol[5]), int(req_protocol[7])
+ except (ValueError, IndexError):
+ self.simple_response("400 Bad Request", "Malformed Request-Line")
+ return
+
+ self.uri = uri
+ self.method = method
+
+ # uri may be an abs_path (including "http://host.domain.tld");
+ scheme, authority, path = self.parse_request_uri(uri)
+ if '#' in path:
+ self.simple_response("400 Bad Request",
+ "Illegal #fragment in Request-URI.")
+ return
+
+ if scheme:
+ self.scheme = scheme
+
+ qs = ''
+ if '?' in path:
+ path, qs = path.split('?', 1)
+
+ # Unquote the path+params (e.g. "/this%20path" -> "/this path").
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
+ #
+ # But note that "...a URI must be separated into its components
+ # before the escaped characters within those components can be
+ # safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2
+ # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not "/this/path".
+ try:
+ atoms = [unquote(x) for x in quoted_slash.split(path)]
+ except ValueError, ex:
+ self.simple_response("400 Bad Request", ex.args[0])
+ return
+ path = "%2F".join(atoms)
+ self.path = path
+
+ # Note that, like wsgiref and most other HTTP servers,
+ # we "% HEX HEX"-unquote the path but not the query string.
+ self.qs = qs
+
+ # Compare request and server HTTP protocol versions, in case our
+ # server does not support the requested protocol. Limit our output
+ # to min(req, server). We want the following output:
+ # request server actual written supported response
+ # protocol protocol response protocol feature set
+ # a 1.0 1.0 1.0 1.0
+ # b 1.0 1.1 1.1 1.0
+ # c 1.1 1.0 1.0 1.0
+ # d 1.1 1.1 1.1 1.1
+ # Notice that, in (b), the response will be "HTTP/1.1" even though
+ # the client only understands 1.0. RFC 2616 10.5.6 says we should
+ # only return 505 if the _major_ version is different.
+ sp = int(self.server.protocol[5]), int(self.server.protocol[7])
+
+ if sp[0] != rp[0]:
+ self.simple_response("505 HTTP Version Not Supported")
+ return
+ self.request_protocol = req_protocol
+ self.response_protocol = "HTTP/%s.%s" % min(rp, sp)
+
+ def read_request_headers(self):
+ """Read self.rfile into self.inheaders. Return success."""
+
+ # then all the http headers
+ try:
+ read_headers(self.rfile, self.inheaders)
+ except ValueError, ex:
+ self.simple_response("400 Bad Request", ex.args[0])
+ return False
+
+ mrbs = self.server.max_request_body_size
+ if mrbs and int(self.inheaders.get("Content-Length", 0)) > mrbs:
+ self.simple_response("413 Request Entity Too Large",
+ "The entity sent with the request exceeds the maximum "
+ "allowed bytes.")
+ return False
+
+ # Persistent connection support
+ if self.response_protocol == "HTTP/1.1":
+ # Both server and client are HTTP/1.1
+ if self.inheaders.get("Connection", "") == "close":
+ self.close_connection = True
+ else:
+ # Either the server or client (or both) are HTTP/1.0
+ if self.inheaders.get("Connection", "") != "Keep-Alive":
+ self.close_connection = True
+
+ # Transfer-Encoding support
+ te = None
+ if self.response_protocol == "HTTP/1.1":
+ te = self.inheaders.get("Transfer-Encoding")
+ if te:
+ te = [x.strip().lower() for x in te.split(",") if x.strip()]
+
+ self.chunked_read = False
+
+ if te:
+ for enc in te:
+ if enc == "chunked":
+ self.chunked_read = True
+ else:
+ # Note that, even if we see "chunked", we must reject
+ # if there is an extension we don't recognize.
+ self.simple_response("501 Unimplemented")
+ self.close_connection = True
+ return False
+
+ # From PEP 333:
+ # "Servers and gateways that implement HTTP 1.1 must provide
+ # transparent support for HTTP 1.1's "expect/continue" mechanism.
+ # This may be done in any of several ways:
+ # 1. Respond to requests containing an Expect: 100-continue request
+ # with an immediate "100 Continue" response, and proceed normally.
+ # 2. Proceed with the request normally, but provide the application
+ # with a wsgi.input stream that will send the "100 Continue"
+ # response if/when the application first attempts to read from
+ # the input stream. The read request must then remain blocked
+ # until the client responds.
+ # 3. Wait until the client decides that the server does not support
+ # expect/continue, and sends the request body on its own.
+ # (This is suboptimal, and is not recommended.)
+ #
+ # We used to do 3, but are now doing 1. Maybe we'll do 2 someday,
+ # but it seems like it would be a big slowdown for such a rare case.
+ if self.inheaders.get("Expect", "") == "100-continue":
+ # Don't use simple_response here, because it emits headers
+ # we don't want. See http://www.cherrypy.org/ticket/951
+ msg = self.server.protocol + " 100 Continue\r\n\r\n"
+ try:
+ self.conn.wfile.sendall(msg)
+ except socket.error, x:
+ if x.args[0] not in socket_errors_to_ignore:
+ raise
+ return True
+
+ def parse_request_uri(self, uri):
+ """Parse a Request-URI into (scheme, authority, path).
+
+ Note that Request-URI's must be one of::
+
+ Request-URI = "*" | absoluteURI | abs_path | authority
+
+ Therefore, a Request-URI which starts with a double forward-slash
+ cannot be a "net_path"::
+
+ net_path = "//" authority [ abs_path ]
+
+ Instead, it must be interpreted as an "abs_path" with an empty first
+ path segment::
+
+ abs_path = "/" path_segments
+ path_segments = segment *( "/" segment )
+ segment = *pchar *( ";" param )
+ param = *pchar
+ """
+ if uri == "*":
+ return None, None, uri
+
+ i = uri.find('://')
+ if i > 0 and '?' not in uri[:i]:
+ # An absoluteURI.
+ # If there's a scheme (and it must be http or https), then:
+ # http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]]
+ scheme, remainder = uri[:i].lower(), uri[i + 3:]
+ authority, path = remainder.split("/", 1)
+ return scheme, authority, path
+
+ if uri.startswith('/'):
+ # An abs_path.
+ return None, None, uri
+ else:
+ # An authority.
+ return None, uri, None
+
+ def respond(self):
+ """Call the gateway and write its iterable output."""
+ mrbs = self.server.max_request_body_size
+ if self.chunked_read:
+ self.rfile = ChunkedRFile(self.conn.rfile, mrbs)
+ else:
+ cl = int(self.inheaders.get("Content-Length", 0))
+ if mrbs and mrbs < cl:
+ if not self.sent_headers:
+ self.simple_response("413 Request Entity Too Large",
+ "The entity sent with the request exceeds the maximum "
+ "allowed bytes.")
+ return
+ self.rfile = KnownLengthRFile(self.conn.rfile, cl)
+
+ self.server.gateway(self).respond()
+
+ if (self.ready and not self.sent_headers):
+ self.sent_headers = True
+ self.send_headers()
+ if self.chunked_write:
+ self.conn.wfile.sendall("0\r\n\r\n")
+
+ def simple_response(self, status, msg=""):
+ """Write a simple response back to the client."""
+ status = str(status)
+ buf = [self.server.protocol + " " +
+ status + CRLF,
+ "Content-Length: %s\r\n" % len(msg),
+ "Content-Type: text/plain\r\n"]
+
+ if status[:3] in ("413", "414"):
+ # Request Entity Too Large / Request-URI Too Long
+ self.close_connection = True
+ if self.response_protocol == 'HTTP/1.1':
+ # This will not be true for 414, since read_request_line
+ # usually raises 414 before reading the whole line, and we
+ # therefore cannot know the proper response_protocol.
+ buf.append("Connection: close\r\n")
+ else:
+ # HTTP/1.0 had no 413/414 status nor Connection header.
+ # Emit 400 instead and trust the message body is enough.
+ status = "400 Bad Request"
+
+ buf.append(CRLF)
+ if msg:
+ if isinstance(msg, unicode):
+ msg = msg.encode("ISO-8859-1")
+ buf.append(msg)
+
+ try:
+ self.conn.wfile.sendall("".join(buf))
+ except socket.error, x:
+ if x.args[0] not in socket_errors_to_ignore:
+ raise
+
+ def write(self, chunk):
+ """Write unbuffered data to the client."""
+ if self.chunked_write and chunk:
+ buf = [hex(len(chunk))[2:], CRLF, chunk, CRLF]
+ self.conn.wfile.sendall("".join(buf))
+ else:
+ self.conn.wfile.sendall(chunk)
+
+ def send_headers(self):
+ """Assert, process, and send the HTTP response message-headers.
+
+ You must set self.status, and self.outheaders before calling this.
+ """
+ hkeys = [key.lower() for key, value in self.outheaders]
+ status = int(self.status[:3])
+
+ if status == 413:
+ # Request Entity Too Large. Close conn to avoid garbage.
+ self.close_connection = True
+ elif "content-length" not in hkeys:
+ # "All 1xx (informational), 204 (no content),
+ # and 304 (not modified) responses MUST NOT
+ # include a message-body." So no point chunking.
+ if status < 200 or status in (204, 205, 304):
+ pass
+ else:
+ if (self.response_protocol == 'HTTP/1.1'
+ and self.method != 'HEAD'):
+ # Use the chunked transfer-coding
+ self.chunked_write = True
+ self.outheaders.append(("Transfer-Encoding", "chunked"))
+ else:
+ # Closing the conn is the only way to determine len.
+ self.close_connection = True
+
+ if "connection" not in hkeys:
+ if self.response_protocol == 'HTTP/1.1':
+ # Both server and client are HTTP/1.1 or better
+ if self.close_connection:
+ self.outheaders.append(("Connection", "close"))
+ else:
+ # Server and/or client are HTTP/1.0
+ if not self.close_connection:
+ self.outheaders.append(("Connection", "Keep-Alive"))
+
+ if (not self.close_connection) and (not self.chunked_read):
+ # Read any remaining request body data on the socket.
+ # "If an origin server receives a request that does not include an
+ # Expect request-header field with the "100-continue" expectation,
+ # the request includes a request body, and the server responds
+ # with a final status code before reading the entire request body
+ # from the transport connection, then the server SHOULD NOT close
+ # the transport connection until it has read the entire request,
+ # or until the client closes the connection. Otherwise, the client
+ # might not reliably receive the response message. However, this
+ # requirement is not be construed as preventing a server from
+ # defending itself against denial-of-service attacks, or from
+ # badly broken client implementations."
+ remaining = getattr(self.rfile, 'remaining', 0)
+ if remaining > 0:
+ self.rfile.read(remaining)
+
+ if "date" not in hkeys:
+ self.outheaders.append(("Date", rfc822.formatdate()))
+
+ if "server" not in hkeys:
+ self.outheaders.append(("Server", self.server.server_name))
+
+ buf = [self.server.protocol + " " + self.status + CRLF]
+ for k, v in self.outheaders:
+ buf.append(k + ": " + v + CRLF)
+ buf.append(CRLF)
+ self.conn.wfile.sendall("".join(buf))
+
+
+class NoSSLError(Exception):
+ """Exception raised when a client speaks HTTP to an HTTPS socket."""
+ pass
+
+
+class FatalSSLAlert(Exception):
+ """Exception raised when the SSL implementation signals a fatal alert."""
+ pass
+
+
+class CP_fileobject(socket._fileobject):
+ """Faux file object attached to a socket object."""
+
+ def __init__(self, *args, **kwargs):
+ self.bytes_read = 0
+ self.bytes_written = 0
+ socket._fileobject.__init__(self, *args, **kwargs)
+
+ def sendall(self, data):
+ """Sendall for non-blocking sockets."""
+ while data:
+ try:
+ bytes_sent = self.send(data)
+ data = data[bytes_sent:]
+ except socket.error, e:
+ if e.args[0] not in socket_errors_nonblocking:
+ raise
+
+ def send(self, data):
+ bytes_sent = self._sock.send(data)
+ self.bytes_written += bytes_sent
+ return bytes_sent
+
+ def flush(self):
+ if self._wbuf:
+ buffer = "".join(self._wbuf)
+ self._wbuf = []
+ self.sendall(buffer)
+
+ def recv(self, size):
+ while True:
+ try:
+ data = self._sock.recv(size)
+ self.bytes_read += len(data)
+ return data
+ except socket.error, e:
+ if (e.args[0] not in socket_errors_nonblocking
+ and e.args[0] not in socket_error_eintr):
+ raise
+
+ if not _fileobject_uses_str_type:
+ def read(self, size=-1):
+ # Use max, disallow tiny reads in a loop as they are very inefficient.
+ # We never leave read() with any leftover data from a new recv() call
+ # in our internal buffer.
+ rbufsize = max(self._rbufsize, self.default_bufsize)
+ # Our use of StringIO rather than lists of string objects returned by
+ # recv() minimizes memory usage and fragmentation that occurs when
+ # rbufsize is large compared to the typical return value of recv().
+ buf = self._rbuf
+ buf.seek(0, 2) # seek end
+ if size < 0:
+ # Read until EOF
+ self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
+ while True:
+ data = self.recv(rbufsize)
+ if not data:
+ break
+ buf.write(data)
+ return buf.getvalue()
+ else:
+ # Read until size bytes or EOF seen, whichever comes first
+ buf_len = buf.tell()
+ if buf_len >= size:
+ # Already have size bytes in our buffer? Extract and return.
+ buf.seek(0)
+ rv = buf.read(size)
+ self._rbuf = StringIO.StringIO()
+ self._rbuf.write(buf.read())
+ return rv
+
+ self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
+ while True:
+ left = size - buf_len
+ # recv() will malloc the amount of memory given as its
+ # parameter even though it often returns much less data
+ # than that. The returned data string is short lived
+ # as we copy it into a StringIO and free it. This avoids
+ # fragmentation issues on many platforms.
+ data = self.recv(left)
+ if not data:
+ break
+ n = len(data)
+ if n == size and not buf_len:
+ # Shortcut. Avoid buffer data copies when:
+ # - We have no data in our buffer.
+ # AND
+ # - Our call to recv returned exactly the
+ # number of bytes we were asked to read.
+ return data
+ if n == left:
+ buf.write(data)
+ del data # explicit free
+ break
+ assert n <= left, "recv(%d) returned %d bytes" % (left, n)
+ buf.write(data)
+ buf_len += n
+ del data # explicit free
+ #assert buf_len == buf.tell()
+ return buf.getvalue()
+
+ def readline(self, size=-1):
+ buf = self._rbuf
+ buf.seek(0, 2) # seek end
+ if buf.tell() > 0:
+ # check if we already have it in our buffer
+ buf.seek(0)
+ bline = buf.readline(size)
+ if bline.endswith('\n') or len(bline) == size:
+ self._rbuf = StringIO.StringIO()
+ self._rbuf.write(buf.read())
+ return bline
+ del bline
+ if size < 0:
+ # Read until \n or EOF, whichever comes first
+ if self._rbufsize <= 1:
+ # Speed up unbuffered case
+ buf.seek(0)
+ buffers = [buf.read()]
+ self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
+ data = None
+ recv = self.recv
+ while data != "\n":
+ data = recv(1)
+ if not data:
+ break
+ buffers.append(data)
+ return "".join(buffers)
+
+ buf.seek(0, 2) # seek end
+ self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
+ while True:
+ data = self.recv(self._rbufsize)
+ if not data:
+ break
+ nl = data.find('\n')
+ if nl >= 0:
+ nl += 1
+ buf.write(data[:nl])
+ self._rbuf.write(data[nl:])
+ del data
+ break
+ buf.write(data)
+ return buf.getvalue()
+ else:
+ # Read until size bytes or \n or EOF seen, whichever comes first
+ buf.seek(0, 2) # seek end
+ buf_len = buf.tell()
+ if buf_len >= size:
+ buf.seek(0)
+ rv = buf.read(size)
+ self._rbuf = StringIO.StringIO()
+ self._rbuf.write(buf.read())
+ return rv
+ self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf.
+ while True:
+ data = self.recv(self._rbufsize)
+ if not data:
+ break
+ left = size - buf_len
+ # did we just receive a newline?
+ nl = data.find('\n', 0, left)
+ if nl >= 0:
+ nl += 1
+ # save the excess data to _rbuf
+ self._rbuf.write(data[nl:])
+ if buf_len:
+ buf.write(data[:nl])
+ break
+ else:
+ # Shortcut. Avoid data copy through buf when returning
+ # a substring of our first recv().
+ return data[:nl]
+ n = len(data)
+ if n == size and not buf_len:
+ # Shortcut. Avoid data copy through buf when
+ # returning exactly all of our first recv().
+ return data
+ if n >= left:
+ buf.write(data[:left])
+ self._rbuf.write(data[left:])
+ break
+ buf.write(data)
+ buf_len += n
+ #assert buf_len == buf.tell()
+ return buf.getvalue()
+ else:
+ def read(self, size=-1):
+ if size < 0:
+ # Read until EOF
+ buffers = [self._rbuf]
+ self._rbuf = ""
+ if self._rbufsize <= 1:
+ recv_size = self.default_bufsize
+ else:
+ recv_size = self._rbufsize
+
+ while True:
+ data = self.recv(recv_size)
+ if not data:
+ break
+ buffers.append(data)
+ return "".join(buffers)
+ else:
+ # Read until size bytes or EOF seen, whichever comes first
+ data = self._rbuf
+ buf_len = len(data)
+ if buf_len >= size:
+ self._rbuf = data[size:]
+ return data[:size]
+ buffers = []
+ if data:
+ buffers.append(data)
+ self._rbuf = ""
+ while True:
+ left = size - buf_len
+ recv_size = max(self._rbufsize, left)
+ data = self.recv(recv_size)
+ if not data:
+ break
+ buffers.append(data)
+ n = len(data)
+ if n >= left:
+ self._rbuf = data[left:]
+ buffers[-1] = data[:left]
+ break
+ buf_len += n
+ return "".join(buffers)
+
+ def readline(self, size=-1):
+ data = self._rbuf
+ if size < 0:
+ # Read until \n or EOF, whichever comes first
+ if self._rbufsize <= 1:
+ # Speed up unbuffered case
+ assert data == ""
+ buffers = []
+ while data != "\n":
+ data = self.recv(1)
+ if not data:
+ break
+ buffers.append(data)
+ return "".join(buffers)
+ nl = data.find('\n')
+ if nl >= 0:
+ nl += 1
+ self._rbuf = data[nl:]
+ return data[:nl]
+ buffers = []
+ if data:
+ buffers.append(data)
+ self._rbuf = ""
+ while True:
+ data = self.recv(self._rbufsize)
+ if not data:
+ break
+ buffers.append(data)
+ nl = data.find('\n')
+ if nl >= 0:
+ nl += 1
+ self._rbuf = data[nl:]
+ buffers[-1] = data[:nl]
+ break
+ return "".join(buffers)
+ else:
+ # Read until size bytes or \n or EOF seen, whichever comes first
+ nl = data.find('\n', 0, size)
+ if nl >= 0:
+ nl += 1
+ self._rbuf = data[nl:]
+ return data[:nl]
+ buf_len = len(data)
+ if buf_len >= size:
+ self._rbuf = data[size:]
+ return data[:size]
+ buffers = []
+ if data:
+ buffers.append(data)
+ self._rbuf = ""
+ while True:
+ data = self.recv(self._rbufsize)
+ if not data:
+ break
+ buffers.append(data)
+ left = size - buf_len
+ nl = data.find('\n', 0, left)
+ if nl >= 0:
+ nl += 1
+ self._rbuf = data[nl:]
+ buffers[-1] = data[:nl]
+ break
+ n = len(data)
+ if n >= left:
+ self._rbuf = data[left:]
+ buffers[-1] = data[:left]
+ break
+ buf_len += n
+ return "".join(buffers)
+
+
+class HTTPConnection(object):
+ """An HTTP connection (active socket).
+
+ server: the Server object which received this connection.
+ socket: the raw socket object (usually TCP) for this connection.
+ makefile: a fileobject class for reading from the socket.
+ """
+
+ remote_addr = None
+ remote_port = None
+ ssl_env = None
+ rbufsize = DEFAULT_BUFFER_SIZE
+ wbufsize = DEFAULT_BUFFER_SIZE
+ RequestHandlerClass = HTTPRequest
+
+ def __init__(self, server, sock, makefile=CP_fileobject):
+ self.server = server
+ self.socket = sock
+ self.rfile = makefile(sock, "rb", self.rbufsize)
+ self.wfile = makefile(sock, "wb", self.wbufsize)
+ self.requests_seen = 0
+
+ def communicate(self):
+ """Read each request and respond appropriately."""
+ request_seen = False
+ try:
+ while True:
+ # (re)set req to None so that if something goes wrong in
+ # the RequestHandlerClass constructor, the error doesn't
+ # get written to the previous request.
+ req = None
+ req = self.RequestHandlerClass(self.server, self)
+
+ # This order of operations should guarantee correct pipelining.
+ req.parse_request()
+ if self.server.stats['Enabled']:
+ self.requests_seen += 1
+ if not req.ready:
+ # Something went wrong in the parsing (and the server has
+ # probably already made a simple_response). Return and
+ # let the conn close.
+ return
+
+ request_seen = True
+ req.respond()
+ if req.close_connection:
+ return
+ except socket.error, e:
+ errnum = e.args[0]
+ # sadly SSL sockets return a different (longer) time out string
+ if errnum == 'timed out' or errnum == 'The read operation timed out':
+ # Don't error if we're between requests; only error
+ # if 1) no request has been started at all, or 2) we're
+ # in the middle of a request.
+ # See http://www.cherrypy.org/ticket/853
+ if (not request_seen) or (req and req.started_request):
+ # Don't bother writing the 408 if the response
+ # has already started being written.
+ if req and not req.sent_headers:
+ try:
+ req.simple_response("408 Request Timeout")
+ except FatalSSLAlert:
+ # Close the connection.
+ return
+ elif errnum not in socket_errors_to_ignore:
+ if req and not req.sent_headers:
+ try:
+ req.simple_response("500 Internal Server Error",
+ format_exc())
+ except FatalSSLAlert:
+ # Close the connection.
+ return
+ return
+ except (KeyboardInterrupt, SystemExit):
+ raise
+ except FatalSSLAlert:
+ # Close the connection.
+ return
+ except NoSSLError:
+ if req and not req.sent_headers:
+ # Unwrap our wfile
+ self.wfile = CP_fileobject(self.socket._sock, "wb", self.wbufsize)
+ req.simple_response("400 Bad Request",
+ "The client sent a plain HTTP request, but "
+ "this server only speaks HTTPS on this port.")
+ self.linger = True
+ except Exception:
+ if req and not req.sent_headers:
+ try:
+ req.simple_response("500 Internal Server Error", format_exc())
+ except FatalSSLAlert:
+ # Close the connection.
+ return
+
+ linger = False
+
+ def close(self):
+ """Close the socket underlying this connection."""
+ self.rfile.close()
+
+ if not self.linger:
+ # Python's socket module does NOT call close on the kernel socket
+ # when you call socket.close(). We do so manually here because we
+ # want this server to send a FIN TCP segment immediately. Note this
+ # must be called *before* calling socket.close(), because the latter
+ # drops its reference to the kernel socket.
+ if hasattr(self.socket, '_sock'):
+ self.socket._sock.close()
+ self.socket.close()
+ else:
+ # On the other hand, sometimes we want to hang around for a bit
+ # to make sure the client has a chance to read our entire
+ # response. Skipping the close() calls here delays the FIN
+ # packet until the socket object is garbage-collected later.
+ # Someday, perhaps, we'll do the full lingering_close that
+ # Apache does, but not today.
+ pass
+
+
+_SHUTDOWNREQUEST = None
+
+class WorkerThread(threading.Thread):
+ """Thread which continuously polls a Queue for Connection objects.
+
+ Due to the timing issues of polling a Queue, a WorkerThread does not
+ check its own 'ready' flag after it has started. To stop the thread,
+ it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue
+ (one for each running WorkerThread).
+ """
+
+ conn = None
+ """The current connection pulled off the Queue, or None."""
+
+ server = None
+ """The HTTP Server which spawned this thread, and which owns the
+ Queue and is placing active connections into it."""
+
+ ready = False
+ """A simple flag for the calling server to know when this thread
+ has begun polling the Queue."""
+
+
+ def __init__(self, server):
+ self.ready = False
+ self.server = server
+
+ self.requests_seen = 0
+ self.bytes_read = 0
+ self.bytes_written = 0
+ self.start_time = None
+ self.work_time = 0
+ self.stats = {
+ 'Requests': lambda s: self.requests_seen + ((self.start_time is None) and 0 or self.conn.requests_seen),
+ 'Bytes Read': lambda s: self.bytes_read + ((self.start_time is None) and 0 or self.conn.rfile.bytes_read),
+ 'Bytes Written': lambda s: self.bytes_written + ((self.start_time is None) and 0 or self.conn.wfile.bytes_written),
+ 'Work Time': lambda s: self.work_time + ((self.start_time is None) and 0 or time.time() - self.start_time),
+ 'Read Throughput': lambda s: s['Bytes Read'](s) / (s['Work Time'](s) or 1e-6),
+ 'Write Throughput': lambda s: s['Bytes Written'](s) / (s['Work Time'](s) or 1e-6),
+ }
+ threading.Thread.__init__(self)
+
+ def run(self):
+ self.server.stats['Worker Threads'][self.getName()] = self.stats
+ try:
+ self.ready = True
+ while True:
+ conn = self.server.requests.get()
+ if conn is _SHUTDOWNREQUEST:
+ return
+
+ self.conn = conn
+ if self.server.stats['Enabled']:
+ self.start_time = time.time()
+ try:
+ conn.communicate()
+ finally:
+ conn.close()
+ if self.server.stats['Enabled']:
+ self.requests_seen += self.conn.requests_seen
+ self.bytes_read += self.conn.rfile.bytes_read
+ self.bytes_written += self.conn.wfile.bytes_written
+ self.work_time += time.time() - self.start_time
+ self.start_time = None
+ self.conn = None
+ except (KeyboardInterrupt, SystemExit), exc:
+ self.server.interrupt = exc
+
+
+class ThreadPool(object):
+ """A Request Queue for the CherryPyWSGIServer which pools threads.
+
+ ThreadPool objects must provide min, get(), put(obj), start()
+ and stop(timeout) attributes.
+ """
+
+ def __init__(self, server, min=10, max=-1):
+ self.server = server
+ self.min = min
+ self.max = max
+ self._threads = []
+ self._queue = Queue.Queue()
+ self.get = self._queue.get
+
+ def start(self):
+ """Start the pool of threads."""
+ for i in range(self.min):
+ self._threads.append(WorkerThread(self.server))
+ for worker in self._threads:
+ worker.setName("CP Server " + worker.getName())
+ worker.start()
+ for worker in self._threads:
+ while not worker.ready:
+ time.sleep(.1)
+
+ def _get_idle(self):
+ """Number of worker threads which are idle. Read-only."""
+ return len([t for t in self._threads if t.conn is None])
+ idle = property(_get_idle, doc=_get_idle.__doc__)
+
+ def put(self, obj):
+ self._queue.put(obj)
+ if obj is _SHUTDOWNREQUEST:
+ return
+
+ def grow(self, amount):
+ """Spawn new worker threads (not above self.max)."""
+ for i in range(amount):
+ if self.max > 0 and len(self._threads) >= self.max:
+ break
+ worker = WorkerThread(self.server)
+ worker.setName("CP Server " + worker.getName())
+ self._threads.append(worker)
+ worker.start()
+
+ def shrink(self, amount):
+ """Kill off worker threads (not below self.min)."""
+ # Grow/shrink the pool if necessary.
+ # Remove any dead threads from our list
+ for t in self._threads:
+ if not t.isAlive():
+ self._threads.remove(t)
+ amount -= 1
+
+ if amount > 0:
+ for i in range(min(amount, len(self._threads) - self.min)):
+ # Put a number of shutdown requests on the queue equal
+ # to 'amount'. Once each of those is processed by a worker,
+ # that worker will terminate and be culled from our list
+ # in self.put.
+ self._queue.put(_SHUTDOWNREQUEST)
+
+ def stop(self, timeout=5):
+ # Must shut down threads here so the code that calls
+ # this method can know when all threads are stopped.
+ for worker in self._threads:
+ self._queue.put(_SHUTDOWNREQUEST)
+
+ # Don't join currentThread (when stop is called inside a request).
+ current = threading.currentThread()
+ if timeout and timeout >= 0:
+ endtime = time.time() + timeout
+ while self._threads:
+ worker = self._threads.pop()
+ if worker is not current and worker.isAlive():
+ try:
+ if timeout is None or timeout < 0:
+ worker.join()
+ else:
+ remaining_time = endtime - time.time()
+ if remaining_time > 0:
+ worker.join(remaining_time)
+ if worker.isAlive():
+ # We exhausted the timeout.
+ # Forcibly shut down the socket.
+ c = worker.conn
+ if c and not c.rfile.closed:
+ try:
+ c.socket.shutdown(socket.SHUT_RD)
+ except TypeError:
+ # pyOpenSSL sockets don't take an arg
+ c.socket.shutdown()
+ worker.join()
+ except (AssertionError,
+ # Ignore repeated Ctrl-C.
+ # See http://www.cherrypy.org/ticket/691.
+ KeyboardInterrupt), exc1:
+ pass
+
+ def _get_qsize(self):
+ return self._queue.qsize()
+ qsize = property(_get_qsize)
+
+
+
+try:
+ import fcntl
+except ImportError:
+ try:
+ from ctypes import windll, WinError
+ except ImportError:
+ def prevent_socket_inheritance(sock):
+ """Dummy function, since neither fcntl nor ctypes are available."""
+ pass
+ else:
+ def prevent_socket_inheritance(sock):
+ """Mark the given socket fd as non-inheritable (Windows)."""
+ if not windll.kernel32.SetHandleInformation(sock.fileno(), 1, 0):
+ raise WinError()
+else:
+ def prevent_socket_inheritance(sock):
+ """Mark the given socket fd as non-inheritable (POSIX)."""
+ fd = sock.fileno()
+ old_flags = fcntl.fcntl(fd, fcntl.F_GETFD)
+ fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
+
+
+class SSLAdapter(object):
+ """Base class for SSL driver library adapters.
+
+ Required methods:
+
+ * ``wrap(sock) -> (wrapped socket, ssl environ dict)``
+ * ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) -> socket file object``
+ """
+
+ def __init__(self, certificate, private_key, certificate_chain=None):
+ self.certificate = certificate
+ self.private_key = private_key
+ self.certificate_chain = certificate_chain
+
+ def wrap(self, sock):
+ raise NotImplemented
+
+ def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
+ raise NotImplemented
+
+
+class HTTPServer(object):
+ """An HTTP server."""
+
+ _bind_addr = "127.0.0.1"
+ _interrupt = None
+
+ gateway = None
+ """A Gateway instance."""
+
+ minthreads = None
+ """The minimum number of worker threads to create (default 10)."""
+
+ maxthreads = None
+ """The maximum number of worker threads to create (default -1 = no limit)."""
+
+ server_name = None
+ """The name of the server; defaults to socket.gethostname()."""
+
+ protocol = "HTTP/1.1"
+ """The version string to write in the Status-Line of all HTTP responses.
+
+ For example, "HTTP/1.1" is the default. This also limits the supported
+ features used in the response."""
+
+ request_queue_size = 5
+ """The 'backlog' arg to socket.listen(); max queued connections (default 5)."""
+
+ shutdown_timeout = 5
+ """The total time, in seconds, to wait for worker threads to cleanly exit."""
+
+ timeout = 10
+ """The timeout in seconds for accepted connections (default 10)."""
+
+ version = "CherryPy/3.2.0"
+ """A version string for the HTTPServer."""
+
+ software = None
+ """The value to set for the SERVER_SOFTWARE entry in the WSGI environ.
+
+ If None, this defaults to ``'%s Server' % self.version``."""
+
+ ready = False
+ """An internal flag which marks whether the socket is accepting connections."""
+
+ max_request_header_size = 0
+ """The maximum size, in bytes, for request headers, or 0 for no limit."""
+
+ max_request_body_size = 0
+ """The maximum size, in bytes, for request bodies, or 0 for no limit."""
+
+ nodelay = True
+ """If True (the default since 3.1), sets the TCP_NODELAY socket option."""
+
+ ConnectionClass = HTTPConnection
+ """The class to use for handling HTTP connections."""
+
+ ssl_adapter = None
+ """An instance of SSLAdapter (or a subclass).
+
+ You must have the corresponding SSL driver library installed."""
+
+ def __init__(self, bind_addr, gateway, minthreads=10, maxthreads=-1,
+ server_name=None):
+ self.bind_addr = bind_addr
+ self.gateway = gateway
+
+ self.requests = ThreadPool(self, min=minthreads or 1, max=maxthreads)
+
+ if not server_name:
+ server_name = socket.gethostname()
+ self.server_name = server_name
+ self.clear_stats()
+
+ def clear_stats(self):
+ self._start_time = None
+ self._run_time = 0
+ self.stats = {
+ 'Enabled': False,
+ 'Bind Address': lambda s: repr(self.bind_addr),
+ 'Run time': lambda s: (not s['Enabled']) and 0 or self.runtime(),
+ 'Accepts': 0,
+ 'Accepts/sec': lambda s: s['Accepts'] / self.runtime(),
+ 'Queue': lambda s: getattr(self.requests, "qsize", None),
+ 'Threads': lambda s: len(getattr(self.requests, "_threads", [])),
+ 'Threads Idle': lambda s: getattr(self.requests, "idle", None),
+ 'Socket Errors': 0,
+ 'Requests': lambda s: (not s['Enabled']) and 0 or sum([w['Requests'](w) for w
+ in s['Worker Threads'].values()], 0),
+ 'Bytes Read': lambda s: (not s['Enabled']) and 0 or sum([w['Bytes Read'](w) for w
+ in s['Worker Threads'].values()], 0),
+ 'Bytes Written': lambda s: (not s['Enabled']) and 0 or sum([w['Bytes Written'](w) for w
+ in s['Worker Threads'].values()], 0),
+ 'Work Time': lambda s: (not s['Enabled']) and 0 or sum([w['Work Time'](w) for w
+ in s['Worker Threads'].values()], 0),
+ 'Read Throughput': lambda s: (not s['Enabled']) and 0 or sum(
+ [w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6)
+ for w in s['Worker Threads'].values()], 0),
+ 'Write Throughput': lambda s: (not s['Enabled']) and 0 or sum(
+ [w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6)
+ for w in s['Worker Threads'].values()], 0),
+ 'Worker Threads': {},
+ }
+ logging.statistics["CherryPy HTTPServer %d" % id(self)] = self.stats
+
+ def runtime(self):
+ if self._start_time is None:
+ return self._run_time
+ else:
+ return self._run_time + (time.time() - self._start_time)
+
+ def __str__(self):
+ return "%s.%s(%r)" % (self.__module__, self.__class__.__name__,
+ self.bind_addr)
+
+ def _get_bind_addr(self):
+ return self._bind_addr
+ def _set_bind_addr(self, value):
+ if isinstance(value, tuple) and value[0] in ('', None):
+ # Despite the socket module docs, using '' does not
+ # allow AI_PASSIVE to work. Passing None instead
+ # returns '0.0.0.0' like we want. In other words:
+ # host AI_PASSIVE result
+ # '' Y 192.168.x.y
+ # '' N 192.168.x.y
+ # None Y 0.0.0.0
+ # None N 127.0.0.1
+ # But since you can get the same effect with an explicit
+ # '0.0.0.0', we deny both the empty string and None as values.
+ raise ValueError("Host values of '' or None are not allowed. "
+ "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead "
+ "to listen on all active interfaces.")
+ self._bind_addr = value
+ bind_addr = property(_get_bind_addr, _set_bind_addr,
+ doc="""The interface on which to listen for connections.
+
+ For TCP sockets, a (host, port) tuple. Host values may be any IPv4
+ or IPv6 address, or any valid hostname. The string 'localhost' is a
+ synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6).
+ The string '0.0.0.0' is a special IPv4 entry meaning "any active
+ interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for
+ IPv6. The empty string or None are not allowed.
+
+ For UNIX sockets, supply the filename as a string.""")
+
+ def start(self):
+ """Run the server forever."""
+ # We don't have to trap KeyboardInterrupt or SystemExit here,
+ # because cherrpy.server already does so, calling self.stop() for us.
+ # If you're using this server with another framework, you should
+ # trap those exceptions in whatever code block calls start().
+ self._interrupt = None
+
+ if self.software is None:
+ self.software = "%s Server" % self.version
+
+ # SSL backward compatibility
+ if (self.ssl_adapter is None and
+ getattr(self, 'ssl_certificate', None) and
+ getattr(self, 'ssl_private_key', None)):
+ warnings.warn(
+ "SSL attributes are deprecated in CherryPy 3.2, and will "
+ "be removed in CherryPy 3.3. Use an ssl_adapter attribute "
+ "instead.",
+ DeprecationWarning
+ )
+ try:
+ from cherrypy.wsgiserver.ssl_pyopenssl import pyOpenSSLAdapter
+ except ImportError:
+ pass
+ else:
+ self.ssl_adapter = pyOpenSSLAdapter(
+ self.ssl_certificate, self.ssl_private_key,
+ getattr(self, 'ssl_certificate_chain', None))
+
+ # Select the appropriate socket
+ if isinstance(self.bind_addr, basestring):
+ # AF_UNIX socket
+
+ # So we can reuse the socket...
+ try: os.unlink(self.bind_addr)
+ except: pass
+
+ # So everyone can access the socket...
+ try: os.chmod(self.bind_addr, 0777)
+ except: pass
+
+ info = [(socket.AF_UNIX, socket.SOCK_STREAM, 0, "", self.bind_addr)]
+ else:
+ # AF_INET or AF_INET6 socket
+ # Get the correct address family for our host (allows IPv6 addresses)
+ host, port = self.bind_addr
+ try:
+ info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
+ socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
+ except socket.gaierror:
+ if ':' in self.bind_addr[0]:
+ info = [(socket.AF_INET6, socket.SOCK_STREAM,
+ 0, "", self.bind_addr + (0, 0))]
+ else:
+ info = [(socket.AF_INET, socket.SOCK_STREAM,
+ 0, "", self.bind_addr)]
+
+ self.socket = None
+ msg = "No socket could be created"
+ for res in info:
+ af, socktype, proto, canonname, sa = res
+ try:
+ self.bind(af, socktype, proto)
+ except socket.error:
+ if self.socket:
+ self.socket.close()
+ self.socket = None
+ continue
+ break
+ if not self.socket:
+ raise socket.error(msg)
+
+ # Timeout so KeyboardInterrupt can be caught on Win32
+ self.socket.settimeout(1)
+ self.socket.listen(self.request_queue_size)
+
+ # Create worker threads
+ self.requests.start()
+
+ self.ready = True
+ self._start_time = time.time()
+ while self.ready:
+ self.tick()
+ if self.interrupt:
+ while self.interrupt is True:
+ # Wait for self.stop() to complete. See _set_interrupt.
+ time.sleep(0.1)
+ if self.interrupt:
+ raise self.interrupt
+
+ def bind(self, family, type, proto=0):
+ """Create (or recreate) the actual socket object."""
+ self.socket = socket.socket(family, type, proto)
+ prevent_socket_inheritance(self.socket)
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ if self.nodelay and not isinstance(self.bind_addr, str):
+ self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+
+ if self.ssl_adapter is not None:
+ self.socket = self.ssl_adapter.bind(self.socket)
+
+ # If listening on the IPV6 any address ('::' = IN6ADDR_ANY),
+ # activate dual-stack. See http://www.cherrypy.org/ticket/871.
+ if (hasattr(socket, 'AF_INET6') and family == socket.AF_INET6
+ and self.bind_addr[0] in ('::', '::0', '::0.0.0.0')):
+ try:
+ self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
+ except (AttributeError, socket.error):
+ # Apparently, the socket option is not available in
+ # this machine's TCP stack
+ pass
+
+ self.socket.bind(self.bind_addr)
+
+ def tick(self):
+ """Accept a new connection and put it on the Queue."""
+ try:
+ s, addr = self.socket.accept()
+ if self.stats['Enabled']:
+ self.stats['Accepts'] += 1
+ if not self.ready:
+ return
+
+ prevent_socket_inheritance(s)
+ if hasattr(s, 'settimeout'):
+ s.settimeout(self.timeout)
+
+ makefile = CP_fileobject
+ ssl_env = {}
+ # if ssl cert and key are set, we try to be a secure HTTP server
+ if self.ssl_adapter is not None:
+ try:
+ s, ssl_env = self.ssl_adapter.wrap(s)
+ except NoSSLError:
+ msg = ("The client sent a plain HTTP request, but "
+ "this server only speaks HTTPS on this port.")
+ buf = ["%s 400 Bad Request\r\n" % self.protocol,
+ "Content-Length: %s\r\n" % len(msg),
+ "Content-Type: text/plain\r\n\r\n",
+ msg]
+
+ wfile = CP_fileobject(s, "wb", DEFAULT_BUFFER_SIZE)
+ try:
+ wfile.sendall("".join(buf))
+ except socket.error, x:
+ if x.args[0] not in socket_errors_to_ignore:
+ raise
+ return
+ if not s:
+ return
+ makefile = self.ssl_adapter.makefile
+ # Re-apply our timeout since we may have a new socket object
+ if hasattr(s, 'settimeout'):
+ s.settimeout(self.timeout)
+
+ conn = self.ConnectionClass(self, s, makefile)
+
+ if not isinstance(self.bind_addr, basestring):
+ # optional values
+ # Until we do DNS lookups, omit REMOTE_HOST
+ if addr is None: # sometimes this can happen
+ # figure out if AF_INET or AF_INET6.
+ if len(s.getsockname()) == 2:
+ # AF_INET
+ addr = ('0.0.0.0', 0)
+ else:
+ # AF_INET6
+ addr = ('::', 0)
+ conn.remote_addr = addr[0]
+ conn.remote_port = addr[1]
+
+ conn.ssl_env = ssl_env
+
+ self.requests.put(conn)
+ except socket.timeout:
+ # The only reason for the timeout in start() is so we can
+ # notice keyboard interrupts on Win32, which don't interrupt
+ # accept() by default
+ return
+ except socket.error, x:
+ if self.stats['Enabled']:
+ self.stats['Socket Errors'] += 1
+ if x.args[0] in socket_error_eintr:
+ # I *think* this is right. EINTR should occur when a signal
+ # is received during the accept() call; all docs say retry
+ # the call, and I *think* I'm reading it right that Python
+ # will then go ahead and poll for and handle the signal
+ # elsewhere. See http://www.cherrypy.org/ticket/707.
+ return
+ if x.args[0] in socket_errors_nonblocking:
+ # Just try again. See http://www.cherrypy.org/ticket/479.
+ return
+ if x.args[0] in socket_errors_to_ignore:
+ # Our socket was closed.
+ # See http://www.cherrypy.org/ticket/686.
+ return
+ raise
+
+ def _get_interrupt(self):
+ return self._interrupt
+ def _set_interrupt(self, interrupt):
+ self._interrupt = True
+ self.stop()
+ self._interrupt = interrupt
+ interrupt = property(_get_interrupt, _set_interrupt,
+ doc="Set this to an Exception instance to "
+ "interrupt the server.")
+
+ def stop(self):
+ """Gracefully shutdown a server that is serving forever."""
+ self.ready = False
+ if self._start_time is not None:
+ self._run_time += (time.time() - self._start_time)
+ self._start_time = None
+
+ sock = getattr(self, "socket", None)
+ if sock:
+ if not isinstance(self.bind_addr, basestring):
+ # Touch our own socket to make accept() return immediately.
+ try:
+ host, port = sock.getsockname()[:2]
+ except socket.error, x:
+ if x.args[0] not in socket_errors_to_ignore:
+ # Changed to use error code and not message
+ # See http://www.cherrypy.org/ticket/860.
+ raise
+ else:
+ # Note that we're explicitly NOT using AI_PASSIVE,
+ # here, because we want an actual IP to touch.
+ # localhost won't work if we've bound to a public IP,
+ # but it will if we bound to '0.0.0.0' (INADDR_ANY).
+ for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
+ socket.SOCK_STREAM):
+ af, socktype, proto, canonname, sa = res
+ s = None
+ try:
+ s = socket.socket(af, socktype, proto)
+ # See http://groups.google.com/group/cherrypy-users/
+ # browse_frm/thread/bbfe5eb39c904fe0
+ s.settimeout(1.0)
+ s.connect((host, port))
+ s.close()
+ except socket.error:
+ if s:
+ s.close()
+ if hasattr(sock, "close"):
+ sock.close()
+ self.socket = None
+
+ self.requests.stop(self.shutdown_timeout)
+
+
+class Gateway(object):
+
+ def __init__(self, req):
+ self.req = req
+
+ def respond(self):
+ raise NotImplemented
+
+
+# These may either be wsgiserver.SSLAdapter subclasses or the string names
+# of such classes (in which case they will be lazily loaded).
+ssl_adapters = {
+ 'builtin': 'cherrypy.wsgiserver.ssl_builtin.BuiltinSSLAdapter',
+ 'pyopenssl': 'cherrypy.wsgiserver.ssl_pyopenssl.pyOpenSSLAdapter',
+ }
+
+def get_ssl_adapter_class(name='pyopenssl'):
+ adapter = ssl_adapters[name.lower()]
+ if isinstance(adapter, basestring):
+ last_dot = adapter.rfind(".")
+ attr_name = adapter[last_dot + 1:]
+ mod_path = adapter[:last_dot]
+
+ try:
+ mod = sys.modules[mod_path]
+ if mod is None:
+ raise KeyError()
+ except KeyError:
+ # The last [''] is important.
+ mod = __import__(mod_path, globals(), locals(), [''])
+
+ # Let an AttributeError propagate outward.
+ try:
+ adapter = getattr(mod, attr_name)
+ except AttributeError:
+ raise AttributeError("'%s' object has no attribute '%s'"
+ % (mod_path, attr_name))
+
+ return adapter
+
+# -------------------------------- WSGI Stuff -------------------------------- #
+
+
+class CherryPyWSGIServer(HTTPServer):
+
+ wsgi_version = (1, 0)
+
+ def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
+ max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5):
+ self.requests = ThreadPool(self, min=numthreads or 1, max=max)
+ self.wsgi_app = wsgi_app
+ self.gateway = wsgi_gateways[self.wsgi_version]
+
+ self.bind_addr = bind_addr
+ if not server_name:
+ server_name = socket.gethostname()
+ self.server_name = server_name
+ self.request_queue_size = request_queue_size
+
+ self.timeout = timeout
+ self.shutdown_timeout = shutdown_timeout
+ self.clear_stats()
+
+ def _get_numthreads(self):
+ return self.requests.min
+ def _set_numthreads(self, value):
+ self.requests.min = value
+ numthreads = property(_get_numthreads, _set_numthreads)
+
+
+class WSGIGateway(Gateway):
+
+ def __init__(self, req):
+ self.req = req
+ self.started_response = False
+ self.env = self.get_environ()
+ self.remaining_bytes_out = None
+
+ def get_environ(self):
+ """Return a new environ dict targeting the given wsgi.version"""
+ raise NotImplemented
+
+ def respond(self):
+ response = self.req.server.wsgi_app(self.env, self.start_response)
+ try:
+ for chunk in response:
+ # "The start_response callable must not actually transmit
+ # the response headers. Instead, it must store them for the
+ # server or gateway to transmit only after the first
+ # iteration of the application return value that yields
+ # a NON-EMPTY string, or upon the application's first
+ # invocation of the write() callable." (PEP 333)
+ if chunk:
+ if isinstance(chunk, unicode):
+ chunk = chunk.encode('ISO-8859-1')
+ self.write(chunk)
+ finally:
+ if hasattr(response, "close"):
+ response.close()
+
+ def start_response(self, status, headers, exc_info = None):
+ """WSGI callable to begin the HTTP response."""
+ # "The application may call start_response more than once,
+ # if and only if the exc_info argument is provided."
+ if self.started_response and not exc_info:
+ raise AssertionError("WSGI start_response called a second "
+ "time with no exc_info.")
+ self.started_response = True
+
+ # "if exc_info is provided, and the HTTP headers have already been
+ # sent, start_response must raise an error, and should raise the
+ # exc_info tuple."
+ if self.req.sent_headers:
+ try:
+ raise exc_info[0], exc_info[1], exc_info[2]
+ finally:
+ exc_info = None
+
+ self.req.status = status
+ for k, v in headers:
+ if not isinstance(k, str):
+ raise TypeError("WSGI response header key %r is not a byte string." % k)
+ if not isinstance(v, str):
+ raise TypeError("WSGI response header value %r is not a byte string." % v)
+ if k.lower() == 'content-length':
+ self.remaining_bytes_out = int(v)
+ self.req.outheaders.extend(headers)
+
+ return self.write
+
+ def write(self, chunk):
+ """WSGI callable to write unbuffered data to the client.
+
+ This method is also used internally by start_response (to write
+ data from the iterable returned by the WSGI application).
+ """
+ if not self.started_response:
+ raise AssertionError("WSGI write called before start_response.")
+
+ chunklen = len(chunk)
+ rbo = self.remaining_bytes_out
+ if rbo is not None and chunklen > rbo:
+ if not self.req.sent_headers:
+ # Whew. We can send a 500 to the client.
+ self.req.simple_response("500 Internal Server Error",
+ "The requested resource returned more bytes than the "
+ "declared Content-Length.")
+ else:
+ # Dang. We have probably already sent data. Truncate the chunk
+ # to fit (so the client doesn't hang) and raise an error later.
+ chunk = chunk[:rbo]
+
+ if not self.req.sent_headers:
+ self.req.sent_headers = True
+ self.req.send_headers()
+
+ self.req.write(chunk)
+
+ if rbo is not None:
+ rbo -= chunklen
+ if rbo < 0:
+ raise ValueError(
+ "Response body exceeds the declared Content-Length.")
+
+
+class WSGIGateway_10(WSGIGateway):
+
+ def get_environ(self):
+ """Return a new environ dict targeting the given wsgi.version"""
+ req = self.req
+ env = {
+ # set a non-standard environ entry so the WSGI app can know what
+ # the *real* server protocol is (and what features to support).
+ # See http://www.faqs.org/rfcs/rfc2145.html.
+ 'ACTUAL_SERVER_PROTOCOL': req.server.protocol,
+ 'PATH_INFO': req.path,
+ 'QUERY_STRING': req.qs,
+ 'REMOTE_ADDR': req.conn.remote_addr or '',
+ 'REMOTE_PORT': str(req.conn.remote_port or ''),
+ 'REQUEST_METHOD': req.method,
+ 'REQUEST_URI': req.uri,
+ 'SCRIPT_NAME': '',
+ 'SERVER_NAME': req.server.server_name,
+ # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol.
+ 'SERVER_PROTOCOL': req.request_protocol,
+ 'SERVER_SOFTWARE': req.server.software,
+ 'wsgi.errors': sys.stderr,
+ 'wsgi.input': req.rfile,
+ 'wsgi.multiprocess': False,
+ 'wsgi.multithread': True,
+ 'wsgi.run_once': False,
+ 'wsgi.url_scheme': req.scheme,
+ 'wsgi.version': (1, 0),
+ }
+
+ if isinstance(req.server.bind_addr, basestring):
+ # AF_UNIX. This isn't really allowed by WSGI, which doesn't
+ # address unix domain sockets. But it's better than nothing.
+ env["SERVER_PORT"] = ""
+ else:
+ env["SERVER_PORT"] = str(req.server.bind_addr[1])
+
+ # Request headers
+ for k, v in req.inheaders.iteritems():
+ env["HTTP_" + k.upper().replace("-", "_")] = v
+
+ # CONTENT_TYPE/CONTENT_LENGTH
+ ct = env.pop("HTTP_CONTENT_TYPE", None)
+ if ct is not None:
+ env["CONTENT_TYPE"] = ct
+ cl = env.pop("HTTP_CONTENT_LENGTH", None)
+ if cl is not None:
+ env["CONTENT_LENGTH"] = cl
+
+ if req.conn.ssl_env:
+ env.update(req.conn.ssl_env)
+
+ return env
+
+
+class WSGIGateway_u0(WSGIGateway_10):
+
+ def get_environ(self):
+ """Return a new environ dict targeting the given wsgi.version"""
+ req = self.req
+ env_10 = WSGIGateway_10.get_environ(self)
+ env = dict([(k.decode('ISO-8859-1'), v) for k, v in env_10.iteritems()])
+ env[u'wsgi.version'] = ('u', 0)
+
+ # Request-URI
+ env.setdefault(u'wsgi.url_encoding', u'utf-8')
+ try:
+ for key in [u"PATH_INFO", u"SCRIPT_NAME", u"QUERY_STRING"]:
+ env[key] = env_10[str(key)].decode(env[u'wsgi.url_encoding'])
+ except UnicodeDecodeError:
+ # Fall back to latin 1 so apps can transcode if needed.
+ env[u'wsgi.url_encoding'] = u'ISO-8859-1'
+ for key in [u"PATH_INFO", u"SCRIPT_NAME", u"QUERY_STRING"]:
+ env[key] = env_10[str(key)].decode(env[u'wsgi.url_encoding'])
+
+ for k, v in sorted(env.items()):
+ if isinstance(v, str) and k not in ('REQUEST_URI', 'wsgi.input'):
+ env[k] = v.decode('ISO-8859-1')
+
+ return env
+
+wsgi_gateways = {
+ (1, 0): WSGIGateway_10,
+ ('u', 0): WSGIGateway_u0,
+}
+
+class WSGIPathInfoDispatcher(object):
+ """A WSGI dispatcher for dispatch based on the PATH_INFO.
+
+ apps: a dict or list of (path_prefix, app) pairs.
+ """
+
+ def __init__(self, apps):
+ try:
+ apps = apps.items()
+ except AttributeError:
+ pass
+
+ # Sort the apps by len(path), descending
+ apps.sort(cmp=lambda x,y: cmp(len(x[0]), len(y[0])))
+ apps.reverse()
+
+ # The path_prefix strings must start, but not end, with a slash.
+ # Use "" instead of "/".
+ self.apps = [(p.rstrip("/"), a) for p, a in apps]
+
+ def __call__(self, environ, start_response):
+ path = environ["PATH_INFO"] or "/"
+ for p, app in self.apps:
+ # The apps list should be sorted by length, descending.
+ if path.startswith(p + "/") or path == p:
+ environ = environ.copy()
+ environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p
+ environ["PATH_INFO"] = path[len(p):]
+ return app(environ, start_response)
+
+ start_response('404 Not Found', [('Content-Type', 'text/plain'),
+ ('Content-Length', '0')])
+ return ['']
+
diff --git a/cherrypy/wsgiserver/ssl_builtin.py b/cherrypy/wsgiserver/ssl_builtin.py
new file mode 100755
index 0000000..64c0eeb
--- /dev/null
+++ b/cherrypy/wsgiserver/ssl_builtin.py
@@ -0,0 +1,72 @@
+"""A library for integrating Python's builtin ``ssl`` library with CherryPy.
+
+The ssl module must be importable for SSL functionality.
+
+To use this module, set ``CherryPyWSGIServer.ssl_adapter`` to an instance of
+``BuiltinSSLAdapter``.
+"""
+
+try:
+ import ssl
+except ImportError:
+ ssl = None
+
+from cherrypy import wsgiserver
+
+
+class BuiltinSSLAdapter(wsgiserver.SSLAdapter):
+ """A wrapper for integrating Python's builtin ssl module with CherryPy."""
+
+ certificate = None
+ """The filename of the server SSL certificate."""
+
+ private_key = None
+ """The filename of the server's private key file."""
+
+ def __init__(self, certificate, private_key, certificate_chain=None):
+ if ssl is None:
+ raise ImportError("You must install the ssl module to use HTTPS.")
+ self.certificate = certificate
+ self.private_key = private_key
+ self.certificate_chain = certificate_chain
+
+ def bind(self, sock):
+ """Wrap and return the given socket."""
+ return sock
+
+ def wrap(self, sock):
+ """Wrap and return the given socket, plus WSGI environ entries."""
+ try:
+ s = ssl.wrap_socket(sock, do_handshake_on_connect=True,
+ server_side=True, certfile=self.certificate,
+ keyfile=self.private_key, ssl_version=ssl.PROTOCOL_SSLv23)
+ except ssl.SSLError, e:
+ if e.errno == ssl.SSL_ERROR_EOF:
+ # This is almost certainly due to the cherrypy engine
+ # 'pinging' the socket to assert it's connectable;
+ # the 'ping' isn't SSL.
+ return None, {}
+ elif e.errno == ssl.SSL_ERROR_SSL:
+ if e.args[1].endswith('http request'):
+ # The client is speaking HTTP to an HTTPS server.
+ raise wsgiserver.NoSSLError
+ raise
+ return s, self.get_environ(s)
+
+ # TODO: fill this out more with mod ssl env
+ def get_environ(self, sock):
+ """Create WSGI environ entries to be merged into each request."""
+ cipher = sock.cipher()
+ ssl_environ = {
+ "wsgi.url_scheme": "https",
+ "HTTPS": "on",
+ 'SSL_PROTOCOL': cipher[1],
+ 'SSL_CIPHER': cipher[0]
+## SSL_VERSION_INTERFACE string The mod_ssl program version
+## SSL_VERSION_LIBRARY string The OpenSSL program version
+ }
+ return ssl_environ
+
+ def makefile(self, sock, mode='r', bufsize=-1):
+ return wsgiserver.CP_fileobject(sock, mode, bufsize)
+
diff --git a/cherrypy/wsgiserver/ssl_pyopenssl.py b/cherrypy/wsgiserver/ssl_pyopenssl.py
new file mode 100755
index 0000000..f3d9bf5
--- /dev/null
+++ b/cherrypy/wsgiserver/ssl_pyopenssl.py
@@ -0,0 +1,256 @@
+"""A library for integrating pyOpenSSL with CherryPy.
+
+The OpenSSL module must be importable for SSL functionality.
+You can obtain it from http://pyopenssl.sourceforge.net/
+
+To use this module, set CherryPyWSGIServer.ssl_adapter to an instance of
+SSLAdapter. There are two ways to use SSL:
+
+Method One
+----------
+
+ * ``ssl_adapter.context``: an instance of SSL.Context.
+
+If this is not None, it is assumed to be an SSL.Context instance,
+and will be passed to SSL.Connection on bind(). The developer is
+responsible for forming a valid Context object. This approach is
+to be preferred for more flexibility, e.g. if the cert and key are
+streams instead of files, or need decryption, or SSL.SSLv3_METHOD
+is desired instead of the default SSL.SSLv23_METHOD, etc. Consult
+the pyOpenSSL documentation for complete options.
+
+Method Two (shortcut)
+---------------------
+
+ * ``ssl_adapter.certificate``: the filename of the server SSL certificate.
+ * ``ssl_adapter.private_key``: the filename of the server's private key file.
+
+Both are None by default. If ssl_adapter.context is None, but .private_key
+and .certificate are both given and valid, they will be read, and the
+context will be automatically created from them.
+"""
+
+import socket
+import threading
+import time
+
+from cherrypy import wsgiserver
+
+try:
+ from OpenSSL import SSL
+ from OpenSSL import crypto
+except ImportError:
+ SSL = None
+
+
+class SSL_fileobject(wsgiserver.CP_fileobject):
+ """SSL file object attached to a socket object."""
+
+ ssl_timeout = 3
+ ssl_retry = .01
+
+ def _safe_call(self, is_reader, call, *args, **kwargs):
+ """Wrap the given call with SSL error-trapping.
+
+ is_reader: if False EOF errors will be raised. If True, EOF errors
+ will return "" (to emulate normal sockets).
+ """
+ start = time.time()
+ while True:
+ try:
+ return call(*args, **kwargs)
+ except SSL.WantReadError:
+ # Sleep and try again. This is dangerous, because it means
+ # the rest of the stack has no way of differentiating
+ # between a "new handshake" error and "client dropped".
+ # Note this isn't an endless loop: there's a timeout below.
+ time.sleep(self.ssl_retry)
+ except SSL.WantWriteError:
+ time.sleep(self.ssl_retry)
+ except SSL.SysCallError, e:
+ if is_reader and e.args == (-1, 'Unexpected EOF'):
+ return ""
+
+ errnum = e.args[0]
+ if is_reader and errnum in wsgiserver.socket_errors_to_ignore:
+ return ""
+ raise socket.error(errnum)
+ except SSL.Error, e:
+ if is_reader and e.args == (-1, 'Unexpected EOF'):
+ return ""
+
+ thirdarg = None
+ try:
+ thirdarg = e.args[0][0][2]
+ except IndexError:
+ pass
+
+ if thirdarg == 'http request':
+ # The client is talking HTTP to an HTTPS server.
+ raise wsgiserver.NoSSLError()
+
+ raise wsgiserver.FatalSSLAlert(*e.args)
+ except:
+ raise
+
+ if time.time() - start > self.ssl_timeout:
+ raise socket.timeout("timed out")
+
+ def recv(self, *args, **kwargs):
+ buf = []
+ r = super(SSL_fileobject, self).recv
+ while True:
+ data = self._safe_call(True, r, *args, **kwargs)
+ buf.append(data)
+ p = self._sock.pending()
+ if not p:
+ return "".join(buf)
+
+ def sendall(self, *args, **kwargs):
+ return self._safe_call(False, super(SSL_fileobject, self).sendall,
+ *args, **kwargs)
+
+ def send(self, *args, **kwargs):
+ return self._safe_call(False, super(SSL_fileobject, self).send,
+ *args, **kwargs)
+
+
+class SSLConnection:
+ """A thread-safe wrapper for an SSL.Connection.
+
+ ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``.
+ """
+
+ def __init__(self, *args):
+ self._ssl_conn = SSL.Connection(*args)
+ self._lock = threading.RLock()
+
+ for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
+ 'renegotiate', 'bind', 'listen', 'connect', 'accept',
+ 'setblocking', 'fileno', 'close', 'get_cipher_list',
+ 'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
+ 'makefile', 'get_app_data', 'set_app_data', 'state_string',
+ 'sock_shutdown', 'get_peer_certificate', 'want_read',
+ 'want_write', 'set_connect_state', 'set_accept_state',
+ 'connect_ex', 'sendall', 'settimeout', 'gettimeout'):
+ exec("""def %s(self, *args):
+ self._lock.acquire()
+ try:
+ return self._ssl_conn.%s(*args)
+ finally:
+ self._lock.release()
+""" % (f, f))
+
+ def shutdown(self, *args):
+ self._lock.acquire()
+ try:
+ # pyOpenSSL.socket.shutdown takes no args
+ return self._ssl_conn.shutdown()
+ finally:
+ self._lock.release()
+
+
+class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
+ """A wrapper for integrating pyOpenSSL with CherryPy."""
+
+ context = None
+ """An instance of SSL.Context."""
+
+ certificate = None
+ """The filename of the server SSL certificate."""
+
+ private_key = None
+ """The filename of the server's private key file."""
+
+ certificate_chain = None
+ """Optional. The filename of CA's intermediate certificate bundle.
+
+ This is needed for cheaper "chained root" SSL certificates, and should be
+ left as None if not required."""
+
+ def __init__(self, certificate, private_key, certificate_chain=None):
+ if SSL is None:
+ raise ImportError("You must install pyOpenSSL to use HTTPS.")
+
+ self.context = None
+ self.certificate = certificate
+ self.private_key = private_key
+ self.certificate_chain = certificate_chain
+ self._environ = None
+
+ def bind(self, sock):
+ """Wrap and return the given socket."""
+ if self.context is None:
+ self.context = self.get_context()
+ conn = SSLConnection(self.context, sock)
+ self._environ = self.get_environ()
+ return conn
+
+ def wrap(self, sock):
+ """Wrap and return the given socket, plus WSGI environ entries."""
+ return sock, self._environ.copy()
+
+ def get_context(self):
+ """Return an SSL.Context from self attributes."""
+ # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473
+ c = SSL.Context(SSL.SSLv23_METHOD)
+ c.use_privatekey_file(self.private_key)
+ if self.certificate_chain:
+ c.load_verify_locations(self.certificate_chain)
+ c.use_certificate_file(self.certificate)
+ return c
+
+ def get_environ(self):
+ """Return WSGI environ entries to be merged into each request."""
+ ssl_environ = {
+ "HTTPS": "on",
+ # pyOpenSSL doesn't provide access to any of these AFAICT
+## 'SSL_PROTOCOL': 'SSLv2',
+## SSL_CIPHER string The cipher specification name
+## SSL_VERSION_INTERFACE string The mod_ssl program version
+## SSL_VERSION_LIBRARY string The OpenSSL program version
+ }
+
+ if self.certificate:
+ # Server certificate attributes
+ cert = open(self.certificate, 'rb').read()
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
+ ssl_environ.update({
+ 'SSL_SERVER_M_VERSION': cert.get_version(),
+ 'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
+## 'SSL_SERVER_V_START': Validity of server's certificate (start time),
+## 'SSL_SERVER_V_END': Validity of server's certificate (end time),
+ })
+
+ for prefix, dn in [("I", cert.get_issuer()),
+ ("S", cert.get_subject())]:
+ # X509Name objects don't seem to have a way to get the
+ # complete DN string. Use str() and slice it instead,
+ # because str(dn) == "<X509Name object '/C=US/ST=...'>"
+ dnstr = str(dn)[18:-2]
+
+ wsgikey = 'SSL_SERVER_%s_DN' % prefix
+ ssl_environ[wsgikey] = dnstr
+
+ # The DN should be of the form: /k1=v1/k2=v2, but we must allow
+ # for any value to contain slashes itself (in a URL).
+ while dnstr:
+ pos = dnstr.rfind("=")
+ dnstr, value = dnstr[:pos], dnstr[pos + 1:]
+ pos = dnstr.rfind("/")
+ dnstr, key = dnstr[:pos], dnstr[pos + 1:]
+ if key and value:
+ wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
+ ssl_environ[wsgikey] = value
+
+ return ssl_environ
+
+ def makefile(self, sock, mode='r', bufsize=-1):
+ if SSL and isinstance(sock, SSL.ConnectionType):
+ timeout = sock.gettimeout()
+ f = SSL_fileobject(sock, mode, bufsize)
+ f.ssl_timeout = timeout
+ return f
+ else:
+ return wsgiserver.CP_fileobject(sock, mode, bufsize)
+
diff --git a/genshi/__init__.py b/genshi/__init__.py
new file mode 100644
index 0000000..02f4347
--- /dev/null
+++ b/genshi/__init__.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""This package provides various means for generating and processing web markup
+(XML or HTML).
+
+The design is centered around the concept of streams of markup events (similar
+in concept to SAX parsing events) which can be processed in a uniform manner
+independently of where or how they are produced.
+"""
+
+__docformat__ = 'restructuredtext en'
+__version__ = '0.6'
+
+from genshi.core import *
+from genshi.input import ParseError, XML, HTML
diff --git a/genshi/builder.py b/genshi/builder.py
new file mode 100644
index 0000000..724e364
--- /dev/null
+++ b/genshi/builder.py
@@ -0,0 +1,359 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Support for programmatically generating markup streams from Python code using
+a very simple syntax. The main entry point to this module is the `tag` object
+(which is actually an instance of the ``ElementFactory`` class). You should
+rarely (if ever) need to directly import and use any of the other classes in
+this module.
+
+Elements can be created using the `tag` object using attribute access. For
+example:
+
+>>> doc = tag.p('Some text and ', tag.a('a link', href='http://example.org/'), '.')
+>>> doc
+<Element "p">
+
+This produces an `Element` instance which can be further modified to add child
+nodes and attributes. This is done by "calling" the element: positional
+arguments are added as child nodes (alternatively, the `Element.append` method
+can be used for that purpose), whereas keywords arguments are added as
+attributes:
+
+>>> doc(tag.br)
+<Element "p">
+>>> print(doc)
+<p>Some text and <a href="http://example.org/">a link</a>.<br/></p>
+
+If an attribute name collides with a Python keyword, simply append an underscore
+to the name:
+
+>>> doc(class_='intro')
+<Element "p">
+>>> print(doc)
+<p class="intro">Some text and <a href="http://example.org/">a link</a>.<br/></p>
+
+As shown above, an `Element` can easily be directly rendered to XML text by
+printing it or using the Python ``str()`` function. This is basically a
+shortcut for converting the `Element` to a stream and serializing that
+stream:
+
+>>> stream = doc.generate()
+>>> stream #doctest: +ELLIPSIS
+<genshi.core.Stream object at ...>
+>>> print(stream)
+<p class="intro">Some text and <a href="http://example.org/">a link</a>.<br/></p>
+
+
+The `tag` object also allows creating "fragments", which are basically lists
+of nodes (elements or text) that don't have a parent element. This can be useful
+for creating snippets of markup that are attached to a parent element later (for
+example in a template). Fragments are created by calling the `tag` object, which
+returns an object of type `Fragment`:
+
+>>> fragment = tag('Hello, ', tag.em('world'), '!')
+>>> fragment
+<Fragment>
+>>> print(fragment)
+Hello, <em>world</em>!
+"""
+
+from genshi.core import Attrs, Markup, Namespace, QName, Stream, \
+ START, END, TEXT
+
+__all__ = ['Fragment', 'Element', 'ElementFactory', 'tag']
+__docformat__ = 'restructuredtext en'
+
+
+class Fragment(object):
+ """Represents a markup fragment, which is basically just a list of element
+ or text nodes.
+ """
+ __slots__ = ['children']
+
+ def __init__(self):
+ """Create a new fragment."""
+ self.children = []
+
+ def __add__(self, other):
+ return Fragment()(self, other)
+
+ def __call__(self, *args):
+ """Append any positional arguments as child nodes.
+
+ :see: `append`
+ """
+ for arg in args:
+ self.append(arg)
+ return self
+
+ def __iter__(self):
+ return self._generate()
+
+ def __repr__(self):
+ return '<%s>' % type(self).__name__
+
+ def __str__(self):
+ return str(self.generate())
+
+ def __unicode__(self):
+ return unicode(self.generate())
+
+ def __html__(self):
+ return Markup(self.generate())
+
+ def append(self, node):
+ """Append an element or string as child node.
+
+ :param node: the node to append; can be an `Element`, `Fragment`, or a
+ `Stream`, or a Python string or number
+ """
+ if isinstance(node, (Stream, Element, basestring, int, float, long)):
+ # For objects of a known/primitive type, we avoid the check for
+ # whether it is iterable for better performance
+ self.children.append(node)
+ elif isinstance(node, Fragment):
+ self.children.extend(node.children)
+ elif node is not None:
+ try:
+ for child in node:
+ self.append(child)
+ except TypeError:
+ self.children.append(node)
+
+ def _generate(self):
+ for child in self.children:
+ if isinstance(child, Fragment):
+ for event in child._generate():
+ yield event
+ elif isinstance(child, Stream):
+ for event in child:
+ yield event
+ else:
+ if not isinstance(child, basestring):
+ child = unicode(child)
+ yield TEXT, child, (None, -1, -1)
+
+ def generate(self):
+ """Return a markup event stream for the fragment.
+
+ :rtype: `Stream`
+ """
+ return Stream(self._generate())
+
+
+def _kwargs_to_attrs(kwargs):
+ attrs = []
+ names = set()
+ for name, value in kwargs.items():
+ name = name.rstrip('_').replace('_', '-')
+ if value is not None and name not in names:
+ attrs.append((QName(name), unicode(value)))
+ names.add(name)
+ return Attrs(attrs)
+
+
+class Element(Fragment):
+ """Simple XML output generator based on the builder pattern.
+
+ Construct XML elements by passing the tag name to the constructor:
+
+ >>> print(Element('strong'))
+ <strong/>
+
+ Attributes can be specified using keyword arguments. The values of the
+ arguments will be converted to strings and any special XML characters
+ escaped:
+
+ >>> print(Element('textarea', rows=10, cols=60))
+ <textarea rows="10" cols="60"/>
+ >>> print(Element('span', title='1 < 2'))
+ <span title="1 &lt; 2"/>
+ >>> print(Element('span', title='"baz"'))
+ <span title="&#34;baz&#34;"/>
+
+ The " character is escaped using a numerical entity.
+ The order in which attributes are rendered is undefined.
+
+ If an attribute value evaluates to `None`, that attribute is not included
+ in the output:
+
+ >>> print(Element('a', name=None))
+ <a/>
+
+ Attribute names that conflict with Python keywords can be specified by
+ appending an underscore:
+
+ >>> print(Element('div', class_='warning'))
+ <div class="warning"/>
+
+ Nested elements can be added to an element using item access notation.
+ The call notation can also be used for this and for adding attributes
+ using keyword arguments, as one would do in the constructor.
+
+ >>> print(Element('ul')(Element('li'), Element('li')))
+ <ul><li/><li/></ul>
+ >>> print(Element('a')('Label'))
+ <a>Label</a>
+ >>> print(Element('a')('Label', href="target"))
+ <a href="target">Label</a>
+
+ Text nodes can be nested in an element by adding strings instead of
+ elements. Any special characters in the strings are escaped automatically:
+
+ >>> print(Element('em')('Hello world'))
+ <em>Hello world</em>
+ >>> print(Element('em')(42))
+ <em>42</em>
+ >>> print(Element('em')('1 < 2'))
+ <em>1 &lt; 2</em>
+
+ This technique also allows mixed content:
+
+ >>> print(Element('p')('Hello ', Element('b')('world')))
+ <p>Hello <b>world</b></p>
+
+ Quotes are not escaped inside text nodes:
+ >>> print(Element('p')('"Hello"'))
+ <p>"Hello"</p>
+
+ Elements can also be combined with other elements or strings using the
+ addition operator, which results in a `Fragment` object that contains the
+ operands:
+
+ >>> print(Element('br') + 'some text' + Element('br'))
+ <br/>some text<br/>
+
+ Elements with a namespace can be generated using the `Namespace` and/or
+ `QName` classes:
+
+ >>> from genshi.core import Namespace
+ >>> xhtml = Namespace('http://www.w3.org/1999/xhtml')
+ >>> print(Element(xhtml.html, lang='en'))
+ <html xmlns="http://www.w3.org/1999/xhtml" lang="en"/>
+ """
+ __slots__ = ['tag', 'attrib']
+
+ def __init__(self, tag_, **attrib):
+ Fragment.__init__(self)
+ self.tag = QName(tag_)
+ self.attrib = _kwargs_to_attrs(attrib)
+
+ def __call__(self, *args, **kwargs):
+ """Append any positional arguments as child nodes, and keyword arguments
+ as attributes.
+
+ :return: the element itself so that calls can be chained
+ :rtype: `Element`
+ :see: `Fragment.append`
+ """
+ self.attrib |= _kwargs_to_attrs(kwargs)
+ Fragment.__call__(self, *args)
+ return self
+
+ def __repr__(self):
+ return '<%s "%s">' % (type(self).__name__, self.tag)
+
+ def _generate(self):
+ yield START, (self.tag, self.attrib), (None, -1, -1)
+ for kind, data, pos in Fragment._generate(self):
+ yield kind, data, pos
+ yield END, self.tag, (None, -1, -1)
+
+ def generate(self):
+ """Return a markup event stream for the fragment.
+
+ :rtype: `Stream`
+ """
+ return Stream(self._generate())
+
+
+class ElementFactory(object):
+ """Factory for `Element` objects.
+
+ A new element is created simply by accessing a correspondingly named
+ attribute of the factory object:
+
+ >>> factory = ElementFactory()
+ >>> print(factory.foo)
+ <foo/>
+ >>> print(factory.foo(id=2))
+ <foo id="2"/>
+
+ Markup fragments (lists of nodes without a parent element) can be created
+ by calling the factory:
+
+ >>> print(factory('Hello, ', factory.em('world'), '!'))
+ Hello, <em>world</em>!
+
+ A factory can also be bound to a specific namespace:
+
+ >>> factory = ElementFactory('http://www.w3.org/1999/xhtml')
+ >>> print(factory.html(lang="en"))
+ <html xmlns="http://www.w3.org/1999/xhtml" lang="en"/>
+
+ The namespace for a specific element can be altered on an existing factory
+ by specifying the new namespace using item access:
+
+ >>> factory = ElementFactory()
+ >>> print(factory.html(factory['http://www.w3.org/2000/svg'].g(id=3)))
+ <html><g xmlns="http://www.w3.org/2000/svg" id="3"/></html>
+
+ Usually, the `ElementFactory` class is not be used directly. Rather, the
+ `tag` instance should be used to create elements.
+ """
+
+ def __init__(self, namespace=None):
+ """Create the factory, optionally bound to the given namespace.
+
+ :param namespace: the namespace URI for any created elements, or `None`
+ for no namespace
+ """
+ if namespace and not isinstance(namespace, Namespace):
+ namespace = Namespace(namespace)
+ self.namespace = namespace
+
+ def __call__(self, *args):
+ """Create a fragment that has the given positional arguments as child
+ nodes.
+
+ :return: the created `Fragment`
+ :rtype: `Fragment`
+ """
+ return Fragment()(*args)
+
+ def __getitem__(self, namespace):
+ """Return a new factory that is bound to the specified namespace.
+
+ :param namespace: the namespace URI or `Namespace` object
+ :return: an `ElementFactory` that produces elements bound to the given
+ namespace
+ :rtype: `ElementFactory`
+ """
+ return ElementFactory(namespace)
+
+ def __getattr__(self, name):
+ """Create an `Element` with the given name.
+
+ :param name: the tag name of the element to create
+ :return: an `Element` with the specified name
+ :rtype: `Element`
+ """
+ return Element(self.namespace and self.namespace[name] or name)
+
+
+tag = ElementFactory()
+"""Global `ElementFactory` bound to the default namespace.
+
+:type: `ElementFactory`
+"""
diff --git a/genshi/core.py b/genshi/core.py
new file mode 100644
index 0000000..f7cddff
--- /dev/null
+++ b/genshi/core.py
@@ -0,0 +1,727 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Core classes for markup processing."""
+
+try:
+ reduce # builtin in Python < 3
+except NameError:
+ from functools import reduce
+from itertools import chain
+import operator
+
+from genshi.util import plaintext, stripentities, striptags, stringrepr
+
+__all__ = ['Stream', 'Markup', 'escape', 'unescape', 'Attrs', 'Namespace',
+ 'QName']
+__docformat__ = 'restructuredtext en'
+
+
+class StreamEventKind(str):
+ """A kind of event on a markup stream."""
+ __slots__ = []
+ _instances = {}
+
+ def __new__(cls, val):
+ return cls._instances.setdefault(val, str.__new__(cls, val))
+
+
+class Stream(object):
+ """Represents a stream of markup events.
+
+ This class is basically an iterator over the events.
+
+ Stream events are tuples of the form::
+
+ (kind, data, position)
+
+ where ``kind`` is the event kind (such as `START`, `END`, `TEXT`, etc),
+ ``data`` depends on the kind of event, and ``position`` is a
+ ``(filename, line, offset)`` tuple that contains the location of the
+ original element or text in the input. If the original location is unknown,
+ ``position`` is ``(None, -1, -1)``.
+
+ Also provided are ways to serialize the stream to text. The `serialize()`
+ method will return an iterator over generated strings, while `render()`
+ returns the complete generated text at once. Both accept various parameters
+ that impact the way the stream is serialized.
+ """
+ __slots__ = ['events', 'serializer']
+
+ START = StreamEventKind('START') #: a start tag
+ END = StreamEventKind('END') #: an end tag
+ TEXT = StreamEventKind('TEXT') #: literal text
+ XML_DECL = StreamEventKind('XML_DECL') #: XML declaration
+ DOCTYPE = StreamEventKind('DOCTYPE') #: doctype declaration
+ START_NS = StreamEventKind('START_NS') #: start namespace mapping
+ END_NS = StreamEventKind('END_NS') #: end namespace mapping
+ START_CDATA = StreamEventKind('START_CDATA') #: start CDATA section
+ END_CDATA = StreamEventKind('END_CDATA') #: end CDATA section
+ PI = StreamEventKind('PI') #: processing instruction
+ COMMENT = StreamEventKind('COMMENT') #: comment
+
+ def __init__(self, events, serializer=None):
+ """Initialize the stream with a sequence of markup events.
+
+ :param events: a sequence or iterable providing the events
+ :param serializer: the default serialization method to use for this
+ stream
+
+ :note: Changed in 0.5: added the `serializer` argument
+ """
+ self.events = events #: The underlying iterable producing the events
+ self.serializer = serializer #: The default serializion method
+
+ def __iter__(self):
+ return iter(self.events)
+
+ def __or__(self, function):
+ """Override the "bitwise or" operator to apply filters or serializers
+ to the stream, providing a syntax similar to pipes on Unix shells.
+
+ Assume the following stream produced by the `HTML` function:
+
+ >>> from genshi.input import HTML
+ >>> html = HTML('''<p onclick="alert('Whoa')">Hello, world!</p>''')
+ >>> print(html)
+ <p onclick="alert('Whoa')">Hello, world!</p>
+
+ A filter such as the HTML sanitizer can be applied to that stream using
+ the pipe notation as follows:
+
+ >>> from genshi.filters import HTMLSanitizer
+ >>> sanitizer = HTMLSanitizer()
+ >>> print(html | sanitizer)
+ <p>Hello, world!</p>
+
+ Filters can be any function that accepts and produces a stream (where
+ a stream is anything that iterates over events):
+
+ >>> def uppercase(stream):
+ ... for kind, data, pos in stream:
+ ... if kind is TEXT:
+ ... data = data.upper()
+ ... yield kind, data, pos
+ >>> print(html | sanitizer | uppercase)
+ <p>HELLO, WORLD!</p>
+
+ Serializers can also be used with this notation:
+
+ >>> from genshi.output import TextSerializer
+ >>> output = TextSerializer()
+ >>> print(html | sanitizer | uppercase | output)
+ HELLO, WORLD!
+
+ Commonly, serializers should be used at the end of the "pipeline";
+ using them somewhere in the middle may produce unexpected results.
+
+ :param function: the callable object that should be applied as a filter
+ :return: the filtered stream
+ :rtype: `Stream`
+ """
+ return Stream(_ensure(function(self)), serializer=self.serializer)
+
+ def filter(self, *filters):
+ """Apply filters to the stream.
+
+ This method returns a new stream with the given filters applied. The
+ filters must be callables that accept the stream object as parameter,
+ and return the filtered stream.
+
+ The call::
+
+ stream.filter(filter1, filter2)
+
+ is equivalent to::
+
+ stream | filter1 | filter2
+
+ :param filters: one or more callable objects that should be applied as
+ filters
+ :return: the filtered stream
+ :rtype: `Stream`
+ """
+ return reduce(operator.or_, (self,) + filters)
+
+ def render(self, method=None, encoding='utf-8', out=None, **kwargs):
+ """Return a string representation of the stream.
+
+ Any additional keyword arguments are passed to the serializer, and thus
+ depend on the `method` parameter value.
+
+ :param method: determines how the stream is serialized; can be either
+ "xml", "xhtml", "html", "text", or a custom serializer
+ class; if `None`, the default serialization method of
+ the stream is used
+ :param encoding: how the output string should be encoded; if set to
+ `None`, this method returns a `unicode` object
+ :param out: a file-like object that the output should be written to
+ instead of being returned as one big string; note that if
+ this is a file or socket (or similar), the `encoding` must
+ not be `None` (that is, the output must be encoded)
+ :return: a `str` or `unicode` object (depending on the `encoding`
+ parameter), or `None` if the `out` parameter is provided
+ :rtype: `basestring`
+
+ :see: XMLSerializer, XHTMLSerializer, HTMLSerializer, TextSerializer
+ :note: Changed in 0.5: added the `out` parameter
+ """
+ from genshi.output import encode
+ if method is None:
+ method = self.serializer or 'xml'
+ generator = self.serialize(method=method, **kwargs)
+ return encode(generator, method=method, encoding=encoding, out=out)
+
+ def select(self, path, namespaces=None, variables=None):
+ """Return a new stream that contains the events matching the given
+ XPath expression.
+
+ >>> from genshi import HTML
+ >>> stream = HTML('<doc><elem>foo</elem><elem>bar</elem></doc>')
+ >>> print(stream.select('elem'))
+ <elem>foo</elem><elem>bar</elem>
+ >>> print(stream.select('elem/text()'))
+ foobar
+
+ Note that the outermost element of the stream becomes the *context
+ node* for the XPath test. That means that the expression "doc" would
+ not match anything in the example above, because it only tests against
+ child elements of the outermost element:
+
+ >>> print(stream.select('doc'))
+ <BLANKLINE>
+
+ You can use the "." expression to match the context node itself
+ (although that usually makes little sense):
+
+ >>> print(stream.select('.'))
+ <doc><elem>foo</elem><elem>bar</elem></doc>
+
+ :param path: a string containing the XPath expression
+ :param namespaces: mapping of namespace prefixes used in the path
+ :param variables: mapping of variable names to values
+ :return: the selected substream
+ :rtype: `Stream`
+ :raises PathSyntaxError: if the given path expression is invalid or not
+ supported
+ """
+ from genshi.path import Path
+ return Path(path).select(self, namespaces, variables)
+
+ def serialize(self, method='xml', **kwargs):
+ """Generate strings corresponding to a specific serialization of the
+ stream.
+
+ Unlike the `render()` method, this method is a generator that returns
+ the serialized output incrementally, as opposed to returning a single
+ string.
+
+ Any additional keyword arguments are passed to the serializer, and thus
+ depend on the `method` parameter value.
+
+ :param method: determines how the stream is serialized; can be either
+ "xml", "xhtml", "html", "text", or a custom serializer
+ class; if `None`, the default serialization method of
+ the stream is used
+ :return: an iterator over the serialization results (`Markup` or
+ `unicode` objects, depending on the serialization method)
+ :rtype: ``iterator``
+ :see: XMLSerializer, XHTMLSerializer, HTMLSerializer, TextSerializer
+ """
+ from genshi.output import get_serializer
+ if method is None:
+ method = self.serializer or 'xml'
+ return get_serializer(method, **kwargs)(_ensure(self))
+
+ def __str__(self):
+ return self.render()
+
+ def __unicode__(self):
+ return self.render(encoding=None)
+
+ def __html__(self):
+ return self
+
+
+START = Stream.START
+END = Stream.END
+TEXT = Stream.TEXT
+XML_DECL = Stream.XML_DECL
+DOCTYPE = Stream.DOCTYPE
+START_NS = Stream.START_NS
+END_NS = Stream.END_NS
+START_CDATA = Stream.START_CDATA
+END_CDATA = Stream.END_CDATA
+PI = Stream.PI
+COMMENT = Stream.COMMENT
+
+
+def _ensure(stream):
+ """Ensure that every item on the stream is actually a markup event."""
+ stream = iter(stream)
+ event = stream.next()
+
+ # Check whether the iterable is a real markup event stream by examining the
+ # first item it yields; if it's not we'll need to do some conversion
+ if type(event) is not tuple or len(event) != 3:
+ for event in chain([event], stream):
+ if hasattr(event, 'totuple'):
+ event = event.totuple()
+ else:
+ event = TEXT, unicode(event), (None, -1, -1)
+ yield event
+ return
+
+ # This looks like a markup event stream, so we'll just pass it through
+ # unchanged
+ yield event
+ for event in stream:
+ yield event
+
+
+class Attrs(tuple):
+ """Immutable sequence type that stores the attributes of an element.
+
+ Ordering of the attributes is preserved, while access by name is also
+ supported.
+
+ >>> attrs = Attrs([('href', '#'), ('title', 'Foo')])
+ >>> attrs
+ Attrs([('href', '#'), ('title', 'Foo')])
+
+ >>> 'href' in attrs
+ True
+ >>> 'tabindex' in attrs
+ False
+ >>> attrs.get('title')
+ 'Foo'
+
+ Instances may not be manipulated directly. Instead, the operators ``|`` and
+ ``-`` can be used to produce new instances that have specific attributes
+ added, replaced or removed.
+
+ To remove an attribute, use the ``-`` operator. The right hand side can be
+ either a string or a set/sequence of strings, identifying the name(s) of
+ the attribute(s) to remove:
+
+ >>> attrs - 'title'
+ Attrs([('href', '#')])
+ >>> attrs - ('title', 'href')
+ Attrs()
+
+ The original instance is not modified, but the operator can of course be
+ used with an assignment:
+
+ >>> attrs
+ Attrs([('href', '#'), ('title', 'Foo')])
+ >>> attrs -= 'title'
+ >>> attrs
+ Attrs([('href', '#')])
+
+ To add a new attribute, use the ``|`` operator, where the right hand value
+ is a sequence of ``(name, value)`` tuples (which includes `Attrs`
+ instances):
+
+ >>> attrs | [('title', 'Bar')]
+ Attrs([('href', '#'), ('title', 'Bar')])
+
+ If the attributes already contain an attribute with a given name, the value
+ of that attribute is replaced:
+
+ >>> attrs | [('href', 'http://example.org/')]
+ Attrs([('href', 'http://example.org/')])
+ """
+ __slots__ = []
+
+ def __contains__(self, name):
+ """Return whether the list includes an attribute with the specified
+ name.
+
+ :return: `True` if the list includes the attribute
+ :rtype: `bool`
+ """
+ for attr, _ in self:
+ if attr == name:
+ return True
+
+ def __getitem__(self, i):
+ """Return an item or slice of the attributes list.
+
+ >>> attrs = Attrs([('href', '#'), ('title', 'Foo')])
+ >>> attrs[1]
+ ('title', 'Foo')
+ >>> attrs[1:]
+ Attrs([('title', 'Foo')])
+ """
+ items = tuple.__getitem__(self, i)
+ if type(i) is slice:
+ return Attrs(items)
+ return items
+
+ def __getslice__(self, i, j):
+ """Return a slice of the attributes list.
+
+ >>> attrs = Attrs([('href', '#'), ('title', 'Foo')])
+ >>> attrs[1:]
+ Attrs([('title', 'Foo')])
+ """
+ return Attrs(tuple.__getslice__(self, i, j))
+
+ def __or__(self, attrs):
+ """Return a new instance that contains the attributes in `attrs` in
+ addition to any already existing attributes.
+
+ :return: a new instance with the merged attributes
+ :rtype: `Attrs`
+ """
+ repl = dict([(an, av) for an, av in attrs if an in self])
+ return Attrs([(sn, repl.get(sn, sv)) for sn, sv in self] +
+ [(an, av) for an, av in attrs if an not in self])
+
+ def __repr__(self):
+ if not self:
+ return 'Attrs()'
+ return 'Attrs([%s])' % ', '.join([repr(item) for item in self])
+
+ def __sub__(self, names):
+ """Return a new instance with all attributes with a name in `names` are
+ removed.
+
+ :param names: the names of the attributes to remove
+ :return: a new instance with the attribute removed
+ :rtype: `Attrs`
+ """
+ if isinstance(names, basestring):
+ names = (names,)
+ return Attrs([(name, val) for name, val in self if name not in names])
+
+ def get(self, name, default=None):
+ """Return the value of the attribute with the specified name, or the
+ value of the `default` parameter if no such attribute is found.
+
+ :param name: the name of the attribute
+ :param default: the value to return when the attribute does not exist
+ :return: the attribute value, or the `default` value if that attribute
+ does not exist
+ :rtype: `object`
+ """
+ for attr, value in self:
+ if attr == name:
+ return value
+ return default
+
+ def totuple(self):
+ """Return the attributes as a markup event.
+
+ The returned event is a `TEXT` event, the data is the value of all
+ attributes joined together.
+
+ >>> Attrs([('href', '#'), ('title', 'Foo')]).totuple()
+ ('TEXT', '#Foo', (None, -1, -1))
+
+ :return: a `TEXT` event
+ :rtype: `tuple`
+ """
+ return TEXT, ''.join([x[1] for x in self]), (None, -1, -1)
+
+
+class Markup(unicode):
+ """Marks a string as being safe for inclusion in HTML/XML output without
+ needing to be escaped.
+ """
+ __slots__ = []
+
+ def __add__(self, other):
+ return Markup(unicode.__add__(self, escape(other)))
+
+ def __radd__(self, other):
+ return Markup(unicode.__add__(escape(other), self))
+
+ def __mod__(self, args):
+ if isinstance(args, dict):
+ args = dict(zip(args.keys(), map(escape, args.values())))
+ elif isinstance(args, (list, tuple)):
+ args = tuple(map(escape, args))
+ else:
+ args = escape(args)
+ return Markup(unicode.__mod__(self, args))
+
+ def __mul__(self, num):
+ return Markup(unicode.__mul__(self, num))
+ __rmul__ = __mul__
+
+ def __repr__(self):
+ return "<%s %s>" % (type(self).__name__, unicode.__repr__(self))
+
+ def join(self, seq, escape_quotes=True):
+ """Return a `Markup` object which is the concatenation of the strings
+ in the given sequence, where this `Markup` object is the separator
+ between the joined elements.
+
+ Any element in the sequence that is not a `Markup` instance is
+ automatically escaped.
+
+ :param seq: the sequence of strings to join
+ :param escape_quotes: whether double quote characters in the elements
+ should be escaped
+ :return: the joined `Markup` object
+ :rtype: `Markup`
+ :see: `escape`
+ """
+ return Markup(unicode.join(self, [escape(item, quotes=escape_quotes)
+ for item in seq]))
+
+ @classmethod
+ def escape(cls, text, quotes=True):
+ """Create a Markup instance from a string and escape special characters
+ it may contain (<, >, & and \").
+
+ >>> escape('"1 < 2"')
+ <Markup u'&#34;1 &lt; 2&#34;'>
+
+ If the `quotes` parameter is set to `False`, the \" character is left
+ as is. Escaping quotes is generally only required for strings that are
+ to be used in attribute values.
+
+ >>> escape('"1 < 2"', quotes=False)
+ <Markup u'"1 &lt; 2"'>
+
+ :param text: the text to escape
+ :param quotes: if ``True``, double quote characters are escaped in
+ addition to the other special characters
+ :return: the escaped `Markup` string
+ :rtype: `Markup`
+ """
+ if not text:
+ return cls()
+ if type(text) is cls:
+ return text
+ if hasattr(text, '__html__'):
+ return Markup(text.__html__())
+
+ text = text.replace('&', '&amp;') \
+ .replace('<', '&lt;') \
+ .replace('>', '&gt;')
+ if quotes:
+ text = text.replace('"', '&#34;')
+ return cls(text)
+
+ def unescape(self):
+ """Reverse-escapes &, <, >, and \" and returns a `unicode` object.
+
+ >>> Markup('1 &lt; 2').unescape()
+ u'1 < 2'
+
+ :return: the unescaped string
+ :rtype: `unicode`
+ :see: `genshi.core.unescape`
+ """
+ if not self:
+ return ''
+ return unicode(self).replace('&#34;', '"') \
+ .replace('&gt;', '>') \
+ .replace('&lt;', '<') \
+ .replace('&amp;', '&')
+
+ def stripentities(self, keepxmlentities=False):
+ """Return a copy of the text with any character or numeric entities
+ replaced by the equivalent UTF-8 characters.
+
+ If the `keepxmlentities` parameter is provided and evaluates to `True`,
+ the core XML entities (``&amp;``, ``&apos;``, ``&gt;``, ``&lt;`` and
+ ``&quot;``) are not stripped.
+
+ :return: a `Markup` instance with entities removed
+ :rtype: `Markup`
+ :see: `genshi.util.stripentities`
+ """
+ return Markup(stripentities(self, keepxmlentities=keepxmlentities))
+
+ def striptags(self):
+ """Return a copy of the text with all XML/HTML tags removed.
+
+ :return: a `Markup` instance with all tags removed
+ :rtype: `Markup`
+ :see: `genshi.util.striptags`
+ """
+ return Markup(striptags(self))
+
+
+try:
+ from genshi._speedups import Markup
+except ImportError:
+ pass # just use the Python implementation
+
+
+escape = Markup.escape
+
+
+def unescape(text):
+ """Reverse-escapes &, <, >, and \" and returns a `unicode` object.
+
+ >>> unescape(Markup('1 &lt; 2'))
+ u'1 < 2'
+
+ If the provided `text` object is not a `Markup` instance, it is returned
+ unchanged.
+
+ >>> unescape('1 &lt; 2')
+ '1 &lt; 2'
+
+ :param text: the text to unescape
+ :return: the unescsaped string
+ :rtype: `unicode`
+ """
+ if not isinstance(text, Markup):
+ return text
+ return text.unescape()
+
+
+class Namespace(object):
+ """Utility class creating and testing elements with a namespace.
+
+ Internally, namespace URIs are encoded in the `QName` of any element or
+ attribute, the namespace URI being enclosed in curly braces. This class
+ helps create and test these strings.
+
+ A `Namespace` object is instantiated with the namespace URI.
+
+ >>> html = Namespace('http://www.w3.org/1999/xhtml')
+ >>> html
+ Namespace('http://www.w3.org/1999/xhtml')
+ >>> html.uri
+ u'http://www.w3.org/1999/xhtml'
+
+ The `Namespace` object can than be used to generate `QName` objects with
+ that namespace:
+
+ >>> html.body
+ QName('http://www.w3.org/1999/xhtml}body')
+ >>> html.body.localname
+ u'body'
+ >>> html.body.namespace
+ u'http://www.w3.org/1999/xhtml'
+
+ The same works using item access notation, which is useful for element or
+ attribute names that are not valid Python identifiers:
+
+ >>> html['body']
+ QName('http://www.w3.org/1999/xhtml}body')
+
+ A `Namespace` object can also be used to test whether a specific `QName`
+ belongs to that namespace using the ``in`` operator:
+
+ >>> qname = html.body
+ >>> qname in html
+ True
+ >>> qname in Namespace('http://www.w3.org/2002/06/xhtml2')
+ False
+ """
+ def __new__(cls, uri):
+ if type(uri) is cls:
+ return uri
+ return object.__new__(cls)
+
+ def __getnewargs__(self):
+ return (self.uri,)
+
+ def __getstate__(self):
+ return self.uri
+
+ def __setstate__(self, uri):
+ self.uri = uri
+
+ def __init__(self, uri):
+ self.uri = unicode(uri)
+
+ def __contains__(self, qname):
+ return qname.namespace == self.uri
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __eq__(self, other):
+ if isinstance(other, Namespace):
+ return self.uri == other.uri
+ return self.uri == other
+
+ def __getitem__(self, name):
+ return QName(self.uri + '}' + name)
+ __getattr__ = __getitem__
+
+ def __hash__(self):
+ return hash(self.uri)
+
+ def __repr__(self):
+ return '%s(%s)' % (type(self).__name__, stringrepr(self.uri))
+
+ def __str__(self):
+ return self.uri.encode('utf-8')
+
+ def __unicode__(self):
+ return self.uri
+
+
+# The namespace used by attributes such as xml:lang and xml:space
+XML_NAMESPACE = Namespace('http://www.w3.org/XML/1998/namespace')
+
+
+class QName(unicode):
+ """A qualified element or attribute name.
+
+ The unicode value of instances of this class contains the qualified name of
+ the element or attribute, in the form ``{namespace-uri}local-name``. The
+ namespace URI can be obtained through the additional `namespace` attribute,
+ while the local name can be accessed through the `localname` attribute.
+
+ >>> qname = QName('foo')
+ >>> qname
+ QName('foo')
+ >>> qname.localname
+ u'foo'
+ >>> qname.namespace
+
+ >>> qname = QName('http://www.w3.org/1999/xhtml}body')
+ >>> qname
+ QName('http://www.w3.org/1999/xhtml}body')
+ >>> qname.localname
+ u'body'
+ >>> qname.namespace
+ u'http://www.w3.org/1999/xhtml'
+ """
+ __slots__ = ['namespace', 'localname']
+
+ def __new__(cls, qname):
+ """Create the `QName` instance.
+
+ :param qname: the qualified name as a string of the form
+ ``{namespace-uri}local-name``, where the leading curly
+ brace is optional
+ """
+ if type(qname) is cls:
+ return qname
+
+ parts = qname.lstrip('{').split('}', 1)
+ if len(parts) > 1:
+ self = unicode.__new__(cls, '{%s' % qname)
+ self.namespace, self.localname = map(unicode, parts)
+ else:
+ self = unicode.__new__(cls, qname)
+ self.namespace, self.localname = None, unicode(qname)
+ return self
+
+ def __getnewargs__(self):
+ return (self.lstrip('{'),)
+
+ def __repr__(self):
+ return '%s(%s)' % (type(self).__name__, stringrepr(self.lstrip('{')))
diff --git a/genshi/filters/__init__.py b/genshi/filters/__init__.py
new file mode 100644
index 0000000..efc2565
--- /dev/null
+++ b/genshi/filters/__init__.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Implementation of a number of stream filters."""
+
+from genshi.filters.html import HTMLFormFiller, HTMLSanitizer
+from genshi.filters.i18n import Translator
+from genshi.filters.transform import Transformer
+
+__docformat__ = 'restructuredtext en'
diff --git a/genshi/filters/html.py b/genshi/filters/html.py
new file mode 100644
index 0000000..d554a54
--- /dev/null
+++ b/genshi/filters/html.py
@@ -0,0 +1,453 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Implementation of a number of stream filters."""
+
+try:
+ any
+except NameError:
+ from genshi.util import any
+import re
+
+from genshi.core import Attrs, QName, stripentities
+from genshi.core import END, START, TEXT, COMMENT
+
+__all__ = ['HTMLFormFiller', 'HTMLSanitizer']
+__docformat__ = 'restructuredtext en'
+
+
+class HTMLFormFiller(object):
+ """A stream filter that can populate HTML forms from a dictionary of values.
+
+ >>> from genshi.input import HTML
+ >>> html = HTML('''<form>
+ ... <p><input type="text" name="foo" /></p>
+ ... </form>''')
+ >>> filler = HTMLFormFiller(data={'foo': 'bar'})
+ >>> print(html | filler)
+ <form>
+ <p><input type="text" name="foo" value="bar"/></p>
+ </form>
+ """
+ # TODO: only select the first radio button, and the first select option
+ # (if not in a multiple-select)
+ # TODO: only apply to elements in the XHTML namespace (or no namespace)?
+
+ def __init__(self, name=None, id=None, data=None, passwords=False):
+ """Create the filter.
+
+ :param name: The name of the form that should be populated. If this
+ parameter is given, only forms where the ``name`` attribute
+ value matches the parameter are processed.
+ :param id: The ID of the form that should be populated. If this
+ parameter is given, only forms where the ``id`` attribute
+ value matches the parameter are processed.
+ :param data: The dictionary of form values, where the keys are the names
+ of the form fields, and the values are the values to fill
+ in.
+ :param passwords: Whether password input fields should be populated.
+ This is off by default for security reasons (for
+ example, a password may end up in the browser cache)
+ :note: Changed in 0.5.2: added the `passwords` option
+ """
+ self.name = name
+ self.id = id
+ if data is None:
+ data = {}
+ self.data = data
+ self.passwords = passwords
+
+ def __call__(self, stream):
+ """Apply the filter to the given stream.
+
+ :param stream: the markup event stream to filter
+ """
+ in_form = in_select = in_option = in_textarea = False
+ select_value = option_value = textarea_value = None
+ option_start = None
+ option_text = []
+ no_option_value = False
+
+ for kind, data, pos in stream:
+
+ if kind is START:
+ tag, attrs = data
+ tagname = tag.localname
+
+ if tagname == 'form' and (
+ self.name and attrs.get('name') == self.name or
+ self.id and attrs.get('id') == self.id or
+ not (self.id or self.name)):
+ in_form = True
+
+ elif in_form:
+ if tagname == 'input':
+ type = attrs.get('type', '').lower()
+ if type in ('checkbox', 'radio'):
+ name = attrs.get('name')
+ if name and name in self.data:
+ value = self.data[name]
+ declval = attrs.get('value')
+ checked = False
+ if isinstance(value, (list, tuple)):
+ if declval:
+ checked = declval in [unicode(v) for v
+ in value]
+ else:
+ checked = any(value)
+ else:
+ if declval:
+ checked = declval == unicode(value)
+ elif type == 'checkbox':
+ checked = bool(value)
+ if checked:
+ attrs |= [(QName('checked'), 'checked')]
+ elif 'checked' in attrs:
+ attrs -= 'checked'
+ elif type in ('', 'hidden', 'text') \
+ or type == 'password' and self.passwords:
+ name = attrs.get('name')
+ if name and name in self.data:
+ value = self.data[name]
+ if isinstance(value, (list, tuple)):
+ value = value[0]
+ if value is not None:
+ attrs |= [
+ (QName('value'), unicode(value))
+ ]
+ elif tagname == 'select':
+ name = attrs.get('name')
+ if name in self.data:
+ select_value = self.data[name]
+ in_select = True
+ elif tagname == 'textarea':
+ name = attrs.get('name')
+ if name in self.data:
+ textarea_value = self.data.get(name)
+ if isinstance(textarea_value, (list, tuple)):
+ textarea_value = textarea_value[0]
+ in_textarea = True
+ elif in_select and tagname == 'option':
+ option_start = kind, data, pos
+ option_value = attrs.get('value')
+ if option_value is None:
+ no_option_value = True
+ option_value = ''
+ in_option = True
+ continue
+ yield kind, (tag, attrs), pos
+
+ elif in_form and kind is TEXT:
+ if in_select and in_option:
+ if no_option_value:
+ option_value += data
+ option_text.append((kind, data, pos))
+ continue
+ elif in_textarea:
+ continue
+ yield kind, data, pos
+
+ elif in_form and kind is END:
+ tagname = data.localname
+ if tagname == 'form':
+ in_form = False
+ elif tagname == 'select':
+ in_select = False
+ select_value = None
+ elif in_select and tagname == 'option':
+ if isinstance(select_value, (tuple, list)):
+ selected = option_value in [unicode(v) for v
+ in select_value]
+ else:
+ selected = option_value == unicode(select_value)
+ okind, (tag, attrs), opos = option_start
+ if selected:
+ attrs |= [(QName('selected'), 'selected')]
+ elif 'selected' in attrs:
+ attrs -= 'selected'
+ yield okind, (tag, attrs), opos
+ if option_text:
+ for event in option_text:
+ yield event
+ in_option = False
+ no_option_value = False
+ option_start = option_value = None
+ option_text = []
+ elif tagname == 'textarea':
+ if textarea_value:
+ yield TEXT, unicode(textarea_value), pos
+ in_textarea = False
+ yield kind, data, pos
+
+ else:
+ yield kind, data, pos
+
+
+class HTMLSanitizer(object):
+ """A filter that removes potentially dangerous HTML tags and attributes
+ from the stream.
+
+ >>> from genshi import HTML
+ >>> html = HTML('<div><script>alert(document.cookie)</script></div>')
+ >>> print(html | HTMLSanitizer())
+ <div/>
+
+ The default set of safe tags and attributes can be modified when the filter
+ is instantiated. For example, to allow inline ``style`` attributes, the
+ following instantation would work:
+
+ >>> html = HTML('<div style="background: #000"></div>')
+ >>> sanitizer = HTMLSanitizer(safe_attrs=HTMLSanitizer.SAFE_ATTRS | set(['style']))
+ >>> print(html | sanitizer)
+ <div style="background: #000"/>
+
+ Note that even in this case, the filter *does* attempt to remove dangerous
+ constructs from style attributes:
+
+ >>> html = HTML('<div style="background: url(javascript:void); color: #000"></div>')
+ >>> print(html | sanitizer)
+ <div style="color: #000"/>
+
+ This handles HTML entities, unicode escapes in CSS and Javascript text, as
+ well as a lot of other things. However, the style tag is still excluded by
+ default because it is very hard for such sanitizing to be completely safe,
+ especially considering how much error recovery current web browsers perform.
+
+ It also does some basic filtering of CSS properties that may be used for
+ typical phishing attacks. For more sophisticated filtering, this class
+ provides a couple of hooks that can be overridden in sub-classes.
+
+ :warn: Note that this special processing of CSS is currently only applied to
+ style attributes, **not** style elements.
+ """
+
+ SAFE_TAGS = frozenset(['a', 'abbr', 'acronym', 'address', 'area', 'b',
+ 'big', 'blockquote', 'br', 'button', 'caption', 'center', 'cite',
+ 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt',
+ 'em', 'fieldset', 'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
+ 'hr', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'map',
+ 'menu', 'ol', 'optgroup', 'option', 'p', 'pre', 'q', 's', 'samp',
+ 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table',
+ 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u',
+ 'ul', 'var'])
+
+ SAFE_ATTRS = frozenset(['abbr', 'accept', 'accept-charset', 'accesskey',
+ 'action', 'align', 'alt', 'axis', 'bgcolor', 'border', 'cellpadding',
+ 'cellspacing', 'char', 'charoff', 'charset', 'checked', 'cite', 'class',
+ 'clear', 'cols', 'colspan', 'color', 'compact', 'coords', 'datetime',
+ 'dir', 'disabled', 'enctype', 'for', 'frame', 'headers', 'height',
+ 'href', 'hreflang', 'hspace', 'id', 'ismap', 'label', 'lang',
+ 'longdesc', 'maxlength', 'media', 'method', 'multiple', 'name',
+ 'nohref', 'noshade', 'nowrap', 'prompt', 'readonly', 'rel', 'rev',
+ 'rows', 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size',
+ 'span', 'src', 'start', 'summary', 'tabindex', 'target', 'title',
+ 'type', 'usemap', 'valign', 'value', 'vspace', 'width'])
+
+ SAFE_SCHEMES = frozenset(['file', 'ftp', 'http', 'https', 'mailto', None])
+
+ URI_ATTRS = frozenset(['action', 'background', 'dynsrc', 'href', 'lowsrc',
+ 'src'])
+
+ def __init__(self, safe_tags=SAFE_TAGS, safe_attrs=SAFE_ATTRS,
+ safe_schemes=SAFE_SCHEMES, uri_attrs=URI_ATTRS):
+ """Create the sanitizer.
+
+ The exact set of allowed elements and attributes can be configured.
+
+ :param safe_tags: a set of tag names that are considered safe
+ :param safe_attrs: a set of attribute names that are considered safe
+ :param safe_schemes: a set of URI schemes that are considered safe
+ :param uri_attrs: a set of names of attributes that contain URIs
+ """
+ self.safe_tags = safe_tags
+ "The set of tag names that are considered safe."
+ self.safe_attrs = safe_attrs
+ "The set of attribute names that are considered safe."
+ self.uri_attrs = uri_attrs
+ "The set of names of attributes that may contain URIs."
+ self.safe_schemes = safe_schemes
+ "The set of URI schemes that are considered safe."
+
+ def __call__(self, stream):
+ """Apply the filter to the given stream.
+
+ :param stream: the markup event stream to filter
+ """
+ waiting_for = None
+
+ for kind, data, pos in stream:
+ if kind is START:
+ if waiting_for:
+ continue
+ tag, attrs = data
+ if not self.is_safe_elem(tag, attrs):
+ waiting_for = tag
+ continue
+
+ new_attrs = []
+ for attr, value in attrs:
+ value = stripentities(value)
+ if attr not in self.safe_attrs:
+ continue
+ elif attr in self.uri_attrs:
+ # Don't allow URI schemes such as "javascript:"
+ if not self.is_safe_uri(value):
+ continue
+ elif attr == 'style':
+ # Remove dangerous CSS declarations from inline styles
+ decls = self.sanitize_css(value)
+ if not decls:
+ continue
+ value = '; '.join(decls)
+ new_attrs.append((attr, value))
+
+ yield kind, (tag, Attrs(new_attrs)), pos
+
+ elif kind is END:
+ tag = data
+ if waiting_for:
+ if waiting_for == tag:
+ waiting_for = None
+ else:
+ yield kind, data, pos
+
+ elif kind is not COMMENT:
+ if not waiting_for:
+ yield kind, data, pos
+
+ def is_safe_css(self, propname, value):
+ """Determine whether the given css property declaration is to be
+ considered safe for inclusion in the output.
+
+ :param propname: the CSS property name
+ :param value: the value of the property
+ :return: whether the property value should be considered safe
+ :rtype: bool
+ :since: version 0.6
+ """
+ if propname == 'position':
+ return False
+ if propname.startswith('margin') and '-' in value:
+ # Negative margins can be used for phishing
+ return False
+ return True
+
+ def is_safe_elem(self, tag, attrs):
+ """Determine whether the given element should be considered safe for
+ inclusion in the output.
+
+ :param tag: the tag name of the element
+ :type tag: QName
+ :param attrs: the element attributes
+ :type attrs: Attrs
+ :return: whether the element should be considered safe
+ :rtype: bool
+ :since: version 0.6
+ """
+ if tag not in self.safe_tags:
+ return False
+ if tag.localname == 'input':
+ input_type = attrs.get('type', '').lower()
+ if input_type == 'password':
+ return False
+ return True
+
+ def is_safe_uri(self, uri):
+ """Determine whether the given URI is to be considered safe for
+ inclusion in the output.
+
+ The default implementation checks whether the scheme of the URI is in
+ the set of allowed URIs (`safe_schemes`).
+
+ >>> sanitizer = HTMLSanitizer()
+ >>> sanitizer.is_safe_uri('http://example.org/')
+ True
+ >>> sanitizer.is_safe_uri('javascript:alert(document.cookie)')
+ False
+
+ :param uri: the URI to check
+ :return: `True` if the URI can be considered safe, `False` otherwise
+ :rtype: `bool`
+ :since: version 0.4.3
+ """
+ if '#' in uri:
+ uri = uri.split('#', 1)[0] # Strip out the fragment identifier
+ if ':' not in uri:
+ return True # This is a relative URI
+ chars = [char for char in uri.split(':', 1)[0] if char.isalnum()]
+ return ''.join(chars).lower() in self.safe_schemes
+
+ def sanitize_css(self, text):
+ """Remove potentially dangerous property declarations from CSS code.
+
+ In particular, properties using the CSS ``url()`` function with a scheme
+ that is not considered safe are removed:
+
+ >>> sanitizer = HTMLSanitizer()
+ >>> sanitizer.sanitize_css(u'''
+ ... background: url(javascript:alert("foo"));
+ ... color: #000;
+ ... ''')
+ [u'color: #000']
+
+ Also, the proprietary Internet Explorer function ``expression()`` is
+ always stripped:
+
+ >>> sanitizer.sanitize_css(u'''
+ ... background: #fff;
+ ... color: #000;
+ ... width: e/**/xpression(alert("foo"));
+ ... ''')
+ [u'background: #fff', u'color: #000']
+
+ :param text: the CSS text; this is expected to be `unicode` and to not
+ contain any character or numeric references
+ :return: a list of declarations that are considered safe
+ :rtype: `list`
+ :since: version 0.4.3
+ """
+ decls = []
+ text = self._strip_css_comments(self._replace_unicode_escapes(text))
+ for decl in text.split(';'):
+ decl = decl.strip()
+ if not decl:
+ continue
+ try:
+ propname, value = decl.split(':', 1)
+ except ValueError:
+ continue
+ if not self.is_safe_css(propname.strip().lower(), value.strip()):
+ continue
+ is_evil = False
+ if 'expression' in value:
+ is_evil = True
+ for match in re.finditer(r'url\s*\(([^)]+)', value):
+ if not self.is_safe_uri(match.group(1)):
+ is_evil = True
+ break
+ if not is_evil:
+ decls.append(decl.strip())
+ return decls
+
+ _NORMALIZE_NEWLINES = re.compile(r'\r\n').sub
+ _UNICODE_ESCAPE = re.compile(r'\\([0-9a-fA-F]{1,6})\s?').sub
+
+ def _replace_unicode_escapes(self, text):
+ def _repl(match):
+ return unichr(int(match.group(1), 16))
+ return self._UNICODE_ESCAPE(_repl, self._NORMALIZE_NEWLINES('\n', text))
+
+ _CSS_COMMENTS = re.compile(r'/\*.*?\*/').sub
+
+ def _strip_css_comments(self, text):
+ return self._CSS_COMMENTS('', text)
diff --git a/genshi/filters/i18n.py b/genshi/filters/i18n.py
new file mode 100644
index 0000000..7852875
--- /dev/null
+++ b/genshi/filters/i18n.py
@@ -0,0 +1,1238 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2010 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Directives and utilities for internationalization and localization of
+templates.
+
+:since: version 0.4
+:note: Directives support added since version 0.6
+"""
+
+try:
+ any
+except NameError:
+ from genshi.util import any
+from gettext import NullTranslations
+import os
+import re
+from types import FunctionType
+
+from genshi.core import Attrs, Namespace, QName, START, END, TEXT, \
+ XML_NAMESPACE, _ensure, StreamEventKind
+from genshi.template.eval import _ast
+from genshi.template.base import DirectiveFactory, EXPR, SUB, _apply_directives
+from genshi.template.directives import Directive, StripDirective
+from genshi.template.markup import MarkupTemplate, EXEC
+
+__all__ = ['Translator', 'extract']
+__docformat__ = 'restructuredtext en'
+
+
+I18N_NAMESPACE = Namespace('http://genshi.edgewall.org/i18n')
+
+MSGBUF = StreamEventKind('MSGBUF')
+SUB_START = StreamEventKind('SUB_START')
+SUB_END = StreamEventKind('SUB_END')
+
+GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext', 'dgettext', 'dngettext',
+ 'ugettext', 'ungettext')
+
+
+class I18NDirective(Directive):
+ """Simple interface for i18n directives to support messages extraction."""
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ return _apply_directives(stream, directives, ctxt, vars)
+
+
+class ExtractableI18NDirective(I18NDirective):
+ """Simple interface for directives to support messages extraction."""
+
+ def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS,
+ search_text=True, comment_stack=None):
+ raise NotImplementedError
+
+
+class CommentDirective(I18NDirective):
+ """Implementation of the ``i18n:comment`` template directive which adds
+ translation comments.
+
+ >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
+ ... <p i18n:comment="As in Foo Bar">Foo</p>
+ ... </html>''')
+ >>> translator = Translator()
+ >>> translator.setup(tmpl)
+ >>> list(translator.extract(tmpl.stream))
+ [(2, None, u'Foo', [u'As in Foo Bar'])]
+ """
+ __slots__ = ['comment']
+
+ def __init__(self, value, template=None, namespaces=None, lineno=-1,
+ offset=-1):
+ Directive.__init__(self, None, template, namespaces, lineno, offset)
+ self.comment = value
+
+
+class MsgDirective(ExtractableI18NDirective):
+ r"""Implementation of the ``i18n:msg`` directive which marks inner content
+ as translatable. Consider the following examples:
+
+ >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
+ ... <div i18n:msg="">
+ ... <p>Foo</p>
+ ... <p>Bar</p>
+ ... </div>
+ ... <p i18n:msg="">Foo <em>bar</em>!</p>
+ ... </html>''')
+
+ >>> translator = Translator()
+ >>> translator.setup(tmpl)
+ >>> list(translator.extract(tmpl.stream))
+ [(2, None, u'[1:Foo]\n [2:Bar]', []), (6, None, u'Foo [1:bar]!', [])]
+ >>> print(tmpl.generate().render())
+ <html>
+ <div><p>Foo</p>
+ <p>Bar</p></div>
+ <p>Foo <em>bar</em>!</p>
+ </html>
+
+ >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
+ ... <div i18n:msg="fname, lname">
+ ... <p>First Name: ${fname}</p>
+ ... <p>Last Name: ${lname}</p>
+ ... </div>
+ ... <p i18n:msg="">Foo <em>bar</em>!</p>
+ ... </html>''')
+ >>> translator.setup(tmpl)
+ >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE
+ [(2, None, u'[1:First Name: %(fname)s]\n [2:Last Name: %(lname)s]', []),
+ (6, None, u'Foo [1:bar]!', [])]
+
+ >>> tmpl = MarkupTemplate('''<html xmlns:i18n="http://genshi.edgewall.org/i18n">
+ ... <div i18n:msg="fname, lname">
+ ... <p>First Name: ${fname}</p>
+ ... <p>Last Name: ${lname}</p>
+ ... </div>
+ ... <p i18n:msg="">Foo <em>bar</em>!</p>
+ ... </html>''')
+ >>> translator.setup(tmpl)
+ >>> print(tmpl.generate(fname='John', lname='Doe').render())
+ <html>
+ <div><p>First Name: John</p>
+ <p>Last Name: Doe</p></div>
+ <p>Foo <em>bar</em>!</p>
+ </html>
+
+ Starting and ending white-space is stripped of to make it simpler for
+ translators. Stripping it is not that important since it's on the html
+ source, the rendered output will remain the same.
+ """
+ __slots__ = ['params', 'lineno']
+
+ def __init__(self, value, template=None, namespaces=None, lineno=-1,
+ offset=-1):
+ Directive.__init__(self, None, template, namespaces, lineno, offset)
+ self.params = [param.strip() for param in value.split(',') if param]
+ self.lineno = lineno
+
+ @classmethod
+ def attach(cls, template, stream, value, namespaces, pos):
+ if type(value) is dict:
+ value = value.get('params', '').strip()
+ return super(MsgDirective, cls).attach(template, stream, value.strip(),
+ namespaces, pos)
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ gettext = ctxt.get('_i18n.gettext')
+ if ctxt.get('_i18n.domain'):
+ dgettext = ctxt.get('_i18n.dgettext')
+ assert hasattr(dgettext, '__call__'), \
+ 'No domain gettext function passed'
+ gettext = lambda msg: dgettext(ctxt.get('_i18n.domain'), msg)
+
+ def _generate():
+ msgbuf = MessageBuffer(self)
+ previous = stream.next()
+ if previous[0] is START:
+ yield previous
+ else:
+ msgbuf.append(*previous)
+ previous = stream.next()
+ for kind, data, pos in stream:
+ msgbuf.append(*previous)
+ previous = kind, data, pos
+ if previous[0] is not END:
+ msgbuf.append(*previous)
+ previous = None
+ for event in msgbuf.translate(gettext(msgbuf.format())):
+ yield event
+ if previous:
+ yield previous
+
+ return _apply_directives(_generate(), directives, ctxt, vars)
+
+ def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS,
+ search_text=True, comment_stack=None):
+ msgbuf = MessageBuffer(self)
+ strip = False
+
+ stream = iter(stream)
+ previous = stream.next()
+ if previous[0] is START:
+ for message in translator._extract_attrs(previous,
+ gettext_functions,
+ search_text=search_text):
+ yield message
+ previous = stream.next()
+ strip = True
+ for event in stream:
+ if event[0] is START:
+ for message in translator._extract_attrs(event,
+ gettext_functions,
+ search_text=search_text):
+ yield message
+ msgbuf.append(*previous)
+ previous = event
+ if not strip:
+ msgbuf.append(*previous)
+
+ yield self.lineno, None, msgbuf.format(), comment_stack[-1:]
+
+
+class ChooseBranchDirective(I18NDirective):
+ __slots__ = ['params']
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ self.params = ctxt.get('_i18n.choose.params', [])[:]
+ msgbuf = MessageBuffer(self)
+ stream = _apply_directives(stream, directives, ctxt, vars)
+
+ previous = stream.next()
+ if previous[0] is START:
+ yield previous
+ else:
+ msgbuf.append(*previous)
+
+ try:
+ previous = stream.next()
+ except StopIteration:
+ # For example <i18n:singular> or <i18n:plural> directives
+ yield MSGBUF, (), -1 # the place holder for msgbuf output
+ ctxt['_i18n.choose.%s' % self.tagname] = msgbuf
+ return
+
+ for event in stream:
+ msgbuf.append(*previous)
+ previous = event
+ yield MSGBUF, (), -1 # the place holder for msgbuf output
+
+ if previous[0] is END:
+ yield previous # the outer end tag
+ else:
+ msgbuf.append(*previous)
+ ctxt['_i18n.choose.%s' % self.tagname] = msgbuf
+
+ def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS,
+ search_text=True, comment_stack=None, msgbuf=None):
+ stream = iter(stream)
+ previous = stream.next()
+
+ if previous[0] is START:
+ # skip the enclosing element
+ for message in translator._extract_attrs(previous,
+ gettext_functions,
+ search_text=search_text):
+ yield message
+ previous = stream.next()
+
+ for event in stream:
+ if previous[0] is START:
+ for message in translator._extract_attrs(previous,
+ gettext_functions,
+ search_text=search_text):
+ yield message
+ msgbuf.append(*previous)
+ previous = event
+
+ if previous[0] is not END:
+ msgbuf.append(*previous)
+
+
+class SingularDirective(ChooseBranchDirective):
+ """Implementation of the ``i18n:singular`` directive to be used with the
+ ``i18n:choose`` directive."""
+
+
+class PluralDirective(ChooseBranchDirective):
+ """Implementation of the ``i18n:plural`` directive to be used with the
+ ``i18n:choose`` directive."""
+
+
+class ChooseDirective(ExtractableI18NDirective):
+ """Implementation of the ``i18n:choose`` directive which provides plural
+ internationalisation of strings.
+
+ This directive requires at least one parameter, the one which evaluates to
+ an integer which will allow to choose the plural/singular form. If you also
+ have expressions inside the singular and plural version of the string you
+ also need to pass a name for those parameters. Consider the following
+ examples:
+
+ >>> tmpl = MarkupTemplate('''\
+ <html xmlns:i18n="http://genshi.edgewall.org/i18n">
+ ... <div i18n:choose="num; num">
+ ... <p i18n:singular="">There is $num coin</p>
+ ... <p i18n:plural="">There are $num coins</p>
+ ... </div>
+ ... </html>''')
+ >>> translator = Translator()
+ >>> translator.setup(tmpl)
+ >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE
+ [(2, 'ngettext', (u'There is %(num)s coin',
+ u'There are %(num)s coins'), [])]
+
+ >>> tmpl = MarkupTemplate('''\
+ <html xmlns:i18n="http://genshi.edgewall.org/i18n">
+ ... <div i18n:choose="num; num">
+ ... <p i18n:singular="">There is $num coin</p>
+ ... <p i18n:plural="">There are $num coins</p>
+ ... </div>
+ ... </html>''')
+ >>> translator.setup(tmpl)
+ >>> print(tmpl.generate(num=1).render())
+ <html>
+ <div>
+ <p>There is 1 coin</p>
+ </div>
+ </html>
+ >>> print(tmpl.generate(num=2).render())
+ <html>
+ <div>
+ <p>There are 2 coins</p>
+ </div>
+ </html>
+
+ When used as a element and not as an attribute:
+
+ >>> tmpl = MarkupTemplate('''\
+ <html xmlns:i18n="http://genshi.edgewall.org/i18n">
+ ... <i18n:choose numeral="num" params="num">
+ ... <p i18n:singular="">There is $num coin</p>
+ ... <p i18n:plural="">There are $num coins</p>
+ ... </i18n:choose>
+ ... </html>''')
+ >>> translator.setup(tmpl)
+ >>> list(translator.extract(tmpl.stream)) #doctest: +NORMALIZE_WHITESPACE
+ [(2, 'ngettext', (u'There is %(num)s coin',
+ u'There are %(num)s coins'), [])]
+ """
+ __slots__ = ['numeral', 'params', 'lineno']
+
+ def __init__(self, value, template=None, namespaces=None, lineno=-1,
+ offset=-1):
+ Directive.__init__(self, None, template, namespaces, lineno, offset)
+ params = [v.strip() for v in value.split(';')]
+ self.numeral = self._parse_expr(params.pop(0), template, lineno, offset)
+ self.params = params and [name.strip() for name in
+ params[0].split(',') if name] or []
+ self.lineno = lineno
+
+ @classmethod
+ def attach(cls, template, stream, value, namespaces, pos):
+ if type(value) is dict:
+ numeral = value.get('numeral', '').strip()
+ assert numeral is not '', "at least pass the numeral param"
+ params = [v.strip() for v in value.get('params', '').split(',')]
+ value = '%s; ' % numeral + ', '.join(params)
+ return super(ChooseDirective, cls).attach(template, stream, value,
+ namespaces, pos)
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ ctxt.push({'_i18n.choose.params': self.params,
+ '_i18n.choose.singular': None,
+ '_i18n.choose.plural': None})
+
+ ngettext = ctxt.get('_i18n.ngettext')
+ assert hasattr(ngettext, '__call__'), 'No ngettext function available'
+ dngettext = ctxt.get('_i18n.dngettext')
+ if not dngettext:
+ dngettext = lambda d, s, p, n: ngettext(s, p, n)
+
+ new_stream = []
+ singular_stream = None
+ singular_msgbuf = None
+ plural_stream = None
+ plural_msgbuf = None
+
+ numeral = self.numeral.evaluate(ctxt)
+ is_plural = self._is_plural(numeral, ngettext)
+
+ for event in stream:
+ if event[0] is SUB and any(isinstance(d, ChooseBranchDirective)
+ for d in event[1][0]):
+ subdirectives, substream = event[1]
+
+ if isinstance(subdirectives[0], SingularDirective):
+ singular_stream = list(_apply_directives(substream,
+ subdirectives,
+ ctxt, vars))
+ new_stream.append((MSGBUF, None, (None, -1, -1)))
+
+ elif isinstance(subdirectives[0], PluralDirective):
+ if is_plural:
+ plural_stream = list(_apply_directives(substream,
+ subdirectives,
+ ctxt, vars))
+
+ else:
+ new_stream.append(event)
+
+ if ctxt.get('_i18n.domain'):
+ ngettext = lambda s, p, n: dngettext(ctxt.get('_i18n.domain'),
+ s, p, n)
+
+ singular_msgbuf = ctxt.get('_i18n.choose.singular')
+ if is_plural:
+ plural_msgbuf = ctxt.get('_i18n.choose.plural')
+ msgbuf, choice = plural_msgbuf, plural_stream
+ else:
+ msgbuf, choice = singular_msgbuf, singular_stream
+ plural_msgbuf = MessageBuffer(self)
+
+ for kind, data, pos in new_stream:
+ if kind is MSGBUF:
+ for event in choice:
+ if event[0] is MSGBUF:
+ translation = ngettext(singular_msgbuf.format(),
+ plural_msgbuf.format(),
+ numeral)
+ for subevent in msgbuf.translate(translation):
+ yield subevent
+ else:
+ yield event
+ else:
+ yield kind, data, pos
+
+ ctxt.pop()
+
+ def extract(self, translator, stream, gettext_functions=GETTEXT_FUNCTIONS,
+ search_text=True, comment_stack=None):
+ strip = False
+ stream = iter(stream)
+ previous = stream.next()
+
+ if previous[0] is START:
+ # skip the enclosing element
+ for message in translator._extract_attrs(previous,
+ gettext_functions,
+ search_text=search_text):
+ yield message
+ previous = stream.next()
+ strip = True
+
+ singular_msgbuf = MessageBuffer(self)
+ plural_msgbuf = MessageBuffer(self)
+
+ for event in stream:
+ if previous[0] is SUB:
+ directives, substream = previous[1]
+ for directive in directives:
+ if isinstance(directive, SingularDirective):
+ for message in directive.extract(translator,
+ substream, gettext_functions, search_text,
+ comment_stack, msgbuf=singular_msgbuf):
+ yield message
+ elif isinstance(directive, PluralDirective):
+ for message in directive.extract(translator,
+ substream, gettext_functions, search_text,
+ comment_stack, msgbuf=plural_msgbuf):
+ yield message
+ elif not isinstance(directive, StripDirective):
+ singular_msgbuf.append(*previous)
+ plural_msgbuf.append(*previous)
+ else:
+ if previous[0] is START:
+ for message in translator._extract_attrs(previous,
+ gettext_functions,
+ search_text):
+ yield message
+ singular_msgbuf.append(*previous)
+ plural_msgbuf.append(*previous)
+ previous = event
+
+ if not strip:
+ singular_msgbuf.append(*previous)
+ plural_msgbuf.append(*previous)
+
+ yield self.lineno, 'ngettext', \
+ (singular_msgbuf.format(), plural_msgbuf.format()), \
+ comment_stack[-1:]
+
+ def _is_plural(self, numeral, ngettext):
+ # XXX: should we test which form was chosen like this!?!?!?
+ # There should be no match in any catalogue for these singular and
+ # plural test strings
+ singular = u'O\x85\xbe\xa9\xa8az\xc3?\xe6\xa1\x02n\x84\x93'
+ plural = u'\xcc\xfb+\xd3Pn\x9d\tT\xec\x1d\xda\x1a\x88\x00'
+ return ngettext(singular, plural, numeral) == plural
+
+
+class DomainDirective(I18NDirective):
+ """Implementation of the ``i18n:domain`` directive which allows choosing
+ another i18n domain(catalog) to translate from.
+
+ >>> from genshi.filters.tests.i18n import DummyTranslations
+ >>> tmpl = MarkupTemplate('''\
+ <html xmlns:i18n="http://genshi.edgewall.org/i18n">
+ ... <p i18n:msg="">Bar</p>
+ ... <div i18n:domain="foo">
+ ... <p i18n:msg="">FooBar</p>
+ ... <p>Bar</p>
+ ... <p i18n:domain="bar" i18n:msg="">Bar</p>
+ ... <p i18n:domain="">Bar</p>
+ ... </div>
+ ... <p>Bar</p>
+ ... </html>''')
+
+ >>> translations = DummyTranslations({'Bar': 'Voh'})
+ >>> translations.add_domain('foo', {'FooBar': 'BarFoo', 'Bar': 'foo_Bar'})
+ >>> translations.add_domain('bar', {'Bar': 'bar_Bar'})
+ >>> translator = Translator(translations)
+ >>> translator.setup(tmpl)
+
+ >>> print(tmpl.generate().render())
+ <html>
+ <p>Voh</p>
+ <div>
+ <p>BarFoo</p>
+ <p>foo_Bar</p>
+ <p>bar_Bar</p>
+ <p>Voh</p>
+ </div>
+ <p>Voh</p>
+ </html>
+ """
+ __slots__ = ['domain']
+
+ def __init__(self, value, template=None, namespaces=None, lineno=-1,
+ offset=-1):
+ Directive.__init__(self, None, template, namespaces, lineno, offset)
+ self.domain = value and value.strip() or '__DEFAULT__'
+
+ @classmethod
+ def attach(cls, template, stream, value, namespaces, pos):
+ if type(value) is dict:
+ value = value.get('name')
+ return super(DomainDirective, cls).attach(template, stream, value,
+ namespaces, pos)
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ ctxt.push({'_i18n.domain': self.domain})
+ for event in _apply_directives(stream, directives, ctxt, vars):
+ yield event
+ ctxt.pop()
+
+
+class Translator(DirectiveFactory):
+ """Can extract and translate localizable strings from markup streams and
+ templates.
+
+ For example, assume the following template:
+
+ >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/">
+ ... <head>
+ ... <title>Example</title>
+ ... </head>
+ ... <body>
+ ... <h1>Example</h1>
+ ... <p>${_("Hello, %(name)s") % dict(name=username)}</p>
+ ... </body>
+ ... </html>''', filename='example.html')
+
+ For demonstration, we define a dummy ``gettext``-style function with a
+ hard-coded translation table, and pass that to the `Translator` initializer:
+
+ >>> def pseudo_gettext(string):
+ ... return {
+ ... 'Example': 'Beispiel',
+ ... 'Hello, %(name)s': 'Hallo, %(name)s'
+ ... }[string]
+ >>> translator = Translator(pseudo_gettext)
+
+ Next, the translator needs to be prepended to any already defined filters
+ on the template:
+
+ >>> tmpl.filters.insert(0, translator)
+
+ When generating the template output, our hard-coded translations should be
+ applied as expected:
+
+ >>> print(tmpl.generate(username='Hans', _=pseudo_gettext))
+ <html>
+ <head>
+ <title>Beispiel</title>
+ </head>
+ <body>
+ <h1>Beispiel</h1>
+ <p>Hallo, Hans</p>
+ </body>
+ </html>
+
+ Note that elements defining ``xml:lang`` attributes that do not contain
+ variable expressions are ignored by this filter. That can be used to
+ exclude specific parts of a template from being extracted and translated.
+ """
+
+ directives = [
+ ('domain', DomainDirective),
+ ('comment', CommentDirective),
+ ('msg', MsgDirective),
+ ('choose', ChooseDirective),
+ ('singular', SingularDirective),
+ ('plural', PluralDirective)
+ ]
+
+ IGNORE_TAGS = frozenset([
+ QName('script'), QName('http://www.w3.org/1999/xhtml}script'),
+ QName('style'), QName('http://www.w3.org/1999/xhtml}style')
+ ])
+ INCLUDE_ATTRS = frozenset([
+ 'abbr', 'alt', 'label', 'prompt', 'standby', 'summary', 'title'
+ ])
+ NAMESPACE = I18N_NAMESPACE
+
+ def __init__(self, translate=NullTranslations(), ignore_tags=IGNORE_TAGS,
+ include_attrs=INCLUDE_ATTRS, extract_text=True):
+ """Initialize the translator.
+
+ :param translate: the translation function, for example ``gettext`` or
+ ``ugettext``.
+ :param ignore_tags: a set of tag names that should not be localized
+ :param include_attrs: a set of attribute names should be localized
+ :param extract_text: whether the content of text nodes should be
+ extracted, or only text in explicit ``gettext``
+ function calls
+
+ :note: Changed in 0.6: the `translate` parameter can now be either
+ a ``gettext``-style function, or an object compatible with the
+ ``NullTransalations`` or ``GNUTranslations`` interface
+ """
+ self.translate = translate
+ self.ignore_tags = ignore_tags
+ self.include_attrs = include_attrs
+ self.extract_text = extract_text
+
+ def __call__(self, stream, ctxt=None, translate_text=True,
+ translate_attrs=True):
+ """Translate any localizable strings in the given stream.
+
+ This function shouldn't be called directly. Instead, an instance of
+ the `Translator` class should be registered as a filter with the
+ `Template` or the `TemplateLoader`, or applied as a regular stream
+ filter. If used as a template filter, it should be inserted in front of
+ all the default filters.
+
+ :param stream: the markup event stream
+ :param ctxt: the template context (not used)
+ :param translate_text: whether text nodes should be translated (used
+ internally)
+ :param translate_attrs: whether attribute values should be translated
+ (used internally)
+ :return: the localized stream
+ """
+ ignore_tags = self.ignore_tags
+ include_attrs = self.include_attrs
+ skip = 0
+ xml_lang = XML_NAMESPACE['lang']
+ if not self.extract_text:
+ translate_text = False
+ translate_attrs = False
+
+ if type(self.translate) is FunctionType:
+ gettext = self.translate
+ if ctxt:
+ ctxt['_i18n.gettext'] = gettext
+ else:
+ gettext = self.translate.ugettext
+ ngettext = self.translate.ungettext
+ try:
+ dgettext = self.translate.dugettext
+ dngettext = self.translate.dungettext
+ except AttributeError:
+ dgettext = lambda _, y: gettext(y)
+ dngettext = lambda _, s, p, n: ngettext(s, p, n)
+ if ctxt:
+ ctxt['_i18n.gettext'] = gettext
+ ctxt['_i18n.ngettext'] = ngettext
+ ctxt['_i18n.dgettext'] = dgettext
+ ctxt['_i18n.dngettext'] = dngettext
+
+ if ctxt and ctxt.get('_i18n.domain'):
+ gettext = lambda msg: dgettext(ctxt.get('_i18n.domain'), msg)
+
+ for kind, data, pos in stream:
+
+ # skip chunks that should not be localized
+ if skip:
+ if kind is START:
+ skip += 1
+ elif kind is END:
+ skip -= 1
+ yield kind, data, pos
+ continue
+
+ # handle different events that can be localized
+ if kind is START:
+ tag, attrs = data
+ if tag in self.ignore_tags or \
+ isinstance(attrs.get(xml_lang), basestring):
+ skip += 1
+ yield kind, data, pos
+ continue
+
+ new_attrs = []
+ changed = False
+
+ for name, value in attrs:
+ newval = value
+ if isinstance(value, basestring):
+ if translate_attrs and name in include_attrs:
+ newval = gettext(value)
+ else:
+ newval = list(
+ self(_ensure(value), ctxt, translate_text=False)
+ )
+ if newval != value:
+ value = newval
+ changed = True
+ new_attrs.append((name, value))
+ if changed:
+ attrs = Attrs(new_attrs)
+
+ yield kind, (tag, attrs), pos
+
+ elif translate_text and kind is TEXT:
+ text = data.strip()
+ if text:
+ data = data.replace(text, unicode(gettext(text)))
+ yield kind, data, pos
+
+ elif kind is SUB:
+ directives, substream = data
+ current_domain = None
+ for idx, directive in enumerate(directives):
+ # Organize directives to make everything work
+ # FIXME: There's got to be a better way to do this!
+ if isinstance(directive, DomainDirective):
+ # Grab current domain and update context
+ current_domain = directive.domain
+ ctxt.push({'_i18n.domain': current_domain})
+ # Put domain directive as the first one in order to
+ # update context before any other directives evaluation
+ directives.insert(0, directives.pop(idx))
+
+ # If this is an i18n directive, no need to translate text
+ # nodes here
+ is_i18n_directive = any([
+ isinstance(d, ExtractableI18NDirective)
+ for d in directives
+ ])
+ substream = list(self(substream, ctxt,
+ translate_text=not is_i18n_directive,
+ translate_attrs=translate_attrs))
+ yield kind, (directives, substream), pos
+
+ if current_domain:
+ ctxt.pop()
+ else:
+ yield kind, data, pos
+
+ def extract(self, stream, gettext_functions=GETTEXT_FUNCTIONS,
+ search_text=True, comment_stack=None):
+ """Extract localizable strings from the given template stream.
+
+ For every string found, this function yields a ``(lineno, function,
+ message, comments)`` tuple, where:
+
+ * ``lineno`` is the number of the line on which the string was found,
+ * ``function`` is the name of the ``gettext`` function used (if the
+ string was extracted from embedded Python code), and
+ * ``message`` is the string itself (a ``unicode`` object, or a tuple
+ of ``unicode`` objects for functions with multiple string
+ arguments).
+ * ``comments`` is a list of comments related to the message, extracted
+ from ``i18n:comment`` attributes found in the markup
+
+ >>> tmpl = MarkupTemplate('''<html xmlns:py="http://genshi.edgewall.org/">
+ ... <head>
+ ... <title>Example</title>
+ ... </head>
+ ... <body>
+ ... <h1>Example</h1>
+ ... <p>${_("Hello, %(name)s") % dict(name=username)}</p>
+ ... <p>${ngettext("You have %d item", "You have %d items", num)}</p>
+ ... </body>
+ ... </html>''', filename='example.html')
+ >>> for line, func, msg, comments in Translator().extract(tmpl.stream):
+ ... print('%d, %r, %r' % (line, func, msg))
+ 3, None, u'Example'
+ 6, None, u'Example'
+ 7, '_', u'Hello, %(name)s'
+ 8, 'ngettext', (u'You have %d item', u'You have %d items', None)
+
+ :param stream: the event stream to extract strings from; can be a
+ regular stream or a template stream
+ :param gettext_functions: a sequence of function names that should be
+ treated as gettext-style localization
+ functions
+ :param search_text: whether the content of text nodes should be
+ extracted (used internally)
+
+ :note: Changed in 0.4.1: For a function with multiple string arguments
+ (such as ``ngettext``), a single item with a tuple of strings is
+ yielded, instead an item for each string argument.
+ :note: Changed in 0.6: The returned tuples now include a fourth
+ element, which is a list of comments for the translator.
+ """
+ if not self.extract_text:
+ search_text = False
+ if comment_stack is None:
+ comment_stack = []
+ skip = 0
+
+ xml_lang = XML_NAMESPACE['lang']
+
+ for kind, data, pos in stream:
+ if skip:
+ if kind is START:
+ skip += 1
+ if kind is END:
+ skip -= 1
+
+ if kind is START and not skip:
+ tag, attrs = data
+ if tag in self.ignore_tags or \
+ isinstance(attrs.get(xml_lang), basestring):
+ skip += 1
+ continue
+
+ for message in self._extract_attrs((kind, data, pos),
+ gettext_functions,
+ search_text=search_text):
+ yield message
+
+ elif not skip and search_text and kind is TEXT:
+ text = data.strip()
+ if text and [ch for ch in text if ch.isalpha()]:
+ yield pos[1], None, text, comment_stack[-1:]
+
+ elif kind is EXPR or kind is EXEC:
+ for funcname, strings in extract_from_code(data,
+ gettext_functions):
+ # XXX: Do we need to grab i18n:comment from comment_stack ???
+ yield pos[1], funcname, strings, []
+
+ elif kind is SUB:
+ directives, substream = data
+ in_comment = False
+
+ for idx, directive in enumerate(directives):
+ # Do a first loop to see if there's a comment directive
+ # If there is update context and pop it from directives
+ if isinstance(directive, CommentDirective):
+ in_comment = True
+ comment_stack.append(directive.comment)
+ if len(directives) == 1:
+ # in case we're in the presence of something like:
+ # <p i18n:comment="foo">Foo</p>
+ for message in self.extract(
+ substream, gettext_functions,
+ search_text=search_text and not skip,
+ comment_stack=comment_stack):
+ yield message
+ directives.pop(idx)
+ elif not isinstance(directive, I18NDirective):
+ # Remove all other non i18n directives from the process
+ directives.pop(idx)
+
+ if not directives and not in_comment:
+ # Extract content if there's no directives because
+ # strip was pop'ed and not because comment was pop'ed.
+ # Extraction in this case has been taken care of.
+ for message in self.extract(
+ substream, gettext_functions,
+ search_text=search_text and not skip):
+ yield message
+
+ for directive in directives:
+ if isinstance(directive, ExtractableI18NDirective):
+ for message in directive.extract(self,
+ substream, gettext_functions,
+ search_text=search_text and not skip,
+ comment_stack=comment_stack):
+ yield message
+ else:
+ for message in self.extract(
+ substream, gettext_functions,
+ search_text=search_text and not skip,
+ comment_stack=comment_stack):
+ yield message
+
+ if in_comment:
+ comment_stack.pop()
+
+ def get_directive_index(self, dir_cls):
+ total = len(self._dir_order)
+ if dir_cls in self._dir_order:
+ return self._dir_order.index(dir_cls) - total
+ return total
+
+ def setup(self, template):
+ """Convenience function to register the `Translator` filter and the
+ related directives with the given template.
+
+ :param template: a `Template` instance
+ """
+ template.filters.insert(0, self)
+ if hasattr(template, 'add_directives'):
+ template.add_directives(Translator.NAMESPACE, self)
+
+ def _extract_attrs(self, event, gettext_functions, search_text):
+ for name, value in event[1][1]:
+ if search_text and isinstance(value, basestring):
+ if name in self.include_attrs:
+ text = value.strip()
+ if text:
+ yield event[2][1], None, text, []
+ else:
+ for message in self.extract(_ensure(value), gettext_functions,
+ search_text=False):
+ yield message
+
+
+class MessageBuffer(object):
+ """Helper class for managing internationalized mixed content.
+
+ :since: version 0.5
+ """
+
+ def __init__(self, directive=None):
+ """Initialize the message buffer.
+
+ :param directive: the directive owning the buffer
+ :type directive: I18NDirective
+ """
+ # params list needs to be copied so that directives can be evaluated
+ # more than once
+ self.orig_params = self.params = directive.params[:]
+ self.directive = directive
+ self.string = []
+ self.events = {}
+ self.values = {}
+ self.depth = 1
+ self.order = 1
+ self.stack = [0]
+ self.subdirectives = {}
+
+ def append(self, kind, data, pos):
+ """Append a stream event to the buffer.
+
+ :param kind: the stream event kind
+ :param data: the event data
+ :param pos: the position of the event in the source
+ """
+ if kind is SUB:
+ # The order needs to be +1 because a new START kind event will
+ # happen and we we need to wrap those events into our custom kind(s)
+ order = self.stack[-1] + 1
+ subdirectives, substream = data
+ # Store the directives that should be applied after translation
+ self.subdirectives.setdefault(order, []).extend(subdirectives)
+ self.events.setdefault(order, []).append((SUB_START, None, pos))
+ for skind, sdata, spos in substream:
+ self.append(skind, sdata, spos)
+ self.events.setdefault(order, []).append((SUB_END, None, pos))
+ elif kind is TEXT:
+ if '[' in data or ']' in data:
+ # Quote [ and ] if it ain't us adding it, ie, if the user is
+ # using those chars in his templates, escape them
+ data = data.replace('[', '\[').replace(']', '\]')
+ self.string.append(data)
+ self.events.setdefault(self.stack[-1], []).append((kind, data, pos))
+ elif kind is EXPR:
+ if self.params:
+ param = self.params.pop(0)
+ else:
+ params = ', '.join(['"%s"' % p for p in self.orig_params if p])
+ if params:
+ params = "(%s)" % params
+ raise IndexError("%d parameters%s given to 'i18n:%s' but "
+ "%d or more expressions used in '%s', line %s"
+ % (len(self.orig_params), params,
+ self.directive.tagname,
+ len(self.orig_params) + 1,
+ os.path.basename(pos[0] or
+ 'In-memory Template'),
+ pos[1]))
+ self.string.append('%%(%s)s' % param)
+ self.events.setdefault(self.stack[-1], []).append((kind, data, pos))
+ self.values[param] = (kind, data, pos)
+ else:
+ if kind is START:
+ self.string.append('[%d:' % self.order)
+ self.stack.append(self.order)
+ self.events.setdefault(self.stack[-1],
+ []).append((kind, data, pos))
+ self.depth += 1
+ self.order += 1
+ elif kind is END:
+ self.depth -= 1
+ if self.depth:
+ self.events[self.stack[-1]].append((kind, data, pos))
+ self.string.append(']')
+ self.stack.pop()
+
+ def format(self):
+ """Return a message identifier representing the content in the
+ buffer.
+ """
+ return ''.join(self.string).strip()
+
+ def translate(self, string, regex=re.compile(r'%\((\w+)\)s')):
+ """Interpolate the given message translation with the events in the
+ buffer and return the translated stream.
+
+ :param string: the translated message string
+ """
+ substream = None
+
+ def yield_parts(string):
+ for idx, part in enumerate(regex.split(string)):
+ if idx % 2:
+ yield self.values[part]
+ elif part:
+ yield (TEXT,
+ part.replace('\[', '[').replace('\]', ']'),
+ (None, -1, -1)
+ )
+
+ parts = parse_msg(string)
+ parts_counter = {}
+ for order, string in parts:
+ parts_counter.setdefault(order, []).append(None)
+
+ while parts:
+ order, string = parts.pop(0)
+ if len(parts_counter[order]) == 1:
+ events = self.events[order]
+ else:
+ events = [self.events[order].pop(0)]
+ parts_counter[order].pop()
+
+ for event in events:
+ if event[0] is SUB_START:
+ substream = []
+ elif event[0] is SUB_END:
+ # Yield a substream which might have directives to be
+ # applied to it (after translation events)
+ yield SUB, (self.subdirectives[order], substream), event[2]
+ substream = None
+ elif event[0] is TEXT:
+ if string:
+ for part in yield_parts(string):
+ if substream is not None:
+ substream.append(part)
+ else:
+ yield part
+ # String handled, reset it
+ string = None
+ elif event[0] is START:
+ if substream is not None:
+ substream.append(event)
+ else:
+ yield event
+ if string:
+ for part in yield_parts(string):
+ if substream is not None:
+ substream.append(part)
+ else:
+ yield part
+ # String handled, reset it
+ string = None
+ elif event[0] is END:
+ if string:
+ for part in yield_parts(string):
+ if substream is not None:
+ substream.append(part)
+ else:
+ yield part
+ # String handled, reset it
+ string = None
+ if substream is not None:
+ substream.append(event)
+ else:
+ yield event
+ elif event[0] is EXPR:
+ # These are handled on the strings itself
+ continue
+ else:
+ if string:
+ for part in yield_parts(string):
+ if substream is not None:
+ substream.append(part)
+ else:
+ yield part
+ # String handled, reset it
+ string = None
+ if substream is not None:
+ substream.append(event)
+ else:
+ yield event
+
+
+def parse_msg(string, regex=re.compile(r'(?:\[(\d+)\:)|(?<!\\)\]')):
+ """Parse a translated message using Genshi mixed content message
+ formatting.
+
+ >>> parse_msg("See [1:Help].")
+ [(0, 'See '), (1, 'Help'), (0, '.')]
+
+ >>> parse_msg("See [1:our [2:Help] page] for details.")
+ [(0, 'See '), (1, 'our '), (2, 'Help'), (1, ' page'), (0, ' for details.')]
+
+ >>> parse_msg("[2:Details] finden Sie in [1:Hilfe].")
+ [(2, 'Details'), (0, ' finden Sie in '), (1, 'Hilfe'), (0, '.')]
+
+ >>> parse_msg("[1:] Bilder pro Seite anzeigen.")
+ [(1, ''), (0, ' Bilder pro Seite anzeigen.')]
+
+ :param string: the translated message string
+ :return: a list of ``(order, string)`` tuples
+ :rtype: `list`
+ """
+ parts = []
+ stack = [0]
+ while True:
+ mo = regex.search(string)
+ if not mo:
+ break
+
+ if mo.start() or stack[-1]:
+ parts.append((stack[-1], string[:mo.start()]))
+ string = string[mo.end():]
+
+ orderno = mo.group(1)
+ if orderno is not None:
+ stack.append(int(orderno))
+ else:
+ stack.pop()
+ if not stack:
+ break
+
+ if string:
+ parts.append((stack[-1], string))
+
+ return parts
+
+
+def extract_from_code(code, gettext_functions):
+ """Extract strings from Python bytecode.
+
+ >>> from genshi.template.eval import Expression
+ >>> expr = Expression('_("Hello")')
+ >>> list(extract_from_code(expr, GETTEXT_FUNCTIONS))
+ [('_', u'Hello')]
+
+ >>> expr = Expression('ngettext("You have %(num)s item", '
+ ... '"You have %(num)s items", num)')
+ >>> list(extract_from_code(expr, GETTEXT_FUNCTIONS))
+ [('ngettext', (u'You have %(num)s item', u'You have %(num)s items', None))]
+
+ :param code: the `Code` object
+ :type code: `genshi.template.eval.Code`
+ :param gettext_functions: a sequence of function names
+ :since: version 0.5
+ """
+ def _walk(node):
+ if isinstance(node, _ast.Call) and isinstance(node.func, _ast.Name) \
+ and node.func.id in gettext_functions:
+ strings = []
+ def _add(arg):
+ if isinstance(arg, _ast.Str) and isinstance(arg.s, basestring):
+ strings.append(unicode(arg.s, 'utf-8'))
+ elif arg:
+ strings.append(None)
+ [_add(arg) for arg in node.args]
+ _add(node.starargs)
+ _add(node.kwargs)
+ if len(strings) == 1:
+ strings = strings[0]
+ else:
+ strings = tuple(strings)
+ yield node.func.id, strings
+ elif node._fields:
+ children = []
+ for field in node._fields:
+ child = getattr(node, field, None)
+ if isinstance(child, list):
+ for elem in child:
+ children.append(elem)
+ elif isinstance(child, _ast.AST):
+ children.append(child)
+ for child in children:
+ for funcname, strings in _walk(child):
+ yield funcname, strings
+ return _walk(code.ast)
+
+
+def extract(fileobj, keywords, comment_tags, options):
+ """Babel extraction method for Genshi templates.
+
+ :param fileobj: the file-like object the messages should be extracted from
+ :param keywords: a list of keywords (i.e. function names) that should be
+ recognized as translation functions
+ :param comment_tags: a list of translator tags to search for and include
+ in the results
+ :param options: a dictionary of additional options (optional)
+ :return: an iterator over ``(lineno, funcname, message, comments)`` tuples
+ :rtype: ``iterator``
+ """
+ template_class = options.get('template_class', MarkupTemplate)
+ if isinstance(template_class, basestring):
+ module, clsname = template_class.split(':', 1)
+ template_class = getattr(__import__(module, {}, {}, [clsname]), clsname)
+ encoding = options.get('encoding', None)
+
+ extract_text = options.get('extract_text', True)
+ if isinstance(extract_text, basestring):
+ extract_text = extract_text.lower() in ('1', 'on', 'yes', 'true')
+
+ ignore_tags = options.get('ignore_tags', Translator.IGNORE_TAGS)
+ if isinstance(ignore_tags, basestring):
+ ignore_tags = ignore_tags.split()
+ ignore_tags = [QName(tag) for tag in ignore_tags]
+
+ include_attrs = options.get('include_attrs', Translator.INCLUDE_ATTRS)
+ if isinstance(include_attrs, basestring):
+ include_attrs = include_attrs.split()
+ include_attrs = [QName(attr) for attr in include_attrs]
+
+ tmpl = template_class(fileobj, filename=getattr(fileobj, 'name', None),
+ encoding=encoding)
+ tmpl.loader = None
+
+ translator = Translator(None, ignore_tags, include_attrs, extract_text)
+ if hasattr(tmpl, 'add_directives'):
+ tmpl.add_directives(Translator.NAMESPACE, translator)
+ for message in translator.extract(tmpl.stream, gettext_functions=keywords):
+ yield message
diff --git a/genshi/filters/transform.py b/genshi/filters/transform.py
new file mode 100644
index 0000000..9b75b06
--- /dev/null
+++ b/genshi/filters/transform.py
@@ -0,0 +1,1310 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""A filter for functional-style transformations of markup streams.
+
+The `Transformer` filter provides a variety of transformations that can be
+applied to parts of streams that match given XPath expressions. These
+transformations can be chained to achieve results that would be comparitively
+tedious to achieve by writing stream filters by hand. The approach of chaining
+node selection and transformation has been inspired by the `jQuery`_ Javascript
+library.
+
+ .. _`jQuery`: http://jquery.com/
+
+For example, the following transformation removes the ``<title>`` element from
+the ``<head>`` of the input document:
+
+>>> from genshi.builder import tag
+>>> html = HTML('''<html>
+... <head><title>Some Title</title></head>
+... <body>
+... Some <em>body</em> text.
+... </body>
+... </html>''')
+>>> print(html | Transformer('body/em').map(unicode.upper, TEXT)
+... .unwrap().wrap(tag.u))
+<html>
+ <head><title>Some Title</title></head>
+ <body>
+ Some <u>BODY</u> text.
+ </body>
+</html>
+
+The ``Transformer`` support a large number of useful transformations out of the
+box, but custom transformations can be added easily.
+
+:since: version 0.5
+"""
+
+import re
+import sys
+
+from genshi.builder import Element
+from genshi.core import Stream, Attrs, QName, TEXT, START, END, _ensure, Markup
+from genshi.path import Path
+
+__all__ = ['Transformer', 'StreamBuffer', 'InjectorTransformation', 'ENTER',
+ 'EXIT', 'INSIDE', 'OUTSIDE', 'BREAK']
+
+
+class TransformMark(str):
+ """A mark on a transformation stream."""
+ __slots__ = []
+ _instances = {}
+
+ def __new__(cls, val):
+ return cls._instances.setdefault(val, str.__new__(cls, val))
+
+
+ENTER = TransformMark('ENTER')
+"""Stream augmentation mark indicating that a selected element is being
+entered."""
+
+INSIDE = TransformMark('INSIDE')
+"""Stream augmentation mark indicating that processing is currently inside a
+selected element."""
+
+OUTSIDE = TransformMark('OUTSIDE')
+"""Stream augmentation mark indicating that a match occurred outside a selected
+element."""
+
+ATTR = TransformMark('ATTR')
+"""Stream augmentation mark indicating a selected element attribute."""
+
+EXIT = TransformMark('EXIT')
+"""Stream augmentation mark indicating that a selected element is being
+exited."""
+
+BREAK = TransformMark('BREAK')
+"""Stream augmentation mark indicating a break between two otherwise contiguous
+blocks of marked events.
+
+This is used primarily by the cut() transform to provide later transforms with
+an opportunity to operate on the cut buffer.
+"""
+
+
+class PushBackStream(object):
+ """Allows a single event to be pushed back onto the stream and re-consumed.
+ """
+ def __init__(self, stream):
+ self.stream = iter(stream)
+ self.peek = None
+
+ def push(self, event):
+ assert self.peek is None
+ self.peek = event
+
+ def __iter__(self):
+ while True:
+ if self.peek is not None:
+ peek = self.peek
+ self.peek = None
+ yield peek
+ else:
+ try:
+ event = self.stream.next()
+ yield event
+ except StopIteration:
+ if self.peek is None:
+ raise
+
+
+class Transformer(object):
+ """Stream filter that can apply a variety of different transformations to
+ a stream.
+
+ This is achieved by selecting the events to be transformed using XPath,
+ then applying the transformations to the events matched by the path
+ expression. Each marked event is in the form (mark, (kind, data, pos)),
+ where mark can be any of `ENTER`, `INSIDE`, `EXIT`, `OUTSIDE`, or `None`.
+
+ The first three marks match `START` and `END` events, and any events
+ contained `INSIDE` any selected XML/HTML element. A non-element match
+ outside a `START`/`END` container (e.g. ``text()``) will yield an `OUTSIDE`
+ mark.
+
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body>Some <em>body</em> text.</body></html>')
+
+ Transformations act on selected stream events matching an XPath expression.
+ Here's an example of removing some markup (the title, in this case)
+ selected by an expression:
+
+ >>> print(html | Transformer('head/title').remove())
+ <html><head/><body>Some <em>body</em> text.</body></html>
+
+ Inserted content can be passed in the form of a string, or a markup event
+ stream, which includes streams generated programmatically via the
+ `builder` module:
+
+ >>> from genshi.builder import tag
+ >>> print(html | Transformer('body').prepend(tag.h1('Document Title')))
+ <html><head><title>Some Title</title></head><body><h1>Document
+ Title</h1>Some <em>body</em> text.</body></html>
+
+ Each XPath expression determines the set of tags that will be acted upon by
+ subsequent transformations. In this example we select the ``<title>`` text,
+ copy it into a buffer, then select the ``<body>`` element and paste the
+ copied text into the body as ``<h1>`` enclosed text:
+
+ >>> buffer = StreamBuffer()
+ >>> print(html | Transformer('head/title/text()').copy(buffer)
+ ... .end().select('body').prepend(tag.h1(buffer)))
+ <html><head><title>Some Title</title></head><body><h1>Some Title</h1>Some
+ <em>body</em> text.</body></html>
+
+ Transformations can also be assigned and reused, although care must be
+ taken when using buffers, to ensure that buffers are cleared between
+ transforms:
+
+ >>> emphasis = Transformer('body//em').attr('class', 'emphasis')
+ >>> print(html | emphasis)
+ <html><head><title>Some Title</title></head><body>Some <em
+ class="emphasis">body</em> text.</body></html>
+ """
+
+ __slots__ = ['transforms']
+
+ def __init__(self, path='.'):
+ """Construct a new transformation filter.
+
+ :param path: an XPath expression (as string) or a `Path` instance
+ """
+ self.transforms = [SelectTransformation(path)]
+
+ def __call__(self, stream, keep_marks=False):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: the marked event stream to filter
+ :param keep_marks: Do not strip transformer selection marks from the
+ stream. Useful for testing.
+ :return: the transformed stream
+ :rtype: `Stream`
+ """
+ transforms = self._mark(stream)
+ for link in self.transforms:
+ transforms = link(transforms)
+ if not keep_marks:
+ transforms = self._unmark(transforms)
+ return Stream(transforms,
+ serializer=getattr(stream, 'serializer', None))
+
+ def apply(self, function):
+ """Apply a transformation to the stream.
+
+ Transformations can be chained, similar to stream filters. Any callable
+ accepting a marked stream can be used as a transform.
+
+ As an example, here is a simple `TEXT` event upper-casing transform:
+
+ >>> def upper(stream):
+ ... for mark, (kind, data, pos) in stream:
+ ... if mark and kind is TEXT:
+ ... yield mark, (kind, data.upper(), pos)
+ ... else:
+ ... yield mark, (kind, data, pos)
+ >>> short_stream = HTML('<body>Some <em>test</em> text</body>')
+ >>> print(short_stream | Transformer('.//em/text()').apply(upper))
+ <body>Some <em>TEST</em> text</body>
+ """
+ transformer = Transformer()
+ transformer.transforms = self.transforms[:]
+ if isinstance(function, Transformer):
+ transformer.transforms.extend(function.transforms)
+ else:
+ transformer.transforms.append(function)
+ return transformer
+
+ #{ Selection operations
+
+ def select(self, path):
+ """Mark events matching the given XPath expression, within the current
+ selection.
+
+ >>> html = HTML('<body>Some <em>test</em> text</body>')
+ >>> print(html | Transformer().select('.//em').trace())
+ (None, ('START', (QName('body'), Attrs()), (None, 1, 0)))
+ (None, ('TEXT', u'Some ', (None, 1, 6)))
+ ('ENTER', ('START', (QName('em'), Attrs()), (None, 1, 11)))
+ ('INSIDE', ('TEXT', u'test', (None, 1, 15)))
+ ('EXIT', ('END', QName('em'), (None, 1, 19)))
+ (None, ('TEXT', u' text', (None, 1, 24)))
+ (None, ('END', QName('body'), (None, 1, 29)))
+ <body>Some <em>test</em> text</body>
+
+ :param path: an XPath expression (as string) or a `Path` instance
+ :return: the stream augmented by transformation marks
+ :rtype: `Transformer`
+ """
+ return self.apply(SelectTransformation(path))
+
+ def invert(self):
+ """Invert selection so that marked events become unmarked, and vice
+ versa.
+
+ Specificaly, all marks are converted to null marks, and all null marks
+ are converted to OUTSIDE marks.
+
+ >>> html = HTML('<body>Some <em>test</em> text</body>')
+ >>> print(html | Transformer('//em').invert().trace())
+ ('OUTSIDE', ('START', (QName('body'), Attrs()), (None, 1, 0)))
+ ('OUTSIDE', ('TEXT', u'Some ', (None, 1, 6)))
+ (None, ('START', (QName('em'), Attrs()), (None, 1, 11)))
+ (None, ('TEXT', u'test', (None, 1, 15)))
+ (None, ('END', QName('em'), (None, 1, 19)))
+ ('OUTSIDE', ('TEXT', u' text', (None, 1, 24)))
+ ('OUTSIDE', ('END', QName('body'), (None, 1, 29)))
+ <body>Some <em>test</em> text</body>
+
+ :rtype: `Transformer`
+ """
+ return self.apply(InvertTransformation())
+
+ def end(self):
+ """End current selection, allowing all events to be selected.
+
+ Example:
+
+ >>> html = HTML('<body>Some <em>test</em> text</body>')
+ >>> print(html | Transformer('//em').end().trace())
+ ('OUTSIDE', ('START', (QName('body'), Attrs()), (None, 1, 0)))
+ ('OUTSIDE', ('TEXT', u'Some ', (None, 1, 6)))
+ ('OUTSIDE', ('START', (QName('em'), Attrs()), (None, 1, 11)))
+ ('OUTSIDE', ('TEXT', u'test', (None, 1, 15)))
+ ('OUTSIDE', ('END', QName('em'), (None, 1, 19)))
+ ('OUTSIDE', ('TEXT', u' text', (None, 1, 24)))
+ ('OUTSIDE', ('END', QName('body'), (None, 1, 29)))
+ <body>Some <em>test</em> text</body>
+
+ :return: the stream augmented by transformation marks
+ :rtype: `Transformer`
+ """
+ return self.apply(EndTransformation())
+
+ #{ Deletion operations
+
+ def empty(self):
+ """Empty selected elements of all content.
+
+ Example:
+
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body>Some <em>body</em> text.</body></html>')
+ >>> print(html | Transformer('.//em').empty())
+ <html><head><title>Some Title</title></head><body>Some <em/>
+ text.</body></html>
+
+ :rtype: `Transformer`
+ """
+ return self.apply(EmptyTransformation())
+
+ def remove(self):
+ """Remove selection from the stream.
+
+ Example:
+
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body>Some <em>body</em> text.</body></html>')
+ >>> print(html | Transformer('.//em').remove())
+ <html><head><title>Some Title</title></head><body>Some
+ text.</body></html>
+
+ :rtype: `Transformer`
+ """
+ return self.apply(RemoveTransformation())
+
+ #{ Direct element operations
+
+ def unwrap(self):
+ """Remove outermost enclosing elements from selection.
+
+ Example:
+
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body>Some <em>body</em> text.</body></html>')
+ >>> print(html | Transformer('.//em').unwrap())
+ <html><head><title>Some Title</title></head><body>Some body
+ text.</body></html>
+
+ :rtype: `Transformer`
+ """
+ return self.apply(UnwrapTransformation())
+
+ def wrap(self, element):
+ """Wrap selection in an element.
+
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body>Some <em>body</em> text.</body></html>')
+ >>> print(html | Transformer('.//em').wrap('strong'))
+ <html><head><title>Some Title</title></head><body>Some
+ <strong><em>body</em></strong> text.</body></html>
+
+ :param element: either a tag name (as string) or an `Element` object
+ :rtype: `Transformer`
+ """
+ return self.apply(WrapTransformation(element))
+
+ #{ Content insertion operations
+
+ def replace(self, content):
+ """Replace selection with content.
+
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body>Some <em>body</em> text.</body></html>')
+ >>> print(html | Transformer('.//title/text()').replace('New Title'))
+ <html><head><title>New Title</title></head><body>Some <em>body</em>
+ text.</body></html>
+
+ :param content: Either a callable, an iterable of events, or a string
+ to insert.
+ :rtype: `Transformer`
+ """
+ return self.apply(ReplaceTransformation(content))
+
+ def before(self, content):
+ """Insert content before selection.
+
+ In this example we insert the word 'emphasised' before the <em> opening
+ tag:
+
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body>Some <em>body</em> text.</body></html>')
+ >>> print(html | Transformer('.//em').before('emphasised '))
+ <html><head><title>Some Title</title></head><body>Some emphasised
+ <em>body</em> text.</body></html>
+
+ :param content: Either a callable, an iterable of events, or a string
+ to insert.
+ :rtype: `Transformer`
+ """
+ return self.apply(BeforeTransformation(content))
+
+ def after(self, content):
+ """Insert content after selection.
+
+ Here, we insert some text after the </em> closing tag:
+
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body>Some <em>body</em> text.</body></html>')
+ >>> print(html | Transformer('.//em').after(' rock'))
+ <html><head><title>Some Title</title></head><body>Some <em>body</em>
+ rock text.</body></html>
+
+ :param content: Either a callable, an iterable of events, or a string
+ to insert.
+ :rtype: `Transformer`
+ """
+ return self.apply(AfterTransformation(content))
+
+ def prepend(self, content):
+ """Insert content after the ENTER event of the selection.
+
+ Inserting some new text at the start of the <body>:
+
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body>Some <em>body</em> text.</body></html>')
+ >>> print(html | Transformer('.//body').prepend('Some new body text. '))
+ <html><head><title>Some Title</title></head><body>Some new body text.
+ Some <em>body</em> text.</body></html>
+
+ :param content: Either a callable, an iterable of events, or a string
+ to insert.
+ :rtype: `Transformer`
+ """
+ return self.apply(PrependTransformation(content))
+
+ def append(self, content):
+ """Insert content before the END event of the selection.
+
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body>Some <em>body</em> text.</body></html>')
+ >>> print(html | Transformer('.//body').append(' Some new body text.'))
+ <html><head><title>Some Title</title></head><body>Some <em>body</em>
+ text. Some new body text.</body></html>
+
+ :param content: Either a callable, an iterable of events, or a string
+ to insert.
+ :rtype: `Transformer`
+ """
+ return self.apply(AppendTransformation(content))
+
+ #{ Attribute manipulation
+
+ def attr(self, name, value):
+ """Add, replace or delete an attribute on selected elements.
+
+ If `value` evaulates to `None` the attribute will be deleted from the
+ element:
+
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body>Some <em class="before">body</em> <em>text</em>.</body>'
+ ... '</html>')
+ >>> print(html | Transformer('body/em').attr('class', None))
+ <html><head><title>Some Title</title></head><body>Some <em>body</em>
+ <em>text</em>.</body></html>
+
+ Otherwise the attribute will be set to `value`:
+
+ >>> print(html | Transformer('body/em').attr('class', 'emphasis'))
+ <html><head><title>Some Title</title></head><body>Some <em
+ class="emphasis">body</em> <em class="emphasis">text</em>.</body></html>
+
+ If `value` is a callable it will be called with the attribute name and
+ the `START` event for the matching element. Its return value will then
+ be used to set the attribute:
+
+ >>> def print_attr(name, event):
+ ... attrs = event[1][1]
+ ... print(attrs)
+ ... return attrs.get(name)
+ >>> print(html | Transformer('body/em').attr('class', print_attr))
+ Attrs([(QName('class'), u'before')])
+ Attrs()
+ <html><head><title>Some Title</title></head><body>Some <em
+ class="before">body</em> <em>text</em>.</body></html>
+
+ :param name: the name of the attribute
+ :param value: the value that should be set for the attribute.
+ :rtype: `Transformer`
+ """
+ return self.apply(AttrTransformation(name, value))
+
+ #{ Buffer operations
+
+ def copy(self, buffer, accumulate=False):
+ """Copy selection into buffer.
+
+ The buffer is replaced by each *contiguous* selection before being passed
+ to the next transformation. If accumulate=True, further selections will
+ be appended to the buffer rather than replacing it.
+
+ >>> from genshi.builder import tag
+ >>> buffer = StreamBuffer()
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body>Some <em>body</em> text.</body></html>')
+ >>> print(html | Transformer('head/title/text()').copy(buffer)
+ ... .end().select('body').prepend(tag.h1(buffer)))
+ <html><head><title>Some Title</title></head><body><h1>Some
+ Title</h1>Some <em>body</em> text.</body></html>
+
+ This example illustrates that only a single contiguous selection will
+ be buffered:
+
+ >>> print(html | Transformer('head/title/text()').copy(buffer)
+ ... .end().select('body/em').copy(buffer).end().select('body')
+ ... .prepend(tag.h1(buffer)))
+ <html><head><title>Some Title</title></head><body><h1>Some
+ Title</h1>Some <em>body</em> text.</body></html>
+ >>> print(buffer)
+ <em>body</em>
+
+ Element attributes can also be copied for later use:
+
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body><em>Some</em> <em class="before">body</em>'
+ ... '<em>text</em>.</body></html>')
+ >>> buffer = StreamBuffer()
+ >>> def apply_attr(name, entry):
+ ... return list(buffer)[0][1][1].get('class')
+ >>> print(html | Transformer('body/em[@class]/@class').copy(buffer)
+ ... .end().buffer().select('body/em[not(@class)]')
+ ... .attr('class', apply_attr))
+ <html><head><title>Some Title</title></head><body><em
+ class="before">Some</em> <em class="before">body</em><em
+ class="before">text</em>.</body></html>
+
+
+ :param buffer: the `StreamBuffer` in which the selection should be
+ stored
+ :rtype: `Transformer`
+ :note: Copy (and cut) copy each individual selected object into the
+ buffer before passing to the next transform. For example, the
+ XPath ``*|text()`` will select all elements and text, each
+ instance of which will be copied to the buffer individually
+ before passing to the next transform. This has implications for
+ how ``StreamBuffer`` objects can be used, so some
+ experimentation may be required.
+
+ """
+ return self.apply(CopyTransformation(buffer, accumulate))
+
+ def cut(self, buffer, accumulate=False):
+ """Copy selection into buffer and remove the selection from the stream.
+
+ >>> from genshi.builder import tag
+ >>> buffer = StreamBuffer()
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body>Some <em>body</em> text.</body></html>')
+ >>> print(html | Transformer('.//em/text()').cut(buffer)
+ ... .end().select('.//em').after(tag.h1(buffer)))
+ <html><head><title>Some Title</title></head><body>Some
+ <em/><h1>body</h1> text.</body></html>
+
+ Specifying accumulate=True, appends all selected intervals onto the
+ buffer. Combining this with the .buffer() operation allows us operate
+ on all copied events rather than per-segment. See the documentation on
+ buffer() for more information.
+
+ :param buffer: the `StreamBuffer` in which the selection should be
+ stored
+ :rtype: `Transformer`
+ :note: this transformation will buffer the entire input stream
+ """
+ return self.apply(CutTransformation(buffer, accumulate))
+
+ def buffer(self):
+ """Buffer the entire stream (can consume a considerable amount of
+ memory).
+
+ Useful in conjunction with copy(accumulate=True) and
+ cut(accumulate=True) to ensure that all marked events in the entire
+ stream are copied to the buffer before further transformations are
+ applied.
+
+ For example, to move all <note> elements inside a <notes> tag at the
+ top of the document:
+
+ >>> doc = HTML('<doc><notes></notes><body>Some <note>one</note> '
+ ... 'text <note>two</note>.</body></doc>')
+ >>> buffer = StreamBuffer()
+ >>> print(doc | Transformer('body/note').cut(buffer, accumulate=True)
+ ... .end().buffer().select('notes').prepend(buffer))
+ <doc><notes><note>one</note><note>two</note></notes><body>Some text
+ .</body></doc>
+
+ """
+ return self.apply(list)
+
+ #{ Miscellaneous operations
+
+ def filter(self, filter):
+ """Apply a normal stream filter to the selection. The filter is called
+ once for each contiguous block of marked events.
+
+ >>> from genshi.filters.html import HTMLSanitizer
+ >>> html = HTML('<html><body>Some text<script>alert(document.cookie)'
+ ... '</script> and some more text</body></html>')
+ >>> print(html | Transformer('body/*').filter(HTMLSanitizer()))
+ <html><body>Some text and some more text</body></html>
+
+ :param filter: The stream filter to apply.
+ :rtype: `Transformer`
+ """
+ return self.apply(FilterTransformation(filter))
+
+ def map(self, function, kind):
+ """Applies a function to the ``data`` element of events of ``kind`` in
+ the selection.
+
+ >>> html = HTML('<html><head><title>Some Title</title></head>'
+ ... '<body>Some <em>body</em> text.</body></html>')
+ >>> print(html | Transformer('head/title').map(unicode.upper, TEXT))
+ <html><head><title>SOME TITLE</title></head><body>Some <em>body</em>
+ text.</body></html>
+
+ :param function: the function to apply
+ :param kind: the kind of event the function should be applied to
+ :rtype: `Transformer`
+ """
+ return self.apply(MapTransformation(function, kind))
+
+ def substitute(self, pattern, replace, count=1):
+ """Replace text matching a regular expression.
+
+ Refer to the documentation for ``re.sub()`` for details.
+
+ >>> html = HTML('<html><body>Some text, some more text and '
+ ... '<b>some bold text</b>\\n'
+ ... '<i>some italicised text</i></body></html>')
+ >>> print(html | Transformer('body/b').substitute('(?i)some', 'SOME'))
+ <html><body>Some text, some more text and <b>SOME bold text</b>
+ <i>some italicised text</i></body></html>
+ >>> tags = tag.html(tag.body('Some text, some more text and\\n',
+ ... Markup('<b>some bold text</b>')))
+ >>> print(tags.generate() | Transformer('body').substitute(
+ ... '(?i)some', 'SOME'))
+ <html><body>SOME text, some more text and
+ <b>SOME bold text</b></body></html>
+
+ :param pattern: A regular expression object or string.
+ :param replace: Replacement pattern.
+ :param count: Number of replacements to make in each text fragment.
+ :rtype: `Transformer`
+ """
+ return self.apply(SubstituteTransformation(pattern, replace, count))
+
+ def rename(self, name):
+ """Rename matching elements.
+
+ >>> html = HTML('<html><body>Some text, some more text and '
+ ... '<b>some bold text</b></body></html>')
+ >>> print(html | Transformer('body/b').rename('strong'))
+ <html><body>Some text, some more text and <strong>some bold text</strong></body></html>
+ """
+ return self.apply(RenameTransformation(name))
+
+ def trace(self, prefix='', fileobj=None):
+ """Print events as they pass through the transform.
+
+ >>> html = HTML('<body>Some <em>test</em> text</body>')
+ >>> print(html | Transformer('em').trace())
+ (None, ('START', (QName('body'), Attrs()), (None, 1, 0)))
+ (None, ('TEXT', u'Some ', (None, 1, 6)))
+ ('ENTER', ('START', (QName('em'), Attrs()), (None, 1, 11)))
+ ('INSIDE', ('TEXT', u'test', (None, 1, 15)))
+ ('EXIT', ('END', QName('em'), (None, 1, 19)))
+ (None, ('TEXT', u' text', (None, 1, 24)))
+ (None, ('END', QName('body'), (None, 1, 29)))
+ <body>Some <em>test</em> text</body>
+
+ :param prefix: a string to prefix each event with in the output
+ :param fileobj: the writable file-like object to write to; defaults to
+ the standard output stream
+ :rtype: `Transformer`
+ """
+ return self.apply(TraceTransformation(prefix, fileobj=fileobj))
+
+ # Internal methods
+
+ def _mark(self, stream):
+ for event in stream:
+ yield OUTSIDE, event
+
+ def _unmark(self, stream):
+ for mark, event in stream:
+ kind = event[0]
+ if not (kind is None or kind is ATTR or kind is BREAK):
+ yield event
+
+
+class SelectTransformation(object):
+ """Select and mark events that match an XPath expression."""
+
+ def __init__(self, path):
+ """Create selection.
+
+ :param path: an XPath expression (as string) or a `Path` object
+ """
+ if not isinstance(path, Path):
+ path = Path(path)
+ self.path = path
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: the marked event stream to filter
+ """
+ namespaces = {}
+ variables = {}
+ test = self.path.test()
+ stream = iter(stream)
+ next = stream.next
+ for mark, event in stream:
+ if mark is None:
+ yield mark, event
+ continue
+ result = test(event, namespaces, variables)
+ # XXX This is effectively genshi.core._ensure() for transform
+ # streams.
+ if result is True:
+ if event[0] is START:
+ yield ENTER, event
+ depth = 1
+ while depth > 0:
+ mark, subevent = next()
+ if subevent[0] is START:
+ depth += 1
+ elif subevent[0] is END:
+ depth -= 1
+ if depth == 0:
+ yield EXIT, subevent
+ else:
+ yield INSIDE, subevent
+ test(subevent, namespaces, variables, updateonly=True)
+ else:
+ yield OUTSIDE, event
+ elif isinstance(result, Attrs):
+ # XXX Selected *attributes* are given a "kind" of None to
+ # indicate they are not really part of the stream.
+ yield ATTR, (ATTR, (QName(event[1][0] + '@*'), result), event[2])
+ yield None, event
+ elif isinstance(result, tuple):
+ yield OUTSIDE, result
+ elif result:
+ # XXX Assume everything else is "text"?
+ yield None, (TEXT, unicode(result), (None, -1, -1))
+ else:
+ yield None, event
+
+
+class InvertTransformation(object):
+ """Invert selection so that marked events become unmarked, and vice versa.
+
+ Specificaly, all input marks are converted to null marks, and all input
+ null marks are converted to OUTSIDE marks.
+ """
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: the marked event stream to filter
+ """
+ for mark, event in stream:
+ if mark:
+ yield None, event
+ else:
+ yield OUTSIDE, event
+
+
+class EndTransformation(object):
+ """End the current selection."""
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: the marked event stream to filter
+ """
+ for mark, event in stream:
+ yield OUTSIDE, event
+
+
+class EmptyTransformation(object):
+ """Empty selected elements of all content."""
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: the marked event stream to filter
+ """
+ for mark, event in stream:
+ yield mark, event
+ if mark is ENTER:
+ for mark, event in stream:
+ if mark is EXIT:
+ yield mark, event
+ break
+
+
+class RemoveTransformation(object):
+ """Remove selection from the stream."""
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: the marked event stream to filter
+ """
+ for mark, event in stream:
+ if mark is None:
+ yield mark, event
+
+
+class UnwrapTransformation(object):
+ """Remove outtermost enclosing elements from selection."""
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: the marked event stream to filter
+ """
+ for mark, event in stream:
+ if mark not in (ENTER, EXIT):
+ yield mark, event
+
+
+class WrapTransformation(object):
+ """Wrap selection in an element."""
+
+ def __init__(self, element):
+ if isinstance(element, Element):
+ self.element = element
+ else:
+ self.element = Element(element)
+
+ def __call__(self, stream):
+ for mark, event in stream:
+ if mark:
+ element = list(self.element.generate())
+ for prefix in element[:-1]:
+ yield None, prefix
+ yield mark, event
+ start = mark
+ stopped = False
+ for mark, event in stream:
+ if start is ENTER and mark is EXIT:
+ yield mark, event
+ stopped = True
+ break
+ if not mark:
+ break
+ yield mark, event
+ else:
+ stopped = True
+ yield None, element[-1]
+ if not stopped:
+ yield mark, event
+ else:
+ yield mark, event
+
+
+class TraceTransformation(object):
+ """Print events as they pass through the transform."""
+
+ def __init__(self, prefix='', fileobj=None):
+ """Trace constructor.
+
+ :param prefix: text to prefix each traced line with.
+ :param fileobj: the writable file-like object to write to
+ """
+ self.prefix = prefix
+ self.fileobj = fileobj or sys.stdout
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: the marked event stream to filter
+ """
+ for event in stream:
+ self.fileobj.write('%s%s\n' % (self.prefix, event))
+ yield event
+
+
+class FilterTransformation(object):
+ """Apply a normal stream filter to the selection. The filter is called once
+ for each selection."""
+
+ def __init__(self, filter):
+ """Create the transform.
+
+ :param filter: The stream filter to apply.
+ """
+ self.filter = filter
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: The marked event stream to filter
+ """
+ def flush(queue):
+ if queue:
+ for event in self.filter(queue):
+ yield OUTSIDE, event
+ del queue[:]
+
+ queue = []
+ for mark, event in stream:
+ if mark is ENTER:
+ queue.append(event)
+ for mark, event in stream:
+ queue.append(event)
+ if mark is EXIT:
+ break
+ for queue_event in flush(queue):
+ yield queue_event
+ elif mark is OUTSIDE:
+ stopped = False
+ queue.append(event)
+ for mark, event in stream:
+ if mark is not OUTSIDE:
+ break
+ queue.append(event)
+ else:
+ stopped = True
+ for queue_event in flush(queue):
+ yield queue_event
+ if not stopped:
+ yield mark, event
+ else:
+ yield mark, event
+ for queue_event in flush(queue):
+ yield queue_event
+
+
+class MapTransformation(object):
+ """Apply a function to the `data` element of events of ``kind`` in the
+ selection.
+ """
+
+ def __init__(self, function, kind):
+ """Create the transform.
+
+ :param function: the function to apply; the function must take one
+ argument, the `data` element of each selected event
+ :param kind: the stream event ``kind`` to apply the `function` to
+ """
+ self.function = function
+ self.kind = kind
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: The marked event stream to filter
+ """
+ for mark, (kind, data, pos) in stream:
+ if mark and self.kind in (None, kind):
+ yield mark, (kind, self.function(data), pos)
+ else:
+ yield mark, (kind, data, pos)
+
+
+class SubstituteTransformation(object):
+ """Replace text matching a regular expression.
+
+ Refer to the documentation for ``re.sub()`` for details.
+ """
+ def __init__(self, pattern, replace, count=0):
+ """Create the transform.
+
+ :param pattern: A regular expression object, or string.
+ :param replace: Replacement pattern.
+ :param count: Number of replacements to make in each text fragment.
+ """
+ if isinstance(pattern, basestring):
+ self.pattern = re.compile(pattern)
+ else:
+ self.pattern = pattern
+ self.count = count
+ self.replace = replace
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: The marked event stream to filter
+ """
+ for mark, (kind, data, pos) in stream:
+ if mark is not None and kind is TEXT:
+ new_data = self.pattern.sub(self.replace, data, self.count)
+ if isinstance(data, Markup):
+ data = Markup(new_data)
+ else:
+ data = new_data
+ yield mark, (kind, data, pos)
+
+
+class RenameTransformation(object):
+ """Rename matching elements."""
+ def __init__(self, name):
+ """Create the transform.
+
+ :param name: New element name.
+ """
+ self.name = QName(name)
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: The marked event stream to filter
+ """
+ for mark, (kind, data, pos) in stream:
+ if mark is ENTER:
+ data = self.name, data[1]
+ elif mark is EXIT:
+ data = self.name
+ yield mark, (kind, data, pos)
+
+
+class InjectorTransformation(object):
+ """Abstract base class for transformations that inject content into a
+ stream.
+
+ >>> class Top(InjectorTransformation):
+ ... def __call__(self, stream):
+ ... for event in self._inject():
+ ... yield event
+ ... for event in stream:
+ ... yield event
+ >>> html = HTML('<body>Some <em>test</em> text</body>')
+ >>> print(html | Transformer('.//em').apply(Top('Prefix ')))
+ Prefix <body>Some <em>test</em> text</body>
+ """
+ def __init__(self, content):
+ """Create a new injector.
+
+ :param content: An iterable of Genshi stream events, or a string to be
+ injected.
+ """
+ self.content = content
+
+ def _inject(self):
+ content = self.content
+ if hasattr(content, '__call__'):
+ content = content()
+ for event in _ensure(content):
+ yield None, event
+
+
+class ReplaceTransformation(InjectorTransformation):
+ """Replace selection with content."""
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: The marked event stream to filter
+ """
+ stream = PushBackStream(stream)
+ for mark, event in stream:
+ if mark is not None:
+ start = mark
+ for subevent in self._inject():
+ yield subevent
+ for mark, event in stream:
+ if start is ENTER:
+ if mark is EXIT:
+ break
+ elif mark != start:
+ stream.push((mark, event))
+ break
+ else:
+ yield mark, event
+
+
+class BeforeTransformation(InjectorTransformation):
+ """Insert content before selection."""
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: The marked event stream to filter
+ """
+ stream = PushBackStream(stream)
+ for mark, event in stream:
+ if mark is not None:
+ start = mark
+ for subevent in self._inject():
+ yield subevent
+ yield mark, event
+ for mark, event in stream:
+ if mark != start and start is not ENTER:
+ stream.push((mark, event))
+ break
+ yield mark, event
+ if start is ENTER and mark is EXIT:
+ break
+ else:
+ yield mark, event
+
+
+class AfterTransformation(InjectorTransformation):
+ """Insert content after selection."""
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: The marked event stream to filter
+ """
+ stream = PushBackStream(stream)
+ for mark, event in stream:
+ yield mark, event
+ if mark:
+ start = mark
+ for mark, event in stream:
+ if start is not ENTER and mark != start:
+ stream.push((mark, event))
+ break
+ yield mark, event
+ if start is ENTER and mark is EXIT:
+ break
+ for subevent in self._inject():
+ yield subevent
+
+
+class PrependTransformation(InjectorTransformation):
+ """Prepend content to the inside of selected elements."""
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: The marked event stream to filter
+ """
+ for mark, event in stream:
+ yield mark, event
+ if mark is ENTER:
+ for subevent in self._inject():
+ yield subevent
+
+
+class AppendTransformation(InjectorTransformation):
+ """Append content after the content of selected elements."""
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: The marked event stream to filter
+ """
+ for mark, event in stream:
+ yield mark, event
+ if mark is ENTER:
+ for mark, event in stream:
+ if mark is EXIT:
+ break
+ yield mark, event
+ for subevent in self._inject():
+ yield subevent
+ yield mark, event
+
+
+class AttrTransformation(object):
+ """Set an attribute on selected elements."""
+
+ def __init__(self, name, value):
+ """Construct transform.
+
+ :param name: name of the attribute that should be set
+ :param value: the value to set
+ """
+ self.name = name
+ self.value = value
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: The marked event stream to filter
+ """
+ callable_value = hasattr(self.value, '__call__')
+ for mark, (kind, data, pos) in stream:
+ if mark is ENTER:
+ if callable_value:
+ value = self.value(self.name, (kind, data, pos))
+ else:
+ value = self.value
+ if value is None:
+ attrs = data[1] - [QName(self.name)]
+ else:
+ attrs = data[1] | [(QName(self.name), value)]
+ data = (data[0], attrs)
+ yield mark, (kind, data, pos)
+
+
+
+class StreamBuffer(Stream):
+ """Stream event buffer used for cut and copy transformations."""
+
+ def __init__(self):
+ """Create the buffer."""
+ Stream.__init__(self, [])
+
+ def append(self, event):
+ """Add an event to the buffer.
+
+ :param event: the markup event to add
+ """
+ self.events.append(event)
+
+ def reset(self):
+ """Empty the buffer of events."""
+ del self.events[:]
+
+
+class CopyTransformation(object):
+ """Copy selected events into a buffer for later insertion."""
+
+ def __init__(self, buffer, accumulate=False):
+ """Create the copy transformation.
+
+ :param buffer: the `StreamBuffer` in which the selection should be
+ stored
+ """
+ if not accumulate:
+ buffer.reset()
+ self.buffer = buffer
+ self.accumulate = accumulate
+
+ def __call__(self, stream):
+ """Apply the transformation to the marked stream.
+
+ :param stream: the marked event stream to filter
+ """
+ stream = PushBackStream(stream)
+
+ for mark, event in stream:
+ if mark:
+ if not self.accumulate:
+ self.buffer.reset()
+ events = [(mark, event)]
+ self.buffer.append(event)
+ start = mark
+ for mark, event in stream:
+ if start is not ENTER and mark != start:
+ stream.push((mark, event))
+ break
+ events.append((mark, event))
+ self.buffer.append(event)
+ if start is ENTER and mark is EXIT:
+ break
+ for i in events:
+ yield i
+ else:
+ yield mark, event
+
+
+class CutTransformation(object):
+ """Cut selected events into a buffer for later insertion and remove the
+ selection.
+ """
+
+ def __init__(self, buffer, accumulate=False):
+ """Create the cut transformation.
+
+ :param buffer: the `StreamBuffer` in which the selection should be
+ stored
+ """
+ self.buffer = buffer
+ self.accumulate = accumulate
+
+
+ def __call__(self, stream):
+ """Apply the transform filter to the marked stream.
+
+ :param stream: the marked event stream to filter
+ """
+ attributes = []
+ stream = PushBackStream(stream)
+ broken = False
+ if not self.accumulate:
+ self.buffer.reset()
+ for mark, event in stream:
+ if mark:
+ # Send a BREAK event if there was no other event sent between
+ if not self.accumulate:
+ if not broken and self.buffer:
+ yield BREAK, (BREAK, None, None)
+ self.buffer.reset()
+ self.buffer.append(event)
+ start = mark
+ if mark is ATTR:
+ attributes.extend([name for name, _ in event[1][1]])
+ for mark, event in stream:
+ if start is mark is ATTR:
+ attributes.extend([name for name, _ in event[1][1]])
+ # Handle non-element contiguous selection
+ if start is not ENTER and mark != start:
+ # Operating on the attributes of a START event
+ if start is ATTR:
+ kind, data, pos = event
+ assert kind is START
+ data = (data[0], data[1] - attributes)
+ attributes = None
+ stream.push((mark, (kind, data, pos)))
+ else:
+ stream.push((mark, event))
+ break
+ self.buffer.append(event)
+ if start is ENTER and mark is EXIT:
+ break
+ broken = False
+ else:
+ broken = True
+ yield mark, event
+ if not broken and self.buffer:
+ yield BREAK, (BREAK, None, None)
diff --git a/genshi/input.py b/genshi/input.py
new file mode 100644
index 0000000..039e5e5
--- /dev/null
+++ b/genshi/input.py
@@ -0,0 +1,443 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Support for constructing markup streams from files, strings, or other
+sources.
+"""
+
+from itertools import chain
+import htmlentitydefs as entities
+import HTMLParser as html
+from StringIO import StringIO
+from xml.parsers import expat
+
+from genshi.core import Attrs, QName, Stream, stripentities
+from genshi.core import START, END, XML_DECL, DOCTYPE, TEXT, START_NS, \
+ END_NS, START_CDATA, END_CDATA, PI, COMMENT
+
+__all__ = ['ET', 'ParseError', 'XMLParser', 'XML', 'HTMLParser', 'HTML']
+__docformat__ = 'restructuredtext en'
+
+
+def ET(element):
+ """Convert a given ElementTree element to a markup stream.
+
+ :param element: an ElementTree element
+ :return: a markup stream
+ """
+ tag_name = QName(element.tag.lstrip('{'))
+ attrs = Attrs([(QName(attr.lstrip('{')), value)
+ for attr, value in element.items()])
+
+ yield START, (tag_name, attrs), (None, -1, -1)
+ if element.text:
+ yield TEXT, element.text, (None, -1, -1)
+ for child in element.getchildren():
+ for item in ET(child):
+ yield item
+ yield END, tag_name, (None, -1, -1)
+ if element.tail:
+ yield TEXT, element.tail, (None, -1, -1)
+
+
+class ParseError(Exception):
+ """Exception raised when fatal syntax errors are found in the input being
+ parsed.
+ """
+
+ def __init__(self, message, filename=None, lineno=-1, offset=-1):
+ """Exception initializer.
+
+ :param message: the error message from the parser
+ :param filename: the path to the file that was parsed
+ :param lineno: the number of the line on which the error was encountered
+ :param offset: the column number where the error was encountered
+ """
+ self.msg = message
+ if filename:
+ message += ', in ' + filename
+ Exception.__init__(self, message)
+ self.filename = filename or '<string>'
+ self.lineno = lineno
+ self.offset = offset
+
+
+class XMLParser(object):
+ """Generator-based XML parser based on roughly equivalent code in
+ Kid/ElementTree.
+
+ The parsing is initiated by iterating over the parser object:
+
+ >>> parser = XMLParser(StringIO('<root id="2"><child>Foo</child></root>'))
+ >>> for kind, data, pos in parser:
+ ... print('%s %s' % (kind, data))
+ START (QName('root'), Attrs([(QName('id'), u'2')]))
+ START (QName('child'), Attrs())
+ TEXT Foo
+ END child
+ END root
+ """
+
+ _entitydefs = ['<!ENTITY %s "&#%d;">' % (name, value) for name, value in
+ entities.name2codepoint.items()]
+ _external_dtd = '\n'.join(_entitydefs)
+
+ def __init__(self, source, filename=None, encoding=None):
+ """Initialize the parser for the given XML input.
+
+ :param source: the XML text as a file-like object
+ :param filename: the name of the file, if appropriate
+ :param encoding: the encoding of the file; if not specified, the
+ encoding is assumed to be ASCII, UTF-8, or UTF-16, or
+ whatever the encoding specified in the XML declaration
+ (if any)
+ """
+ self.source = source
+ self.filename = filename
+
+ # Setup the Expat parser
+ parser = expat.ParserCreate(encoding, '}')
+ parser.buffer_text = True
+ parser.returns_unicode = True
+ parser.ordered_attributes = True
+
+ parser.StartElementHandler = self._handle_start
+ parser.EndElementHandler = self._handle_end
+ parser.CharacterDataHandler = self._handle_data
+ parser.StartDoctypeDeclHandler = self._handle_doctype
+ parser.StartNamespaceDeclHandler = self._handle_start_ns
+ parser.EndNamespaceDeclHandler = self._handle_end_ns
+ parser.StartCdataSectionHandler = self._handle_start_cdata
+ parser.EndCdataSectionHandler = self._handle_end_cdata
+ parser.ProcessingInstructionHandler = self._handle_pi
+ parser.XmlDeclHandler = self._handle_xml_decl
+ parser.CommentHandler = self._handle_comment
+
+ # Tell Expat that we'll handle non-XML entities ourselves
+ # (in _handle_other)
+ parser.DefaultHandler = self._handle_other
+ parser.SetParamEntityParsing(expat.XML_PARAM_ENTITY_PARSING_ALWAYS)
+ parser.UseForeignDTD()
+ parser.ExternalEntityRefHandler = self._build_foreign
+
+ self.expat = parser
+ self._queue = []
+
+ def parse(self):
+ """Generator that parses the XML source, yielding markup events.
+
+ :return: a markup event stream
+ :raises ParseError: if the XML text is not well formed
+ """
+ def _generate():
+ try:
+ bufsize = 4 * 1024 # 4K
+ done = False
+ while 1:
+ while not done and len(self._queue) == 0:
+ data = self.source.read(bufsize)
+ if data == '': # end of data
+ if hasattr(self, 'expat'):
+ self.expat.Parse('', True)
+ del self.expat # get rid of circular references
+ done = True
+ else:
+ if isinstance(data, unicode):
+ data = data.encode('utf-8')
+ self.expat.Parse(data, False)
+ for event in self._queue:
+ yield event
+ self._queue = []
+ if done:
+ break
+ except expat.ExpatError, e:
+ msg = str(e)
+ raise ParseError(msg, self.filename, e.lineno, e.offset)
+ return Stream(_generate()).filter(_coalesce)
+
+ def __iter__(self):
+ return iter(self.parse())
+
+ def _build_foreign(self, context, base, sysid, pubid):
+ parser = self.expat.ExternalEntityParserCreate(context)
+ parser.ParseFile(StringIO(self._external_dtd))
+ return 1
+
+ def _enqueue(self, kind, data=None, pos=None):
+ if pos is None:
+ pos = self._getpos()
+ if kind is TEXT:
+ # Expat reports the *end* of the text event as current position. We
+ # try to fix that up here as much as possible. Unfortunately, the
+ # offset is only valid for single-line text. For multi-line text,
+ # it is apparently not possible to determine at what offset it
+ # started
+ if '\n' in data:
+ lines = data.splitlines()
+ lineno = pos[1] - len(lines) + 1
+ offset = -1
+ else:
+ lineno = pos[1]
+ offset = pos[2] - len(data)
+ pos = (pos[0], lineno, offset)
+ self._queue.append((kind, data, pos))
+
+ def _getpos_unknown(self):
+ return (self.filename, -1, -1)
+
+ def _getpos(self):
+ return (self.filename, self.expat.CurrentLineNumber,
+ self.expat.CurrentColumnNumber)
+
+ def _handle_start(self, tag, attrib):
+ attrs = Attrs([(QName(name), value) for name, value in
+ zip(*[iter(attrib)] * 2)])
+ self._enqueue(START, (QName(tag), attrs))
+
+ def _handle_end(self, tag):
+ self._enqueue(END, QName(tag))
+
+ def _handle_data(self, text):
+ self._enqueue(TEXT, text)
+
+ def _handle_xml_decl(self, version, encoding, standalone):
+ self._enqueue(XML_DECL, (version, encoding, standalone))
+
+ def _handle_doctype(self, name, sysid, pubid, has_internal_subset):
+ self._enqueue(DOCTYPE, (name, pubid, sysid))
+
+ def _handle_start_ns(self, prefix, uri):
+ self._enqueue(START_NS, (prefix or '', uri))
+
+ def _handle_end_ns(self, prefix):
+ self._enqueue(END_NS, prefix or '')
+
+ def _handle_start_cdata(self):
+ self._enqueue(START_CDATA)
+
+ def _handle_end_cdata(self):
+ self._enqueue(END_CDATA)
+
+ def _handle_pi(self, target, data):
+ self._enqueue(PI, (target, data))
+
+ def _handle_comment(self, text):
+ self._enqueue(COMMENT, text)
+
+ def _handle_other(self, text):
+ if text.startswith('&'):
+ # deal with undefined entities
+ try:
+ text = unichr(entities.name2codepoint[text[1:-1]])
+ self._enqueue(TEXT, text)
+ except KeyError:
+ filename, lineno, offset = self._getpos()
+ error = expat.error('undefined entity "%s": line %d, column %d'
+ % (text, lineno, offset))
+ error.code = expat.errors.XML_ERROR_UNDEFINED_ENTITY
+ error.lineno = lineno
+ error.offset = offset
+ raise error
+
+
+def XML(text):
+ """Parse the given XML source and return a markup stream.
+
+ Unlike with `XMLParser`, the returned stream is reusable, meaning it can be
+ iterated over multiple times:
+
+ >>> xml = XML('<doc><elem>Foo</elem><elem>Bar</elem></doc>')
+ >>> print(xml)
+ <doc><elem>Foo</elem><elem>Bar</elem></doc>
+ >>> print(xml.select('elem'))
+ <elem>Foo</elem><elem>Bar</elem>
+ >>> print(xml.select('elem/text()'))
+ FooBar
+
+ :param text: the XML source
+ :return: the parsed XML event stream
+ :raises ParseError: if the XML text is not well-formed
+ """
+ return Stream(list(XMLParser(StringIO(text))))
+
+
+class HTMLParser(html.HTMLParser, object):
+ """Parser for HTML input based on the Python `HTMLParser` module.
+
+ This class provides the same interface for generating stream events as
+ `XMLParser`, and attempts to automatically balance tags.
+
+ The parsing is initiated by iterating over the parser object:
+
+ >>> parser = HTMLParser(StringIO('<UL compact><LI>Foo</UL>'))
+ >>> for kind, data, pos in parser:
+ ... print('%s %s' % (kind, data))
+ START (QName('ul'), Attrs([(QName('compact'), u'compact')]))
+ START (QName('li'), Attrs())
+ TEXT Foo
+ END li
+ END ul
+ """
+
+ _EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'br', 'col', 'frame',
+ 'hr', 'img', 'input', 'isindex', 'link', 'meta',
+ 'param'])
+
+ def __init__(self, source, filename=None, encoding='utf-8'):
+ """Initialize the parser for the given HTML input.
+
+ :param source: the HTML text as a file-like object
+ :param filename: the name of the file, if known
+ :param filename: encoding of the file; ignored if the input is unicode
+ """
+ html.HTMLParser.__init__(self)
+ self.source = source
+ self.filename = filename
+ self.encoding = encoding
+ self._queue = []
+ self._open_tags = []
+
+ def parse(self):
+ """Generator that parses the HTML source, yielding markup events.
+
+ :return: a markup event stream
+ :raises ParseError: if the HTML text is not well formed
+ """
+ def _generate():
+ try:
+ bufsize = 4 * 1024 # 4K
+ done = False
+ while 1:
+ while not done and len(self._queue) == 0:
+ data = self.source.read(bufsize)
+ if data == '': # end of data
+ self.close()
+ done = True
+ else:
+ self.feed(data)
+ for kind, data, pos in self._queue:
+ yield kind, data, pos
+ self._queue = []
+ if done:
+ open_tags = self._open_tags
+ open_tags.reverse()
+ for tag in open_tags:
+ yield END, QName(tag), pos
+ break
+ except html.HTMLParseError, e:
+ msg = '%s: line %d, column %d' % (e.msg, e.lineno, e.offset)
+ raise ParseError(msg, self.filename, e.lineno, e.offset)
+ return Stream(_generate()).filter(_coalesce)
+
+ def __iter__(self):
+ return iter(self.parse())
+
+ def _enqueue(self, kind, data, pos=None):
+ if pos is None:
+ pos = self._getpos()
+ self._queue.append((kind, data, pos))
+
+ def _getpos(self):
+ lineno, column = self.getpos()
+ return (self.filename, lineno, column)
+
+ def handle_starttag(self, tag, attrib):
+ fixed_attrib = []
+ for name, value in attrib: # Fixup minimized attributes
+ if value is None:
+ value = unicode(name)
+ elif not isinstance(value, unicode):
+ value = value.decode(self.encoding, 'replace')
+ fixed_attrib.append((QName(name), stripentities(value)))
+
+ self._enqueue(START, (QName(tag), Attrs(fixed_attrib)))
+ if tag in self._EMPTY_ELEMS:
+ self._enqueue(END, QName(tag))
+ else:
+ self._open_tags.append(tag)
+
+ def handle_endtag(self, tag):
+ if tag not in self._EMPTY_ELEMS:
+ while self._open_tags:
+ open_tag = self._open_tags.pop()
+ self._enqueue(END, QName(open_tag))
+ if open_tag.lower() == tag.lower():
+ break
+
+ def handle_data(self, text):
+ if not isinstance(text, unicode):
+ text = text.decode(self.encoding, 'replace')
+ self._enqueue(TEXT, text)
+
+ def handle_charref(self, name):
+ if name.lower().startswith('x'):
+ text = unichr(int(name[1:], 16))
+ else:
+ text = unichr(int(name))
+ self._enqueue(TEXT, text)
+
+ def handle_entityref(self, name):
+ try:
+ text = unichr(entities.name2codepoint[name])
+ except KeyError:
+ text = '&%s;' % name
+ self._enqueue(TEXT, text)
+
+ def handle_pi(self, data):
+ target, data = data.split(None, 1)
+ if data.endswith('?'):
+ data = data[:-1]
+ self._enqueue(PI, (target.strip(), data.strip()))
+
+ def handle_comment(self, text):
+ self._enqueue(COMMENT, text)
+
+
+def HTML(text, encoding='utf-8'):
+ """Parse the given HTML source and return a markup stream.
+
+ Unlike with `HTMLParser`, the returned stream is reusable, meaning it can be
+ iterated over multiple times:
+
+ >>> html = HTML('<body><h1>Foo</h1></body>')
+ >>> print(html)
+ <body><h1>Foo</h1></body>
+ >>> print(html.select('h1'))
+ <h1>Foo</h1>
+ >>> print(html.select('h1/text()'))
+ Foo
+
+ :param text: the HTML source
+ :return: the parsed XML event stream
+ :raises ParseError: if the HTML text is not well-formed, and error recovery
+ fails
+ """
+ return Stream(list(HTMLParser(StringIO(text), encoding=encoding)))
+
+
+def _coalesce(stream):
+ """Coalesces adjacent TEXT events into a single event."""
+ textbuf = []
+ textpos = None
+ for kind, data, pos in chain(stream, [(None, None, None)]):
+ if kind is TEXT:
+ textbuf.append(data)
+ if textpos is None:
+ textpos = pos
+ else:
+ if textbuf:
+ yield TEXT, ''.join(textbuf), textpos
+ del textbuf[:]
+ textpos = None
+ if kind:
+ yield kind, data, pos
diff --git a/genshi/output.py b/genshi/output.py
new file mode 100644
index 0000000..2ebb38b
--- /dev/null
+++ b/genshi/output.py
@@ -0,0 +1,838 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""This module provides different kinds of serialization methods for XML event
+streams.
+"""
+
+from itertools import chain
+import re
+
+from genshi.core import escape, Attrs, Markup, Namespace, QName, StreamEventKind
+from genshi.core import START, END, TEXT, XML_DECL, DOCTYPE, START_NS, END_NS, \
+ START_CDATA, END_CDATA, PI, COMMENT, XML_NAMESPACE
+
+__all__ = ['encode', 'get_serializer', 'DocType', 'XMLSerializer',
+ 'XHTMLSerializer', 'HTMLSerializer', 'TextSerializer']
+__docformat__ = 'restructuredtext en'
+
+
+def encode(iterator, method='xml', encoding='utf-8', out=None):
+ """Encode serializer output into a string.
+
+ :param iterator: the iterator returned from serializing a stream (basically
+ any iterator that yields unicode objects)
+ :param method: the serialization method; determines how characters not
+ representable in the specified encoding are treated
+ :param encoding: how the output string should be encoded; if set to `None`,
+ this method returns a `unicode` object
+ :param out: a file-like object that the output should be written to
+ instead of being returned as one big string; note that if
+ this is a file or socket (or similar), the `encoding` must
+ not be `None` (that is, the output must be encoded)
+ :return: a `str` or `unicode` object (depending on the `encoding`
+ parameter), or `None` if the `out` parameter is provided
+
+ :since: version 0.4.1
+ :note: Changed in 0.5: added the `out` parameter
+ """
+ if encoding is not None:
+ errors = 'replace'
+ if method != 'text' and not isinstance(method, TextSerializer):
+ errors = 'xmlcharrefreplace'
+ _encode = lambda string: string.encode(encoding, errors)
+ else:
+ _encode = lambda string: string
+ if out is None:
+ return _encode(''.join(list(iterator)))
+ for chunk in iterator:
+ out.write(_encode(chunk))
+
+
+def get_serializer(method='xml', **kwargs):
+ """Return a serializer object for the given method.
+
+ :param method: the serialization method; can be either "xml", "xhtml",
+ "html", "text", or a custom serializer class
+
+ Any additional keyword arguments are passed to the serializer, and thus
+ depend on the `method` parameter value.
+
+ :see: `XMLSerializer`, `XHTMLSerializer`, `HTMLSerializer`, `TextSerializer`
+ :since: version 0.4.1
+ """
+ if isinstance(method, basestring):
+ method = {'xml': XMLSerializer,
+ 'xhtml': XHTMLSerializer,
+ 'html': HTMLSerializer,
+ 'text': TextSerializer}[method.lower()]
+ return method(**kwargs)
+
+
+class DocType(object):
+ """Defines a number of commonly used DOCTYPE declarations as constants."""
+
+ HTML_STRICT = (
+ 'html', '-//W3C//DTD HTML 4.01//EN',
+ 'http://www.w3.org/TR/html4/strict.dtd'
+ )
+ HTML_TRANSITIONAL = (
+ 'html', '-//W3C//DTD HTML 4.01 Transitional//EN',
+ 'http://www.w3.org/TR/html4/loose.dtd'
+ )
+ HTML_FRAMESET = (
+ 'html', '-//W3C//DTD HTML 4.01 Frameset//EN',
+ 'http://www.w3.org/TR/html4/frameset.dtd'
+ )
+ HTML = HTML_STRICT
+
+ HTML5 = ('html', None, None)
+
+ XHTML_STRICT = (
+ 'html', '-//W3C//DTD XHTML 1.0 Strict//EN',
+ 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
+ )
+ XHTML_TRANSITIONAL = (
+ 'html', '-//W3C//DTD XHTML 1.0 Transitional//EN',
+ 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'
+ )
+ XHTML_FRAMESET = (
+ 'html', '-//W3C//DTD XHTML 1.0 Frameset//EN',
+ 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd'
+ )
+ XHTML = XHTML_STRICT
+
+ XHTML11 = (
+ 'html', '-//W3C//DTD XHTML 1.1//EN',
+ 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
+ )
+
+ SVG_FULL = (
+ 'svg', '-//W3C//DTD SVG 1.1//EN',
+ 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'
+ )
+ SVG_BASIC = (
+ 'svg', '-//W3C//DTD SVG Basic 1.1//EN',
+ 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd'
+ )
+ SVG_TINY = (
+ 'svg', '-//W3C//DTD SVG Tiny 1.1//EN',
+ 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd'
+ )
+ SVG = SVG_FULL
+
+ @classmethod
+ def get(cls, name):
+ """Return the ``(name, pubid, sysid)`` tuple of the ``DOCTYPE``
+ declaration for the specified name.
+
+ The following names are recognized in this version:
+ * "html" or "html-strict" for the HTML 4.01 strict DTD
+ * "html-transitional" for the HTML 4.01 transitional DTD
+ * "html-frameset" for the HTML 4.01 frameset DTD
+ * "html5" for the ``DOCTYPE`` proposed for HTML5
+ * "xhtml" or "xhtml-strict" for the XHTML 1.0 strict DTD
+ * "xhtml-transitional" for the XHTML 1.0 transitional DTD
+ * "xhtml-frameset" for the XHTML 1.0 frameset DTD
+ * "xhtml11" for the XHTML 1.1 DTD
+ * "svg" or "svg-full" for the SVG 1.1 DTD
+ * "svg-basic" for the SVG Basic 1.1 DTD
+ * "svg-tiny" for the SVG Tiny 1.1 DTD
+
+ :param name: the name of the ``DOCTYPE``
+ :return: the ``(name, pubid, sysid)`` tuple for the requested
+ ``DOCTYPE``, or ``None`` if the name is not recognized
+ :since: version 0.4.1
+ """
+ return {
+ 'html': cls.HTML, 'html-strict': cls.HTML_STRICT,
+ 'html-transitional': DocType.HTML_TRANSITIONAL,
+ 'html-frameset': DocType.HTML_FRAMESET,
+ 'html5': cls.HTML5,
+ 'xhtml': cls.XHTML, 'xhtml-strict': cls.XHTML_STRICT,
+ 'xhtml-transitional': cls.XHTML_TRANSITIONAL,
+ 'xhtml-frameset': cls.XHTML_FRAMESET,
+ 'xhtml11': cls.XHTML11,
+ 'svg': cls.SVG, 'svg-full': cls.SVG_FULL,
+ 'svg-basic': cls.SVG_BASIC,
+ 'svg-tiny': cls.SVG_TINY
+ }.get(name.lower())
+
+
+class XMLSerializer(object):
+ """Produces XML text from an event stream.
+
+ >>> from genshi.builder import tag
+ >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
+ >>> print(''.join(XMLSerializer()(elem.generate())))
+ <div><a href="foo"/><br/><hr noshade="True"/></div>
+ """
+
+ _PRESERVE_SPACE = frozenset()
+
+ def __init__(self, doctype=None, strip_whitespace=True,
+ namespace_prefixes=None, cache=True):
+ """Initialize the XML serializer.
+
+ :param doctype: a ``(name, pubid, sysid)`` tuple that represents the
+ DOCTYPE declaration that should be included at the top
+ of the generated output, or the name of a DOCTYPE as
+ defined in `DocType.get`
+ :param strip_whitespace: whether extraneous whitespace should be
+ stripped from the output
+ :param cache: whether to cache the text output per event, which
+ improves performance for repetitive markup
+ :note: Changed in 0.4.2: The `doctype` parameter can now be a string.
+ :note: Changed in 0.6: The `cache` parameter was added
+ """
+ self.filters = [EmptyTagFilter()]
+ if strip_whitespace:
+ self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE))
+ self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes,
+ cache=cache))
+ if doctype:
+ self.filters.append(DocTypeInserter(doctype))
+ self.cache = cache
+
+ def __call__(self, stream):
+ have_decl = have_doctype = False
+ in_cdata = False
+
+ cache = {}
+ cache_get = cache.get
+ if self.cache:
+ def _emit(kind, input, output):
+ cache[kind, input] = output
+ return output
+ else:
+ def _emit(kind, input, output):
+ return output
+
+ for filter_ in self.filters:
+ stream = filter_(stream)
+ for kind, data, pos in stream:
+ cached = cache_get((kind, data))
+ if cached is not None:
+ yield cached
+
+ elif kind is START or kind is EMPTY:
+ tag, attrib = data
+ buf = ['<', tag]
+ for attr, value in attrib:
+ buf += [' ', attr, '="', escape(value), '"']
+ buf.append(kind is EMPTY and '/>' or '>')
+ yield _emit(kind, data, Markup(''.join(buf)))
+
+ elif kind is END:
+ yield _emit(kind, data, Markup('</%s>' % data))
+
+ elif kind is TEXT:
+ if in_cdata:
+ yield _emit(kind, data, data)
+ else:
+ yield _emit(kind, data, escape(data, quotes=False))
+
+ elif kind is COMMENT:
+ yield _emit(kind, data, Markup('<!--%s-->' % data))
+
+ elif kind is XML_DECL and not have_decl:
+ version, encoding, standalone = data
+ buf = ['<?xml version="%s"' % version]
+ if encoding:
+ buf.append(' encoding="%s"' % encoding)
+ if standalone != -1:
+ standalone = standalone and 'yes' or 'no'
+ buf.append(' standalone="%s"' % standalone)
+ buf.append('?>\n')
+ yield Markup(''.join(buf))
+ have_decl = True
+
+ elif kind is DOCTYPE and not have_doctype:
+ name, pubid, sysid = data
+ buf = ['<!DOCTYPE %s']
+ if pubid:
+ buf.append(' PUBLIC "%s"')
+ elif sysid:
+ buf.append(' SYSTEM')
+ if sysid:
+ buf.append(' "%s"')
+ buf.append('>\n')
+ yield Markup(''.join(buf)) % tuple([p for p in data if p])
+ have_doctype = True
+
+ elif kind is START_CDATA:
+ yield Markup('<![CDATA[')
+ in_cdata = True
+
+ elif kind is END_CDATA:
+ yield Markup(']]>')
+ in_cdata = False
+
+ elif kind is PI:
+ yield _emit(kind, data, Markup('<?%s %s?>' % data))
+
+
+class XHTMLSerializer(XMLSerializer):
+ """Produces XHTML text from an event stream.
+
+ >>> from genshi.builder import tag
+ >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
+ >>> print(''.join(XHTMLSerializer()(elem.generate())))
+ <div><a href="foo"></a><br /><hr noshade="noshade" /></div>
+ """
+
+ _EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'br', 'col', 'frame',
+ 'hr', 'img', 'input', 'isindex', 'link', 'meta',
+ 'param'])
+ _BOOLEAN_ATTRS = frozenset(['selected', 'checked', 'compact', 'declare',
+ 'defer', 'disabled', 'ismap', 'multiple',
+ 'nohref', 'noresize', 'noshade', 'nowrap'])
+ _PRESERVE_SPACE = frozenset([
+ QName('pre'), QName('http://www.w3.org/1999/xhtml}pre'),
+ QName('textarea'), QName('http://www.w3.org/1999/xhtml}textarea')
+ ])
+
+ def __init__(self, doctype=None, strip_whitespace=True,
+ namespace_prefixes=None, drop_xml_decl=True, cache=True):
+ super(XHTMLSerializer, self).__init__(doctype, False)
+ self.filters = [EmptyTagFilter()]
+ if strip_whitespace:
+ self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE))
+ namespace_prefixes = namespace_prefixes or {}
+ namespace_prefixes['http://www.w3.org/1999/xhtml'] = ''
+ self.filters.append(NamespaceFlattener(prefixes=namespace_prefixes,
+ cache=cache))
+ if doctype:
+ self.filters.append(DocTypeInserter(doctype))
+ self.drop_xml_decl = drop_xml_decl
+ self.cache = cache
+
+ def __call__(self, stream):
+ boolean_attrs = self._BOOLEAN_ATTRS
+ empty_elems = self._EMPTY_ELEMS
+ drop_xml_decl = self.drop_xml_decl
+ have_decl = have_doctype = False
+ in_cdata = False
+
+ cache = {}
+ cache_get = cache.get
+ if self.cache:
+ def _emit(kind, input, output):
+ cache[kind, input] = output
+ return output
+ else:
+ def _emit(kind, input, output):
+ return output
+
+ for filter_ in self.filters:
+ stream = filter_(stream)
+ for kind, data, pos in stream:
+ cached = cache_get((kind, data))
+ if cached is not None:
+ yield cached
+
+ elif kind is START or kind is EMPTY:
+ tag, attrib = data
+ buf = ['<', tag]
+ for attr, value in attrib:
+ if attr in boolean_attrs:
+ value = attr
+ elif attr == 'xml:lang' and 'lang' not in attrib:
+ buf += [' lang="', escape(value), '"']
+ elif attr == 'xml:space':
+ continue
+ buf += [' ', attr, '="', escape(value), '"']
+ if kind is EMPTY:
+ if tag in empty_elems:
+ buf.append(' />')
+ else:
+ buf.append('></%s>' % tag)
+ else:
+ buf.append('>')
+ yield _emit(kind, data, Markup(''.join(buf)))
+
+ elif kind is END:
+ yield _emit(kind, data, Markup('</%s>' % data))
+
+ elif kind is TEXT:
+ if in_cdata:
+ yield _emit(kind, data, data)
+ else:
+ yield _emit(kind, data, escape(data, quotes=False))
+
+ elif kind is COMMENT:
+ yield _emit(kind, data, Markup('<!--%s-->' % data))
+
+ elif kind is DOCTYPE and not have_doctype:
+ name, pubid, sysid = data
+ buf = ['<!DOCTYPE %s']
+ if pubid:
+ buf.append(' PUBLIC "%s"')
+ elif sysid:
+ buf.append(' SYSTEM')
+ if sysid:
+ buf.append(' "%s"')
+ buf.append('>\n')
+ yield Markup(''.join(buf)) % tuple([p for p in data if p])
+ have_doctype = True
+
+ elif kind is XML_DECL and not have_decl and not drop_xml_decl:
+ version, encoding, standalone = data
+ buf = ['<?xml version="%s"' % version]
+ if encoding:
+ buf.append(' encoding="%s"' % encoding)
+ if standalone != -1:
+ standalone = standalone and 'yes' or 'no'
+ buf.append(' standalone="%s"' % standalone)
+ buf.append('?>\n')
+ yield Markup(''.join(buf))
+ have_decl = True
+
+ elif kind is START_CDATA:
+ yield Markup('<![CDATA[')
+ in_cdata = True
+
+ elif kind is END_CDATA:
+ yield Markup(']]>')
+ in_cdata = False
+
+ elif kind is PI:
+ yield _emit(kind, data, Markup('<?%s %s?>' % data))
+
+
+class HTMLSerializer(XHTMLSerializer):
+ """Produces HTML text from an event stream.
+
+ >>> from genshi.builder import tag
+ >>> elem = tag.div(tag.a(href='foo'), tag.br, tag.hr(noshade=True))
+ >>> print(''.join(HTMLSerializer()(elem.generate())))
+ <div><a href="foo"></a><br><hr noshade></div>
+ """
+
+ _NOESCAPE_ELEMS = frozenset([
+ QName('script'), QName('http://www.w3.org/1999/xhtml}script'),
+ QName('style'), QName('http://www.w3.org/1999/xhtml}style')
+ ])
+
+ def __init__(self, doctype=None, strip_whitespace=True, cache=True):
+ """Initialize the HTML serializer.
+
+ :param doctype: a ``(name, pubid, sysid)`` tuple that represents the
+ DOCTYPE declaration that should be included at the top
+ of the generated output
+ :param strip_whitespace: whether extraneous whitespace should be
+ stripped from the output
+ :param cache: whether to cache the text output per event, which
+ improves performance for repetitive markup
+ :note: Changed in 0.6: The `cache` parameter was added
+ """
+ super(HTMLSerializer, self).__init__(doctype, False)
+ self.filters = [EmptyTagFilter()]
+ if strip_whitespace:
+ self.filters.append(WhitespaceFilter(self._PRESERVE_SPACE,
+ self._NOESCAPE_ELEMS))
+ self.filters.append(NamespaceFlattener(prefixes={
+ 'http://www.w3.org/1999/xhtml': ''
+ }, cache=cache))
+ if doctype:
+ self.filters.append(DocTypeInserter(doctype))
+ self.cache = True
+
+ def __call__(self, stream):
+ boolean_attrs = self._BOOLEAN_ATTRS
+ empty_elems = self._EMPTY_ELEMS
+ noescape_elems = self._NOESCAPE_ELEMS
+ have_doctype = False
+ noescape = False
+
+ cache = {}
+ cache_get = cache.get
+ if self.cache:
+ def _emit(kind, input, output):
+ cache[kind, input] = output
+ return output
+ else:
+ def _emit(kind, input, output):
+ return output
+
+ for filter_ in self.filters:
+ stream = filter_(stream)
+ for kind, data, _ in stream:
+ output = cache_get((kind, data))
+ if output is not None:
+ yield output
+ if (kind is START or kind is EMPTY) \
+ and data[0] in noescape_elems:
+ noescape = True
+ elif kind is END:
+ noescape = False
+
+ elif kind is START or kind is EMPTY:
+ tag, attrib = data
+ buf = ['<', tag]
+ for attr, value in attrib:
+ if attr in boolean_attrs:
+ if value:
+ buf += [' ', attr]
+ elif ':' in attr:
+ if attr == 'xml:lang' and 'lang' not in attrib:
+ buf += [' lang="', escape(value), '"']
+ elif attr != 'xmlns':
+ buf += [' ', attr, '="', escape(value), '"']
+ buf.append('>')
+ if kind is EMPTY:
+ if tag not in empty_elems:
+ buf.append('</%s>' % tag)
+ yield _emit(kind, data, Markup(''.join(buf)))
+ if tag in noescape_elems:
+ noescape = True
+
+ elif kind is END:
+ yield _emit(kind, data, Markup('</%s>' % data))
+ noescape = False
+
+ elif kind is TEXT:
+ if noescape:
+ yield _emit(kind, data, data)
+ else:
+ yield _emit(kind, data, escape(data, quotes=False))
+
+ elif kind is COMMENT:
+ yield _emit(kind, data, Markup('<!--%s-->' % data))
+
+ elif kind is DOCTYPE and not have_doctype:
+ name, pubid, sysid = data
+ buf = ['<!DOCTYPE %s']
+ if pubid:
+ buf.append(' PUBLIC "%s"')
+ elif sysid:
+ buf.append(' SYSTEM')
+ if sysid:
+ buf.append(' "%s"')
+ buf.append('>\n')
+ yield Markup(''.join(buf)) % tuple([p for p in data if p])
+ have_doctype = True
+
+ elif kind is PI:
+ yield _emit(kind, data, Markup('<?%s %s?>' % data))
+
+
+class TextSerializer(object):
+ """Produces plain text from an event stream.
+
+ Only text events are included in the output. Unlike the other serializer,
+ special XML characters are not escaped:
+
+ >>> from genshi.builder import tag
+ >>> elem = tag.div(tag.a('<Hello!>', href='foo'), tag.br)
+ >>> print(elem)
+ <div><a href="foo">&lt;Hello!&gt;</a><br/></div>
+ >>> print(''.join(TextSerializer()(elem.generate())))
+ <Hello!>
+
+ If text events contain literal markup (instances of the `Markup` class),
+ that markup is by default passed through unchanged:
+
+ >>> elem = tag.div(Markup('<a href="foo">Hello &amp; Bye!</a><br/>'))
+ >>> print(elem.generate().render(TextSerializer, encoding=None))
+ <a href="foo">Hello &amp; Bye!</a><br/>
+
+ You can use the ``strip_markup`` to change this behavior, so that tags and
+ entities are stripped from the output (or in the case of entities,
+ replaced with the equivalent character):
+
+ >>> print(elem.generate().render(TextSerializer, strip_markup=True,
+ ... encoding=None))
+ Hello & Bye!
+ """
+
+ def __init__(self, strip_markup=False):
+ """Create the serializer.
+
+ :param strip_markup: whether markup (tags and encoded characters) found
+ in the text should be removed
+ """
+ self.strip_markup = strip_markup
+
+ def __call__(self, stream):
+ strip_markup = self.strip_markup
+ for event in stream:
+ if event[0] is TEXT:
+ data = event[1]
+ if strip_markup and type(data) is Markup:
+ data = data.striptags().stripentities()
+ yield unicode(data)
+
+
+class EmptyTagFilter(object):
+ """Combines `START` and `STOP` events into `EMPTY` events for elements that
+ have no contents.
+ """
+
+ EMPTY = StreamEventKind('EMPTY')
+
+ def __call__(self, stream):
+ prev = (None, None, None)
+ for ev in stream:
+ if prev[0] is START:
+ if ev[0] is END:
+ prev = EMPTY, prev[1], prev[2]
+ yield prev
+ continue
+ else:
+ yield prev
+ if ev[0] is not START:
+ yield ev
+ prev = ev
+
+
+EMPTY = EmptyTagFilter.EMPTY
+
+
+class NamespaceFlattener(object):
+ r"""Output stream filter that removes namespace information from the stream,
+ instead adding namespace attributes and prefixes as needed.
+
+ :param prefixes: optional mapping of namespace URIs to prefixes
+
+ >>> from genshi.input import XML
+ >>> xml = XML('''<doc xmlns="NS1" xmlns:two="NS2">
+ ... <two:item/>
+ ... </doc>''')
+ >>> for kind, data, pos in NamespaceFlattener()(xml):
+ ... print('%s %r' % (kind, data))
+ START (u'doc', Attrs([('xmlns', u'NS1'), (u'xmlns:two', u'NS2')]))
+ TEXT u'\n '
+ START (u'two:item', Attrs())
+ END u'two:item'
+ TEXT u'\n'
+ END u'doc'
+ """
+
+ def __init__(self, prefixes=None, cache=True):
+ self.prefixes = {XML_NAMESPACE.uri: 'xml'}
+ if prefixes is not None:
+ self.prefixes.update(prefixes)
+ self.cache = cache
+
+ def __call__(self, stream):
+ cache = {}
+ cache_get = cache.get
+ if self.cache:
+ def _emit(kind, input, output, pos):
+ cache[kind, input] = output
+ return kind, output, pos
+ else:
+ def _emit(kind, input, output, pos):
+ return output
+
+ prefixes = dict([(v, [k]) for k, v in self.prefixes.items()])
+ namespaces = {XML_NAMESPACE.uri: ['xml']}
+ def _push_ns(prefix, uri):
+ namespaces.setdefault(uri, []).append(prefix)
+ prefixes.setdefault(prefix, []).append(uri)
+ cache.clear()
+ def _pop_ns(prefix):
+ uris = prefixes.get(prefix)
+ uri = uris.pop()
+ if not uris:
+ del prefixes[prefix]
+ if uri not in uris or uri != uris[-1]:
+ uri_prefixes = namespaces[uri]
+ uri_prefixes.pop()
+ if not uri_prefixes:
+ del namespaces[uri]
+ cache.clear()
+ return uri
+
+ ns_attrs = []
+ _push_ns_attr = ns_attrs.append
+ def _make_ns_attr(prefix, uri):
+ return 'xmlns%s' % (prefix and ':%s' % prefix or ''), uri
+
+ def _gen_prefix():
+ val = 0
+ while 1:
+ val += 1
+ yield 'ns%d' % val
+ _gen_prefix = _gen_prefix().next
+
+ for kind, data, pos in stream:
+ output = cache_get((kind, data))
+ if output is not None:
+ yield kind, output, pos
+
+ elif kind is START or kind is EMPTY:
+ tag, attrs = data
+
+ tagname = tag.localname
+ tagns = tag.namespace
+ if tagns:
+ if tagns in namespaces:
+ prefix = namespaces[tagns][-1]
+ if prefix:
+ tagname = '%s:%s' % (prefix, tagname)
+ else:
+ _push_ns_attr(('xmlns', tagns))
+ _push_ns('', tagns)
+
+ new_attrs = []
+ for attr, value in attrs:
+ attrname = attr.localname
+ attrns = attr.namespace
+ if attrns:
+ if attrns not in namespaces:
+ prefix = _gen_prefix()
+ _push_ns(prefix, attrns)
+ _push_ns_attr(('xmlns:%s' % prefix, attrns))
+ else:
+ prefix = namespaces[attrns][-1]
+ if prefix:
+ attrname = '%s:%s' % (prefix, attrname)
+ new_attrs.append((attrname, value))
+
+ yield _emit(kind, data, (tagname, Attrs(ns_attrs + new_attrs)), pos)
+ del ns_attrs[:]
+
+ elif kind is END:
+ tagname = data.localname
+ tagns = data.namespace
+ if tagns:
+ prefix = namespaces[tagns][-1]
+ if prefix:
+ tagname = '%s:%s' % (prefix, tagname)
+ yield _emit(kind, data, tagname, pos)
+
+ elif kind is START_NS:
+ prefix, uri = data
+ if uri not in namespaces:
+ prefix = prefixes.get(uri, [prefix])[-1]
+ _push_ns_attr(_make_ns_attr(prefix, uri))
+ _push_ns(prefix, uri)
+
+ elif kind is END_NS:
+ if data in prefixes:
+ uri = _pop_ns(data)
+ if ns_attrs:
+ attr = _make_ns_attr(data, uri)
+ if attr in ns_attrs:
+ ns_attrs.remove(attr)
+
+ else:
+ yield kind, data, pos
+
+
+class WhitespaceFilter(object):
+ """A filter that removes extraneous ignorable white space from the
+ stream.
+ """
+
+ def __init__(self, preserve=None, noescape=None):
+ """Initialize the filter.
+
+ :param preserve: a set or sequence of tag names for which white-space
+ should be preserved
+ :param noescape: a set or sequence of tag names for which text content
+ should not be escaped
+
+ The `noescape` set is expected to refer to elements that cannot contain
+ further child elements (such as ``<style>`` or ``<script>`` in HTML
+ documents).
+ """
+ if preserve is None:
+ preserve = []
+ self.preserve = frozenset(preserve)
+ if noescape is None:
+ noescape = []
+ self.noescape = frozenset(noescape)
+
+ def __call__(self, stream, ctxt=None, space=XML_NAMESPACE['space'],
+ trim_trailing_space=re.compile('[ \t]+(?=\n)').sub,
+ collapse_lines=re.compile('\n{2,}').sub):
+ mjoin = Markup('').join
+ preserve_elems = self.preserve
+ preserve = 0
+ noescape_elems = self.noescape
+ noescape = False
+
+ textbuf = []
+ push_text = textbuf.append
+ pop_text = textbuf.pop
+ for kind, data, pos in chain(stream, [(None, None, None)]):
+
+ if kind is TEXT:
+ if noescape:
+ data = Markup(data)
+ push_text(data)
+ else:
+ if textbuf:
+ if len(textbuf) > 1:
+ text = mjoin(textbuf, escape_quotes=False)
+ del textbuf[:]
+ else:
+ text = escape(pop_text(), quotes=False)
+ if not preserve:
+ text = collapse_lines('\n', trim_trailing_space('', text))
+ yield TEXT, Markup(text), pos
+
+ if kind is START:
+ tag, attrs = data
+ if preserve or (tag in preserve_elems or
+ attrs.get(space) == 'preserve'):
+ preserve += 1
+ if not noescape and tag in noescape_elems:
+ noescape = True
+
+ elif kind is END:
+ noescape = False
+ if preserve:
+ preserve -= 1
+
+ elif kind is START_CDATA:
+ noescape = True
+
+ elif kind is END_CDATA:
+ noescape = False
+
+ if kind:
+ yield kind, data, pos
+
+
+class DocTypeInserter(object):
+ """A filter that inserts the DOCTYPE declaration in the correct location,
+ after the XML declaration.
+ """
+ def __init__(self, doctype):
+ """Initialize the filter.
+
+ :param doctype: DOCTYPE as a string or DocType object.
+ """
+ if isinstance(doctype, basestring):
+ doctype = DocType.get(doctype)
+ self.doctype_event = (DOCTYPE, doctype, (None, -1, -1))
+
+ def __call__(self, stream):
+ doctype_inserted = False
+ for kind, data, pos in stream:
+ if not doctype_inserted:
+ doctype_inserted = True
+ if kind is XML_DECL:
+ yield (kind, data, pos)
+ yield self.doctype_event
+ continue
+ yield self.doctype_event
+
+ yield (kind, data, pos)
+
+ if not doctype_inserted:
+ yield self.doctype_event
diff --git a/genshi/path.py b/genshi/path.py
new file mode 100644
index 0000000..122fbf0
--- /dev/null
+++ b/genshi/path.py
@@ -0,0 +1,1528 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Basic support for evaluating XPath expressions against streams.
+
+>>> from genshi.input import XML
+>>> doc = XML('''<doc>
+... <items count="4">
+... <item status="new">
+... <summary>Foo</summary>
+... </item>
+... <item status="closed">
+... <summary>Bar</summary>
+... </item>
+... <item status="closed" resolution="invalid">
+... <summary>Baz</summary>
+... </item>
+... <item status="closed" resolution="fixed">
+... <summary>Waz</summary>
+... </item>
+... </items>
+... </doc>''')
+>>> print(doc.select('items/item[@status="closed" and '
+... '(@resolution="invalid" or not(@resolution))]/summary/text()'))
+BarBaz
+
+Because the XPath engine operates on markup streams (as opposed to tree
+structures), it only implements a subset of the full XPath 1.0 language.
+"""
+
+from collections import deque
+try:
+ reduce # builtin in Python < 3
+except NameError:
+ from functools import reduce
+from math import ceil, floor
+import operator
+import re
+from itertools import chain
+
+from genshi.core import Stream, Attrs, Namespace, QName
+from genshi.core import START, END, TEXT, START_NS, END_NS, COMMENT, PI, \
+ START_CDATA, END_CDATA
+
+__all__ = ['Path', 'PathSyntaxError']
+__docformat__ = 'restructuredtext en'
+
+
+class Axis(object):
+ """Defines constants for the various supported XPath axes."""
+
+ ATTRIBUTE = 'attribute'
+ CHILD = 'child'
+ DESCENDANT = 'descendant'
+ DESCENDANT_OR_SELF = 'descendant-or-self'
+ SELF = 'self'
+
+ @classmethod
+ def forname(cls, name):
+ """Return the axis constant for the given name, or `None` if no such
+ axis was defined.
+ """
+ return getattr(cls, name.upper().replace('-', '_'), None)
+
+
+ATTRIBUTE = Axis.ATTRIBUTE
+CHILD = Axis.CHILD
+DESCENDANT = Axis.DESCENDANT
+DESCENDANT_OR_SELF = Axis.DESCENDANT_OR_SELF
+SELF = Axis.SELF
+
+
+class GenericStrategy(object):
+
+ @classmethod
+ def supports(cls, path):
+ return True
+
+ def __init__(self, path):
+ self.path = path
+
+ def test(self, ignore_context):
+ p = self.path
+ if ignore_context:
+ if p[0][0] is ATTRIBUTE:
+ steps = [_DOTSLASHSLASH] + p
+ else:
+ steps = [(DESCENDANT_OR_SELF, p[0][1], p[0][2])] + p[1:]
+ elif p[0][0] is CHILD or p[0][0] is ATTRIBUTE \
+ or p[0][0] is DESCENDANT:
+ steps = [_DOTSLASH] + p
+ else:
+ steps = p
+
+ # for node it contains all positions of xpath expression
+ # where its child should start checking for matches
+ # with list of corresponding context counters
+ # there can be many of them, because position that is from
+ # descendant-like axis can be achieved from different nodes
+ # for example <a><a><b/></a></a> should match both //a//b[1]
+ # and //a//b[2]
+ # positions always form increasing sequence (invariant)
+ stack = [[(0, [[]])]]
+
+ def _test(event, namespaces, variables, updateonly=False):
+ kind, data, pos = event[:3]
+ retval = None
+
+ # Manage the stack that tells us "where we are" in the stream
+ if kind is END:
+ if stack:
+ stack.pop()
+ return None
+ if kind is START_NS or kind is END_NS \
+ or kind is START_CDATA or kind is END_CDATA:
+ # should we make namespaces work?
+ return None
+
+ pos_queue = deque([(pos, cou, []) for pos, cou in stack[-1]])
+ next_pos = []
+
+ # length of real part of path - we omit attribute axis
+ real_len = len(steps) - ((steps[-1][0] == ATTRIBUTE) or 1 and 0)
+ last_checked = -1
+
+ # places where we have to check for match, are these
+ # provided by parent
+ while pos_queue:
+ x, pcou, mcou = pos_queue.popleft()
+ axis, nodetest, predicates = steps[x]
+
+ # we need to push descendant-like positions from parent
+ # further
+ if (axis is DESCENDANT or axis is DESCENDANT_OR_SELF) and pcou:
+ if next_pos and next_pos[-1][0] == x:
+ next_pos[-1][1].extend(pcou)
+ else:
+ next_pos.append((x, pcou))
+
+ # nodetest first
+ if not nodetest(kind, data, pos, namespaces, variables):
+ continue
+
+ # counters packs that were already bad
+ missed = set()
+ counters_len = len(pcou) + len(mcou)
+
+ # number of counters - we have to create one
+ # for every context position based predicate
+ cnum = 0
+
+ # tells if we have match with position x
+ matched = True
+
+ if predicates:
+ for predicate in predicates:
+ pretval = predicate(kind, data, pos,
+ namespaces,
+ variables)
+ if type(pretval) is float: # FIXME <- need to check
+ # this for other types that
+ # can be coerced to float
+
+ # each counter pack needs to be checked
+ for i, cou in enumerate(chain(pcou, mcou)):
+ # it was bad before
+ if i in missed:
+ continue
+
+ if len(cou) < cnum + 1:
+ cou.append(0)
+ cou[cnum] += 1
+
+ # it is bad now
+ if cou[cnum] != int(pretval):
+ missed.add(i)
+
+ # none of counters pack was good
+ if len(missed) == counters_len:
+ pretval = False
+ cnum += 1
+
+ if not pretval:
+ matched = False
+ break
+
+ if not matched:
+ continue
+
+ # counter for next position with current node as context node
+ child_counter = []
+
+ if x + 1 == real_len:
+ # we reached end of expression, because x + 1
+ # is equal to the length of expression
+ matched = True
+ axis, nodetest, predicates = steps[-1]
+ if axis is ATTRIBUTE:
+ matched = nodetest(kind, data, pos, namespaces,
+ variables)
+ if matched:
+ retval = matched
+ else:
+ next_axis = steps[x + 1][0]
+
+ # if next axis allows matching self we have
+ # to add next position to our queue
+ if next_axis is DESCENDANT_OR_SELF or next_axis is SELF:
+ if not pos_queue or pos_queue[0][0] > x + 1:
+ pos_queue.appendleft((x + 1, [], [child_counter]))
+ else:
+ pos_queue[0][2].append(child_counter)
+
+ # if axis is not self we have to add it to child's list
+ if next_axis is not SELF:
+ next_pos.append((x + 1, [child_counter]))
+
+ if kind is START:
+ stack.append(next_pos)
+
+ return retval
+
+ return _test
+
+
+class SimplePathStrategy(object):
+ """Strategy for path with only local names, attributes and text nodes."""
+
+ @classmethod
+ def supports(cls, path):
+ if path[0][0] is ATTRIBUTE:
+ return False
+ allowed_tests = (LocalNameTest, CommentNodeTest, TextNodeTest)
+ for _, nodetest, predicates in path:
+ if predicates:
+ return False
+ if not isinstance(nodetest, allowed_tests):
+ return False
+ return True
+
+ def __init__(self, path):
+ # fragments is list of tuples (fragment, pi, attr, self_beginning)
+ # fragment is list of nodetests for fragment of path with only
+ # child:: axes between
+ # pi is KMP partial match table for this fragment
+ # attr is attribute nodetest if fragment ends with @ and None otherwise
+ # self_beginning is True if axis for first fragment element
+ # was self (first fragment) or descendant-or-self (farther fragment)
+ self.fragments = []
+
+ self_beginning = False
+ fragment = []
+
+ def nodes_equal(node1, node2):
+ """Tests if two node tests are equal"""
+ if type(node1) is not type(node2):
+ return False
+ if type(node1) == LocalNameTest:
+ return node1.name == node2.name
+ return True
+
+ def calculate_pi(f):
+ """KMP prefix calculation for table"""
+ # the indexes in prefix table are shifted by one
+ # in comparision with common implementations
+ # pi[i] = NORMAL_PI[i + 1]
+ if len(f) == 0:
+ return []
+ pi = [0]
+ s = 0
+ for i in range(1, len(f)):
+ while s > 0 and not nodes_equal(f[s], f[i]):
+ s = pi[s-1]
+ if nodes_equal(f[s], f[i]):
+ s += 1
+ pi.append(s)
+ return pi
+
+ for axis in path:
+ if axis[0] is SELF:
+ if len(fragment) != 0:
+ # if element is not first in fragment it has to be
+ # the same as previous one
+ # for example child::a/self::b is always wrong
+ if axis[1] != fragment[-1][1]:
+ self.fragments = None
+ return
+ else:
+ self_beginning = True
+ fragment.append(axis[1])
+ elif axis[0] is CHILD:
+ fragment.append(axis[1])
+ elif axis[0] is ATTRIBUTE:
+ pi = calculate_pi(fragment)
+ self.fragments.append((fragment, pi, axis[1], self_beginning))
+ # attribute has always to be at the end, so we can jump out
+ return
+ else:
+ pi = calculate_pi(fragment)
+ self.fragments.append((fragment, pi, None, self_beginning))
+ fragment = [axis[1]]
+ if axis[0] is DESCENDANT:
+ self_beginning = False
+ else: # DESCENDANT_OR_SELF
+ self_beginning = True
+ pi = calculate_pi(fragment)
+ self.fragments.append((fragment, pi, None, self_beginning))
+
+ def test(self, ignore_context):
+ # stack of triples (fid, p, ic)
+ # fid is index of current fragment
+ # p is position in this fragment
+ # ic is if we ignore context in this fragment
+ stack = []
+ stack_push = stack.append
+ stack_pop = stack.pop
+ frags = self.fragments
+ frags_len = len(frags)
+
+ def _test(event, namespaces, variables, updateonly=False):
+ # expression found impossible during init
+ if frags is None:
+ return None
+
+ kind, data, pos = event[:3]
+
+ # skip events we don't care about
+ if kind is END:
+ if stack:
+ stack_pop()
+ return None
+ if kind is START_NS or kind is END_NS \
+ or kind is START_CDATA or kind is END_CDATA:
+ return None
+
+ if not stack:
+ # root node, nothing on stack, special case
+ fid = 0
+ # skip empty fragments (there can be actually only one)
+ while not frags[fid][0]:
+ fid += 1
+ p = 0
+ # empty fragment means descendant node at beginning
+ ic = ignore_context or (fid > 0)
+
+ # expression can match first node, if first axis is self::,
+ # descendant-or-self:: or if ignore_context is True and
+ # axis is not descendant::
+ if not frags[fid][3] and (not ignore_context or fid > 0):
+ # axis is not self-beggining, we have to skip this node
+ stack_push((fid, p, ic))
+ return None
+ else:
+ # take position of parent
+ fid, p, ic = stack[-1]
+
+ if fid is not None and not ic:
+ # fragment not ignoring context - we can't jump back
+ frag, pi, attrib, _ = frags[fid]
+ frag_len = len(frag)
+
+ if p == frag_len:
+ # that probably means empty first fragment
+ pass
+ elif frag[p](kind, data, pos, namespaces, variables):
+ # match, so we can go further
+ p += 1
+ else:
+ # not matched, so there will be no match in subtree
+ fid, p = None, None
+
+ if p == frag_len and fid + 1 != frags_len:
+ # we made it to end of fragment, we can go to following
+ fid += 1
+ p = 0
+ ic = True
+
+ if fid is None:
+ # there was no match in fragment not ignoring context
+ if kind is START:
+ stack_push((fid, p, ic))
+ return None
+
+ if ic:
+ # we are in fragment ignoring context
+ while True:
+ frag, pi, attrib, _ = frags[fid]
+ frag_len = len(frag)
+
+ # KMP new "character"
+ while p > 0 and (p >= frag_len or not \
+ frag[p](kind, data, pos, namespaces, variables)):
+ p = pi[p-1]
+ if frag[p](kind, data, pos, namespaces, variables):
+ p += 1
+
+ if p == frag_len:
+ # end of fragment reached
+ if fid + 1 == frags_len:
+ # that was last fragment
+ break
+ else:
+ fid += 1
+ p = 0
+ ic = True
+ if not frags[fid][3]:
+ # next fragment not self-beginning
+ break
+ else:
+ break
+
+ if kind is START:
+ # we have to put new position on stack, for children
+
+ if not ic and fid + 1 == frags_len and p == frag_len:
+ # it is end of the only, not context ignoring fragment
+ # so there will be no matches in subtree
+ stack_push((None, None, ic))
+ else:
+ stack_push((fid, p, ic))
+
+ # have we reached the end of the last fragment?
+ if fid + 1 == frags_len and p == frag_len:
+ if attrib: # attribute ended path, return value
+ return attrib(kind, data, pos, namespaces, variables)
+ return True
+
+ return None
+
+ return _test
+
+
+class SingleStepStrategy(object):
+
+ @classmethod
+ def supports(cls, path):
+ return len(path) == 1
+
+ def __init__(self, path):
+ self.path = path
+
+ def test(self, ignore_context):
+ steps = self.path
+ if steps[0][0] is ATTRIBUTE:
+ steps = [_DOTSLASH] + steps
+ select_attr = steps[-1][0] is ATTRIBUTE and steps[-1][1] or None
+
+ # for every position in expression stores counters' list
+ # it is used for position based predicates
+ counters = []
+ depth = [0]
+
+ def _test(event, namespaces, variables, updateonly=False):
+ kind, data, pos = event[:3]
+
+ # Manage the stack that tells us "where we are" in the stream
+ if kind is END:
+ if not ignore_context:
+ depth[0] -= 1
+ return None
+ elif kind is START_NS or kind is END_NS \
+ or kind is START_CDATA or kind is END_CDATA:
+ # should we make namespaces work?
+ return None
+
+ if not ignore_context:
+ outside = (steps[0][0] is SELF and depth[0] != 0) \
+ or (steps[0][0] is CHILD and depth[0] != 1) \
+ or (steps[0][0] is DESCENDANT and depth[0] < 1)
+ if kind is START:
+ depth[0] += 1
+ if outside:
+ return None
+
+ axis, nodetest, predicates = steps[0]
+ if not nodetest(kind, data, pos, namespaces, variables):
+ return None
+
+ if predicates:
+ cnum = 0
+ for predicate in predicates:
+ pretval = predicate(kind, data, pos, namespaces, variables)
+ if type(pretval) is float: # FIXME <- need to check this
+ # for other types that can be
+ # coerced to float
+ if len(counters) < cnum + 1:
+ counters.append(0)
+ counters[cnum] += 1
+ if counters[cnum] != int(pretval):
+ pretval = False
+ cnum += 1
+ if not pretval:
+ return None
+
+ if select_attr:
+ return select_attr(kind, data, pos, namespaces, variables)
+
+ return True
+
+ return _test
+
+
+class Path(object):
+ """Implements basic XPath support on streams.
+
+ Instances of this class represent a "compiled" XPath expression, and
+ provide methods for testing the path against a stream, as well as
+ extracting a substream matching that path.
+ """
+
+ STRATEGIES = (SingleStepStrategy, SimplePathStrategy, GenericStrategy)
+
+ def __init__(self, text, filename=None, lineno=-1):
+ """Create the path object from a string.
+
+ :param text: the path expression
+ :param filename: the name of the file in which the path expression was
+ found (used in error messages)
+ :param lineno: the line on which the expression was found
+ """
+ self.source = text
+ self.paths = PathParser(text, filename, lineno).parse()
+ self.strategies = []
+ for path in self.paths:
+ for strategy_class in self.STRATEGIES:
+ if strategy_class.supports(path):
+ self.strategies.append(strategy_class(path))
+ break
+ else:
+ raise NotImplemented('No strategy found for path')
+
+ def __repr__(self):
+ paths = []
+ for path in self.paths:
+ steps = []
+ for axis, nodetest, predicates in path:
+ steps.append('%s::%s' % (axis, nodetest))
+ for predicate in predicates:
+ steps[-1] += '[%s]' % predicate
+ paths.append('/'.join(steps))
+ return '<%s "%s">' % (type(self).__name__, '|'.join(paths))
+
+ def select(self, stream, namespaces=None, variables=None):
+ """Returns a substream of the given stream that matches the path.
+
+ If there are no matches, this method returns an empty stream.
+
+ >>> from genshi.input import XML
+ >>> xml = XML('<root><elem><child>Text</child></elem></root>')
+
+ >>> print(Path('.//child').select(xml))
+ <child>Text</child>
+
+ >>> print(Path('.//child/text()').select(xml))
+ Text
+
+ :param stream: the stream to select from
+ :param namespaces: (optional) a mapping of namespace prefixes to URIs
+ :param variables: (optional) a mapping of variable names to values
+ :return: the substream matching the path, or an empty stream
+ :rtype: `Stream`
+ """
+ if namespaces is None:
+ namespaces = {}
+ if variables is None:
+ variables = {}
+ stream = iter(stream)
+ def _generate(stream=stream, ns=namespaces, vs=variables):
+ next = stream.next
+ test = self.test()
+ for event in stream:
+ result = test(event, ns, vs)
+ if result is True:
+ yield event
+ if event[0] is START:
+ depth = 1
+ while depth > 0:
+ subevent = next()
+ if subevent[0] is START:
+ depth += 1
+ elif subevent[0] is END:
+ depth -= 1
+ yield subevent
+ test(subevent, ns, vs, updateonly=True)
+ elif result:
+ yield result
+ return Stream(_generate(),
+ serializer=getattr(stream, 'serializer', None))
+
+ def test(self, ignore_context=False):
+ """Returns a function that can be used to track whether the path matches
+ a specific stream event.
+
+ The function returned expects the positional arguments ``event``,
+ ``namespaces`` and ``variables``. The first is a stream event, while the
+ latter two are a mapping of namespace prefixes to URIs, and a mapping
+ of variable names to values, respectively. In addition, the function
+ accepts an ``updateonly`` keyword argument that default to ``False``. If
+ it is set to ``True``, the function only updates its internal state,
+ but does not perform any tests or return a result.
+
+ If the path matches the event, the function returns the match (for
+ example, a `START` or `TEXT` event.) Otherwise, it returns ``None``.
+
+ >>> from genshi.input import XML
+ >>> xml = XML('<root><elem><child id="1"/></elem><child id="2"/></root>')
+ >>> test = Path('child').test()
+ >>> namespaces, variables = {}, {}
+ >>> for event in xml:
+ ... if test(event, namespaces, variables):
+ ... print('%s %r' % (event[0], event[1]))
+ START (QName('child'), Attrs([(QName('id'), u'2')]))
+
+ :param ignore_context: if `True`, the path is interpreted like a pattern
+ in XSLT, meaning for example that it will match
+ at any depth
+ :return: a function that can be used to test individual events in a
+ stream against the path
+ :rtype: ``function``
+ """
+ tests = [s.test(ignore_context) for s in self.strategies]
+ if len(tests) == 1:
+ return tests[0]
+
+ def _multi(event, namespaces, variables, updateonly=False):
+ retval = None
+ for test in tests:
+ val = test(event, namespaces, variables, updateonly=updateonly)
+ if retval is None:
+ retval = val
+ return retval
+ return _multi
+
+
+class PathSyntaxError(Exception):
+ """Exception raised when an XPath expression is syntactically incorrect."""
+
+ def __init__(self, message, filename=None, lineno=-1, offset=-1):
+ if filename:
+ message = '%s (%s, line %d)' % (message, filename, lineno)
+ Exception.__init__(self, message)
+ self.filename = filename
+ self.lineno = lineno
+ self.offset = offset
+
+
+class PathParser(object):
+ """Tokenizes and parses an XPath expression."""
+
+ _QUOTES = (("'", "'"), ('"', '"'))
+ _TOKENS = ('::', ':', '..', '.', '//', '/', '[', ']', '()', '(', ')', '@',
+ '=', '!=', '!', '|', ',', '>=', '>', '<=', '<', '$')
+ _tokenize = re.compile('("[^"]*")|(\'[^\']*\')|((?:\d+)?\.\d+)|(%s)|([^%s\s]+)|\s+' % (
+ '|'.join([re.escape(t) for t in _TOKENS]),
+ ''.join([re.escape(t[0]) for t in _TOKENS]))).findall
+
+ def __init__(self, text, filename=None, lineno=-1):
+ self.filename = filename
+ self.lineno = lineno
+ self.tokens = [t for t in [dqstr or sqstr or number or token or name
+ for dqstr, sqstr, number, token, name in
+ self._tokenize(text)] if t]
+ self.pos = 0
+
+ # Tokenizer
+
+ @property
+ def at_end(self):
+ return self.pos == len(self.tokens) - 1
+
+ @property
+ def cur_token(self):
+ return self.tokens[self.pos]
+
+ def next_token(self):
+ self.pos += 1
+ return self.tokens[self.pos]
+
+ def peek_token(self):
+ if not self.at_end:
+ return self.tokens[self.pos + 1]
+ return None
+
+ # Recursive descent parser
+
+ def parse(self):
+ """Parses the XPath expression and returns a list of location path
+ tests.
+
+ For union expressions (such as `*|text()`), this function returns one
+ test for each operand in the union. For patch expressions that don't
+ use the union operator, the function always returns a list of size 1.
+
+ Each path test in turn is a sequence of tests that correspond to the
+ location steps, each tuples of the form `(axis, testfunc, predicates)`
+ """
+ paths = [self._location_path()]
+ while self.cur_token == '|':
+ self.next_token()
+ paths.append(self._location_path())
+ if not self.at_end:
+ raise PathSyntaxError('Unexpected token %r after end of expression'
+ % self.cur_token, self.filename, self.lineno)
+ return paths
+
+ def _location_path(self):
+ steps = []
+ while True:
+ if self.cur_token.startswith('/'):
+ if not steps:
+ if self.cur_token == '//':
+ # hack to make //* match every node - also root
+ self.next_token()
+ axis, nodetest, predicates = self._location_step()
+ steps.append((DESCENDANT_OR_SELF, nodetest,
+ predicates))
+ if self.at_end or not self.cur_token.startswith('/'):
+ break
+ continue
+ else:
+ raise PathSyntaxError('Absolute location paths not '
+ 'supported', self.filename,
+ self.lineno)
+ elif self.cur_token == '//':
+ steps.append((DESCENDANT_OR_SELF, NodeTest(), []))
+ self.next_token()
+
+ axis, nodetest, predicates = self._location_step()
+ if not axis:
+ axis = CHILD
+ steps.append((axis, nodetest, predicates))
+ if self.at_end or not self.cur_token.startswith('/'):
+ break
+
+ return steps
+
+ def _location_step(self):
+ if self.cur_token == '@':
+ axis = ATTRIBUTE
+ self.next_token()
+ elif self.cur_token == '.':
+ axis = SELF
+ elif self.cur_token == '..':
+ raise PathSyntaxError('Unsupported axis "parent"', self.filename,
+ self.lineno)
+ elif self.peek_token() == '::':
+ axis = Axis.forname(self.cur_token)
+ if axis is None:
+ raise PathSyntaxError('Unsupport axis "%s"' % axis,
+ self.filename, self.lineno)
+ self.next_token()
+ self.next_token()
+ else:
+ axis = None
+ nodetest = self._node_test(axis or CHILD)
+ predicates = []
+ while self.cur_token == '[':
+ predicates.append(self._predicate())
+ return axis, nodetest, predicates
+
+ def _node_test(self, axis=None):
+ test = prefix = None
+ next_token = self.peek_token()
+ if next_token in ('(', '()'): # Node type test
+ test = self._node_type()
+
+ elif next_token == ':': # Namespace prefix
+ prefix = self.cur_token
+ self.next_token()
+ localname = self.next_token()
+ if localname == '*':
+ test = QualifiedPrincipalTypeTest(axis, prefix)
+ else:
+ test = QualifiedNameTest(axis, prefix, localname)
+
+ else: # Name test
+ if self.cur_token == '*':
+ test = PrincipalTypeTest(axis)
+ elif self.cur_token == '.':
+ test = NodeTest()
+ else:
+ test = LocalNameTest(axis, self.cur_token)
+
+ if not self.at_end:
+ self.next_token()
+ return test
+
+ def _node_type(self):
+ name = self.cur_token
+ self.next_token()
+
+ args = []
+ if self.cur_token != '()':
+ # The processing-instruction() function optionally accepts the
+ # name of the PI as argument, which must be a literal string
+ self.next_token() # (
+ if self.cur_token != ')':
+ string = self.cur_token
+ if (string[0], string[-1]) in self._QUOTES:
+ string = string[1:-1]
+ args.append(string)
+
+ cls = _nodetest_map.get(name)
+ if not cls:
+ raise PathSyntaxError('%s() not allowed here' % name, self.filename,
+ self.lineno)
+ return cls(*args)
+
+ def _predicate(self):
+ assert self.cur_token == '['
+ self.next_token()
+ expr = self._or_expr()
+ if self.cur_token != ']':
+ raise PathSyntaxError('Expected "]" to close predicate, '
+ 'but found "%s"' % self.cur_token,
+ self.filename, self.lineno)
+ if not self.at_end:
+ self.next_token()
+ return expr
+
+ def _or_expr(self):
+ expr = self._and_expr()
+ while self.cur_token == 'or':
+ self.next_token()
+ expr = OrOperator(expr, self._and_expr())
+ return expr
+
+ def _and_expr(self):
+ expr = self._equality_expr()
+ while self.cur_token == 'and':
+ self.next_token()
+ expr = AndOperator(expr, self._equality_expr())
+ return expr
+
+ def _equality_expr(self):
+ expr = self._relational_expr()
+ while self.cur_token in ('=', '!='):
+ op = _operator_map[self.cur_token]
+ self.next_token()
+ expr = op(expr, self._relational_expr())
+ return expr
+
+ def _relational_expr(self):
+ expr = self._sub_expr()
+ while self.cur_token in ('>', '>=', '<', '>='):
+ op = _operator_map[self.cur_token]
+ self.next_token()
+ expr = op(expr, self._sub_expr())
+ return expr
+
+ def _sub_expr(self):
+ token = self.cur_token
+ if token != '(':
+ return self._primary_expr()
+ self.next_token()
+ expr = self._or_expr()
+ if self.cur_token != ')':
+ raise PathSyntaxError('Expected ")" to close sub-expression, '
+ 'but found "%s"' % self.cur_token,
+ self.filename, self.lineno)
+ self.next_token()
+ return expr
+
+ def _primary_expr(self):
+ token = self.cur_token
+ if len(token) > 1 and (token[0], token[-1]) in self._QUOTES:
+ self.next_token()
+ return StringLiteral(token[1:-1])
+ elif token[0].isdigit() or token[0] == '.':
+ self.next_token()
+ return NumberLiteral(as_float(token))
+ elif token == '$':
+ token = self.next_token()
+ self.next_token()
+ return VariableReference(token)
+ elif not self.at_end and self.peek_token().startswith('('):
+ return self._function_call()
+ else:
+ axis = None
+ if token == '@':
+ axis = ATTRIBUTE
+ self.next_token()
+ return self._node_test(axis)
+
+ def _function_call(self):
+ name = self.cur_token
+ if self.next_token() == '()':
+ args = []
+ else:
+ assert self.cur_token == '('
+ self.next_token()
+ args = [self._or_expr()]
+ while self.cur_token == ',':
+ self.next_token()
+ args.append(self._or_expr())
+ if not self.cur_token == ')':
+ raise PathSyntaxError('Expected ")" to close function argument '
+ 'list, but found "%s"' % self.cur_token,
+ self.filename, self.lineno)
+ self.next_token()
+ cls = _function_map.get(name)
+ if not cls:
+ raise PathSyntaxError('Unsupported function "%s"' % name,
+ self.filename, self.lineno)
+ return cls(*args)
+
+
+# Type coercion
+
+def as_scalar(value):
+ """Convert value to a scalar. If a single element Attrs() object is passed
+ the value of the single attribute will be returned."""
+ if isinstance(value, Attrs):
+ assert len(value) == 1
+ return value[0][1]
+ else:
+ return value
+
+def as_float(value):
+ # FIXME - if value is a bool it will be coerced to 0.0 and consequently
+ # compared as a float. This is probably not ideal.
+ return float(as_scalar(value))
+
+def as_long(value):
+ return long(as_scalar(value))
+
+def as_string(value):
+ value = as_scalar(value)
+ if value is False:
+ return ''
+ return unicode(value)
+
+def as_bool(value):
+ return bool(as_scalar(value))
+
+
+# Node tests
+
+class PrincipalTypeTest(object):
+ """Node test that matches any event with the given principal type."""
+ __slots__ = ['principal_type']
+ def __init__(self, principal_type):
+ self.principal_type = principal_type
+ def __call__(self, kind, data, pos, namespaces, variables):
+ if kind is START:
+ if self.principal_type is ATTRIBUTE:
+ return data[1] or None
+ else:
+ return True
+ def __repr__(self):
+ return '*'
+
+class QualifiedPrincipalTypeTest(object):
+ """Node test that matches any event with the given principal type in a
+ specific namespace."""
+ __slots__ = ['principal_type', 'prefix']
+ def __init__(self, principal_type, prefix):
+ self.principal_type = principal_type
+ self.prefix = prefix
+ def __call__(self, kind, data, pos, namespaces, variables):
+ namespace = Namespace(namespaces.get(self.prefix))
+ if kind is START:
+ if self.principal_type is ATTRIBUTE and data[1]:
+ return Attrs([(name, value) for name, value in data[1]
+ if name in namespace]) or None
+ else:
+ return data[0] in namespace
+ def __repr__(self):
+ return '%s:*' % self.prefix
+
+class LocalNameTest(object):
+ """Node test that matches any event with the given principal type and
+ local name.
+ """
+ __slots__ = ['principal_type', 'name']
+ def __init__(self, principal_type, name):
+ self.principal_type = principal_type
+ self.name = name
+ def __call__(self, kind, data, pos, namespaces, variables):
+ if kind is START:
+ if self.principal_type is ATTRIBUTE and self.name in data[1]:
+ return Attrs([(self.name, data[1].get(self.name))])
+ else:
+ return data[0].localname == self.name
+ def __repr__(self):
+ return self.name
+
+class QualifiedNameTest(object):
+ """Node test that matches any event with the given principal type and
+ qualified name.
+ """
+ __slots__ = ['principal_type', 'prefix', 'name']
+ def __init__(self, principal_type, prefix, name):
+ self.principal_type = principal_type
+ self.prefix = prefix
+ self.name = name
+ def __call__(self, kind, data, pos, namespaces, variables):
+ qname = QName('%s}%s' % (namespaces.get(self.prefix), self.name))
+ if kind is START:
+ if self.principal_type is ATTRIBUTE and qname in data[1]:
+ return Attrs([(self.name, data[1].get(self.name))])
+ else:
+ return data[0] == qname
+ def __repr__(self):
+ return '%s:%s' % (self.prefix, self.name)
+
+class CommentNodeTest(object):
+ """Node test that matches any comment events."""
+ __slots__ = []
+ def __call__(self, kind, data, pos, namespaces, variables):
+ return kind is COMMENT
+ def __repr__(self):
+ return 'comment()'
+
+class NodeTest(object):
+ """Node test that matches any node."""
+ __slots__ = []
+ def __call__(self, kind, data, pos, namespaces, variables):
+ if kind is START:
+ return True
+ return kind, data, pos
+ def __repr__(self):
+ return 'node()'
+
+class ProcessingInstructionNodeTest(object):
+ """Node test that matches any processing instruction event."""
+ __slots__ = ['target']
+ def __init__(self, target=None):
+ self.target = target
+ def __call__(self, kind, data, pos, namespaces, variables):
+ return kind is PI and (not self.target or data[0] == self.target)
+ def __repr__(self):
+ arg = ''
+ if self.target:
+ arg = '"' + self.target + '"'
+ return 'processing-instruction(%s)' % arg
+
+class TextNodeTest(object):
+ """Node test that matches any text event."""
+ __slots__ = []
+ def __call__(self, kind, data, pos, namespaces, variables):
+ return kind is TEXT
+ def __repr__(self):
+ return 'text()'
+
+_nodetest_map = {'comment': CommentNodeTest, 'node': NodeTest,
+ 'processing-instruction': ProcessingInstructionNodeTest,
+ 'text': TextNodeTest}
+
+# Functions
+
+class Function(object):
+ """Base class for function nodes in XPath expressions."""
+
+class BooleanFunction(Function):
+ """The `boolean` function, which converts its argument to a boolean
+ value.
+ """
+ __slots__ = ['expr']
+ _return_type = bool
+ def __init__(self, expr):
+ self.expr = expr
+ def __call__(self, kind, data, pos, namespaces, variables):
+ val = self.expr(kind, data, pos, namespaces, variables)
+ return as_bool(val)
+ def __repr__(self):
+ return 'boolean(%r)' % self.expr
+
+class CeilingFunction(Function):
+ """The `ceiling` function, which returns the nearest lower integer number
+ for the given number.
+ """
+ __slots__ = ['number']
+ def __init__(self, number):
+ self.number = number
+ def __call__(self, kind, data, pos, namespaces, variables):
+ number = self.number(kind, data, pos, namespaces, variables)
+ return ceil(as_float(number))
+ def __repr__(self):
+ return 'ceiling(%r)' % self.number
+
+class ConcatFunction(Function):
+ """The `concat` function, which concatenates (joins) the variable number of
+ strings it gets as arguments.
+ """
+ __slots__ = ['exprs']
+ def __init__(self, *exprs):
+ self.exprs = exprs
+ def __call__(self, kind, data, pos, namespaces, variables):
+ strings = []
+ for item in [expr(kind, data, pos, namespaces, variables)
+ for expr in self.exprs]:
+ strings.append(as_string(item))
+ return ''.join(strings)
+ def __repr__(self):
+ return 'concat(%s)' % ', '.join([repr(expr) for expr in self.exprs])
+
+class ContainsFunction(Function):
+ """The `contains` function, which returns whether a string contains a given
+ substring.
+ """
+ __slots__ = ['string1', 'string2']
+ def __init__(self, string1, string2):
+ self.string1 = string1
+ self.string2 = string2
+ def __call__(self, kind, data, pos, namespaces, variables):
+ string1 = self.string1(kind, data, pos, namespaces, variables)
+ string2 = self.string2(kind, data, pos, namespaces, variables)
+ return as_string(string2) in as_string(string1)
+ def __repr__(self):
+ return 'contains(%r, %r)' % (self.string1, self.string2)
+
+class MatchesFunction(Function):
+ """The `matches` function, which returns whether a string matches a regular
+ expression.
+ """
+ __slots__ = ['string1', 'string2']
+ flag_mapping = {'s': re.S, 'm': re.M, 'i': re.I, 'x': re.X}
+
+ def __init__(self, string1, string2, flags=''):
+ self.string1 = string1
+ self.string2 = string2
+ self.flags = self._map_flags(flags)
+ def __call__(self, kind, data, pos, namespaces, variables):
+ string1 = as_string(self.string1(kind, data, pos, namespaces, variables))
+ string2 = as_string(self.string2(kind, data, pos, namespaces, variables))
+ return re.search(string2, string1, self.flags)
+ def _map_flags(self, flags):
+ return reduce(operator.or_,
+ [self.flag_map[flag] for flag in flags], re.U)
+ def __repr__(self):
+ return 'contains(%r, %r)' % (self.string1, self.string2)
+
+class FalseFunction(Function):
+ """The `false` function, which always returns the boolean `false` value."""
+ __slots__ = []
+ def __call__(self, kind, data, pos, namespaces, variables):
+ return False
+ def __repr__(self):
+ return 'false()'
+
+class FloorFunction(Function):
+ """The `ceiling` function, which returns the nearest higher integer number
+ for the given number.
+ """
+ __slots__ = ['number']
+ def __init__(self, number):
+ self.number = number
+ def __call__(self, kind, data, pos, namespaces, variables):
+ number = self.number(kind, data, pos, namespaces, variables)
+ return floor(as_float(number))
+ def __repr__(self):
+ return 'floor(%r)' % self.number
+
+class LocalNameFunction(Function):
+ """The `local-name` function, which returns the local name of the current
+ element.
+ """
+ __slots__ = []
+ def __call__(self, kind, data, pos, namespaces, variables):
+ if kind is START:
+ return data[0].localname
+ def __repr__(self):
+ return 'local-name()'
+
+class NameFunction(Function):
+ """The `name` function, which returns the qualified name of the current
+ element.
+ """
+ __slots__ = []
+ def __call__(self, kind, data, pos, namespaces, variables):
+ if kind is START:
+ return data[0]
+ def __repr__(self):
+ return 'name()'
+
+class NamespaceUriFunction(Function):
+ """The `namespace-uri` function, which returns the namespace URI of the
+ current element.
+ """
+ __slots__ = []
+ def __call__(self, kind, data, pos, namespaces, variables):
+ if kind is START:
+ return data[0].namespace
+ def __repr__(self):
+ return 'namespace-uri()'
+
+class NotFunction(Function):
+ """The `not` function, which returns the negated boolean value of its
+ argument.
+ """
+ __slots__ = ['expr']
+ def __init__(self, expr):
+ self.expr = expr
+ def __call__(self, kind, data, pos, namespaces, variables):
+ return not as_bool(self.expr(kind, data, pos, namespaces, variables))
+ def __repr__(self):
+ return 'not(%s)' % self.expr
+
+class NormalizeSpaceFunction(Function):
+ """The `normalize-space` function, which removes leading and trailing
+ whitespace in the given string, and replaces multiple adjacent whitespace
+ characters inside the string with a single space.
+ """
+ __slots__ = ['expr']
+ _normalize = re.compile(r'\s{2,}').sub
+ def __init__(self, expr):
+ self.expr = expr
+ def __call__(self, kind, data, pos, namespaces, variables):
+ string = self.expr(kind, data, pos, namespaces, variables)
+ return self._normalize(' ', as_string(string).strip())
+ def __repr__(self):
+ return 'normalize-space(%s)' % repr(self.expr)
+
+class NumberFunction(Function):
+ """The `number` function that converts its argument to a number."""
+ __slots__ = ['expr']
+ def __init__(self, expr):
+ self.expr = expr
+ def __call__(self, kind, data, pos, namespaces, variables):
+ val = self.expr(kind, data, pos, namespaces, variables)
+ return as_float(val)
+ def __repr__(self):
+ return 'number(%r)' % self.expr
+
+class RoundFunction(Function):
+ """The `round` function, which returns the nearest integer number for the
+ given number.
+ """
+ __slots__ = ['number']
+ def __init__(self, number):
+ self.number = number
+ def __call__(self, kind, data, pos, namespaces, variables):
+ number = self.number(kind, data, pos, namespaces, variables)
+ return round(as_float(number))
+ def __repr__(self):
+ return 'round(%r)' % self.number
+
+class StartsWithFunction(Function):
+ """The `starts-with` function that returns whether one string starts with
+ a given substring.
+ """
+ __slots__ = ['string1', 'string2']
+ def __init__(self, string1, string2):
+ self.string1 = string1
+ self.string2 = string2
+ def __call__(self, kind, data, pos, namespaces, variables):
+ string1 = self.string1(kind, data, pos, namespaces, variables)
+ string2 = self.string2(kind, data, pos, namespaces, variables)
+ return as_string(string1).startswith(as_string(string2))
+ def __repr__(self):
+ return 'starts-with(%r, %r)' % (self.string1, self.string2)
+
+class StringLengthFunction(Function):
+ """The `string-length` function that returns the length of the given
+ string.
+ """
+ __slots__ = ['expr']
+ def __init__(self, expr):
+ self.expr = expr
+ def __call__(self, kind, data, pos, namespaces, variables):
+ string = self.expr(kind, data, pos, namespaces, variables)
+ return len(as_string(string))
+ def __repr__(self):
+ return 'string-length(%r)' % self.expr
+
+class SubstringFunction(Function):
+ """The `substring` function that returns the part of a string that starts
+ at the given offset, and optionally limited to the given length.
+ """
+ __slots__ = ['string', 'start', 'length']
+ def __init__(self, string, start, length=None):
+ self.string = string
+ self.start = start
+ self.length = length
+ def __call__(self, kind, data, pos, namespaces, variables):
+ string = self.string(kind, data, pos, namespaces, variables)
+ start = self.start(kind, data, pos, namespaces, variables)
+ length = 0
+ if self.length is not None:
+ length = self.length(kind, data, pos, namespaces, variables)
+ return string[as_long(start):len(as_string(string)) - as_long(length)]
+ def __repr__(self):
+ if self.length is not None:
+ return 'substring(%r, %r, %r)' % (self.string, self.start,
+ self.length)
+ else:
+ return 'substring(%r, %r)' % (self.string, self.start)
+
+class SubstringAfterFunction(Function):
+ """The `substring-after` function that returns the part of a string that
+ is found after the given substring.
+ """
+ __slots__ = ['string1', 'string2']
+ def __init__(self, string1, string2):
+ self.string1 = string1
+ self.string2 = string2
+ def __call__(self, kind, data, pos, namespaces, variables):
+ string1 = as_string(self.string1(kind, data, pos, namespaces, variables))
+ string2 = as_string(self.string2(kind, data, pos, namespaces, variables))
+ index = string1.find(string2)
+ if index >= 0:
+ return string1[index + len(string2):]
+ return ''
+ def __repr__(self):
+ return 'substring-after(%r, %r)' % (self.string1, self.string2)
+
+class SubstringBeforeFunction(Function):
+ """The `substring-before` function that returns the part of a string that
+ is found before the given substring.
+ """
+ __slots__ = ['string1', 'string2']
+ def __init__(self, string1, string2):
+ self.string1 = string1
+ self.string2 = string2
+ def __call__(self, kind, data, pos, namespaces, variables):
+ string1 = as_string(self.string1(kind, data, pos, namespaces, variables))
+ string2 = as_string(self.string2(kind, data, pos, namespaces, variables))
+ index = string1.find(string2)
+ if index >= 0:
+ return string1[:index]
+ return ''
+ def __repr__(self):
+ return 'substring-after(%r, %r)' % (self.string1, self.string2)
+
+class TranslateFunction(Function):
+ """The `translate` function that translates a set of characters in a
+ string to target set of characters.
+ """
+ __slots__ = ['string', 'fromchars', 'tochars']
+ def __init__(self, string, fromchars, tochars):
+ self.string = string
+ self.fromchars = fromchars
+ self.tochars = tochars
+ def __call__(self, kind, data, pos, namespaces, variables):
+ string = as_string(self.string(kind, data, pos, namespaces, variables))
+ fromchars = as_string(self.fromchars(kind, data, pos, namespaces, variables))
+ tochars = as_string(self.tochars(kind, data, pos, namespaces, variables))
+ table = dict(zip([ord(c) for c in fromchars],
+ [ord(c) for c in tochars]))
+ return string.translate(table)
+ def __repr__(self):
+ return 'translate(%r, %r, %r)' % (self.string, self.fromchars,
+ self.tochars)
+
+class TrueFunction(Function):
+ """The `true` function, which always returns the boolean `true` value."""
+ __slots__ = []
+ def __call__(self, kind, data, pos, namespaces, variables):
+ return True
+ def __repr__(self):
+ return 'true()'
+
+_function_map = {'boolean': BooleanFunction, 'ceiling': CeilingFunction,
+ 'concat': ConcatFunction, 'contains': ContainsFunction,
+ 'matches': MatchesFunction, 'false': FalseFunction, 'floor':
+ FloorFunction, 'local-name': LocalNameFunction, 'name':
+ NameFunction, 'namespace-uri': NamespaceUriFunction,
+ 'normalize-space': NormalizeSpaceFunction, 'not': NotFunction,
+ 'number': NumberFunction, 'round': RoundFunction,
+ 'starts-with': StartsWithFunction, 'string-length':
+ StringLengthFunction, 'substring': SubstringFunction,
+ 'substring-after': SubstringAfterFunction, 'substring-before':
+ SubstringBeforeFunction, 'translate': TranslateFunction,
+ 'true': TrueFunction}
+
+# Literals & Variables
+
+class Literal(object):
+ """Abstract base class for literal nodes."""
+
+class StringLiteral(Literal):
+ """A string literal node."""
+ __slots__ = ['text']
+ def __init__(self, text):
+ self.text = text
+ def __call__(self, kind, data, pos, namespaces, variables):
+ return self.text
+ def __repr__(self):
+ return '"%s"' % self.text
+
+class NumberLiteral(Literal):
+ """A number literal node."""
+ __slots__ = ['number']
+ def __init__(self, number):
+ self.number = number
+ def __call__(self, kind, data, pos, namespaces, variables):
+ return self.number
+ def __repr__(self):
+ return str(self.number)
+
+class VariableReference(Literal):
+ """A variable reference node."""
+ __slots__ = ['name']
+ def __init__(self, name):
+ self.name = name
+ def __call__(self, kind, data, pos, namespaces, variables):
+ return variables.get(self.name)
+ def __repr__(self):
+ return str(self.name)
+
+# Operators
+
+class AndOperator(object):
+ """The boolean operator `and`."""
+ __slots__ = ['lval', 'rval']
+ def __init__(self, lval, rval):
+ self.lval = lval
+ self.rval = rval
+ def __call__(self, kind, data, pos, namespaces, variables):
+ lval = as_bool(self.lval(kind, data, pos, namespaces, variables))
+ if not lval:
+ return False
+ rval = self.rval(kind, data, pos, namespaces, variables)
+ return as_bool(rval)
+ def __repr__(self):
+ return '%s and %s' % (self.lval, self.rval)
+
+class EqualsOperator(object):
+ """The equality operator `=`."""
+ __slots__ = ['lval', 'rval']
+ def __init__(self, lval, rval):
+ self.lval = lval
+ self.rval = rval
+ def __call__(self, kind, data, pos, namespaces, variables):
+ lval = as_scalar(self.lval(kind, data, pos, namespaces, variables))
+ rval = as_scalar(self.rval(kind, data, pos, namespaces, variables))
+ return lval == rval
+ def __repr__(self):
+ return '%s=%s' % (self.lval, self.rval)
+
+class NotEqualsOperator(object):
+ """The equality operator `!=`."""
+ __slots__ = ['lval', 'rval']
+ def __init__(self, lval, rval):
+ self.lval = lval
+ self.rval = rval
+ def __call__(self, kind, data, pos, namespaces, variables):
+ lval = as_scalar(self.lval(kind, data, pos, namespaces, variables))
+ rval = as_scalar(self.rval(kind, data, pos, namespaces, variables))
+ return lval != rval
+ def __repr__(self):
+ return '%s!=%s' % (self.lval, self.rval)
+
+class OrOperator(object):
+ """The boolean operator `or`."""
+ __slots__ = ['lval', 'rval']
+ def __init__(self, lval, rval):
+ self.lval = lval
+ self.rval = rval
+ def __call__(self, kind, data, pos, namespaces, variables):
+ lval = as_bool(self.lval(kind, data, pos, namespaces, variables))
+ if lval:
+ return True
+ rval = self.rval(kind, data, pos, namespaces, variables)
+ return as_bool(rval)
+ def __repr__(self):
+ return '%s or %s' % (self.lval, self.rval)
+
+class GreaterThanOperator(object):
+ """The relational operator `>` (greater than)."""
+ __slots__ = ['lval', 'rval']
+ def __init__(self, lval, rval):
+ self.lval = lval
+ self.rval = rval
+ def __call__(self, kind, data, pos, namespaces, variables):
+ lval = self.lval(kind, data, pos, namespaces, variables)
+ rval = self.rval(kind, data, pos, namespaces, variables)
+ return as_float(lval) > as_float(rval)
+ def __repr__(self):
+ return '%s>%s' % (self.lval, self.rval)
+
+class GreaterThanOrEqualOperator(object):
+ """The relational operator `>=` (greater than or equal)."""
+ __slots__ = ['lval', 'rval']
+ def __init__(self, lval, rval):
+ self.lval = lval
+ self.rval = rval
+ def __call__(self, kind, data, pos, namespaces, variables):
+ lval = self.lval(kind, data, pos, namespaces, variables)
+ rval = self.rval(kind, data, pos, namespaces, variables)
+ return as_float(lval) >= as_float(rval)
+ def __repr__(self):
+ return '%s>=%s' % (self.lval, self.rval)
+
+class LessThanOperator(object):
+ """The relational operator `<` (less than)."""
+ __slots__ = ['lval', 'rval']
+ def __init__(self, lval, rval):
+ self.lval = lval
+ self.rval = rval
+ def __call__(self, kind, data, pos, namespaces, variables):
+ lval = self.lval(kind, data, pos, namespaces, variables)
+ rval = self.rval(kind, data, pos, namespaces, variables)
+ return as_float(lval) < as_float(rval)
+ def __repr__(self):
+ return '%s<%s' % (self.lval, self.rval)
+
+class LessThanOrEqualOperator(object):
+ """The relational operator `<=` (less than or equal)."""
+ __slots__ = ['lval', 'rval']
+ def __init__(self, lval, rval):
+ self.lval = lval
+ self.rval = rval
+ def __call__(self, kind, data, pos, namespaces, variables):
+ lval = self.lval(kind, data, pos, namespaces, variables)
+ rval = self.rval(kind, data, pos, namespaces, variables)
+ return as_float(lval) <= as_float(rval)
+ def __repr__(self):
+ return '%s<=%s' % (self.lval, self.rval)
+
+_operator_map = {'=': EqualsOperator, '!=': NotEqualsOperator,
+ '>': GreaterThanOperator, '>=': GreaterThanOrEqualOperator,
+ '<': LessThanOperator, '>=': LessThanOrEqualOperator}
+
+
+_DOTSLASHSLASH = (DESCENDANT_OR_SELF, PrincipalTypeTest(None), ())
+_DOTSLASH = (SELF, PrincipalTypeTest(None), ())
diff --git a/genshi/template/__init__.py b/genshi/template/__init__.py
new file mode 100644
index 0000000..47a9310
--- /dev/null
+++ b/genshi/template/__init__.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2007 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Implementation of the template engine."""
+
+from genshi.template.base import Context, Template, TemplateError, \
+ TemplateRuntimeError, TemplateSyntaxError, \
+ BadDirectiveError
+from genshi.template.loader import TemplateLoader, TemplateNotFound
+from genshi.template.markup import MarkupTemplate
+from genshi.template.text import TextTemplate, OldTextTemplate, NewTextTemplate
+
+__docformat__ = 'restructuredtext en'
diff --git a/genshi/template/_ast24.py b/genshi/template/_ast24.py
new file mode 100644
index 0000000..05d241b
--- /dev/null
+++ b/genshi/template/_ast24.py
@@ -0,0 +1,446 @@
+# Generated automatically, please do not edit
+# Generator can be found in Genshi SVN, scripts/ast-generator.py
+
+__version__ = 43614
+
+class AST(object):
+ _fields = None
+ __doc__ = None
+
+class operator(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = []
+class Add(operator):
+ _fields = None
+ __doc__ = None
+
+class boolop(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = []
+class And(boolop):
+ _fields = None
+ __doc__ = None
+
+class stmt(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = ['lineno', 'col_offset']
+class Assert(stmt):
+ _fields = ('test', 'msg')
+ __doc__ = None
+
+class Assign(stmt):
+ _fields = ('targets', 'value')
+ __doc__ = None
+
+class expr(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = ['lineno', 'col_offset']
+class Attribute(expr):
+ _fields = ('value', 'attr', 'ctx')
+ __doc__ = None
+
+class AugAssign(stmt):
+ _fields = ('target', 'op', 'value')
+ __doc__ = None
+
+class expr_context(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = []
+class AugLoad(expr_context):
+ _fields = None
+ __doc__ = None
+
+class AugStore(expr_context):
+ _fields = None
+ __doc__ = None
+
+class BinOp(expr):
+ _fields = ('left', 'op', 'right')
+ __doc__ = None
+
+class BitAnd(operator):
+ _fields = None
+ __doc__ = None
+
+class BitOr(operator):
+ _fields = None
+ __doc__ = None
+
+class BitXor(operator):
+ _fields = None
+ __doc__ = None
+
+class BoolOp(expr):
+ _fields = ('op', 'values')
+ __doc__ = None
+
+class Break(stmt):
+ _fields = None
+ __doc__ = None
+
+class Call(expr):
+ _fields = ('func', 'args', 'keywords', 'starargs', 'kwargs')
+ __doc__ = None
+
+class ClassDef(stmt):
+ _fields = ('name', 'bases', 'body')
+ __doc__ = None
+
+class Compare(expr):
+ _fields = ('left', 'ops', 'comparators')
+ __doc__ = None
+
+class Continue(stmt):
+ _fields = None
+ __doc__ = None
+
+class Del(expr_context):
+ _fields = None
+ __doc__ = None
+
+class Delete(stmt):
+ _fields = ('targets',)
+ __doc__ = None
+
+class Dict(expr):
+ _fields = ('keys', 'values')
+ __doc__ = None
+
+class Div(operator):
+ _fields = None
+ __doc__ = None
+
+class slice(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = []
+class Ellipsis(slice):
+ _fields = None
+ __doc__ = None
+
+class cmpop(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = []
+class Eq(cmpop):
+ _fields = None
+ __doc__ = None
+
+class Exec(stmt):
+ _fields = ('body', 'globals', 'locals')
+ __doc__ = None
+
+class Expr(stmt):
+ _fields = ('value',)
+ __doc__ = None
+
+class mod(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = []
+class Expression(mod):
+ _fields = ('body',)
+ __doc__ = None
+
+class ExtSlice(slice):
+ _fields = ('dims',)
+ __doc__ = None
+
+class FloorDiv(operator):
+ _fields = None
+ __doc__ = None
+
+class For(stmt):
+ _fields = ('target', 'iter', 'body', 'orelse')
+ __doc__ = None
+
+class FunctionDef(stmt):
+ _fields = ('name', 'args', 'body', 'decorators')
+ __doc__ = None
+
+class GeneratorExp(expr):
+ _fields = ('elt', 'generators')
+ __doc__ = None
+
+class Global(stmt):
+ _fields = ('names',)
+ __doc__ = None
+
+class Gt(cmpop):
+ _fields = None
+ __doc__ = None
+
+class GtE(cmpop):
+ _fields = None
+ __doc__ = None
+
+class If(stmt):
+ _fields = ('test', 'body', 'orelse')
+ __doc__ = None
+
+class IfExp(expr):
+ _fields = ('test', 'body', 'orelse')
+ __doc__ = None
+
+class Import(stmt):
+ _fields = ('names',)
+ __doc__ = None
+
+class ImportFrom(stmt):
+ _fields = ('module', 'names', 'level')
+ __doc__ = None
+
+class In(cmpop):
+ _fields = None
+ __doc__ = None
+
+class Index(slice):
+ _fields = ('value',)
+ __doc__ = None
+
+class Interactive(mod):
+ _fields = ('body',)
+ __doc__ = None
+
+class unaryop(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = []
+class Invert(unaryop):
+ _fields = None
+ __doc__ = None
+
+class Is(cmpop):
+ _fields = None
+ __doc__ = None
+
+class IsNot(cmpop):
+ _fields = None
+ __doc__ = None
+
+class LShift(operator):
+ _fields = None
+ __doc__ = None
+
+class Lambda(expr):
+ _fields = ('args', 'body')
+ __doc__ = None
+
+class List(expr):
+ _fields = ('elts', 'ctx')
+ __doc__ = None
+
+class ListComp(expr):
+ _fields = ('elt', 'generators')
+ __doc__ = None
+
+class Load(expr_context):
+ _fields = None
+ __doc__ = None
+
+class Lt(cmpop):
+ _fields = None
+ __doc__ = None
+
+class LtE(cmpop):
+ _fields = None
+ __doc__ = None
+
+class Mod(operator):
+ _fields = None
+ __doc__ = None
+
+class Module(mod):
+ _fields = ('body',)
+ __doc__ = None
+
+class Mult(operator):
+ _fields = None
+ __doc__ = None
+
+class Name(expr):
+ _fields = ('id', 'ctx')
+ __doc__ = None
+
+class Not(unaryop):
+ _fields = None
+ __doc__ = None
+
+class NotEq(cmpop):
+ _fields = None
+ __doc__ = None
+
+class NotIn(cmpop):
+ _fields = None
+ __doc__ = None
+
+class Num(expr):
+ _fields = ('n',)
+ __doc__ = None
+
+class Or(boolop):
+ _fields = None
+ __doc__ = None
+
+class Param(expr_context):
+ _fields = None
+ __doc__ = None
+
+class Pass(stmt):
+ _fields = None
+ __doc__ = None
+
+class Pow(operator):
+ _fields = None
+ __doc__ = None
+
+class Print(stmt):
+ _fields = ('dest', 'values', 'nl')
+ __doc__ = None
+
+class RShift(operator):
+ _fields = None
+ __doc__ = None
+
+class Raise(stmt):
+ _fields = ('type', 'inst', 'tback')
+ __doc__ = None
+
+class Repr(expr):
+ _fields = ('value',)
+ __doc__ = None
+
+class Return(stmt):
+ _fields = ('value',)
+ __doc__ = None
+
+class Slice(slice):
+ _fields = ('lower', 'upper', 'step')
+ __doc__ = None
+
+class Store(expr_context):
+ _fields = None
+ __doc__ = None
+
+class Str(expr):
+ _fields = ('s',)
+ __doc__ = None
+
+class Sub(operator):
+ _fields = None
+ __doc__ = None
+
+class Subscript(expr):
+ _fields = ('value', 'slice', 'ctx')
+ __doc__ = None
+
+class Suite(mod):
+ _fields = ('body',)
+ __doc__ = None
+
+class TryExcept(stmt):
+ _fields = ('body', 'handlers', 'orelse')
+ __doc__ = None
+
+class TryFinally(stmt):
+ _fields = ('body', 'finalbody')
+ __doc__ = None
+
+class Tuple(expr):
+ _fields = ('elts', 'ctx')
+ __doc__ = None
+
+class UAdd(unaryop):
+ _fields = None
+ __doc__ = None
+
+class USub(unaryop):
+ _fields = None
+ __doc__ = None
+
+class UnaryOp(expr):
+ _fields = ('op', 'operand')
+ __doc__ = None
+
+class While(stmt):
+ _fields = ('test', 'body', 'orelse')
+ __doc__ = None
+
+class With(stmt):
+ _fields = ('context_expr', 'optional_vars', 'body')
+ __doc__ = None
+
+class Yield(expr):
+ _fields = ('value',)
+ __doc__ = None
+
+class alias(AST):
+ _fields = ('name', 'asname')
+ __doc__ = None
+
+class arguments(AST):
+ _fields = ('args', 'vararg', 'kwarg', 'defaults')
+ __doc__ = None
+
+class boolop(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = []
+
+class cmpop(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = []
+
+class comprehension(AST):
+ _fields = ('target', 'iter', 'ifs')
+ __doc__ = None
+
+class excepthandler(AST):
+ _fields = ('type', 'name', 'body', 'lineno', 'col_offset')
+ __doc__ = None
+
+class expr(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = ['lineno', 'col_offset']
+
+class expr_context(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = []
+
+class keyword(AST):
+ _fields = ('arg', 'value')
+ __doc__ = None
+
+class mod(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = []
+
+class operator(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = []
+
+class slice(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = []
+
+class stmt(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = ['lineno', 'col_offset']
+
+class unaryop(AST):
+ _fields = None
+ __doc__ = None
+ _attributes = []
+
diff --git a/genshi/template/ast24.py b/genshi/template/ast24.py
new file mode 100644
index 0000000..af6dce9
--- /dev/null
+++ b/genshi/template/ast24.py
@@ -0,0 +1,505 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Emulation of the proper abstract syntax tree API for Python 2.4."""
+
+import compiler
+import compiler.ast
+
+from genshi.template import _ast24 as _ast
+
+__all__ = ['_ast', 'parse']
+__docformat__ = 'restructuredtext en'
+
+
+def _new(cls, *args, **kwargs):
+ ret = cls()
+ if ret._fields:
+ for attr, value in zip(ret._fields, args):
+ if attr in kwargs:
+ raise ValueError('Field set both in args and kwargs')
+ setattr(ret, attr, value)
+ for attr in kwargs:
+ if (getattr(ret, '_fields', None) and attr in ret._fields) \
+ or (getattr(ret, '_attributes', None) and
+ attr in ret._attributes):
+ setattr(ret, attr, kwargs[attr])
+ return ret
+
+
+class ASTUpgrader(object):
+ """Transformer changing structure of Python 2.4 ASTs to
+ Python 2.5 ones.
+
+ Transforms ``compiler.ast`` Abstract Syntax Tree to builtin ``_ast``.
+ It can use fake`` _ast`` classes and this way allow ``_ast`` emulation
+ in Python 2.4.
+ """
+
+ def __init__(self):
+ self.out_flags = None
+ self.lines = [-1]
+
+ def _new(self, *args, **kwargs):
+ return _new(lineno = self.lines[-1], *args, **kwargs)
+
+ def visit(self, node):
+ if node is None:
+ return None
+ if type(node) is tuple:
+ return tuple([self.visit(n) for n in node])
+ lno = getattr(node, 'lineno', None)
+ if lno is not None:
+ self.lines.append(lno)
+ visitor = getattr(self, 'visit_%s' % node.__class__.__name__, None)
+ if visitor is None:
+ raise Exception('Unhandled node type %r' % type(node))
+
+ retval = visitor(node)
+ if lno is not None:
+ self.lines.pop()
+ return retval
+
+ def visit_Module(self, node):
+ body = self.visit(node.node)
+ if node.doc:
+ body = [self._new(_ast.Expr, self._new(_ast.Str, node.doc))] + body
+ return self._new(_ast.Module, body)
+
+ def visit_Expression(self, node):
+ return self._new(_ast.Expression, self.visit(node.node))
+
+ def _extract_args(self, node):
+ tab = node.argnames[:]
+ if node.flags & compiler.ast.CO_VARKEYWORDS:
+ kwarg = tab[-1]
+ tab = tab[:-1]
+ else:
+ kwarg = None
+
+ if node.flags & compiler.ast.CO_VARARGS:
+ vararg = tab[-1]
+ tab = tab[:-1]
+ else:
+ vararg = None
+
+ def _tup(t):
+ if isinstance(t, str):
+ return self._new(_ast.Name, t, _ast.Store())
+ elif isinstance(t, tuple):
+ elts = [_tup(x) for x in t]
+ return self._new(_ast.Tuple, elts, _ast.Store())
+ else:
+ raise NotImplemented
+
+ args = []
+ for arg in tab:
+ if isinstance(arg, str):
+ args.append(self._new(_ast.Name, arg, _ast.Param()))
+ elif isinstance(arg, tuple):
+ args.append(_tup(arg))
+ else:
+ assert False, node.__class__
+
+ defaults = [self.visit(d) for d in node.defaults]
+ return self._new(_ast.arguments, args, vararg, kwarg, defaults)
+
+
+ def visit_Function(self, node):
+ if getattr(node, 'decorators', ()):
+ decorators = [self.visit(d) for d in node.decorators.nodes]
+ else:
+ decorators = []
+
+ args = self._extract_args(node)
+ body = self.visit(node.code)
+ if node.doc:
+ body = [self._new(_ast.Expr, self._new(_ast.Str, node.doc))] + body
+ return self._new(_ast.FunctionDef, node.name, args, body, decorators)
+
+ def visit_Class(self, node):
+ #self.name_types.append(_ast.Load)
+ bases = [self.visit(b) for b in node.bases]
+ #self.name_types.pop()
+ body = self.visit(node.code)
+ if node.doc:
+ body = [self._new(_ast.Expr, self._new(_ast.Str, node.doc))] + body
+ return self._new(_ast.ClassDef, node.name, bases, body)
+
+ def visit_Return(self, node):
+ return self._new(_ast.Return, self.visit(node.value))
+
+ def visit_Assign(self, node):
+ #self.name_types.append(_ast.Store)
+ targets = [self.visit(t) for t in node.nodes]
+ #self.name_types.pop()
+ return self._new(_ast.Assign, targets, self.visit(node.expr))
+
+ aug_operators = {
+ '+=': _ast.Add,
+ '/=': _ast.Div,
+ '//=': _ast.FloorDiv,
+ '<<=': _ast.LShift,
+ '%=': _ast.Mod,
+ '*=': _ast.Mult,
+ '**=': _ast.Pow,
+ '>>=': _ast.RShift,
+ '-=': _ast.Sub,
+ }
+
+ def visit_AugAssign(self, node):
+ target = self.visit(node.node)
+
+ # Because it's AugAssign target can't be list nor tuple
+ # so we only have to change context of one node
+ target.ctx = _ast.Store()
+ op = self.aug_operators[node.op]()
+ return self._new(_ast.AugAssign, target, op, self.visit(node.expr))
+
+ def _visit_Print(nl):
+ def _visit(self, node):
+ values = [self.visit(v) for v in node.nodes]
+ return self._new(_ast.Print, self.visit(node.dest), values, nl)
+ return _visit
+
+ visit_Print = _visit_Print(False)
+ visit_Printnl = _visit_Print(True)
+ del _visit_Print
+
+ def visit_For(self, node):
+ return self._new(_ast.For, self.visit(node.assign), self.visit(node.list),
+ self.visit(node.body), self.visit(node.else_))
+
+ def visit_While(self, node):
+ return self._new(_ast.While, self.visit(node.test), self.visit(node.body),
+ self.visit(node.else_))
+
+ def visit_If(self, node):
+ def _level(tests, else_):
+ test = self.visit(tests[0][0])
+ body = self.visit(tests[0][1])
+ if len(tests) == 1:
+ orelse = self.visit(else_)
+ else:
+ orelse = [_level(tests[1:], else_)]
+ return self._new(_ast.If, test, body, orelse)
+ return _level(node.tests, node.else_)
+
+ def visit_With(self, node):
+ return self._new(_ast.With, self.visit(node.expr),
+ self.visit(node.vars), self.visit(node.body))
+
+ def visit_Raise(self, node):
+ return self._new(_ast.Raise, self.visit(node.expr1),
+ self.visit(node.expr2), self.visit(node.expr3))
+
+ def visit_TryExcept(self, node):
+ handlers = []
+ for type, name, body in node.handlers:
+ handlers.append(self._new(_ast.excepthandler, self.visit(type),
+ self.visit(name), self.visit(body)))
+ return self._new(_ast.TryExcept, self.visit(node.body),
+ handlers, self.visit(node.else_))
+
+ def visit_TryFinally(self, node):
+ return self._new(_ast.TryFinally, self.visit(node.body),
+ self.visit(node.final))
+
+ def visit_Assert(self, node):
+ return self._new(_ast.Assert, self.visit(node.test), self.visit(node.fail))
+
+ def visit_Import(self, node):
+ names = [self._new(_ast.alias, n[0], n[1]) for n in node.names]
+ return self._new(_ast.Import, names)
+
+ def visit_From(self, node):
+ names = [self._new(_ast.alias, n[0], n[1]) for n in node.names]
+ return self._new(_ast.ImportFrom, node.modname, names, 0)
+
+ def visit_Exec(self, node):
+ return self._new(_ast.Exec, self.visit(node.expr),
+ self.visit(node.locals), self.visit(node.globals))
+
+ def visit_Global(self, node):
+ return self._new(_ast.Global, node.names[:])
+
+ def visit_Discard(self, node):
+ return self._new(_ast.Expr, self.visit(node.expr))
+
+ def _map_class(to):
+ def _visit(self, node):
+ return self._new(to)
+ return _visit
+
+ visit_Pass = _map_class(_ast.Pass)
+ visit_Break = _map_class(_ast.Break)
+ visit_Continue = _map_class(_ast.Continue)
+
+ def _visit_BinOperator(opcls):
+ def _visit(self, node):
+ return self._new(_ast.BinOp, self.visit(node.left),
+ opcls(), self.visit(node.right))
+ return _visit
+ visit_Add = _visit_BinOperator(_ast.Add)
+ visit_Div = _visit_BinOperator(_ast.Div)
+ visit_FloorDiv = _visit_BinOperator(_ast.FloorDiv)
+ visit_LeftShift = _visit_BinOperator(_ast.LShift)
+ visit_Mod = _visit_BinOperator(_ast.Mod)
+ visit_Mul = _visit_BinOperator(_ast.Mult)
+ visit_Power = _visit_BinOperator(_ast.Pow)
+ visit_RightShift = _visit_BinOperator(_ast.RShift)
+ visit_Sub = _visit_BinOperator(_ast.Sub)
+ del _visit_BinOperator
+
+ def _visit_BitOperator(opcls):
+ def _visit(self, node):
+ def _make(nodes):
+ if len(nodes) == 1:
+ return self.visit(nodes[0])
+ left = _make(nodes[:-1])
+ right = self.visit(nodes[-1])
+ return self._new(_ast.BinOp, left, opcls(), right)
+ return _make(node.nodes)
+ return _visit
+ visit_Bitand = _visit_BitOperator(_ast.BitAnd)
+ visit_Bitor = _visit_BitOperator(_ast.BitOr)
+ visit_Bitxor = _visit_BitOperator(_ast.BitXor)
+ del _visit_BitOperator
+
+ def _visit_UnaryOperator(opcls):
+ def _visit(self, node):
+ return self._new(_ast.UnaryOp, opcls(), self.visit(node.expr))
+ return _visit
+
+ visit_Invert = _visit_UnaryOperator(_ast.Invert)
+ visit_Not = _visit_UnaryOperator(_ast.Not)
+ visit_UnaryAdd = _visit_UnaryOperator(_ast.UAdd)
+ visit_UnarySub = _visit_UnaryOperator(_ast.USub)
+ del _visit_UnaryOperator
+
+ def _visit_BoolOperator(opcls):
+ def _visit(self, node):
+ values = [self.visit(n) for n in node.nodes]
+ return self._new(_ast.BoolOp, opcls(), values)
+ return _visit
+ visit_And = _visit_BoolOperator(_ast.And)
+ visit_Or = _visit_BoolOperator(_ast.Or)
+ del _visit_BoolOperator
+
+ cmp_operators = {
+ '==': _ast.Eq,
+ '!=': _ast.NotEq,
+ '<': _ast.Lt,
+ '<=': _ast.LtE,
+ '>': _ast.Gt,
+ '>=': _ast.GtE,
+ 'is': _ast.Is,
+ 'is not': _ast.IsNot,
+ 'in': _ast.In,
+ 'not in': _ast.NotIn,
+ }
+
+ def visit_Compare(self, node):
+ left = self.visit(node.expr)
+ ops = []
+ comparators = []
+ for optype, expr in node.ops:
+ ops.append(self.cmp_operators[optype]())
+ comparators.append(self.visit(expr))
+ return self._new(_ast.Compare, left, ops, comparators)
+
+ def visit_Lambda(self, node):
+ args = self._extract_args(node)
+ body = self.visit(node.code)
+ return self._new(_ast.Lambda, args, body)
+
+ def visit_IfExp(self, node):
+ return self._new(_ast.IfExp, self.visit(node.test), self.visit(node.then),
+ self.visit(node.else_))
+
+ def visit_Dict(self, node):
+ keys = [self.visit(x[0]) for x in node.items]
+ values = [self.visit(x[1]) for x in node.items]
+ return self._new(_ast.Dict, keys, values)
+
+ def visit_ListComp(self, node):
+ generators = [self.visit(q) for q in node.quals]
+ return self._new(_ast.ListComp, self.visit(node.expr), generators)
+
+ def visit_GenExprInner(self, node):
+ generators = [self.visit(q) for q in node.quals]
+ return self._new(_ast.GeneratorExp, self.visit(node.expr), generators)
+
+ def visit_GenExpr(self, node):
+ return self.visit(node.code)
+
+ def visit_GenExprFor(self, node):
+ ifs = [self.visit(i) for i in node.ifs]
+ return self._new(_ast.comprehension, self.visit(node.assign),
+ self.visit(node.iter), ifs)
+
+ def visit_ListCompFor(self, node):
+ ifs = [self.visit(i) for i in node.ifs]
+ return self._new(_ast.comprehension, self.visit(node.assign),
+ self.visit(node.list), ifs)
+
+ def visit_GenExprIf(self, node):
+ return self.visit(node.test)
+ visit_ListCompIf = visit_GenExprIf
+
+ def visit_Yield(self, node):
+ return self._new(_ast.Yield, self.visit(node.value))
+
+ def visit_CallFunc(self, node):
+ args = []
+ keywords = []
+ for arg in node.args:
+ if isinstance(arg, compiler.ast.Keyword):
+ keywords.append(self._new(_ast.keyword, arg.name,
+ self.visit(arg.expr)))
+ else:
+ args.append(self.visit(arg))
+ return self._new(_ast.Call, self.visit(node.node), args, keywords,
+ self.visit(node.star_args), self.visit(node.dstar_args))
+
+ def visit_Backquote(self, node):
+ return self._new(_ast.Repr, self.visit(node.expr))
+
+ def visit_Const(self, node):
+ if node.value is None: # appears in slices
+ return None
+ elif isinstance(node.value, basestring):
+ return self._new(_ast.Str, node.value)
+ else:
+ return self._new(_ast.Num, node.value)
+
+ def visit_Name(self, node):
+ return self._new(_ast.Name, node.name, _ast.Load())
+
+ def visit_Getattr(self, node):
+ return self._new(_ast.Attribute, self.visit(node.expr), node.attrname,
+ _ast.Load())
+
+ def visit_Tuple(self, node):
+ nodes = [self.visit(n) for n in node.nodes]
+ return self._new(_ast.Tuple, nodes, _ast.Load())
+
+ def visit_List(self, node):
+ nodes = [self.visit(n) for n in node.nodes]
+ return self._new(_ast.List, nodes, _ast.Load())
+
+ def get_ctx(self, flags):
+ if flags == 'OP_DELETE':
+ return _ast.Del()
+ elif flags == 'OP_APPLY':
+ return _ast.Load()
+ elif flags == 'OP_ASSIGN':
+ return _ast.Store()
+ else:
+ # FIXME Exception here
+ assert False, repr(flags)
+
+ def visit_AssName(self, node):
+ self.out_flags = node.flags
+ ctx = self.get_ctx(node.flags)
+ return self._new(_ast.Name, node.name, ctx)
+
+ def visit_AssAttr(self, node):
+ self.out_flags = node.flags
+ ctx = self.get_ctx(node.flags)
+ return self._new(_ast.Attribute, self.visit(node.expr),
+ node.attrname, ctx)
+
+ def _visit_AssCollection(cls):
+ def _visit(self, node):
+ flags = None
+ elts = []
+ for n in node.nodes:
+ elts.append(self.visit(n))
+ if flags is None:
+ flags = self.out_flags
+ else:
+ assert flags == self.out_flags
+ self.out_flags = flags
+ ctx = self.get_ctx(flags)
+ return self._new(cls, elts, ctx)
+ return _visit
+
+ visit_AssList = _visit_AssCollection(_ast.List)
+ visit_AssTuple = _visit_AssCollection(_ast.Tuple)
+ del _visit_AssCollection
+
+ def visit_Slice(self, node):
+ lower = self.visit(node.lower)
+ upper = self.visit(node.upper)
+ ctx = self.get_ctx(node.flags)
+ self.out_flags = node.flags
+ return self._new(_ast.Subscript, self.visit(node.expr),
+ self._new(_ast.Slice, lower, upper, None), ctx)
+
+ def visit_Subscript(self, node):
+ ctx = self.get_ctx(node.flags)
+ subs = [self.visit(s) for s in node.subs]
+
+ advanced = (_ast.Slice, _ast.Ellipsis)
+ slices = []
+ nonindex = False
+ for sub in subs:
+ if isinstance(sub, advanced):
+ nonindex = True
+ slices.append(sub)
+ else:
+ slices.append(self._new(_ast.Index, sub))
+ if len(slices) == 1:
+ slice = slices[0]
+ elif nonindex:
+ slice = self._new(_ast.ExtSlice, slices)
+ else:
+ slice = self._new(_ast.Tuple, slices, _ast.Load())
+
+ self.out_flags = node.flags
+ return self._new(_ast.Subscript, self.visit(node.expr), slice, ctx)
+
+ def visit_Sliceobj(self, node):
+ a = [self.visit(n) for n in node.nodes + [None]*(3 - len(node.nodes))]
+ return self._new(_ast.Slice, a[0], a[1], a[2])
+
+ def visit_Ellipsis(self, node):
+ return self._new(_ast.Ellipsis)
+
+ def visit_Stmt(self, node):
+ def _check_del(n):
+ # del x is just AssName('x', 'OP_DELETE')
+ # we want to transform it to Delete([Name('x', Del())])
+ dcls = (_ast.Name, _ast.List, _ast.Subscript, _ast.Attribute)
+ if isinstance(n, dcls) and isinstance(n.ctx, _ast.Del):
+ return self._new(_ast.Delete, [n])
+ elif isinstance(n, _ast.Tuple) and isinstance(n.ctx, _ast.Del):
+ # unpack last tuple to avoid making del (x, y, z,);
+ # out of del x, y, z; (there's no difference between
+ # this two in compiler.ast)
+ return self._new(_ast.Delete, n.elts)
+ else:
+ return n
+ def _keep(n):
+ if isinstance(n, _ast.Expr) and n.value is None:
+ return False
+ else:
+ return True
+ return [s for s in [_check_del(self.visit(n)) for n in node.nodes]
+ if _keep(s)]
+
+
+def parse(source, mode):
+ node = compiler.parse(source, mode)
+ return ASTUpgrader().visit(node)
diff --git a/genshi/template/astutil.py b/genshi/template/astutil.py
new file mode 100644
index 0000000..c3ad107
--- /dev/null
+++ b/genshi/template/astutil.py
@@ -0,0 +1,784 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2008-2010 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Support classes for generating code from abstract syntax trees."""
+
+try:
+ import _ast
+except ImportError:
+ from genshi.template.ast24 import _ast, parse
+else:
+ def parse(source, mode):
+ return compile(source, '', mode, _ast.PyCF_ONLY_AST)
+
+
+__docformat__ = 'restructuredtext en'
+
+
+class ASTCodeGenerator(object):
+ """General purpose base class for AST transformations.
+
+ Every visitor method can be overridden to return an AST node that has been
+ altered or replaced in some way.
+ """
+ def __init__(self, tree):
+ self.lines_info = []
+ self.line_info = None
+ self.code = ''
+ self.line = None
+ self.last = None
+ self.indent = 0
+ self.blame_stack = []
+ self.visit(tree)
+ if self.line.strip():
+ self.code += self.line + '\n'
+ self.lines_info.append(self.line_info)
+ self.line = None
+ self.line_info = None
+
+ def _change_indent(self, delta):
+ self.indent += delta
+
+ def _new_line(self):
+ if self.line is not None:
+ self.code += self.line + '\n'
+ self.lines_info.append(self.line_info)
+ self.line = ' '*4*self.indent
+ if len(self.blame_stack) == 0:
+ self.line_info = []
+ self.last = None
+ else:
+ self.line_info = [(0, self.blame_stack[-1],)]
+ self.last = self.blame_stack[-1]
+
+ def _write(self, s):
+ if len(s) == 0:
+ return
+ if len(self.blame_stack) == 0:
+ if self.last is not None:
+ self.last = None
+ self.line_info.append((len(self.line), self.last))
+ else:
+ if self.last != self.blame_stack[-1]:
+ self.last = self.blame_stack[-1]
+ self.line_info.append((len(self.line), self.last))
+ self.line += s
+
+ def visit(self, node):
+ if node is None:
+ return None
+ if type(node) is tuple:
+ return tuple([self.visit(n) for n in node])
+ try:
+ self.blame_stack.append((node.lineno, node.col_offset,))
+ info = True
+ except AttributeError:
+ info = False
+ visitor = getattr(self, 'visit_%s' % node.__class__.__name__, None)
+ if visitor is None:
+ raise Exception('Unhandled node type %r' % type(node))
+ ret = visitor(node)
+ if info:
+ self.blame_stack.pop()
+ return ret
+
+ def visit_Module(self, node):
+ for n in node.body:
+ self.visit(n)
+ visit_Interactive = visit_Module
+ visit_Suite = visit_Module
+
+ def visit_Expression(self, node):
+ self._new_line()
+ return self.visit(node.body)
+
+ # arguments = (expr* args, identifier? vararg,
+ # identifier? kwarg, expr* defaults)
+ def visit_arguments(self, node):
+ first = True
+ no_default_count = len(node.args) - len(node.defaults)
+ for i, arg in enumerate(node.args):
+ if not first:
+ self._write(', ')
+ else:
+ first = False
+ self.visit(arg)
+ if i >= no_default_count:
+ self._write('=')
+ self.visit(node.defaults[i - no_default_count])
+ if getattr(node, 'vararg', None):
+ if not first:
+ self._write(', ')
+ else:
+ first = False
+ self._write('*' + node.vararg)
+ if getattr(node, 'kwarg', None):
+ if not first:
+ self._write(', ')
+ else:
+ first = False
+ self._write('**' + node.kwarg)
+
+ # FunctionDef(identifier name, arguments args,
+ # stmt* body, expr* decorator_list)
+ def visit_FunctionDef(self, node):
+ decarators = ()
+ if hasattr(node, 'decorator_list'):
+ decorators = getattr(node, 'decorator_list')
+ else: # different name in earlier Python versions
+ decorators = getattr(node, 'decorators', ())
+ for decorator in decorators:
+ self._new_line()
+ self._write('@')
+ self.visit(decorator)
+ self._new_line()
+ self._write('def ' + node.name + '(')
+ self.visit(node.args)
+ self._write('):')
+ self._change_indent(1)
+ for statement in node.body:
+ self.visit(statement)
+ self._change_indent(-1)
+
+ # ClassDef(identifier name, expr* bases, stmt* body)
+ def visit_ClassDef(self, node):
+ self._new_line()
+ self._write('class ' + node.name)
+ if node.bases:
+ self._write('(')
+ self.visit(node.bases[0])
+ for base in node.bases[1:]:
+ self._write(', ')
+ self.visit(base)
+ self._write(')')
+ self._write(':')
+ self._change_indent(1)
+ for statement in node.body:
+ self.visit(statement)
+ self._change_indent(-1)
+
+ # Return(expr? value)
+ def visit_Return(self, node):
+ self._new_line()
+ self._write('return')
+ if getattr(node, 'value', None):
+ self._write(' ')
+ self.visit(node.value)
+
+ # Delete(expr* targets)
+ def visit_Delete(self, node):
+ self._new_line()
+ self._write('del ')
+ self.visit(node.targets[0])
+ for target in node.targets[1:]:
+ self._write(', ')
+ self.visit(target)
+
+ # Assign(expr* targets, expr value)
+ def visit_Assign(self, node):
+ self._new_line()
+ for target in node.targets:
+ self.visit(target)
+ self._write(' = ')
+ self.visit(node.value)
+
+ # AugAssign(expr target, operator op, expr value)
+ def visit_AugAssign(self, node):
+ self._new_line()
+ self.visit(node.target)
+ self._write(' ' + self.binary_operators[node.op.__class__] + '= ')
+ self.visit(node.value)
+
+ # Print(expr? dest, expr* values, bool nl)
+ def visit_Print(self, node):
+ self._new_line()
+ self._write('print')
+ if getattr(node, 'dest', None):
+ self._write(' >> ')
+ self.visit(node.dest)
+ if getattr(node, 'values', None):
+ self._write(', ')
+ else:
+ self._write(' ')
+ if getattr(node, 'values', None):
+ self.visit(node.values[0])
+ for value in node.values[1:]:
+ self._write(', ')
+ self.visit(value)
+ if not node.nl:
+ self._write(',')
+
+ # For(expr target, expr iter, stmt* body, stmt* orelse)
+ def visit_For(self, node):
+ self._new_line()
+ self._write('for ')
+ self.visit(node.target)
+ self._write(' in ')
+ self.visit(node.iter)
+ self._write(':')
+ self._change_indent(1)
+ for statement in node.body:
+ self.visit(statement)
+ self._change_indent(-1)
+ if getattr(node, 'orelse', None):
+ self._new_line()
+ self._write('else:')
+ self._change_indent(1)
+ for statement in node.orelse:
+ self.visit(statement)
+ self._change_indent(-1)
+
+ # While(expr test, stmt* body, stmt* orelse)
+ def visit_While(self, node):
+ self._new_line()
+ self._write('while ')
+ self.visit(node.test)
+ self._write(':')
+ self._change_indent(1)
+ for statement in node.body:
+ self.visit(statement)
+ self._change_indent(-1)
+ if getattr(node, 'orelse', None):
+ self._new_line()
+ self._write('else:')
+ self._change_indent(1)
+ for statement in node.orelse:
+ self.visit(statement)
+ self._change_indent(-1)
+
+ # If(expr test, stmt* body, stmt* orelse)
+ def visit_If(self, node):
+ self._new_line()
+ self._write('if ')
+ self.visit(node.test)
+ self._write(':')
+ self._change_indent(1)
+ for statement in node.body:
+ self.visit(statement)
+ self._change_indent(-1)
+ if getattr(node, 'orelse', None):
+ self._new_line()
+ self._write('else:')
+ self._change_indent(1)
+ for statement in node.orelse:
+ self.visit(statement)
+ self._change_indent(-1)
+
+ # With(expr context_expr, expr? optional_vars, stmt* body)
+ def visit_With(self, node):
+ self._new_line()
+ self._write('with ')
+ self.visit(node.context_expr)
+ if getattr(node, 'optional_vars', None):
+ self._write(' as ')
+ self.visit(node.optional_vars)
+ self._write(':')
+ self._change_indent(1)
+ for statement in node.body:
+ self.visit(statement)
+ self._change_indent(-1)
+
+
+ # Raise(expr? type, expr? inst, expr? tback)
+ def visit_Raise(self, node):
+ self._new_line()
+ self._write('raise')
+ if not node.type:
+ return
+ self._write(' ')
+ self.visit(node.type)
+ if not node.inst:
+ return
+ self._write(', ')
+ self.visit(node.inst)
+ if not node.tback:
+ return
+ self._write(', ')
+ self.visit(node.tback)
+
+ # TryExcept(stmt* body, excepthandler* handlers, stmt* orelse)
+ def visit_TryExcept(self, node):
+ self._new_line()
+ self._write('try:')
+ self._change_indent(1)
+ for statement in node.body:
+ self.visit(statement)
+ self._change_indent(-1)
+ if getattr(node, 'handlers', None):
+ for handler in node.handlers:
+ self.visit(handler)
+ self._new_line()
+ if getattr(node, 'orelse', None):
+ self._write('else:')
+ self._change_indent(1)
+ for statement in node.orelse:
+ self.visit(statement)
+ self._change_indent(-1)
+
+ # excepthandler = (expr? type, expr? name, stmt* body)
+ def visit_ExceptHandler(self, node):
+ self._new_line()
+ self._write('except')
+ if getattr(node, 'type', None):
+ self._write(' ')
+ self.visit(node.type)
+ if getattr(node, 'name', None):
+ self._write(', ')
+ self.visit(node.name)
+ self._write(':')
+ self._change_indent(1)
+ for statement in node.body:
+ self.visit(statement)
+ self._change_indent(-1)
+ visit_excepthandler = visit_ExceptHandler
+
+ # TryFinally(stmt* body, stmt* finalbody)
+ def visit_TryFinally(self, node):
+ self._new_line()
+ self._write('try:')
+ self._change_indent(1)
+ for statement in node.body:
+ self.visit(statement)
+ self._change_indent(-1)
+
+ if getattr(node, 'finalbody', None):
+ self._new_line()
+ self._write('finally:')
+ self._change_indent(1)
+ for statement in node.finalbody:
+ self.visit(statement)
+ self._change_indent(-1)
+
+ # Assert(expr test, expr? msg)
+ def visit_Assert(self, node):
+ self._new_line()
+ self._write('assert ')
+ self.visit(node.test)
+ if getattr(node, 'msg', None):
+ self._write(', ')
+ self.visit(node.msg)
+
+ def visit_alias(self, node):
+ self._write(node.name)
+ if getattr(node, 'asname', None):
+ self._write(' as ')
+ self._write(node.asname)
+
+ # Import(alias* names)
+ def visit_Import(self, node):
+ self._new_line()
+ self._write('import ')
+ self.visit(node.names[0])
+ for name in node.names[1:]:
+ self._write(', ')
+ self.visit(name)
+
+ # ImportFrom(identifier module, alias* names, int? level)
+ def visit_ImportFrom(self, node):
+ self._new_line()
+ self._write('from ')
+ if node.level:
+ self._write('.' * node.level)
+ self._write(node.module)
+ self._write(' import ')
+ self.visit(node.names[0])
+ for name in node.names[1:]:
+ self._write(', ')
+ self.visit(name)
+
+ # Exec(expr body, expr? globals, expr? locals)
+ def visit_Exec(self, node):
+ self._new_line()
+ self._write('exec ')
+ self.visit(node.body)
+ if not node.globals:
+ return
+ self._write(', ')
+ self.visit(node.globals)
+ if not node.locals:
+ return
+ self._write(', ')
+ self.visit(node.locals)
+
+ # Global(identifier* names)
+ def visit_Global(self, node):
+ self._new_line()
+ self._write('global ')
+ self.visit(node.names[0])
+ for name in node.names[1:]:
+ self._write(', ')
+ self.visit(name)
+
+ # Expr(expr value)
+ def visit_Expr(self, node):
+ self._new_line()
+ self.visit(node.value)
+
+ # Pass
+ def visit_Pass(self, node):
+ self._new_line()
+ self._write('pass')
+
+ # Break
+ def visit_Break(self, node):
+ self._new_line()
+ self._write('break')
+
+ # Continue
+ def visit_Continue(self, node):
+ self._new_line()
+ self._write('continue')
+
+ ### EXPRESSIONS
+ def with_parens(f):
+ def _f(self, node):
+ self._write('(')
+ f(self, node)
+ self._write(')')
+ return _f
+
+ bool_operators = {_ast.And: 'and', _ast.Or: 'or'}
+
+ # BoolOp(boolop op, expr* values)
+ @with_parens
+ def visit_BoolOp(self, node):
+ joiner = ' ' + self.bool_operators[node.op.__class__] + ' '
+ self.visit(node.values[0])
+ for value in node.values[1:]:
+ self._write(joiner)
+ self.visit(value)
+
+ binary_operators = {
+ _ast.Add: '+',
+ _ast.Sub: '-',
+ _ast.Mult: '*',
+ _ast.Div: '/',
+ _ast.Mod: '%',
+ _ast.Pow: '**',
+ _ast.LShift: '<<',
+ _ast.RShift: '>>',
+ _ast.BitOr: '|',
+ _ast.BitXor: '^',
+ _ast.BitAnd: '&',
+ _ast.FloorDiv: '//'
+ }
+
+ # BinOp(expr left, operator op, expr right)
+ @with_parens
+ def visit_BinOp(self, node):
+ self.visit(node.left)
+ self._write(' ' + self.binary_operators[node.op.__class__] + ' ')
+ self.visit(node.right)
+
+ unary_operators = {
+ _ast.Invert: '~',
+ _ast.Not: 'not',
+ _ast.UAdd: '+',
+ _ast.USub: '-',
+ }
+
+ # UnaryOp(unaryop op, expr operand)
+ def visit_UnaryOp(self, node):
+ self._write(self.unary_operators[node.op.__class__] + ' ')
+ self.visit(node.operand)
+
+ # Lambda(arguments args, expr body)
+ @with_parens
+ def visit_Lambda(self, node):
+ self._write('lambda ')
+ self.visit(node.args)
+ self._write(': ')
+ self.visit(node.body)
+
+ # IfExp(expr test, expr body, expr orelse)
+ @with_parens
+ def visit_IfExp(self, node):
+ self.visit(node.body)
+ self._write(' if ')
+ self.visit(node.test)
+ self._write(' else ')
+ self.visit(node.orelse)
+
+ # Dict(expr* keys, expr* values)
+ def visit_Dict(self, node):
+ self._write('{')
+ for key, value in zip(node.keys, node.values):
+ self.visit(key)
+ self._write(': ')
+ self.visit(value)
+ self._write(', ')
+ self._write('}')
+
+ # ListComp(expr elt, comprehension* generators)
+ def visit_ListComp(self, node):
+ self._write('[')
+ self.visit(node.elt)
+ for generator in node.generators:
+ # comprehension = (expr target, expr iter, expr* ifs)
+ self._write(' for ')
+ self.visit(generator.target)
+ self._write(' in ')
+ self.visit(generator.iter)
+ for ifexpr in generator.ifs:
+ self._write(' if ')
+ self.visit(ifexpr)
+ self._write(']')
+
+ # GeneratorExp(expr elt, comprehension* generators)
+ def visit_GeneratorExp(self, node):
+ self._write('(')
+ self.visit(node.elt)
+ for generator in node.generators:
+ # comprehension = (expr target, expr iter, expr* ifs)
+ self._write(' for ')
+ self.visit(generator.target)
+ self._write(' in ')
+ self.visit(generator.iter)
+ for ifexpr in generator.ifs:
+ self._write(' if ')
+ self.visit(ifexpr)
+ self._write(')')
+
+ # Yield(expr? value)
+ def visit_Yield(self, node):
+ self._write('yield')
+ if getattr(node, 'value', None):
+ self._write(' ')
+ self.visit(node.value)
+
+ comparision_operators = {
+ _ast.Eq: '==',
+ _ast.NotEq: '!=',
+ _ast.Lt: '<',
+ _ast.LtE: '<=',
+ _ast.Gt: '>',
+ _ast.GtE: '>=',
+ _ast.Is: 'is',
+ _ast.IsNot: 'is not',
+ _ast.In: 'in',
+ _ast.NotIn: 'not in',
+ }
+
+ # Compare(expr left, cmpop* ops, expr* comparators)
+ @with_parens
+ def visit_Compare(self, node):
+ self.visit(node.left)
+ for op, comparator in zip(node.ops, node.comparators):
+ self._write(' ' + self.comparision_operators[op.__class__] + ' ')
+ self.visit(comparator)
+
+ # Call(expr func, expr* args, keyword* keywords,
+ # expr? starargs, expr? kwargs)
+ def visit_Call(self, node):
+ self.visit(node.func)
+ self._write('(')
+ first = True
+ for arg in node.args:
+ if not first:
+ self._write(', ')
+ first = False
+ self.visit(arg)
+
+ for keyword in node.keywords:
+ if not first:
+ self._write(', ')
+ first = False
+ # keyword = (identifier arg, expr value)
+ self._write(keyword.arg)
+ self._write('=')
+ self.visit(keyword.value)
+ if getattr(node, 'starargs', None):
+ if not first:
+ self._write(', ')
+ first = False
+ self._write('*')
+ self.visit(node.starargs)
+
+ if getattr(node, 'kwargs', None):
+ if not first:
+ self._write(', ')
+ first = False
+ self._write('**')
+ self.visit(node.kwargs)
+ self._write(')')
+
+ # Repr(expr value)
+ def visit_Repr(self, node):
+ self._write('`')
+ self.visit(node.value)
+ self._write('`')
+
+ # Num(object n)
+ def visit_Num(self, node):
+ self._write(repr(node.n))
+
+ # Str(string s)
+ def visit_Str(self, node):
+ self._write(repr(node.s))
+
+ # Attribute(expr value, identifier attr, expr_context ctx)
+ def visit_Attribute(self, node):
+ self.visit(node.value)
+ self._write('.')
+ self._write(node.attr)
+
+ # Subscript(expr value, slice slice, expr_context ctx)
+ def visit_Subscript(self, node):
+ self.visit(node.value)
+ self._write('[')
+ def _process_slice(node):
+ if isinstance(node, _ast.Ellipsis):
+ self._write('...')
+ elif isinstance(node, _ast.Slice):
+ if getattr(node, 'lower', 'None'):
+ self.visit(node.lower)
+ self._write(':')
+ if getattr(node, 'upper', None):
+ self.visit(node.upper)
+ if getattr(node, 'step', None):
+ self._write(':')
+ self.visit(node.step)
+ elif isinstance(node, _ast.Index):
+ self.visit(node.value)
+ elif isinstance(node, _ast.ExtSlice):
+ self.visit(node.dims[0])
+ for dim in node.dims[1:]:
+ self._write(', ')
+ self.visit(dim)
+ else:
+ raise NotImplemented('Slice type not implemented')
+ _process_slice(node.slice)
+ self._write(']')
+
+ # Name(identifier id, expr_context ctx)
+ def visit_Name(self, node):
+ self._write(node.id)
+
+ # List(expr* elts, expr_context ctx)
+ def visit_List(self, node):
+ self._write('[')
+ for elt in node.elts:
+ self.visit(elt)
+ self._write(', ')
+ self._write(']')
+
+ # Tuple(expr *elts, expr_context ctx)
+ def visit_Tuple(self, node):
+ self._write('(')
+ for elt in node.elts:
+ self.visit(elt)
+ self._write(', ')
+ self._write(')')
+
+
+class ASTTransformer(object):
+ """General purpose base class for AST transformations.
+
+ Every visitor method can be overridden to return an AST node that has been
+ altered or replaced in some way.
+ """
+
+ def visit(self, node):
+ if node is None:
+ return None
+ if type(node) is tuple:
+ return tuple([self.visit(n) for n in node])
+ visitor = getattr(self, 'visit_%s' % node.__class__.__name__, None)
+ if visitor is None:
+ return node
+ return visitor(node)
+
+ def _clone(self, node):
+ clone = node.__class__()
+ for name in getattr(clone, '_attributes', ()):
+ try:
+ setattr(clone, 'name', getattr(node, name))
+ except AttributeError:
+ pass
+ for name in clone._fields:
+ try:
+ value = getattr(node, name)
+ except AttributeError:
+ pass
+ else:
+ if value is None:
+ pass
+ elif isinstance(value, list):
+ value = [self.visit(x) for x in value]
+ elif isinstance(value, tuple):
+ value = tuple(self.visit(x) for x in value)
+ else:
+ value = self.visit(value)
+ setattr(clone, name, value)
+ return clone
+
+ visit_Module = _clone
+ visit_Interactive = _clone
+ visit_Expression = _clone
+ visit_Suite = _clone
+
+ visit_FunctionDef = _clone
+ visit_ClassDef = _clone
+ visit_Return = _clone
+ visit_Delete = _clone
+ visit_Assign = _clone
+ visit_AugAssign = _clone
+ visit_Print = _clone
+ visit_For = _clone
+ visit_While = _clone
+ visit_If = _clone
+ visit_With = _clone
+ visit_Raise = _clone
+ visit_TryExcept = _clone
+ visit_TryFinally = _clone
+ visit_Assert = _clone
+ visit_ExceptHandler = _clone
+
+ visit_Import = _clone
+ visit_ImportFrom = _clone
+ visit_Exec = _clone
+ visit_Global = _clone
+ visit_Expr = _clone
+ # Pass, Break, Continue don't need to be copied
+
+ visit_BoolOp = _clone
+ visit_BinOp = _clone
+ visit_UnaryOp = _clone
+ visit_Lambda = _clone
+ visit_IfExp = _clone
+ visit_Dict = _clone
+ visit_ListComp = _clone
+ visit_GeneratorExp = _clone
+ visit_Yield = _clone
+ visit_Compare = _clone
+ visit_Call = _clone
+ visit_Repr = _clone
+ # Num, Str don't need to be copied
+
+ visit_Attribute = _clone
+ visit_Subscript = _clone
+ visit_Name = _clone
+ visit_List = _clone
+ visit_Tuple = _clone
+
+ visit_comprehension = _clone
+ visit_excepthandler = _clone
+ visit_arguments = _clone
+ visit_keyword = _clone
+ visit_alias = _clone
+
+ visit_Slice = _clone
+ visit_ExtSlice = _clone
+ visit_Index = _clone
+
+ del _clone
diff --git a/genshi/template/base.py b/genshi/template/base.py
new file mode 100644
index 0000000..202faae
--- /dev/null
+++ b/genshi/template/base.py
@@ -0,0 +1,634 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2010 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Basic templating functionality."""
+
+from collections import deque
+import os
+from StringIO import StringIO
+import sys
+
+from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
+from genshi.input import ParseError
+
+__all__ = ['Context', 'DirectiveFactory', 'Template', 'TemplateError',
+ 'TemplateRuntimeError', 'TemplateSyntaxError', 'BadDirectiveError']
+__docformat__ = 'restructuredtext en'
+
+
+class TemplateError(Exception):
+ """Base exception class for errors related to template processing."""
+
+ def __init__(self, message, filename=None, lineno=-1, offset=-1):
+ """Create the exception.
+
+ :param message: the error message
+ :param filename: the filename of the template
+ :param lineno: the number of line in the template at which the error
+ occurred
+ :param offset: the column number at which the error occurred
+ """
+ if filename is None:
+ filename = '<string>'
+ self.msg = message #: the error message string
+ if filename != '<string>' or lineno >= 0:
+ message = '%s (%s, line %d)' % (self.msg, filename, lineno)
+ Exception.__init__(self, message)
+ self.filename = filename #: the name of the template file
+ self.lineno = lineno #: the number of the line containing the error
+ self.offset = offset #: the offset on the line
+
+
+class TemplateSyntaxError(TemplateError):
+ """Exception raised when an expression in a template causes a Python syntax
+ error, or the template is not well-formed.
+ """
+
+ def __init__(self, message, filename=None, lineno=-1, offset=-1):
+ """Create the exception
+
+ :param message: the error message
+ :param filename: the filename of the template
+ :param lineno: the number of line in the template at which the error
+ occurred
+ :param offset: the column number at which the error occurred
+ """
+ if isinstance(message, SyntaxError) and message.lineno is not None:
+ message = str(message).replace(' (line %d)' % message.lineno, '')
+ TemplateError.__init__(self, message, filename, lineno)
+
+
+class BadDirectiveError(TemplateSyntaxError):
+ """Exception raised when an unknown directive is encountered when parsing
+ a template.
+
+ An unknown directive is any attribute using the namespace for directives,
+ with a local name that doesn't match any registered directive.
+ """
+
+ def __init__(self, name, filename=None, lineno=-1):
+ """Create the exception
+
+ :param name: the name of the directive
+ :param filename: the filename of the template
+ :param lineno: the number of line in the template at which the error
+ occurred
+ """
+ TemplateSyntaxError.__init__(self, 'bad directive "%s"' % name,
+ filename, lineno)
+
+
+class TemplateRuntimeError(TemplateError):
+ """Exception raised when an the evaluation of a Python expression in a
+ template causes an error.
+ """
+
+
+class Context(object):
+ """Container for template input data.
+
+ A context provides a stack of scopes (represented by dictionaries).
+
+ Template directives such as loops can push a new scope on the stack with
+ data that should only be available inside the loop. When the loop
+ terminates, that scope can get popped off the stack again.
+
+ >>> ctxt = Context(one='foo', other=1)
+ >>> ctxt.get('one')
+ 'foo'
+ >>> ctxt.get('other')
+ 1
+ >>> ctxt.push(dict(one='frost'))
+ >>> ctxt.get('one')
+ 'frost'
+ >>> ctxt.get('other')
+ 1
+ >>> ctxt.pop()
+ {'one': 'frost'}
+ >>> ctxt.get('one')
+ 'foo'
+ """
+
+ def __init__(self, **data):
+ """Initialize the template context with the given keyword arguments as
+ data.
+ """
+ self.frames = deque([data])
+ self.pop = self.frames.popleft
+ self.push = self.frames.appendleft
+ self._match_templates = []
+ self._choice_stack = []
+
+ # Helper functions for use in expressions
+ def defined(name):
+ """Return whether a variable with the specified name exists in the
+ expression scope."""
+ return name in self
+ def value_of(name, default=None):
+ """If a variable of the specified name is defined, return its value.
+ Otherwise, return the provided default value, or ``None``."""
+ return self.get(name, default)
+ data.setdefault('defined', defined)
+ data.setdefault('value_of', value_of)
+
+ def __repr__(self):
+ return repr(list(self.frames))
+
+ def __contains__(self, key):
+ """Return whether a variable exists in any of the scopes.
+
+ :param key: the name of the variable
+ """
+ return self._find(key)[1] is not None
+ has_key = __contains__
+
+ def __delitem__(self, key):
+ """Remove a variable from all scopes.
+
+ :param key: the name of the variable
+ """
+ for frame in self.frames:
+ if key in frame:
+ del frame[key]
+
+ def __getitem__(self, key):
+ """Get a variables's value, starting at the current scope and going
+ upward.
+
+ :param key: the name of the variable
+ :return: the variable value
+ :raises KeyError: if the requested variable wasn't found in any scope
+ """
+ value, frame = self._find(key)
+ if frame is None:
+ raise KeyError(key)
+ return value
+
+ def __len__(self):
+ """Return the number of distinctly named variables in the context.
+
+ :return: the number of variables in the context
+ """
+ return len(self.items())
+
+ def __setitem__(self, key, value):
+ """Set a variable in the current scope.
+
+ :param key: the name of the variable
+ :param value: the variable value
+ """
+ self.frames[0][key] = value
+
+ def _find(self, key, default=None):
+ """Retrieve a given variable's value and the frame it was found in.
+
+ Intended primarily for internal use by directives.
+
+ :param key: the name of the variable
+ :param default: the default value to return when the variable is not
+ found
+ """
+ for frame in self.frames:
+ if key in frame:
+ return frame[key], frame
+ return default, None
+
+ def get(self, key, default=None):
+ """Get a variable's value, starting at the current scope and going
+ upward.
+
+ :param key: the name of the variable
+ :param default: the default value to return when the variable is not
+ found
+ """
+ for frame in self.frames:
+ if key in frame:
+ return frame[key]
+ return default
+
+ def keys(self):
+ """Return the name of all variables in the context.
+
+ :return: a list of variable names
+ """
+ keys = []
+ for frame in self.frames:
+ keys += [key for key in frame if key not in keys]
+ return keys
+
+ def items(self):
+ """Return a list of ``(name, value)`` tuples for all variables in the
+ context.
+
+ :return: a list of variables
+ """
+ return [(key, self.get(key)) for key in self.keys()]
+
+ def update(self, mapping):
+ """Update the context from the mapping provided."""
+ self.frames[0].update(mapping)
+
+ def push(self, data):
+ """Push a new scope on the stack.
+
+ :param data: the data dictionary to push on the context stack.
+ """
+
+ def pop(self):
+ """Pop the top-most scope from the stack."""
+
+
+def _apply_directives(stream, directives, ctxt, vars):
+ """Apply the given directives to the stream.
+
+ :param stream: the stream the directives should be applied to
+ :param directives: the list of directives to apply
+ :param ctxt: the `Context`
+ :param vars: additional variables that should be available when Python
+ code is executed
+ :return: the stream with the given directives applied
+ """
+ if directives:
+ stream = directives[0](iter(stream), directives[1:], ctxt, **vars)
+ return stream
+
+
+def _eval_expr(expr, ctxt, vars=None):
+ """Evaluate the given `Expression` object.
+
+ :param expr: the expression to evaluate
+ :param ctxt: the `Context`
+ :param vars: additional variables that should be available to the
+ expression
+ :return: the result of the evaluation
+ """
+ if vars:
+ ctxt.push(vars)
+ retval = expr.evaluate(ctxt)
+ if vars:
+ ctxt.pop()
+ return retval
+
+
+def _exec_suite(suite, ctxt, vars=None):
+ """Execute the given `Suite` object.
+
+ :param suite: the code suite to execute
+ :param ctxt: the `Context`
+ :param vars: additional variables that should be available to the
+ code
+ """
+ if vars:
+ ctxt.push(vars)
+ ctxt.push({})
+ suite.execute(ctxt)
+ if vars:
+ top = ctxt.pop()
+ ctxt.pop()
+ ctxt.frames[0].update(top)
+
+
+class DirectiveFactoryMeta(type):
+ """Meta class for directive factories."""
+
+ def __new__(cls, name, bases, d):
+ if 'directives' in d:
+ d['_dir_by_name'] = dict(d['directives'])
+ d['_dir_order'] = [directive[1] for directive in d['directives']]
+
+ return type.__new__(cls, name, bases, d)
+
+
+class DirectiveFactory(object):
+ """Base for classes that provide a set of template directives.
+
+ :since: version 0.6
+ """
+ __metaclass__ = DirectiveFactoryMeta
+
+ directives = []
+ """A list of ``(name, cls)`` tuples that define the set of directives
+ provided by this factory.
+ """
+
+ def get_directive(self, name):
+ """Return the directive class for the given name.
+
+ :param name: the directive name as used in the template
+ :return: the directive class
+ :see: `Directive`
+ """
+ return self._dir_by_name.get(name)
+
+ def get_directive_index(self, dir_cls):
+ """Return a key for the given directive class that should be used to
+ sort it among other directives on the same `SUB` event.
+
+ The default implementation simply returns the index of the directive in
+ the `directives` list.
+
+ :param dir_cls: the directive class
+ :return: the sort key
+ """
+ if dir_cls in self._dir_order:
+ return self._dir_order.index(dir_cls)
+ return len(self._dir_order)
+
+
+class Template(DirectiveFactory):
+ """Abstract template base class.
+
+ This class implements most of the template processing model, but does not
+ specify the syntax of templates.
+ """
+
+ EXEC = StreamEventKind('EXEC')
+ """Stream event kind representing a Python code suite to execute."""
+
+ EXPR = StreamEventKind('EXPR')
+ """Stream event kind representing a Python expression."""
+
+ INCLUDE = StreamEventKind('INCLUDE')
+ """Stream event kind representing the inclusion of another template."""
+
+ SUB = StreamEventKind('SUB')
+ """Stream event kind representing a nested stream to which one or more
+ directives should be applied.
+ """
+
+ serializer = None
+ _number_conv = unicode # function used to convert numbers to event data
+
+ def __init__(self, source, filepath=None, filename=None, loader=None,
+ encoding=None, lookup='strict', allow_exec=True):
+ """Initialize a template from either a string, a file-like object, or
+ an already parsed markup stream.
+
+ :param source: a string, file-like object, or markup stream to read the
+ template from
+ :param filepath: the absolute path to the template file
+ :param filename: the path to the template file relative to the search
+ path
+ :param loader: the `TemplateLoader` to use for loading included
+ templates
+ :param encoding: the encoding of the `source`
+ :param lookup: the variable lookup mechanism; either "strict" (the
+ default), "lenient", or a custom lookup class
+ :param allow_exec: whether Python code blocks in templates should be
+ allowed
+
+ :note: Changed in 0.5: Added the `allow_exec` argument
+ """
+ self.filepath = filepath or filename
+ self.filename = filename
+ self.loader = loader
+ self.lookup = lookup
+ self.allow_exec = allow_exec
+ self._init_filters()
+ self._init_loader()
+ self._prepared = False
+
+ if isinstance(source, basestring):
+ source = StringIO(source)
+ else:
+ source = source
+ try:
+ self._stream = self._parse(source, encoding)
+ except ParseError, e:
+ raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset)
+
+ def __getstate__(self):
+ state = self.__dict__.copy()
+ state['filters'] = []
+ return state
+
+ def __setstate__(self, state):
+ self.__dict__ = state
+ self._init_filters()
+
+ def __repr__(self):
+ return '<%s "%s">' % (type(self).__name__, self.filename)
+
+ def _init_filters(self):
+ self.filters = [self._flatten, self._include]
+
+ def _init_loader(self):
+ if self.loader is None:
+ from genshi.template.loader import TemplateLoader
+ if self.filename:
+ if self.filepath != self.filename:
+ basedir = os.path.normpath(self.filepath)[:-len(
+ os.path.normpath(self.filename))
+ ]
+ else:
+ basedir = os.path.dirname(self.filename)
+ else:
+ basedir = '.'
+ self.loader = TemplateLoader([os.path.abspath(basedir)])
+
+ @property
+ def stream(self):
+ if not self._prepared:
+ self._stream = list(self._prepare(self._stream))
+ self._prepared = True
+ return self._stream
+
+ def _parse(self, source, encoding):
+ """Parse the template.
+
+ The parsing stage parses the template and constructs a list of
+ directives that will be executed in the render stage. The input is
+ split up into literal output (text that does not depend on the context
+ data) and directives or expressions.
+
+ :param source: a file-like object containing the XML source of the
+ template, or an XML event stream
+ :param encoding: the encoding of the `source`
+ """
+ raise NotImplementedError
+
+ def _prepare(self, stream):
+ """Call the `attach` method of every directive found in the template.
+
+ :param stream: the event stream of the template
+ """
+ from genshi.template.loader import TemplateNotFound
+
+ for kind, data, pos in stream:
+ if kind is SUB:
+ directives = []
+ substream = data[1]
+ for _, cls, value, namespaces, pos in sorted(data[0]):
+ directive, substream = cls.attach(self, substream, value,
+ namespaces, pos)
+ if directive:
+ directives.append(directive)
+ substream = self._prepare(substream)
+ if directives:
+ yield kind, (directives, list(substream)), pos
+ else:
+ for event in substream:
+ yield event
+ else:
+ if kind is INCLUDE:
+ href, cls, fallback = data
+ if isinstance(href, basestring) and \
+ not getattr(self.loader, 'auto_reload', True):
+ # If the path to the included template is static, and
+ # auto-reloading is disabled on the template loader,
+ # the template is inlined into the stream
+ try:
+ tmpl = self.loader.load(href, relative_to=pos[0],
+ cls=cls or self.__class__)
+ for event in tmpl.stream:
+ yield event
+ except TemplateNotFound:
+ if fallback is None:
+ raise
+ for event in self._prepare(fallback):
+ yield event
+ continue
+ elif fallback:
+ # Otherwise the include is performed at run time
+ data = href, cls, list(self._prepare(fallback))
+
+ yield kind, data, pos
+
+ def generate(self, *args, **kwargs):
+ """Apply the template to the given context data.
+
+ Any keyword arguments are made available to the template as context
+ data.
+
+ Only one positional argument is accepted: if it is provided, it must be
+ an instance of the `Context` class, and keyword arguments are ignored.
+ This calling style is used for internal processing.
+
+ :return: a markup event stream representing the result of applying
+ the template to the context data.
+ """
+ vars = {}
+ if args:
+ assert len(args) == 1
+ ctxt = args[0]
+ if ctxt is None:
+ ctxt = Context(**kwargs)
+ else:
+ vars = kwargs
+ assert isinstance(ctxt, Context)
+ else:
+ ctxt = Context(**kwargs)
+
+ stream = self.stream
+ for filter_ in self.filters:
+ stream = filter_(iter(stream), ctxt, **vars)
+ return Stream(stream, self.serializer)
+
+ def _flatten(self, stream, ctxt, **vars):
+ number_conv = self._number_conv
+ stack = []
+ push = stack.append
+ pop = stack.pop
+ stream = iter(stream)
+
+ while 1:
+ for kind, data, pos in stream:
+
+ if kind is START and data[1]:
+ # Attributes may still contain expressions in start tags at
+ # this point, so do some evaluation
+ tag, attrs = data
+ new_attrs = []
+ for name, value in attrs:
+ if type(value) is list: # this is an interpolated string
+ values = [event[1]
+ for event in self._flatten(value, ctxt, **vars)
+ if event[0] is TEXT and event[1] is not None
+ ]
+ if not values:
+ continue
+ value = ''.join(values)
+ new_attrs.append((name, value))
+ yield kind, (tag, Attrs(new_attrs)), pos
+
+ elif kind is EXPR:
+ result = _eval_expr(data, ctxt, vars)
+ if result is not None:
+ # First check for a string, otherwise the iterable test
+ # below succeeds, and the string will be chopped up into
+ # individual characters
+ if isinstance(result, basestring):
+ yield TEXT, result, pos
+ elif isinstance(result, (int, float, long)):
+ yield TEXT, number_conv(result), pos
+ elif hasattr(result, '__iter__'):
+ push(stream)
+ stream = _ensure(result)
+ break
+ else:
+ yield TEXT, unicode(result), pos
+
+ elif kind is SUB:
+ # This event is a list of directives and a list of nested
+ # events to which those directives should be applied
+ push(stream)
+ stream = _apply_directives(data[1], data[0], ctxt, vars)
+ break
+
+ elif kind is EXEC:
+ _exec_suite(data, ctxt, vars)
+
+ else:
+ yield kind, data, pos
+
+ else:
+ if not stack:
+ break
+ stream = pop()
+
+ def _include(self, stream, ctxt, **vars):
+ """Internal stream filter that performs inclusion of external
+ template files.
+ """
+ from genshi.template.loader import TemplateNotFound
+
+ for event in stream:
+ if event[0] is INCLUDE:
+ href, cls, fallback = event[1]
+ if not isinstance(href, basestring):
+ parts = []
+ for subkind, subdata, subpos in self._flatten(href, ctxt,
+ **vars):
+ if subkind is TEXT:
+ parts.append(subdata)
+ href = ''.join([x for x in parts if x is not None])
+ try:
+ tmpl = self.loader.load(href, relative_to=event[2][0],
+ cls=cls or self.__class__)
+ for event in tmpl.generate(ctxt, **vars):
+ yield event
+ except TemplateNotFound:
+ if fallback is None:
+ raise
+ for filter_ in self.filters:
+ fallback = filter_(iter(fallback), ctxt, **vars)
+ for event in fallback:
+ yield event
+ else:
+ yield event
+
+
+EXEC = Template.EXEC
+EXPR = Template.EXPR
+INCLUDE = Template.INCLUDE
+SUB = Template.SUB
diff --git a/genshi/template/directives.py b/genshi/template/directives.py
new file mode 100644
index 0000000..e2c9424
--- /dev/null
+++ b/genshi/template/directives.py
@@ -0,0 +1,725 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Implementation of the various template directives."""
+
+from genshi.core import QName, Stream
+from genshi.path import Path
+from genshi.template.base import TemplateRuntimeError, TemplateSyntaxError, \
+ EXPR, _apply_directives, _eval_expr
+from genshi.template.eval import Expression, ExpressionASTTransformer, \
+ _ast, _parse
+
+__all__ = ['AttrsDirective', 'ChooseDirective', 'ContentDirective',
+ 'DefDirective', 'ForDirective', 'IfDirective', 'MatchDirective',
+ 'OtherwiseDirective', 'ReplaceDirective', 'StripDirective',
+ 'WhenDirective', 'WithDirective']
+__docformat__ = 'restructuredtext en'
+
+
+class DirectiveMeta(type):
+ """Meta class for template directives."""
+
+ def __new__(cls, name, bases, d):
+ d['tagname'] = name.lower().replace('directive', '')
+ return type.__new__(cls, name, bases, d)
+
+
+class Directive(object):
+ """Abstract base class for template directives.
+
+ A directive is basically a callable that takes three positional arguments:
+ ``ctxt`` is the template data context, ``stream`` is an iterable over the
+ events that the directive applies to, and ``directives`` is is a list of
+ other directives on the same stream that need to be applied.
+
+ Directives can be "anonymous" or "registered". Registered directives can be
+ applied by the template author using an XML attribute with the
+ corresponding name in the template. Such directives should be subclasses of
+ this base class that can be instantiated with the value of the directive
+ attribute as parameter.
+
+ Anonymous directives are simply functions conforming to the protocol
+ described above, and can only be applied programmatically (for example by
+ template filters).
+ """
+ __metaclass__ = DirectiveMeta
+ __slots__ = ['expr']
+
+ def __init__(self, value, template=None, namespaces=None, lineno=-1,
+ offset=-1):
+ self.expr = self._parse_expr(value, template, lineno, offset)
+
+ @classmethod
+ def attach(cls, template, stream, value, namespaces, pos):
+ """Called after the template stream has been completely parsed.
+
+ :param template: the `Template` object
+ :param stream: the event stream associated with the directive
+ :param value: the argument value for the directive; if the directive was
+ specified as an element, this will be an `Attrs` instance
+ with all specified attributes, otherwise it will be a
+ `unicode` object with just the attribute value
+ :param namespaces: a mapping of namespace URIs to prefixes
+ :param pos: a ``(filename, lineno, offset)`` tuple describing the
+ location where the directive was found in the source
+
+ This class method should return a ``(directive, stream)`` tuple. If
+ ``directive`` is not ``None``, it should be an instance of the `Directive`
+ class, and gets added to the list of directives applied to the substream
+ at runtime. `stream` is an event stream that replaces the original
+ stream associated with the directive.
+ """
+ return cls(value, template, namespaces, *pos[1:]), stream
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ """Apply the directive to the given stream.
+
+ :param stream: the event stream
+ :param directives: a list of the remaining directives that should
+ process the stream
+ :param ctxt: the context data
+ :param vars: additional variables that should be made available when
+ Python code is executed
+ """
+ raise NotImplementedError
+
+ def __repr__(self):
+ expr = ''
+ if getattr(self, 'expr', None) is not None:
+ expr = ' "%s"' % self.expr.source
+ return '<%s%s>' % (type(self).__name__, expr)
+
+ @classmethod
+ def _parse_expr(cls, expr, template, lineno=-1, offset=-1):
+ """Parses the given expression, raising a useful error message when a
+ syntax error is encountered.
+ """
+ try:
+ return expr and Expression(expr, template.filepath, lineno,
+ lookup=template.lookup) or None
+ except SyntaxError, err:
+ err.msg += ' in expression "%s" of "%s" directive' % (expr,
+ cls.tagname)
+ raise TemplateSyntaxError(err, template.filepath, lineno,
+ offset + (err.offset or 0))
+
+
+def _assignment(ast):
+ """Takes the AST representation of an assignment, and returns a
+ function that applies the assignment of a given value to a dictionary.
+ """
+ def _names(node):
+ if isinstance(node, _ast.Tuple):
+ return tuple([_names(child) for child in node.elts])
+ elif isinstance(node, _ast.Name):
+ return node.id
+ def _assign(data, value, names=_names(ast)):
+ if type(names) is tuple:
+ for idx in range(len(names)):
+ _assign(data, value[idx], names[idx])
+ else:
+ data[names] = value
+ return _assign
+
+
+class AttrsDirective(Directive):
+ """Implementation of the ``py:attrs`` template directive.
+
+ The value of the ``py:attrs`` attribute should be a dictionary or a sequence
+ of ``(name, value)`` tuples. The items in that dictionary or sequence are
+ added as attributes to the element:
+
+ >>> from genshi.template import MarkupTemplate
+ >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
+ ... <li py:attrs="foo">Bar</li>
+ ... </ul>''')
+ >>> print(tmpl.generate(foo={'class': 'collapse'}))
+ <ul>
+ <li class="collapse">Bar</li>
+ </ul>
+ >>> print(tmpl.generate(foo=[('class', 'collapse')]))
+ <ul>
+ <li class="collapse">Bar</li>
+ </ul>
+
+ If the value evaluates to ``None`` (or any other non-truth value), no
+ attributes are added:
+
+ >>> print(tmpl.generate(foo=None))
+ <ul>
+ <li>Bar</li>
+ </ul>
+ """
+ __slots__ = []
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ def _generate():
+ kind, (tag, attrib), pos = stream.next()
+ attrs = _eval_expr(self.expr, ctxt, vars)
+ if attrs:
+ if isinstance(attrs, Stream):
+ try:
+ attrs = iter(attrs).next()
+ except StopIteration:
+ attrs = []
+ elif not isinstance(attrs, list): # assume it's a dict
+ attrs = attrs.items()
+ attrib -= [name for name, val in attrs if val is None]
+ attrib |= [(QName(name), unicode(val).strip()) for name, val
+ in attrs if val is not None]
+ yield kind, (tag, attrib), pos
+ for event in stream:
+ yield event
+
+ return _apply_directives(_generate(), directives, ctxt, vars)
+
+
+class ContentDirective(Directive):
+ """Implementation of the ``py:content`` template directive.
+
+ This directive replaces the content of the element with the result of
+ evaluating the value of the ``py:content`` attribute:
+
+ >>> from genshi.template import MarkupTemplate
+ >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
+ ... <li py:content="bar">Hello</li>
+ ... </ul>''')
+ >>> print(tmpl.generate(bar='Bye'))
+ <ul>
+ <li>Bye</li>
+ </ul>
+ """
+ __slots__ = []
+
+ @classmethod
+ def attach(cls, template, stream, value, namespaces, pos):
+ if type(value) is dict:
+ raise TemplateSyntaxError('The content directive can not be used '
+ 'as an element', template.filepath,
+ *pos[1:])
+ expr = cls._parse_expr(value, template, *pos[1:])
+ return None, [stream[0], (EXPR, expr, pos), stream[-1]]
+
+
+class DefDirective(Directive):
+ """Implementation of the ``py:def`` template directive.
+
+ This directive can be used to create "Named Template Functions", which
+ are template snippets that are not actually output during normal
+ processing, but rather can be expanded from expressions in other places
+ in the template.
+
+ A named template function can be used just like a normal Python function
+ from template expressions:
+
+ >>> from genshi.template import MarkupTemplate
+ >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+ ... <p py:def="echo(greeting, name='world')" class="message">
+ ... ${greeting}, ${name}!
+ ... </p>
+ ... ${echo('Hi', name='you')}
+ ... </div>''')
+ >>> print(tmpl.generate(bar='Bye'))
+ <div>
+ <p class="message">
+ Hi, you!
+ </p>
+ </div>
+
+ If a function does not require parameters, the parenthesis can be omitted
+ in the definition:
+
+ >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+ ... <p py:def="helloworld" class="message">
+ ... Hello, world!
+ ... </p>
+ ... ${helloworld()}
+ ... </div>''')
+ >>> print(tmpl.generate(bar='Bye'))
+ <div>
+ <p class="message">
+ Hello, world!
+ </p>
+ </div>
+ """
+ __slots__ = ['name', 'args', 'star_args', 'dstar_args', 'defaults']
+
+ def __init__(self, args, template, namespaces=None, lineno=-1, offset=-1):
+ Directive.__init__(self, None, template, namespaces, lineno, offset)
+ ast = _parse(args).body
+ self.args = []
+ self.star_args = None
+ self.dstar_args = None
+ self.defaults = {}
+ if isinstance(ast, _ast.Call):
+ self.name = ast.func.id
+ for arg in ast.args:
+ # only names
+ self.args.append(arg.id)
+ for kwd in ast.keywords:
+ self.args.append(kwd.arg)
+ exp = Expression(kwd.value, template.filepath,
+ lineno, lookup=template.lookup)
+ self.defaults[kwd.arg] = exp
+ if getattr(ast, 'starargs', None):
+ self.star_args = ast.starargs.id
+ if getattr(ast, 'kwargs', None):
+ self.dstar_args = ast.kwargs.id
+ else:
+ self.name = ast.id
+
+ @classmethod
+ def attach(cls, template, stream, value, namespaces, pos):
+ if type(value) is dict:
+ value = value.get('function')
+ return super(DefDirective, cls).attach(template, stream, value,
+ namespaces, pos)
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ stream = list(stream)
+
+ def function(*args, **kwargs):
+ scope = {}
+ args = list(args) # make mutable
+ for name in self.args:
+ if args:
+ scope[name] = args.pop(0)
+ else:
+ if name in kwargs:
+ val = kwargs.pop(name)
+ else:
+ val = _eval_expr(self.defaults.get(name), ctxt, vars)
+ scope[name] = val
+ if not self.star_args is None:
+ scope[self.star_args] = args
+ if not self.dstar_args is None:
+ scope[self.dstar_args] = kwargs
+ ctxt.push(scope)
+ for event in _apply_directives(stream, directives, ctxt, vars):
+ yield event
+ ctxt.pop()
+ function.__name__ = self.name
+
+ # Store the function reference in the bottom context frame so that it
+ # doesn't get popped off before processing the template has finished
+ # FIXME: this makes context data mutable as a side-effect
+ ctxt.frames[-1][self.name] = function
+
+ return []
+
+ def __repr__(self):
+ return '<%s "%s">' % (type(self).__name__, self.name)
+
+
+class ForDirective(Directive):
+ """Implementation of the ``py:for`` template directive for repeating an
+ element based on an iterable in the context data.
+
+ >>> from genshi.template import MarkupTemplate
+ >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
+ ... <li py:for="item in items">${item}</li>
+ ... </ul>''')
+ >>> print(tmpl.generate(items=[1, 2, 3]))
+ <ul>
+ <li>1</li><li>2</li><li>3</li>
+ </ul>
+ """
+ __slots__ = ['assign', 'filename']
+
+ def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
+ if ' in ' not in value:
+ raise TemplateSyntaxError('"in" keyword missing in "for" directive',
+ template.filepath, lineno, offset)
+ assign, value = value.split(' in ', 1)
+ ast = _parse(assign, 'exec')
+ value = 'iter(%s)' % value.strip()
+ self.assign = _assignment(ast.body[0].value)
+ self.filename = template.filepath
+ Directive.__init__(self, value, template, namespaces, lineno, offset)
+
+ @classmethod
+ def attach(cls, template, stream, value, namespaces, pos):
+ if type(value) is dict:
+ value = value.get('each')
+ return super(ForDirective, cls).attach(template, stream, value,
+ namespaces, pos)
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ iterable = _eval_expr(self.expr, ctxt, vars)
+ if iterable is None:
+ return
+
+ assign = self.assign
+ scope = {}
+ stream = list(stream)
+ for item in iterable:
+ assign(scope, item)
+ ctxt.push(scope)
+ for event in _apply_directives(stream, directives, ctxt, vars):
+ yield event
+ ctxt.pop()
+
+ def __repr__(self):
+ return '<%s>' % type(self).__name__
+
+
+class IfDirective(Directive):
+ """Implementation of the ``py:if`` template directive for conditionally
+ excluding elements from being output.
+
+ >>> from genshi.template import MarkupTemplate
+ >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+ ... <b py:if="foo">${bar}</b>
+ ... </div>''')
+ >>> print(tmpl.generate(foo=True, bar='Hello'))
+ <div>
+ <b>Hello</b>
+ </div>
+ """
+ __slots__ = []
+
+ @classmethod
+ def attach(cls, template, stream, value, namespaces, pos):
+ if type(value) is dict:
+ value = value.get('test')
+ return super(IfDirective, cls).attach(template, stream, value,
+ namespaces, pos)
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ value = _eval_expr(self.expr, ctxt, vars)
+ if value:
+ return _apply_directives(stream, directives, ctxt, vars)
+ return []
+
+
+class MatchDirective(Directive):
+ """Implementation of the ``py:match`` template directive.
+
+ >>> from genshi.template import MarkupTemplate
+ >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+ ... <span py:match="greeting">
+ ... Hello ${select('@name')}
+ ... </span>
+ ... <greeting name="Dude" />
+ ... </div>''')
+ >>> print(tmpl.generate())
+ <div>
+ <span>
+ Hello Dude
+ </span>
+ </div>
+ """
+ __slots__ = ['path', 'namespaces', 'hints']
+
+ def __init__(self, value, template, hints=None, namespaces=None,
+ lineno=-1, offset=-1):
+ Directive.__init__(self, None, template, namespaces, lineno, offset)
+ self.path = Path(value, template.filepath, lineno)
+ self.namespaces = namespaces or {}
+ self.hints = hints or ()
+
+ @classmethod
+ def attach(cls, template, stream, value, namespaces, pos):
+ hints = []
+ if type(value) is dict:
+ if value.get('buffer', '').lower() == 'false':
+ hints.append('not_buffered')
+ if value.get('once', '').lower() == 'true':
+ hints.append('match_once')
+ if value.get('recursive', '').lower() == 'false':
+ hints.append('not_recursive')
+ value = value.get('path')
+ return cls(value, template, frozenset(hints), namespaces, *pos[1:]), \
+ stream
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ ctxt._match_templates.append((self.path.test(ignore_context=True),
+ self.path, list(stream), self.hints,
+ self.namespaces, directives))
+ return []
+
+ def __repr__(self):
+ return '<%s "%s">' % (type(self).__name__, self.path.source)
+
+
+class ReplaceDirective(Directive):
+ """Implementation of the ``py:replace`` template directive.
+
+ This directive replaces the element with the result of evaluating the
+ value of the ``py:replace`` attribute:
+
+ >>> from genshi.template import MarkupTemplate
+ >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+ ... <span py:replace="bar">Hello</span>
+ ... </div>''')
+ >>> print(tmpl.generate(bar='Bye'))
+ <div>
+ Bye
+ </div>
+
+ This directive is equivalent to ``py:content`` combined with ``py:strip``,
+ providing a less verbose way to achieve the same effect:
+
+ >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+ ... <span py:content="bar" py:strip="">Hello</span>
+ ... </div>''')
+ >>> print(tmpl.generate(bar='Bye'))
+ <div>
+ Bye
+ </div>
+ """
+ __slots__ = []
+
+ @classmethod
+ def attach(cls, template, stream, value, namespaces, pos):
+ if type(value) is dict:
+ value = value.get('value')
+ if not value:
+ raise TemplateSyntaxError('missing value for "replace" directive',
+ template.filepath, *pos[1:])
+ expr = cls._parse_expr(value, template, *pos[1:])
+ return None, [(EXPR, expr, pos)]
+
+
+class StripDirective(Directive):
+ """Implementation of the ``py:strip`` template directive.
+
+ When the value of the ``py:strip`` attribute evaluates to ``True``, the
+ element is stripped from the output
+
+ >>> from genshi.template import MarkupTemplate
+ >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+ ... <div py:strip="True"><b>foo</b></div>
+ ... </div>''')
+ >>> print(tmpl.generate())
+ <div>
+ <b>foo</b>
+ </div>
+
+ Leaving the attribute value empty is equivalent to a truth value.
+
+ This directive is particulary interesting for named template functions or
+ match templates that do not generate a top-level element:
+
+ >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+ ... <div py:def="echo(what)" py:strip="">
+ ... <b>${what}</b>
+ ... </div>
+ ... ${echo('foo')}
+ ... </div>''')
+ >>> print(tmpl.generate())
+ <div>
+ <b>foo</b>
+ </div>
+ """
+ __slots__ = []
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ def _generate():
+ if not self.expr or _eval_expr(self.expr, ctxt, vars):
+ stream.next() # skip start tag
+ previous = stream.next()
+ for event in stream:
+ yield previous
+ previous = event
+ else:
+ for event in stream:
+ yield event
+ return _apply_directives(_generate(), directives, ctxt, vars)
+
+
+class ChooseDirective(Directive):
+ """Implementation of the ``py:choose`` directive for conditionally selecting
+ one of several body elements to display.
+
+ If the ``py:choose`` expression is empty the expressions of nested
+ ``py:when`` directives are tested for truth. The first true ``py:when``
+ body is output. If no ``py:when`` directive is matched then the fallback
+ directive ``py:otherwise`` will be used.
+
+ >>> from genshi.template import MarkupTemplate
+ >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"
+ ... py:choose="">
+ ... <span py:when="0 == 1">0</span>
+ ... <span py:when="1 == 1">1</span>
+ ... <span py:otherwise="">2</span>
+ ... </div>''')
+ >>> print(tmpl.generate())
+ <div>
+ <span>1</span>
+ </div>
+
+ If the ``py:choose`` directive contains an expression, the nested
+ ``py:when`` directives are tested for equality to the ``py:choose``
+ expression:
+
+ >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/"
+ ... py:choose="2">
+ ... <span py:when="1">1</span>
+ ... <span py:when="2">2</span>
+ ... </div>''')
+ >>> print(tmpl.generate())
+ <div>
+ <span>2</span>
+ </div>
+
+ Behavior is undefined if a ``py:choose`` block contains content outside a
+ ``py:when`` or ``py:otherwise`` block. Behavior is also undefined if a
+ ``py:otherwise`` occurs before ``py:when`` blocks.
+ """
+ __slots__ = ['matched', 'value']
+
+ @classmethod
+ def attach(cls, template, stream, value, namespaces, pos):
+ if type(value) is dict:
+ value = value.get('test')
+ return super(ChooseDirective, cls).attach(template, stream, value,
+ namespaces, pos)
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ info = [False, bool(self.expr), None]
+ if self.expr:
+ info[2] = _eval_expr(self.expr, ctxt, vars)
+ ctxt._choice_stack.append(info)
+ for event in _apply_directives(stream, directives, ctxt, vars):
+ yield event
+ ctxt._choice_stack.pop()
+
+
+class WhenDirective(Directive):
+ """Implementation of the ``py:when`` directive for nesting in a parent with
+ the ``py:choose`` directive.
+
+ See the documentation of the `ChooseDirective` for usage.
+ """
+ __slots__ = ['filename']
+
+ def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
+ Directive.__init__(self, value, template, namespaces, lineno, offset)
+ self.filename = template.filepath
+
+ @classmethod
+ def attach(cls, template, stream, value, namespaces, pos):
+ if type(value) is dict:
+ value = value.get('test')
+ return super(WhenDirective, cls).attach(template, stream, value,
+ namespaces, pos)
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ info = ctxt._choice_stack and ctxt._choice_stack[-1]
+ if not info:
+ raise TemplateRuntimeError('"when" directives can only be used '
+ 'inside a "choose" directive',
+ self.filename, *stream.next()[2][1:])
+ if info[0]:
+ return []
+ if not self.expr and not info[1]:
+ raise TemplateRuntimeError('either "choose" or "when" directive '
+ 'must have a test expression',
+ self.filename, *stream.next()[2][1:])
+ if info[1]:
+ value = info[2]
+ if self.expr:
+ matched = value == _eval_expr(self.expr, ctxt, vars)
+ else:
+ matched = bool(value)
+ else:
+ matched = bool(_eval_expr(self.expr, ctxt, vars))
+ info[0] = matched
+ if not matched:
+ return []
+
+ return _apply_directives(stream, directives, ctxt, vars)
+
+
+class OtherwiseDirective(Directive):
+ """Implementation of the ``py:otherwise`` directive for nesting in a parent
+ with the ``py:choose`` directive.
+
+ See the documentation of `ChooseDirective` for usage.
+ """
+ __slots__ = ['filename']
+
+ def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
+ Directive.__init__(self, None, template, namespaces, lineno, offset)
+ self.filename = template.filepath
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ info = ctxt._choice_stack and ctxt._choice_stack[-1]
+ if not info:
+ raise TemplateRuntimeError('an "otherwise" directive can only be '
+ 'used inside a "choose" directive',
+ self.filename, *stream.next()[2][1:])
+ if info[0]:
+ return []
+ info[0] = True
+
+ return _apply_directives(stream, directives, ctxt, vars)
+
+
+class WithDirective(Directive):
+ """Implementation of the ``py:with`` template directive, which allows
+ shorthand access to variables and expressions.
+
+ >>> from genshi.template import MarkupTemplate
+ >>> tmpl = MarkupTemplate('''<div xmlns:py="http://genshi.edgewall.org/">
+ ... <span py:with="y=7; z=x+10">$x $y $z</span>
+ ... </div>''')
+ >>> print(tmpl.generate(x=42))
+ <div>
+ <span>42 7 52</span>
+ </div>
+ """
+ __slots__ = ['vars']
+
+ def __init__(self, value, template, namespaces=None, lineno=-1, offset=-1):
+ Directive.__init__(self, None, template, namespaces, lineno, offset)
+ self.vars = []
+ value = value.strip()
+ try:
+ ast = _parse(value, 'exec')
+ for node in ast.body:
+ if not isinstance(node, _ast.Assign):
+ raise TemplateSyntaxError('only assignment allowed in '
+ 'value of the "with" directive',
+ template.filepath, lineno, offset)
+ self.vars.append(([_assignment(n) for n in node.targets],
+ Expression(node.value, template.filepath,
+ lineno, lookup=template.lookup)))
+ except SyntaxError, err:
+ err.msg += ' in expression "%s" of "%s" directive' % (value,
+ self.tagname)
+ raise TemplateSyntaxError(err, template.filepath, lineno,
+ offset + (err.offset or 0))
+
+ @classmethod
+ def attach(cls, template, stream, value, namespaces, pos):
+ if type(value) is dict:
+ value = value.get('vars')
+ return super(WithDirective, cls).attach(template, stream, value,
+ namespaces, pos)
+
+ def __call__(self, stream, directives, ctxt, **vars):
+ frame = {}
+ ctxt.push(frame)
+ for targets, expr in self.vars:
+ value = _eval_expr(expr, ctxt, vars)
+ for assign in targets:
+ assign(frame, value)
+ for event in _apply_directives(stream, directives, ctxt, vars):
+ yield event
+ ctxt.pop()
+
+ def __repr__(self):
+ return '<%s>' % (type(self).__name__)
diff --git a/genshi/template/eval.py b/genshi/template/eval.py
new file mode 100644
index 0000000..8593aaa
--- /dev/null
+++ b/genshi/template/eval.py
@@ -0,0 +1,629 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2010 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Support for "safe" evaluation of Python expressions."""
+
+import __builtin__
+
+from textwrap import dedent
+from types import CodeType
+
+from genshi.core import Markup
+from genshi.template.astutil import ASTTransformer, ASTCodeGenerator, \
+ _ast, parse
+from genshi.template.base import TemplateRuntimeError
+from genshi.util import flatten
+
+__all__ = ['Code', 'Expression', 'Suite', 'LenientLookup', 'StrictLookup',
+ 'Undefined', 'UndefinedError']
+__docformat__ = 'restructuredtext en'
+
+
+# Check for a Python 2.4 bug in the eval loop
+has_star_import_bug = False
+try:
+ class _FakeMapping(object):
+ __getitem__ = __setitem__ = lambda *a: None
+ exec 'from sys import *' in {}, _FakeMapping()
+except SystemError:
+ has_star_import_bug = True
+del _FakeMapping
+
+
+def _star_import_patch(mapping, modname):
+ """This function is used as helper if a Python version with a broken
+ star-import opcode is in use.
+ """
+ module = __import__(modname, None, None, ['__all__'])
+ if hasattr(module, '__all__'):
+ members = module.__all__
+ else:
+ members = [x for x in module.__dict__ if not x.startswith('_')]
+ mapping.update([(name, getattr(module, name)) for name in members])
+
+
+class Code(object):
+ """Abstract base class for the `Expression` and `Suite` classes."""
+ __slots__ = ['source', 'code', 'ast', '_globals']
+
+ def __init__(self, source, filename=None, lineno=-1, lookup='strict',
+ xform=None):
+ """Create the code object, either from a string, or from an AST node.
+
+ :param source: either a string containing the source code, or an AST
+ node
+ :param filename: the (preferably absolute) name of the file containing
+ the code
+ :param lineno: the number of the line on which the code was found
+ :param lookup: the lookup class that defines how variables are looked
+ up in the context; can be either "strict" (the default),
+ "lenient", or a custom lookup class
+ :param xform: the AST transformer that should be applied to the code;
+ if `None`, the appropriate transformation is chosen
+ depending on the mode
+ """
+ if isinstance(source, basestring):
+ self.source = source
+ node = _parse(source, mode=self.mode)
+ else:
+ assert isinstance(source, _ast.AST), \
+ 'Expected string or AST node, but got %r' % source
+ self.source = '?'
+ if self.mode == 'eval':
+ node = _ast.Expression()
+ node.body = source
+ else:
+ node = _ast.Module()
+ node.body = [source]
+
+ self.ast = node
+ self.code = _compile(node, self.source, mode=self.mode,
+ filename=filename, lineno=lineno, xform=xform)
+ if lookup is None:
+ lookup = LenientLookup
+ elif isinstance(lookup, basestring):
+ lookup = {'lenient': LenientLookup, 'strict': StrictLookup}[lookup]
+ self._globals = lookup.globals
+
+ def __getstate__(self):
+ state = {'source': self.source, 'ast': self.ast,
+ 'lookup': self._globals.im_self}
+ c = self.code
+ state['code'] = (c.co_nlocals, c.co_stacksize, c.co_flags, c.co_code,
+ c.co_consts, c.co_names, c.co_varnames, c.co_filename,
+ c.co_name, c.co_firstlineno, c.co_lnotab, (), ())
+ return state
+
+ def __setstate__(self, state):
+ self.source = state['source']
+ self.ast = state['ast']
+ self.code = CodeType(0, *state['code'])
+ self._globals = state['lookup'].globals
+
+ def __eq__(self, other):
+ return (type(other) == type(self)) and (self.code == other.code)
+
+ def __hash__(self):
+ return hash(self.code)
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __repr__(self):
+ return '%s(%r)' % (type(self).__name__, self.source)
+
+
+class Expression(Code):
+ """Evaluates Python expressions used in templates.
+
+ >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
+ >>> Expression('test').evaluate(data)
+ 'Foo'
+
+ >>> Expression('items[0]').evaluate(data)
+ 1
+ >>> Expression('items[-1]').evaluate(data)
+ 3
+ >>> Expression('dict["some"]').evaluate(data)
+ 'thing'
+
+ Similar to e.g. Javascript, expressions in templates can use the dot
+ notation for attribute access to access items in mappings:
+
+ >>> Expression('dict.some').evaluate(data)
+ 'thing'
+
+ This also works the other way around: item access can be used to access
+ any object attribute:
+
+ >>> class MyClass(object):
+ ... myattr = 'Bar'
+ >>> data = dict(mine=MyClass(), key='myattr')
+ >>> Expression('mine.myattr').evaluate(data)
+ 'Bar'
+ >>> Expression('mine["myattr"]').evaluate(data)
+ 'Bar'
+ >>> Expression('mine[key]').evaluate(data)
+ 'Bar'
+
+ All of the standard Python operators are available to template expressions.
+ Built-in functions such as ``len()`` are also available in template
+ expressions:
+
+ >>> data = dict(items=[1, 2, 3])
+ >>> Expression('len(items)').evaluate(data)
+ 3
+ """
+ __slots__ = []
+ mode = 'eval'
+
+ def evaluate(self, data):
+ """Evaluate the expression against the given data dictionary.
+
+ :param data: a mapping containing the data to evaluate against
+ :return: the result of the evaluation
+ """
+ __traceback_hide__ = 'before_and_this'
+ _globals = self._globals(data)
+ return eval(self.code, _globals, {'__data__': data})
+
+
+class Suite(Code):
+ """Executes Python statements used in templates.
+
+ >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
+ >>> Suite("foo = dict['some']").execute(data)
+ >>> data['foo']
+ 'thing'
+ """
+ __slots__ = []
+ mode = 'exec'
+
+ def execute(self, data):
+ """Execute the suite in the given data dictionary.
+
+ :param data: a mapping containing the data to execute in
+ """
+ __traceback_hide__ = 'before_and_this'
+ _globals = self._globals(data)
+ exec self.code in _globals, data
+
+
+UNDEFINED = object()
+
+
+class UndefinedError(TemplateRuntimeError):
+ """Exception thrown when a template expression attempts to access a variable
+ not defined in the context.
+
+ :see: `LenientLookup`, `StrictLookup`
+ """
+ def __init__(self, name, owner=UNDEFINED):
+ if owner is not UNDEFINED:
+ message = '%s has no member named "%s"' % (repr(owner), name)
+ else:
+ message = '"%s" not defined' % name
+ TemplateRuntimeError.__init__(self, message)
+
+
+class Undefined(object):
+ """Represents a reference to an undefined variable.
+
+ Unlike the Python runtime, template expressions can refer to an undefined
+ variable without causing a `NameError` to be raised. The result will be an
+ instance of the `Undefined` class, which is treated the same as ``False`` in
+ conditions, but raise an exception on any other operation:
+
+ >>> foo = Undefined('foo')
+ >>> bool(foo)
+ False
+ >>> list(foo)
+ []
+ >>> print(foo)
+ undefined
+
+ However, calling an undefined variable, or trying to access an attribute
+ of that variable, will raise an exception that includes the name used to
+ reference that undefined variable.
+
+ >>> foo('bar')
+ Traceback (most recent call last):
+ ...
+ UndefinedError: "foo" not defined
+
+ >>> foo.bar
+ Traceback (most recent call last):
+ ...
+ UndefinedError: "foo" not defined
+
+ :see: `LenientLookup`
+ """
+ __slots__ = ['_name', '_owner']
+
+ def __init__(self, name, owner=UNDEFINED):
+ """Initialize the object.
+
+ :param name: the name of the reference
+ :param owner: the owning object, if the variable is accessed as a member
+ """
+ self._name = name
+ self._owner = owner
+
+ def __iter__(self):
+ return iter([])
+
+ def __nonzero__(self):
+ return False
+
+ def __repr__(self):
+ return '<%s %r>' % (type(self).__name__, self._name)
+
+ def __str__(self):
+ return 'undefined'
+
+ def _die(self, *args, **kwargs):
+ """Raise an `UndefinedError`."""
+ __traceback_hide__ = True
+ raise UndefinedError(self._name, self._owner)
+ __call__ = __getattr__ = __getitem__ = _die
+
+ # Hack around some behavior introduced in Python 2.6.2
+ # http://genshi.edgewall.org/ticket/324
+ __length_hint__ = None
+
+
+class LookupBase(object):
+ """Abstract base class for variable lookup implementations."""
+
+ @classmethod
+ def globals(cls, data):
+ """Construct the globals dictionary to use as the execution context for
+ the expression or suite.
+ """
+ return {
+ '__data__': data,
+ '_lookup_name': cls.lookup_name,
+ '_lookup_attr': cls.lookup_attr,
+ '_lookup_item': cls.lookup_item,
+ '_star_import_patch': _star_import_patch,
+ 'UndefinedError': UndefinedError,
+ }
+
+ @classmethod
+ def lookup_name(cls, data, name):
+ __traceback_hide__ = True
+ val = data.get(name, UNDEFINED)
+ if val is UNDEFINED:
+ val = BUILTINS.get(name, val)
+ if val is UNDEFINED:
+ val = cls.undefined(name)
+ return val
+
+ @classmethod
+ def lookup_attr(cls, obj, key):
+ __traceback_hide__ = True
+ try:
+ val = getattr(obj, key)
+ except AttributeError:
+ if hasattr(obj.__class__, key):
+ raise
+ else:
+ try:
+ val = obj[key]
+ except (KeyError, TypeError):
+ val = cls.undefined(key, owner=obj)
+ return val
+
+ @classmethod
+ def lookup_item(cls, obj, key):
+ __traceback_hide__ = True
+ if len(key) == 1:
+ key = key[0]
+ try:
+ return obj[key]
+ except (AttributeError, KeyError, IndexError, TypeError), e:
+ if isinstance(key, basestring):
+ val = getattr(obj, key, UNDEFINED)
+ if val is UNDEFINED:
+ val = cls.undefined(key, owner=obj)
+ return val
+ raise
+
+ @classmethod
+ def undefined(cls, key, owner=UNDEFINED):
+ """Can be overridden by subclasses to specify behavior when undefined
+ variables are accessed.
+
+ :param key: the name of the variable
+ :param owner: the owning object, if the variable is accessed as a member
+ """
+ raise NotImplementedError
+
+
+class LenientLookup(LookupBase):
+ """Default variable lookup mechanism for expressions.
+
+ When an undefined variable is referenced using this lookup style, the
+ reference evaluates to an instance of the `Undefined` class:
+
+ >>> expr = Expression('nothing', lookup='lenient')
+ >>> undef = expr.evaluate({})
+ >>> undef
+ <Undefined 'nothing'>
+
+ The same will happen when a non-existing attribute or item is accessed on
+ an existing object:
+
+ >>> expr = Expression('something.nil', lookup='lenient')
+ >>> expr.evaluate({'something': dict()})
+ <Undefined 'nil'>
+
+ See the documentation of the `Undefined` class for details on the behavior
+ of such objects.
+
+ :see: `StrictLookup`
+ """
+
+ @classmethod
+ def undefined(cls, key, owner=UNDEFINED):
+ """Return an ``Undefined`` object."""
+ __traceback_hide__ = True
+ return Undefined(key, owner=owner)
+
+
+class StrictLookup(LookupBase):
+ """Strict variable lookup mechanism for expressions.
+
+ Referencing an undefined variable using this lookup style will immediately
+ raise an ``UndefinedError``:
+
+ >>> expr = Expression('nothing', lookup='strict')
+ >>> expr.evaluate({})
+ Traceback (most recent call last):
+ ...
+ UndefinedError: "nothing" not defined
+
+ The same happens when a non-existing attribute or item is accessed on an
+ existing object:
+
+ >>> expr = Expression('something.nil', lookup='strict')
+ >>> expr.evaluate({'something': dict()})
+ Traceback (most recent call last):
+ ...
+ UndefinedError: {} has no member named "nil"
+ """
+
+ @classmethod
+ def undefined(cls, key, owner=UNDEFINED):
+ """Raise an ``UndefinedError`` immediately."""
+ __traceback_hide__ = True
+ raise UndefinedError(key, owner=owner)
+
+
+def _parse(source, mode='eval'):
+ source = source.strip()
+ if mode == 'exec':
+ lines = [line.expandtabs() for line in source.splitlines()]
+ if lines:
+ first = lines[0]
+ rest = dedent('\n'.join(lines[1:])).rstrip()
+ if first.rstrip().endswith(':') and not rest[0].isspace():
+ rest = '\n'.join([' %s' % line for line in rest.splitlines()])
+ source = '\n'.join([first, rest])
+ if isinstance(source, unicode):
+ source = '\xef\xbb\xbf' + source.encode('utf-8')
+ return parse(source, mode)
+
+
+def _compile(node, source=None, mode='eval', filename=None, lineno=-1,
+ xform=None):
+ if isinstance(filename, unicode):
+ # unicode file names not allowed for code objects
+ filename = filename.encode('utf-8', 'replace')
+ elif not filename:
+ filename = '<string>'
+ if lineno <= 0:
+ lineno = 1
+
+ if xform is None:
+ xform = {
+ 'eval': ExpressionASTTransformer
+ }.get(mode, TemplateASTTransformer)
+ tree = xform().visit(node)
+
+ if mode == 'eval':
+ name = '<Expression %r>' % (source or '?')
+ else:
+ lines = source.splitlines()
+ if not lines:
+ extract = ''
+ else:
+ extract = lines[0]
+ if len(lines) > 1:
+ extract += ' ...'
+ name = '<Suite %r>' % (extract)
+ new_source = ASTCodeGenerator(tree).code
+ code = compile(new_source, filename, mode)
+
+ try:
+ # We'd like to just set co_firstlineno, but it's readonly. So we need
+ # to clone the code object while adjusting the line number
+ return CodeType(0, code.co_nlocals, code.co_stacksize,
+ code.co_flags | 0x0040, code.co_code, code.co_consts,
+ code.co_names, code.co_varnames, filename, name,
+ lineno, code.co_lnotab, (), ())
+ except RuntimeError:
+ return code
+
+
+def _new(class_, *args, **kwargs):
+ ret = class_()
+ for attr, value in zip(ret._fields, args):
+ if attr in kwargs:
+ raise ValueError('Field set both in args and kwargs')
+ setattr(ret, attr, value)
+ for attr, value in kwargs:
+ setattr(ret, attr, value)
+ return ret
+
+
+BUILTINS = __builtin__.__dict__.copy()
+BUILTINS.update({'Markup': Markup, 'Undefined': Undefined})
+CONSTANTS = frozenset(['False', 'True', 'None', 'NotImplemented', 'Ellipsis'])
+
+
+class TemplateASTTransformer(ASTTransformer):
+ """Concrete AST transformer that implements the AST transformations needed
+ for code embedded in templates.
+ """
+
+ def __init__(self):
+ self.locals = [CONSTANTS]
+
+ def _extract_names(self, node):
+ names = set()
+ def _process(node):
+ if isinstance(node, _ast.Name):
+ names.add(node.id)
+ elif isinstance(node, _ast.alias):
+ names.add(node.asname or node.name)
+ elif isinstance(node, _ast.Tuple):
+ for elt in node.elts:
+ _process(elt)
+ if hasattr(node, 'args'):
+ for arg in node.args:
+ _process(arg)
+ if hasattr(node, 'vararg'):
+ names.add(node.vararg)
+ if hasattr(node, 'kwarg'):
+ names.add(node.kwarg)
+ elif hasattr(node, 'names'):
+ for elt in node.names:
+ _process(elt)
+ return names
+
+ def visit_Str(self, node):
+ if isinstance(node.s, str):
+ try: # If the string is ASCII, return a `str` object
+ node.s.decode('ascii')
+ except ValueError: # Otherwise return a `unicode` object
+ return _new(_ast.Str, node.s.decode('utf-8'))
+ return node
+
+ def visit_ClassDef(self, node):
+ if len(self.locals) > 1:
+ self.locals[-1].add(node.name)
+ self.locals.append(set())
+ try:
+ return ASTTransformer.visit_ClassDef(self, node)
+ finally:
+ self.locals.pop()
+
+ def visit_Import(self, node):
+ if len(self.locals) > 1:
+ self.locals[-1].update(self._extract_names(node))
+ return ASTTransformer.visit_Import(self, node)
+
+ def visit_ImportFrom(self, node):
+ if [a.name for a in node.names] == ['*']:
+ if has_star_import_bug:
+ # This is a Python 2.4 bug. Only if we have a broken Python
+ # version do we need to apply this hack
+ node = _new(_ast.Expr, _new(_ast.Call,
+ _new(_ast.Name, '_star_import_patch'), [
+ _new(_ast.Name, '__data__'),
+ _new(_ast.Str, node.module)
+ ], (), ()))
+ return node
+ if len(self.locals) > 1:
+ self.locals[-1].update(self._extract_names(node))
+ return ASTTransformer.visit_ImportFrom(self, node)
+
+ def visit_FunctionDef(self, node):
+ if len(self.locals) > 1:
+ self.locals[-1].add(node.name)
+
+ self.locals.append(self._extract_names(node.args))
+ try:
+ return ASTTransformer.visit_FunctionDef(self, node)
+ finally:
+ self.locals.pop()
+
+ # GeneratorExp(expr elt, comprehension* generators)
+ def visit_GeneratorExp(self, node):
+ gens = []
+ for generator in node.generators:
+ # comprehension = (expr target, expr iter, expr* ifs)
+ self.locals.append(set())
+ gen = _new(_ast.comprehension, self.visit(generator.target),
+ self.visit(generator.iter),
+ [self.visit(if_) for if_ in generator.ifs])
+ gens.append(gen)
+
+ # use node.__class__ to make it reusable as ListComp
+ ret = _new(node.__class__, self.visit(node.elt), gens)
+ #delete inserted locals
+ del self.locals[-len(node.generators):]
+ return ret
+
+ # ListComp(expr elt, comprehension* generators)
+ visit_ListComp = visit_GeneratorExp
+
+ def visit_Lambda(self, node):
+ self.locals.append(self._extract_names(node.args))
+ try:
+ return ASTTransformer.visit_Lambda(self, node)
+ finally:
+ self.locals.pop()
+
+ def visit_Name(self, node):
+ # If the name refers to a local inside a lambda, list comprehension, or
+ # generator expression, leave it alone
+ if isinstance(node.ctx, _ast.Load) and \
+ node.id not in flatten(self.locals):
+ # Otherwise, translate the name ref into a context lookup
+ name = _new(_ast.Name, '_lookup_name', _ast.Load())
+ namearg = _new(_ast.Name, '__data__', _ast.Load())
+ strarg = _new(_ast.Str, node.id)
+ node = _new(_ast.Call, name, [namearg, strarg], [])
+ elif isinstance(node.ctx, _ast.Store):
+ if len(self.locals) > 1:
+ self.locals[-1].add(node.id)
+
+ return node
+
+
+class ExpressionASTTransformer(TemplateASTTransformer):
+ """Concrete AST transformer that implements the AST transformations needed
+ for code embedded in templates.
+ """
+
+ def visit_Attribute(self, node):
+ if not isinstance(node.ctx, _ast.Load):
+ return ASTTransformer.visit_Attribute(self, node)
+
+ func = _new(_ast.Name, '_lookup_attr', _ast.Load())
+ args = [self.visit(node.value), _new(_ast.Str, node.attr)]
+ return _new(_ast.Call, func, args, [])
+
+ def visit_Subscript(self, node):
+ if not isinstance(node.ctx, _ast.Load) or \
+ not isinstance(node.slice, _ast.Index):
+ return ASTTransformer.visit_Subscript(self, node)
+
+ func = _new(_ast.Name, '_lookup_item', _ast.Load())
+ args = [
+ self.visit(node.value),
+ _new(_ast.Tuple, (self.visit(node.slice.value),), _ast.Load())
+ ]
+ return _new(_ast.Call, func, args, [])
diff --git a/genshi/template/interpolation.py b/genshi/template/interpolation.py
new file mode 100644
index 0000000..1e1a385
--- /dev/null
+++ b/genshi/template/interpolation.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2007-2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""String interpolation routines, i.e. the splitting up a given text into some
+parts that are literal strings, and others that are Python expressions.
+"""
+
+from itertools import chain
+import os
+import re
+from tokenize import PseudoToken
+
+from genshi.core import TEXT
+from genshi.template.base import TemplateSyntaxError, EXPR
+from genshi.template.eval import Expression
+
+__all__ = ['interpolate']
+__docformat__ = 'restructuredtext en'
+
+NAMESTART = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'
+NAMECHARS = NAMESTART + '.0123456789'
+PREFIX = '$'
+
+token_re = re.compile('%s|%s(?s)' % (
+ r'[uU]?[rR]?("""|\'\'\')((?<!\\)\\\1|.)*?\1',
+ PseudoToken
+))
+
+
+def interpolate(text, filepath=None, lineno=-1, offset=0, lookup='strict'):
+ """Parse the given string and extract expressions.
+
+ This function is a generator that yields `TEXT` events for literal strings,
+ and `EXPR` events for expressions, depending on the results of parsing the
+ string.
+
+ >>> for kind, data, pos in interpolate("hey ${foo}bar"):
+ ... print('%s %r' % (kind, data))
+ TEXT 'hey '
+ EXPR Expression('foo')
+ TEXT 'bar'
+
+ :param text: the text to parse
+ :param filepath: absolute path to the file in which the text was found
+ (optional)
+ :param lineno: the line number at which the text was found (optional)
+ :param offset: the column number at which the text starts in the source
+ (optional)
+ :param lookup: the variable lookup mechanism; either "lenient" (the
+ default), "strict", or a custom lookup class
+ :return: a list of `TEXT` and `EXPR` events
+ :raise TemplateSyntaxError: when a syntax error in an expression is
+ encountered
+ """
+ pos = [filepath, lineno, offset]
+
+ textbuf = []
+ textpos = None
+ for is_expr, chunk in chain(lex(text, pos, filepath), [(True, '')]):
+ if is_expr:
+ if textbuf:
+ yield TEXT, ''.join(textbuf), textpos
+ del textbuf[:]
+ textpos = None
+ if chunk:
+ try:
+ expr = Expression(chunk.strip(), pos[0], pos[1],
+ lookup=lookup)
+ yield EXPR, expr, tuple(pos)
+ except SyntaxError, err:
+ raise TemplateSyntaxError(err, filepath, pos[1],
+ pos[2] + (err.offset or 0))
+ else:
+ textbuf.append(chunk)
+ if textpos is None:
+ textpos = tuple(pos)
+
+ if '\n' in chunk:
+ lines = chunk.splitlines()
+ pos[1] += len(lines) - 1
+ pos[2] += len(lines[-1])
+ else:
+ pos[2] += len(chunk)
+
+
+def lex(text, textpos, filepath):
+ offset = pos = 0
+ end = len(text)
+ escaped = False
+
+ while 1:
+ if escaped:
+ offset = text.find(PREFIX, offset + 2)
+ escaped = False
+ else:
+ offset = text.find(PREFIX, pos)
+ if offset < 0 or offset == end - 1:
+ break
+ next = text[offset + 1]
+
+ if next == '{':
+ if offset > pos:
+ yield False, text[pos:offset]
+ pos = offset + 2
+ level = 1
+ while level:
+ match = token_re.match(text, pos)
+ if match is None:
+ raise TemplateSyntaxError('invalid syntax', filepath,
+ *textpos[1:])
+ pos = match.end()
+ tstart, tend = match.regs[3]
+ token = text[tstart:tend]
+ if token == '{':
+ level += 1
+ elif token == '}':
+ level -= 1
+ yield True, text[offset + 2:pos - 1]
+
+ elif next in NAMESTART:
+ if offset > pos:
+ yield False, text[pos:offset]
+ pos = offset
+ pos += 1
+ while pos < end:
+ char = text[pos]
+ if char not in NAMECHARS:
+ break
+ pos += 1
+ yield True, text[offset + 1:pos].strip()
+
+ elif not escaped and next == PREFIX:
+ if offset > pos:
+ yield False, text[pos:offset]
+ escaped = True
+ pos = offset + 1
+
+ else:
+ yield False, text[pos:offset + 1]
+ pos = offset + 1
+
+ if pos < end:
+ yield False, text[pos:]
diff --git a/genshi/template/loader.py b/genshi/template/loader.py
new file mode 100644
index 0000000..0e7cda7
--- /dev/null
+++ b/genshi/template/loader.py
@@ -0,0 +1,344 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2010 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Template loading and caching."""
+
+import os
+try:
+ import threading
+except ImportError:
+ import dummy_threading as threading
+
+from genshi.template.base import TemplateError
+from genshi.util import LRUCache
+
+__all__ = ['TemplateLoader', 'TemplateNotFound', 'directory', 'package',
+ 'prefixed']
+__docformat__ = 'restructuredtext en'
+
+
+class TemplateNotFound(TemplateError):
+ """Exception raised when a specific template file could not be found."""
+
+ def __init__(self, name, search_path):
+ """Create the exception.
+
+ :param name: the filename of the template
+ :param search_path: the search path used to lookup the template
+ """
+ TemplateError.__init__(self, 'Template "%s" not found' % name)
+ self.search_path = search_path
+
+
+class TemplateLoader(object):
+ """Responsible for loading templates from files on the specified search
+ path.
+
+ >>> import tempfile
+ >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template')
+ >>> os.write(fd, '<p>$var</p>')
+ 11
+ >>> os.close(fd)
+
+ The template loader accepts a list of directory paths that are then used
+ when searching for template files, in the given order:
+
+ >>> loader = TemplateLoader([os.path.dirname(path)])
+
+ The `load()` method first checks the template cache whether the requested
+ template has already been loaded. If not, it attempts to locate the
+ template file, and returns the corresponding `Template` object:
+
+ >>> from genshi.template import MarkupTemplate
+ >>> template = loader.load(os.path.basename(path))
+ >>> isinstance(template, MarkupTemplate)
+ True
+
+ Template instances are cached: requesting a template with the same name
+ results in the same instance being returned:
+
+ >>> loader.load(os.path.basename(path)) is template
+ True
+
+ The `auto_reload` option can be used to control whether a template should
+ be automatically reloaded when the file it was loaded from has been
+ changed. Disable this automatic reloading to improve performance.
+
+ >>> os.remove(path)
+ """
+ def __init__(self, search_path=None, auto_reload=False,
+ default_encoding=None, max_cache_size=25, default_class=None,
+ variable_lookup='strict', allow_exec=True, callback=None):
+ """Create the template laoder.
+
+ :param search_path: a list of absolute path names that should be
+ searched for template files, or a string containing
+ a single absolute path; alternatively, any item on
+ the list may be a ''load function'' that is passed
+ a filename and returns a file-like object and some
+ metadata
+ :param auto_reload: whether to check the last modification time of
+ template files, and reload them if they have changed
+ :param default_encoding: the default encoding to assume when loading
+ templates; defaults to UTF-8
+ :param max_cache_size: the maximum number of templates to keep in the
+ cache
+ :param default_class: the default `Template` subclass to use when
+ instantiating templates
+ :param variable_lookup: the variable lookup mechanism; either "strict"
+ (the default), "lenient", or a custom lookup
+ class
+ :param allow_exec: whether to allow Python code blocks in templates
+ :param callback: (optional) a callback function that is invoked after a
+ template was initialized by this loader; the function
+ is passed the template object as only argument. This
+ callback can be used for example to add any desired
+ filters to the template
+ :see: `LenientLookup`, `StrictLookup`
+
+ :note: Changed in 0.5: Added the `allow_exec` argument
+ """
+ from genshi.template.markup import MarkupTemplate
+
+ self.search_path = search_path
+ if self.search_path is None:
+ self.search_path = []
+ elif not isinstance(self.search_path, (list, tuple)):
+ self.search_path = [self.search_path]
+
+ self.auto_reload = auto_reload
+ """Whether templates should be reloaded when the underlying file is
+ changed"""
+
+ self.default_encoding = default_encoding
+ self.default_class = default_class or MarkupTemplate
+ self.variable_lookup = variable_lookup
+ self.allow_exec = allow_exec
+ if callback is not None and not hasattr(callback, '__call__'):
+ raise TypeError('The "callback" parameter needs to be callable')
+ self.callback = callback
+ self._cache = LRUCache(max_cache_size)
+ self._uptodate = {}
+ self._lock = threading.RLock()
+
+ def __getstate__(self):
+ state = self.__dict__.copy()
+ state['_lock'] = None
+ return state
+
+ def __setstate__(self, state):
+ self.__dict__ = state
+ self._lock = threading.RLock()
+
+ def load(self, filename, relative_to=None, cls=None, encoding=None):
+ """Load the template with the given name.
+
+ If the `filename` parameter is relative, this method searches the
+ search path trying to locate a template matching the given name. If the
+ file name is an absolute path, the search path is ignored.
+
+ If the requested template is not found, a `TemplateNotFound` exception
+ is raised. Otherwise, a `Template` object is returned that represents
+ the parsed template.
+
+ Template instances are cached to avoid having to parse the same
+ template file more than once. Thus, subsequent calls of this method
+ with the same template file name will return the same `Template`
+ object (unless the ``auto_reload`` option is enabled and the file was
+ changed since the last parse.)
+
+ If the `relative_to` parameter is provided, the `filename` is
+ interpreted as being relative to that path.
+
+ :param filename: the relative path of the template file to load
+ :param relative_to: the filename of the template from which the new
+ template is being loaded, or ``None`` if the
+ template is being loaded directly
+ :param cls: the class of the template object to instantiate
+ :param encoding: the encoding of the template to load; defaults to the
+ ``default_encoding`` of the loader instance
+ :return: the loaded `Template` instance
+ :raises TemplateNotFound: if a template with the given name could not
+ be found
+ """
+ if cls is None:
+ cls = self.default_class
+ search_path = self.search_path
+
+ # Make the filename relative to the template file its being loaded
+ # from, but only if that file is specified as a relative path, or no
+ # search path has been set up
+ if relative_to and (not search_path or not os.path.isabs(relative_to)):
+ filename = os.path.join(os.path.dirname(relative_to), filename)
+
+ filename = os.path.normpath(filename)
+ cachekey = filename
+
+ self._lock.acquire()
+ try:
+ # First check the cache to avoid reparsing the same file
+ try:
+ tmpl = self._cache[cachekey]
+ if not self.auto_reload:
+ return tmpl
+ uptodate = self._uptodate[cachekey]
+ if uptodate is not None and uptodate():
+ return tmpl
+ except (KeyError, OSError):
+ pass
+
+ isabs = False
+
+ if os.path.isabs(filename):
+ # Bypass the search path if the requested filename is absolute
+ search_path = [os.path.dirname(filename)]
+ isabs = True
+
+ elif relative_to and os.path.isabs(relative_to):
+ # Make sure that the directory containing the including
+ # template is on the search path
+ dirname = os.path.dirname(relative_to)
+ if dirname not in search_path:
+ search_path = list(search_path) + [dirname]
+ isabs = True
+
+ elif not search_path:
+ # Uh oh, don't know where to look for the template
+ raise TemplateError('Search path for templates not configured')
+
+ for loadfunc in search_path:
+ if isinstance(loadfunc, basestring):
+ loadfunc = directory(loadfunc)
+ try:
+ filepath, filename, fileobj, uptodate = loadfunc(filename)
+ except IOError:
+ continue
+ else:
+ try:
+ if isabs:
+ # If the filename of either the included or the
+ # including template is absolute, make sure the
+ # included template gets an absolute path, too,
+ # so that nested includes work properly without a
+ # search path
+ filename = filepath
+ tmpl = self._instantiate(cls, fileobj, filepath,
+ filename, encoding=encoding)
+ if self.callback:
+ self.callback(tmpl)
+ self._cache[cachekey] = tmpl
+ self._uptodate[cachekey] = uptodate
+ finally:
+ if hasattr(fileobj, 'close'):
+ fileobj.close()
+ return tmpl
+
+ raise TemplateNotFound(filename, search_path)
+
+ finally:
+ self._lock.release()
+
+ def _instantiate(self, cls, fileobj, filepath, filename, encoding=None):
+ """Instantiate and return the `Template` object based on the given
+ class and parameters.
+
+ This function is intended for subclasses to override if they need to
+ implement special template instantiation logic. Code that just uses
+ the `TemplateLoader` should use the `load` method instead.
+
+ :param cls: the class of the template object to instantiate
+ :param fileobj: a readable file-like object containing the template
+ source
+ :param filepath: the absolute path to the template file
+ :param filename: the path to the template file relative to the search
+ path
+ :param encoding: the encoding of the template to load; defaults to the
+ ``default_encoding`` of the loader instance
+ :return: the loaded `Template` instance
+ :rtype: `Template`
+ """
+ if encoding is None:
+ encoding = self.default_encoding
+ return cls(fileobj, filepath=filepath, filename=filename, loader=self,
+ encoding=encoding, lookup=self.variable_lookup,
+ allow_exec=self.allow_exec)
+
+ @staticmethod
+ def directory(path):
+ """Loader factory for loading templates from a local directory.
+
+ :param path: the path to the local directory containing the templates
+ :return: the loader function to load templates from the given directory
+ :rtype: ``function``
+ """
+ def _load_from_directory(filename):
+ filepath = os.path.join(path, filename)
+ fileobj = open(filepath, 'U')
+ mtime = os.path.getmtime(filepath)
+ def _uptodate():
+ return mtime == os.path.getmtime(filepath)
+ return filepath, filename, fileobj, _uptodate
+ return _load_from_directory
+
+ @staticmethod
+ def package(name, path):
+ """Loader factory for loading templates from egg package data.
+
+ :param name: the name of the package containing the resources
+ :param path: the path inside the package data
+ :return: the loader function to load templates from the given package
+ :rtype: ``function``
+ """
+ from pkg_resources import resource_stream
+ def _load_from_package(filename):
+ filepath = os.path.join(path, filename)
+ return filepath, filename, resource_stream(name, filepath), None
+ return _load_from_package
+
+ @staticmethod
+ def prefixed(**delegates):
+ """Factory for a load function that delegates to other loaders
+ depending on the prefix of the requested template path.
+
+ The prefix is stripped from the filename when passing on the load
+ request to the delegate.
+
+ >>> load = prefixed(
+ ... app1 = lambda filename: ('app1', filename, None, None),
+ ... app2 = lambda filename: ('app2', filename, None, None)
+ ... )
+ >>> print(load('app1/foo.html'))
+ ('app1', 'app1/foo.html', None, None)
+ >>> print(load('app2/bar.html'))
+ ('app2', 'app2/bar.html', None, None)
+
+ :param delegates: mapping of path prefixes to loader functions
+ :return: the loader function
+ :rtype: ``function``
+ """
+ def _dispatch_by_prefix(filename):
+ for prefix, delegate in delegates.items():
+ if filename.startswith(prefix):
+ if isinstance(delegate, basestring):
+ delegate = directory(delegate)
+ filepath, _, fileobj, uptodate = delegate(
+ filename[len(prefix):].lstrip('/\\')
+ )
+ return filepath, filename, fileobj, uptodate
+ raise TemplateNotFound(filename, list(delegates.keys()))
+ return _dispatch_by_prefix
+
+
+directory = TemplateLoader.directory
+package = TemplateLoader.package
+prefixed = TemplateLoader.prefixed
diff --git a/genshi/template/markup.py b/genshi/template/markup.py
new file mode 100644
index 0000000..0e31632
--- /dev/null
+++ b/genshi/template/markup.py
@@ -0,0 +1,397 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2010 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Markup templating engine."""
+
+from itertools import chain
+
+from genshi.core import Attrs, Markup, Namespace, Stream, StreamEventKind
+from genshi.core import START, END, START_NS, END_NS, TEXT, PI, COMMENT
+from genshi.input import XMLParser
+from genshi.template.base import BadDirectiveError, Template, \
+ TemplateSyntaxError, _apply_directives, \
+ EXEC, INCLUDE, SUB
+from genshi.template.eval import Suite
+from genshi.template.interpolation import interpolate
+from genshi.template.directives import *
+from genshi.template.text import NewTextTemplate
+
+__all__ = ['MarkupTemplate']
+__docformat__ = 'restructuredtext en'
+
+
+class MarkupTemplate(Template):
+ """Implementation of the template language for XML-based templates.
+
+ >>> tmpl = MarkupTemplate('''<ul xmlns:py="http://genshi.edgewall.org/">
+ ... <li py:for="item in items">${item}</li>
+ ... </ul>''')
+ >>> print(tmpl.generate(items=[1, 2, 3]))
+ <ul>
+ <li>1</li><li>2</li><li>3</li>
+ </ul>
+ """
+
+ DIRECTIVE_NAMESPACE = 'http://genshi.edgewall.org/'
+ XINCLUDE_NAMESPACE = 'http://www.w3.org/2001/XInclude'
+
+ directives = [('def', DefDirective),
+ ('match', MatchDirective),
+ ('when', WhenDirective),
+ ('otherwise', OtherwiseDirective),
+ ('for', ForDirective),
+ ('if', IfDirective),
+ ('choose', ChooseDirective),
+ ('with', WithDirective),
+ ('replace', ReplaceDirective),
+ ('content', ContentDirective),
+ ('attrs', AttrsDirective),
+ ('strip', StripDirective)]
+ serializer = 'xml'
+ _number_conv = Markup
+
+ def __init__(self, source, filepath=None, filename=None, loader=None,
+ encoding=None, lookup='strict', allow_exec=True):
+ Template.__init__(self, source, filepath=filepath, filename=filename,
+ loader=loader, encoding=encoding, lookup=lookup,
+ allow_exec=allow_exec)
+ self.add_directives(self.DIRECTIVE_NAMESPACE, self)
+
+ def _init_filters(self):
+ Template._init_filters(self)
+ # Make sure the include filter comes after the match filter
+ self.filters.remove(self._include)
+ self.filters += [self._match, self._include]
+
+ def _parse(self, source, encoding):
+ if not isinstance(source, Stream):
+ source = XMLParser(source, filename=self.filename,
+ encoding=encoding)
+ stream = []
+
+ for kind, data, pos in source:
+
+ if kind is TEXT:
+ for kind, data, pos in interpolate(data, self.filepath, pos[1],
+ pos[2], lookup=self.lookup):
+ stream.append((kind, data, pos))
+
+ elif kind is PI and data[0] == 'python':
+ if not self.allow_exec:
+ raise TemplateSyntaxError('Python code blocks not allowed',
+ self.filepath, *pos[1:])
+ try:
+ suite = Suite(data[1], self.filepath, pos[1],
+ lookup=self.lookup)
+ except SyntaxError, err:
+ raise TemplateSyntaxError(err, self.filepath,
+ pos[1] + (err.lineno or 1) - 1,
+ pos[2] + (err.offset or 0))
+ stream.append((EXEC, suite, pos))
+
+ elif kind is COMMENT:
+ if not data.lstrip().startswith('!'):
+ stream.append((kind, data, pos))
+
+ else:
+ stream.append((kind, data, pos))
+
+ return stream
+
+ def _extract_directives(self, stream, namespace, factory):
+ depth = 0
+ dirmap = {} # temporary mapping of directives to elements
+ new_stream = []
+ ns_prefix = {} # namespace prefixes in use
+
+ for kind, data, pos in stream:
+
+ if kind is START:
+ tag, attrs = data
+ directives = []
+ strip = False
+
+ if tag.namespace == namespace:
+ cls = factory.get_directive(tag.localname)
+ if cls is None:
+ raise BadDirectiveError(tag.localname,
+ self.filepath, pos[1])
+ args = dict([(name.localname, value) for name, value
+ in attrs if not name.namespace])
+ directives.append((factory.get_directive_index(cls), cls,
+ args, ns_prefix.copy(), pos))
+ strip = True
+
+ new_attrs = []
+ for name, value in attrs:
+ if name.namespace == namespace:
+ cls = factory.get_directive(name.localname)
+ if cls is None:
+ raise BadDirectiveError(name.localname,
+ self.filepath, pos[1])
+ if type(value) is list and len(value) == 1:
+ value = value[0][1]
+ directives.append((factory.get_directive_index(cls),
+ cls, value, ns_prefix.copy(), pos))
+ else:
+ new_attrs.append((name, value))
+ new_attrs = Attrs(new_attrs)
+
+ if directives:
+ directives.sort()
+ dirmap[(depth, tag)] = (directives, len(new_stream),
+ strip)
+
+ new_stream.append((kind, (tag, new_attrs), pos))
+ depth += 1
+
+ elif kind is END:
+ depth -= 1
+ new_stream.append((kind, data, pos))
+
+ # If there have have directive attributes with the
+ # corresponding start tag, move the events inbetween into
+ # a "subprogram"
+ if (depth, data) in dirmap:
+ directives, offset, strip = dirmap.pop((depth, data))
+ substream = new_stream[offset:]
+ if strip:
+ substream = substream[1:-1]
+ new_stream[offset:] = [
+ (SUB, (directives, substream), pos)
+ ]
+
+ elif kind is SUB:
+ directives, substream = data
+ substream = self._extract_directives(substream, namespace,
+ factory)
+
+ if len(substream) == 1 and substream[0][0] is SUB:
+ added_directives, substream = substream[0][1]
+ directives += added_directives
+
+ new_stream.append((kind, (directives, substream), pos))
+
+ elif kind is START_NS:
+ # Strip out the namespace declaration for template
+ # directives
+ prefix, uri = data
+ ns_prefix[prefix] = uri
+ if uri != namespace:
+ new_stream.append((kind, data, pos))
+
+ elif kind is END_NS:
+ uri = ns_prefix.pop(data, None)
+ if uri and uri != namespace:
+ new_stream.append((kind, data, pos))
+
+ else:
+ new_stream.append((kind, data, pos))
+
+ return new_stream
+
+ def _extract_includes(self, stream):
+ streams = [[]] # stacked lists of events of the "compiled" template
+ prefixes = {}
+ fallbacks = []
+ includes = []
+ xinclude_ns = Namespace(self.XINCLUDE_NAMESPACE)
+
+ for kind, data, pos in stream:
+ stream = streams[-1]
+
+ if kind is START:
+ # Record any directive attributes in start tags
+ tag, attrs = data
+ if tag in xinclude_ns:
+ if tag.localname == 'include':
+ include_href = attrs.get('href')
+ if not include_href:
+ raise TemplateSyntaxError('Include misses required '
+ 'attribute "href"',
+ self.filepath, *pos[1:])
+ includes.append((include_href, attrs.get('parse')))
+ streams.append([])
+ elif tag.localname == 'fallback':
+ streams.append([])
+ fallbacks.append(streams[-1])
+ else:
+ stream.append((kind, (tag, attrs), pos))
+
+ elif kind is END:
+ if fallbacks and data == xinclude_ns['fallback']:
+ assert streams.pop() is fallbacks[-1]
+ elif data == xinclude_ns['include']:
+ fallback = None
+ if len(fallbacks) == len(includes):
+ fallback = fallbacks.pop()
+ streams.pop() # discard anything between the include tags
+ # and the fallback element
+ stream = streams[-1]
+ href, parse = includes.pop()
+ try:
+ cls = {
+ 'xml': MarkupTemplate,
+ 'text': NewTextTemplate
+ }.get(parse) or self.__class__
+ except KeyError:
+ raise TemplateSyntaxError('Invalid value for "parse" '
+ 'attribute of include',
+ self.filepath, *pos[1:])
+ stream.append((INCLUDE, (href, cls, fallback), pos))
+ else:
+ stream.append((kind, data, pos))
+
+ elif kind is START_NS and data[1] == xinclude_ns:
+ # Strip out the XInclude namespace
+ prefixes[data[0]] = data[1]
+
+ elif kind is END_NS and data in prefixes:
+ prefixes.pop(data)
+
+ else:
+ stream.append((kind, data, pos))
+
+ assert len(streams) == 1
+ return streams[0]
+
+ def _interpolate_attrs(self, stream):
+ for kind, data, pos in stream:
+
+ if kind is START:
+ # Record any directive attributes in start tags
+ tag, attrs = data
+ new_attrs = []
+ for name, value in attrs:
+ if value:
+ value = list(interpolate(value, self.filepath, pos[1],
+ pos[2], lookup=self.lookup))
+ if len(value) == 1 and value[0][0] is TEXT:
+ value = value[0][1]
+ new_attrs.append((name, value))
+ data = tag, Attrs(new_attrs)
+
+ yield kind, data, pos
+
+ def _prepare(self, stream):
+ return Template._prepare(self,
+ self._extract_includes(self._interpolate_attrs(stream))
+ )
+
+ def add_directives(self, namespace, factory):
+ """Register a custom `DirectiveFactory` for a given namespace.
+
+ :param namespace: the namespace URI
+ :type namespace: `basestring`
+ :param factory: the directive factory to register
+ :type factory: `DirectiveFactory`
+ :since: version 0.6
+ """
+ assert not self._prepared, 'Too late for adding directives, ' \
+ 'template already prepared'
+ self._stream = self._extract_directives(self._stream, namespace,
+ factory)
+
+ def _match(self, stream, ctxt, start=0, end=None, **vars):
+ """Internal stream filter that applies any defined match templates
+ to the stream.
+ """
+ match_templates = ctxt._match_templates
+
+ tail = []
+ def _strip(stream, append=tail.append):
+ depth = 1
+ next = stream.next
+ while 1:
+ event = next()
+ if event[0] is START:
+ depth += 1
+ elif event[0] is END:
+ depth -= 1
+ if depth > 0:
+ yield event
+ else:
+ append(event)
+ break
+
+ for event in stream:
+
+ # We (currently) only care about start and end events for matching
+ # We might care about namespace events in the future, though
+ if not match_templates or (event[0] is not START and
+ event[0] is not END):
+ yield event
+ continue
+
+ for idx, (test, path, template, hints, namespaces, directives) \
+ in enumerate(match_templates):
+ if idx < start or end is not None and idx >= end:
+ continue
+
+ if test(event, namespaces, ctxt) is True:
+ if 'match_once' in hints:
+ del match_templates[idx]
+ idx -= 1
+
+ # Let the remaining match templates know about the event so
+ # they get a chance to update their internal state
+ for test in [mt[0] for mt in match_templates[idx + 1:]]:
+ test(event, namespaces, ctxt, updateonly=True)
+
+ # Consume and store all events until an end event
+ # corresponding to this start event is encountered
+ pre_end = idx + 1
+ if 'match_once' not in hints and 'not_recursive' in hints:
+ pre_end -= 1
+ inner = _strip(stream)
+ if pre_end > 0:
+ inner = self._match(inner, ctxt, start=start,
+ end=pre_end, **vars)
+ content = self._include(chain([event], inner, tail), ctxt)
+ if 'not_buffered' not in hints:
+ content = list(content)
+ content = Stream(content)
+
+ # Make the select() function available in the body of the
+ # match template
+ selected = [False]
+ def select(path):
+ selected[0] = True
+ return content.select(path, namespaces, ctxt)
+ vars = dict(select=select)
+
+ # Recursively process the output
+ template = _apply_directives(template, directives, ctxt,
+ vars)
+ for event in self._match(self._flatten(template, ctxt,
+ **vars),
+ ctxt, start=idx + 1, **vars):
+ yield event
+
+ # If the match template did not actually call select to
+ # consume the matched stream, the original events need to
+ # be consumed here or they'll get appended to the output
+ if not selected[0]:
+ for event in content:
+ pass
+
+ # Let the remaining match templates know about the last
+ # event in the matched content, so they can update their
+ # internal state accordingly
+ for test in [mt[0] for mt in match_templates[idx + 1:]]:
+ test(tail[0], namespaces, ctxt, updateonly=True)
+
+ break
+
+ else: # no matches
+ yield event
diff --git a/genshi/template/plugin.py b/genshi/template/plugin.py
new file mode 100644
index 0000000..70d56af
--- /dev/null
+++ b/genshi/template/plugin.py
@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Edgewall Software
+# Copyright (C) 2006 Matthew Good
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Basic support for the template engine plugin API used by TurboGears and
+CherryPy/Buffet.
+"""
+
+from genshi.input import ET, HTML, XML
+from genshi.output import DocType
+from genshi.template.base import Template
+from genshi.template.loader import TemplateLoader
+from genshi.template.markup import MarkupTemplate
+from genshi.template.text import TextTemplate, NewTextTemplate
+
+__all__ = ['ConfigurationError', 'AbstractTemplateEnginePlugin',
+ 'MarkupTemplateEnginePlugin', 'TextTemplateEnginePlugin']
+__docformat__ = 'restructuredtext en'
+
+
+class ConfigurationError(ValueError):
+ """Exception raised when invalid plugin options are encountered."""
+
+
+class AbstractTemplateEnginePlugin(object):
+ """Implementation of the plugin API."""
+
+ template_class = None
+ extension = None
+
+ def __init__(self, extra_vars_func=None, options=None):
+ self.get_extra_vars = extra_vars_func
+ if options is None:
+ options = {}
+ self.options = options
+
+ self.default_encoding = options.get('genshi.default_encoding', 'utf-8')
+ auto_reload = options.get('genshi.auto_reload', '1')
+ if isinstance(auto_reload, basestring):
+ auto_reload = auto_reload.lower() in ('1', 'on', 'yes', 'true')
+ search_path = [p for p in
+ options.get('genshi.search_path', '').split(':') if p]
+ self.use_package_naming = not search_path
+ try:
+ max_cache_size = int(options.get('genshi.max_cache_size', 25))
+ except ValueError:
+ raise ConfigurationError('Invalid value for max_cache_size: "%s"' %
+ options.get('genshi.max_cache_size'))
+
+ loader_callback = options.get('genshi.loader_callback', None)
+ if loader_callback and not hasattr(loader_callback, '__call__'):
+ raise ConfigurationError('loader callback must be a function')
+
+ lookup_errors = options.get('genshi.lookup_errors', 'strict')
+ if lookup_errors not in ('lenient', 'strict'):
+ raise ConfigurationError('Unknown lookup errors mode "%s"' %
+ lookup_errors)
+
+ try:
+ allow_exec = bool(options.get('genshi.allow_exec', True))
+ except ValueError:
+ raise ConfigurationError('Invalid value for allow_exec "%s"' %
+ options.get('genshi.allow_exec'))
+
+ self.loader = TemplateLoader([p for p in search_path if p],
+ auto_reload=auto_reload,
+ max_cache_size=max_cache_size,
+ default_class=self.template_class,
+ variable_lookup=lookup_errors,
+ allow_exec=allow_exec,
+ callback=loader_callback)
+
+ def load_template(self, templatename, template_string=None):
+ """Find a template specified in python 'dot' notation, or load one from
+ a string.
+ """
+ if template_string is not None:
+ return self.template_class(template_string)
+
+ if self.use_package_naming:
+ divider = templatename.rfind('.')
+ if divider >= 0:
+ from pkg_resources import resource_filename
+ package = templatename[:divider]
+ basename = templatename[divider + 1:] + self.extension
+ templatename = resource_filename(package, basename)
+
+ return self.loader.load(templatename)
+
+ def _get_render_options(self, format=None, fragment=False):
+ if format is None:
+ format = self.default_format
+ kwargs = {'method': format}
+ if self.default_encoding:
+ kwargs['encoding'] = self.default_encoding
+ return kwargs
+
+ def render(self, info, format=None, fragment=False, template=None):
+ """Render the template to a string using the provided info."""
+ kwargs = self._get_render_options(format=format, fragment=fragment)
+ return self.transform(info, template).render(**kwargs)
+
+ def transform(self, info, template):
+ """Render the output to an event stream."""
+ if not isinstance(template, Template):
+ template = self.load_template(template)
+ return template.generate(**info)
+
+
+class MarkupTemplateEnginePlugin(AbstractTemplateEnginePlugin):
+ """Implementation of the plugin API for markup templates."""
+
+ template_class = MarkupTemplate
+ extension = '.html'
+
+ def __init__(self, extra_vars_func=None, options=None):
+ AbstractTemplateEnginePlugin.__init__(self, extra_vars_func, options)
+
+ default_doctype = self.options.get('genshi.default_doctype')
+ if default_doctype:
+ doctype = DocType.get(default_doctype)
+ if doctype is None:
+ raise ConfigurationError('Unknown doctype %r' % default_doctype)
+ self.default_doctype = doctype
+ else:
+ self.default_doctype = None
+
+ format = self.options.get('genshi.default_format', 'html').lower()
+ if format not in ('html', 'xhtml', 'xml', 'text'):
+ raise ConfigurationError('Unknown output format %r' % format)
+ self.default_format = format
+
+ def _get_render_options(self, format=None, fragment=False):
+ kwargs = super(MarkupTemplateEnginePlugin,
+ self)._get_render_options(format, fragment)
+ if self.default_doctype and not fragment:
+ kwargs['doctype'] = self.default_doctype
+ return kwargs
+
+ def transform(self, info, template):
+ """Render the output to an event stream."""
+ data = {'ET': ET, 'HTML': HTML, 'XML': XML}
+ if self.get_extra_vars:
+ data.update(self.get_extra_vars())
+ data.update(info)
+ return super(MarkupTemplateEnginePlugin, self).transform(data, template)
+
+
+class TextTemplateEnginePlugin(AbstractTemplateEnginePlugin):
+ """Implementation of the plugin API for text templates."""
+
+ template_class = TextTemplate
+ extension = '.txt'
+ default_format = 'text'
+
+ def __init__(self, extra_vars_func=None, options=None):
+ if options is None:
+ options = {}
+
+ new_syntax = options.get('genshi.new_text_syntax')
+ if isinstance(new_syntax, basestring):
+ new_syntax = new_syntax.lower() in ('1', 'on', 'yes', 'true')
+ if new_syntax:
+ self.template_class = NewTextTemplate
+
+ AbstractTemplateEnginePlugin.__init__(self, extra_vars_func, options)
diff --git a/genshi/template/text.py b/genshi/template/text.py
new file mode 100644
index 0000000..746226c
--- /dev/null
+++ b/genshi/template/text.py
@@ -0,0 +1,333 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Plain text templating engine.
+
+This module implements two template language syntaxes, at least for a certain
+transitional period. `OldTextTemplate` (aliased to just `TextTemplate`) defines
+a syntax that was inspired by Cheetah/Velocity. `NewTextTemplate` on the other
+hand is inspired by the syntax of the Django template language, which has more
+explicit delimiting of directives, and is more flexible with regards to
+white space and line breaks.
+
+In a future release, `OldTextTemplate` will be phased out in favor of
+`NewTextTemplate`, as the names imply. Therefore the new syntax is strongly
+recommended for new projects, and existing projects may want to migrate to the
+new syntax to remain compatible with future Genshi releases.
+"""
+
+import re
+
+from genshi.core import TEXT
+from genshi.template.base import BadDirectiveError, Template, \
+ TemplateSyntaxError, EXEC, INCLUDE, SUB
+from genshi.template.eval import Suite
+from genshi.template.directives import *
+from genshi.template.directives import Directive
+from genshi.template.interpolation import interpolate
+
+__all__ = ['NewTextTemplate', 'OldTextTemplate', 'TextTemplate']
+__docformat__ = 'restructuredtext en'
+
+
+class NewTextTemplate(Template):
+ r"""Implementation of a simple text-based template engine. This class will
+ replace `OldTextTemplate` in a future release.
+
+ It uses a more explicit delimiting style for directives: instead of the old
+ style which required putting directives on separate lines that were prefixed
+ with a ``#`` sign, directives and commenbtsr are enclosed in delimiter pairs
+ (by default ``{% ... %}`` and ``{# ... #}``, respectively).
+
+ Variable substitution uses the same interpolation syntax as for markup
+ languages: simple references are prefixed with a dollar sign, more complex
+ expression enclosed in curly braces.
+
+ >>> tmpl = NewTextTemplate('''Dear $name,
+ ...
+ ... {# This is a comment #}
+ ... We have the following items for you:
+ ... {% for item in items %}
+ ... * ${'Item %d' % item}
+ ... {% end %}
+ ... ''')
+ >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None))
+ Dear Joe,
+ <BLANKLINE>
+ <BLANKLINE>
+ We have the following items for you:
+ <BLANKLINE>
+ * Item 1
+ <BLANKLINE>
+ * Item 2
+ <BLANKLINE>
+ * Item 3
+ <BLANKLINE>
+ <BLANKLINE>
+
+ By default, no spaces or line breaks are removed. If a line break should
+ not be included in the output, prefix it with a backslash:
+
+ >>> tmpl = NewTextTemplate('''Dear $name,
+ ...
+ ... {# This is a comment #}\
+ ... We have the following items for you:
+ ... {% for item in items %}\
+ ... * $item
+ ... {% end %}\
+ ... ''')
+ >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None))
+ Dear Joe,
+ <BLANKLINE>
+ We have the following items for you:
+ * 1
+ * 2
+ * 3
+ <BLANKLINE>
+
+ Backslashes are also used to escape the start delimiter of directives and
+ comments:
+
+ >>> tmpl = NewTextTemplate('''Dear $name,
+ ...
+ ... \{# This is a comment #}
+ ... We have the following items for you:
+ ... {% for item in items %}\
+ ... * $item
+ ... {% end %}\
+ ... ''')
+ >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None))
+ Dear Joe,
+ <BLANKLINE>
+ {# This is a comment #}
+ We have the following items for you:
+ * 1
+ * 2
+ * 3
+ <BLANKLINE>
+
+ :since: version 0.5
+ """
+ directives = [('def', DefDirective),
+ ('when', WhenDirective),
+ ('otherwise', OtherwiseDirective),
+ ('for', ForDirective),
+ ('if', IfDirective),
+ ('choose', ChooseDirective),
+ ('with', WithDirective)]
+ serializer = 'text'
+
+ _DIRECTIVE_RE = r'((?<!\\)%s\s*(\w+)\s*(.*?)\s*%s|(?<!\\)%s.*?%s)'
+ _ESCAPE_RE = r'\\\n|\\(\\)|\\(%s)|\\(%s)'
+
+ def __init__(self, source, filepath=None, filename=None, loader=None,
+ encoding=None, lookup='strict', allow_exec=False,
+ delims=('{%', '%}', '{#', '#}')):
+ self.delimiters = delims
+ Template.__init__(self, source, filepath=filepath, filename=filename,
+ loader=loader, encoding=encoding, lookup=lookup)
+
+ def _get_delims(self):
+ return self._delims
+ def _set_delims(self, delims):
+ if len(delims) != 4:
+ raise ValueError('delimiers tuple must have exactly four elements')
+ self._delims = delims
+ self._directive_re = re.compile(self._DIRECTIVE_RE % tuple(
+ [re.escape(d) for d in delims]
+ ), re.DOTALL)
+ self._escape_re = re.compile(self._ESCAPE_RE % tuple(
+ [re.escape(d) for d in delims[::2]]
+ ))
+ delimiters = property(_get_delims, _set_delims, """\
+ The delimiters for directives and comments. This should be a four item tuple
+ of the form ``(directive_start, directive_end, comment_start,
+ comment_end)``, where each item is a string.
+ """)
+
+ def _parse(self, source, encoding):
+ """Parse the template from text input."""
+ stream = [] # list of events of the "compiled" template
+ dirmap = {} # temporary mapping of directives to elements
+ depth = 0
+
+ source = source.read()
+ if isinstance(source, str):
+ source = source.decode(encoding or 'utf-8', 'replace')
+ offset = 0
+ lineno = 1
+
+ _escape_sub = self._escape_re.sub
+ def _escape_repl(mo):
+ groups = [g for g in mo.groups() if g]
+ if not groups:
+ return ''
+ return groups[0]
+
+ for idx, mo in enumerate(self._directive_re.finditer(source)):
+ start, end = mo.span(1)
+ if start > offset:
+ text = _escape_sub(_escape_repl, source[offset:start])
+ for kind, data, pos in interpolate(text, self.filepath, lineno,
+ lookup=self.lookup):
+ stream.append((kind, data, pos))
+ lineno += len(text.splitlines())
+
+ lineno += len(source[start:end].splitlines())
+ command, value = mo.group(2, 3)
+
+ if command == 'include':
+ pos = (self.filename, lineno, 0)
+ value = list(interpolate(value, self.filepath, lineno, 0,
+ lookup=self.lookup))
+ if len(value) == 1 and value[0][0] is TEXT:
+ value = value[0][1]
+ stream.append((INCLUDE, (value, None, []), pos))
+
+ elif command == 'python':
+ if not self.allow_exec:
+ raise TemplateSyntaxError('Python code blocks not allowed',
+ self.filepath, lineno)
+ try:
+ suite = Suite(value, self.filepath, lineno,
+ lookup=self.lookup)
+ except SyntaxError, err:
+ raise TemplateSyntaxError(err, self.filepath,
+ lineno + (err.lineno or 1) - 1)
+ pos = (self.filename, lineno, 0)
+ stream.append((EXEC, suite, pos))
+
+ elif command == 'end':
+ depth -= 1
+ if depth in dirmap:
+ directive, start_offset = dirmap.pop(depth)
+ substream = stream[start_offset:]
+ stream[start_offset:] = [(SUB, ([directive], substream),
+ (self.filepath, lineno, 0))]
+
+ elif command:
+ cls = self.get_directive(command)
+ if cls is None:
+ raise BadDirectiveError(command)
+ directive = 0, cls, value, None, (self.filepath, lineno, 0)
+ dirmap[depth] = (directive, len(stream))
+ depth += 1
+
+ offset = end
+
+ if offset < len(source):
+ text = _escape_sub(_escape_repl, source[offset:])
+ for kind, data, pos in interpolate(text, self.filepath, lineno,
+ lookup=self.lookup):
+ stream.append((kind, data, pos))
+
+ return stream
+
+
+class OldTextTemplate(Template):
+ """Legacy implementation of the old syntax text-based templates. This class
+ is provided in a transition phase for backwards compatibility. New code
+ should use the `NewTextTemplate` class and the improved syntax it provides.
+
+ >>> tmpl = OldTextTemplate('''Dear $name,
+ ...
+ ... We have the following items for you:
+ ... #for item in items
+ ... * $item
+ ... #end
+ ...
+ ... All the best,
+ ... Foobar''')
+ >>> print(tmpl.generate(name='Joe', items=[1, 2, 3]).render(encoding=None))
+ Dear Joe,
+ <BLANKLINE>
+ We have the following items for you:
+ * 1
+ * 2
+ * 3
+ <BLANKLINE>
+ All the best,
+ Foobar
+ """
+ directives = [('def', DefDirective),
+ ('when', WhenDirective),
+ ('otherwise', OtherwiseDirective),
+ ('for', ForDirective),
+ ('if', IfDirective),
+ ('choose', ChooseDirective),
+ ('with', WithDirective)]
+ serializer = 'text'
+
+ _DIRECTIVE_RE = re.compile(r'(?:^[ \t]*(?<!\\)#(end).*\n?)|'
+ r'(?:^[ \t]*(?<!\\)#((?:\w+|#).*)\n?)',
+ re.MULTILINE)
+
+ def _parse(self, source, encoding):
+ """Parse the template from text input."""
+ stream = [] # list of events of the "compiled" template
+ dirmap = {} # temporary mapping of directives to elements
+ depth = 0
+
+ source = source.read()
+ if isinstance(source, str):
+ source = source.decode(encoding or 'utf-8', 'replace')
+ offset = 0
+ lineno = 1
+
+ for idx, mo in enumerate(self._DIRECTIVE_RE.finditer(source)):
+ start, end = mo.span()
+ if start > offset:
+ text = source[offset:start]
+ for kind, data, pos in interpolate(text, self.filepath, lineno,
+ lookup=self.lookup):
+ stream.append((kind, data, pos))
+ lineno += len(text.splitlines())
+
+ text = source[start:end].lstrip()[1:]
+ lineno += len(text.splitlines())
+ directive = text.split(None, 1)
+ if len(directive) > 1:
+ command, value = directive
+ else:
+ command, value = directive[0], None
+
+ if command == 'end':
+ depth -= 1
+ if depth in dirmap:
+ directive, start_offset = dirmap.pop(depth)
+ substream = stream[start_offset:]
+ stream[start_offset:] = [(SUB, ([directive], substream),
+ (self.filepath, lineno, 0))]
+ elif command == 'include':
+ pos = (self.filename, lineno, 0)
+ stream.append((INCLUDE, (value.strip(), None, []), pos))
+ elif command != '#':
+ cls = self.get_directive(command)
+ if cls is None:
+ raise BadDirectiveError(command)
+ directive = 0, cls, value, None, (self.filepath, lineno, 0)
+ dirmap[depth] = (directive, len(stream))
+ depth += 1
+
+ offset = end
+
+ if offset < len(source):
+ text = source[offset:].replace('\\#', '#')
+ for kind, data, pos in interpolate(text, self.filepath, lineno,
+ lookup=self.lookup):
+ stream.append((kind, data, pos))
+
+ return stream
+
+
+TextTemplate = OldTextTemplate
diff --git a/genshi/util.py b/genshi/util.py
new file mode 100644
index 0000000..b964a01
--- /dev/null
+++ b/genshi/util.py
@@ -0,0 +1,274 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006-2009 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://genshi.edgewall.org/wiki/License.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://genshi.edgewall.org/log/.
+
+"""Various utility classes and functions."""
+
+import htmlentitydefs as entities
+import re
+
+__docformat__ = 'restructuredtext en'
+
+
+class LRUCache(dict):
+ """A dictionary-like object that stores only a certain number of items, and
+ discards its least recently used item when full.
+
+ >>> cache = LRUCache(3)
+ >>> cache['A'] = 0
+ >>> cache['B'] = 1
+ >>> cache['C'] = 2
+ >>> len(cache)
+ 3
+
+ >>> cache['A']
+ 0
+
+ Adding new items to the cache does not increase its size. Instead, the least
+ recently used item is dropped:
+
+ >>> cache['D'] = 3
+ >>> len(cache)
+ 3
+ >>> 'B' in cache
+ False
+
+ Iterating over the cache returns the keys, starting with the most recently
+ used:
+
+ >>> for key in cache:
+ ... print(key)
+ D
+ A
+ C
+
+ This code is based on the LRUCache class from ``myghtyutils.util``, written
+ by Mike Bayer and released under the MIT license. See:
+
+ http://svn.myghty.org/myghtyutils/trunk/lib/myghtyutils/util.py
+ """
+
+ class _Item(object):
+ def __init__(self, key, value):
+ self.prv = self.nxt = None
+ self.key = key
+ self.value = value
+ def __repr__(self):
+ return repr(self.value)
+
+ def __init__(self, capacity):
+ self._dict = dict()
+ self.capacity = capacity
+ self.head = None
+ self.tail = None
+
+ def __contains__(self, key):
+ return key in self._dict
+
+ def __iter__(self):
+ cur = self.head
+ while cur:
+ yield cur.key
+ cur = cur.nxt
+
+ def __len__(self):
+ return len(self._dict)
+
+ def __getitem__(self, key):
+ item = self._dict[key]
+ self._update_item(item)
+ return item.value
+
+ def __setitem__(self, key, value):
+ item = self._dict.get(key)
+ if item is None:
+ item = self._Item(key, value)
+ self._dict[key] = item
+ self._insert_item(item)
+ else:
+ item.value = value
+ self._update_item(item)
+ self._manage_size()
+
+ def __repr__(self):
+ return repr(self._dict)
+
+ def _insert_item(self, item):
+ item.prv = None
+ item.nxt = self.head
+ if self.head is not None:
+ self.head.prv = item
+ else:
+ self.tail = item
+ self.head = item
+ self._manage_size()
+
+ def _manage_size(self):
+ while len(self._dict) > self.capacity:
+ olditem = self._dict[self.tail.key]
+ del self._dict[self.tail.key]
+ if self.tail != self.head:
+ self.tail = self.tail.prv
+ self.tail.nxt = None
+ else:
+ self.head = self.tail = None
+
+ def _update_item(self, item):
+ if self.head == item:
+ return
+
+ prv = item.prv
+ prv.nxt = item.nxt
+ if item.nxt is not None:
+ item.nxt.prv = prv
+ else:
+ self.tail = prv
+
+ item.prv = None
+ item.nxt = self.head
+ self.head.prv = self.head = item
+
+
+def flatten(items):
+ """Flattens a potentially nested sequence into a flat list.
+
+ :param items: the sequence to flatten
+
+ >>> flatten((1, 2))
+ [1, 2]
+ >>> flatten([1, (2, 3), 4])
+ [1, 2, 3, 4]
+ >>> flatten([1, (2, [3, 4]), 5])
+ [1, 2, 3, 4, 5]
+ """
+ retval = []
+ for item in items:
+ if isinstance(item, (frozenset, list, set, tuple)):
+ retval += flatten(item)
+ else:
+ retval.append(item)
+ return retval
+
+
+def plaintext(text, keeplinebreaks=True):
+ """Return the text with all entities and tags removed.
+
+ >>> plaintext('<b>1 &lt; 2</b>')
+ u'1 < 2'
+
+ The `keeplinebreaks` parameter can be set to ``False`` to replace any line
+ breaks by simple spaces:
+
+ >>> plaintext('''<b>1
+ ... &lt;
+ ... 2</b>''', keeplinebreaks=False)
+ u'1 < 2'
+
+ :param text: the text to convert to plain text
+ :param keeplinebreaks: whether line breaks in the text should be kept intact
+ :return: the text with tags and entities removed
+ """
+ text = stripentities(striptags(text))
+ if not keeplinebreaks:
+ text = text.replace('\n', ' ')
+ return text
+
+
+_STRIPENTITIES_RE = re.compile(r'&(?:#((?:\d+)|(?:[xX][0-9a-fA-F]+));?|(\w+);)')
+def stripentities(text, keepxmlentities=False):
+ """Return a copy of the given text with any character or numeric entities
+ replaced by the equivalent UTF-8 characters.
+
+ >>> stripentities('1 &lt; 2')
+ u'1 < 2'
+ >>> stripentities('more &hellip;')
+ u'more \u2026'
+ >>> stripentities('&#8230;')
+ u'\u2026'
+ >>> stripentities('&#x2026;')
+ u'\u2026'
+
+ If the `keepxmlentities` parameter is provided and is a truth value, the
+ core XML entities (&amp;, &apos;, &gt;, &lt; and &quot;) are left intact.
+
+ >>> stripentities('1 &lt; 2 &hellip;', keepxmlentities=True)
+ u'1 &lt; 2 \u2026'
+ """
+ def _replace_entity(match):
+ if match.group(1): # numeric entity
+ ref = match.group(1)
+ if ref.startswith('x'):
+ ref = int(ref[1:], 16)
+ else:
+ ref = int(ref, 10)
+ return unichr(ref)
+ else: # character entity
+ ref = match.group(2)
+ if keepxmlentities and ref in ('amp', 'apos', 'gt', 'lt', 'quot'):
+ return '&%s;' % ref
+ try:
+ return unichr(entities.name2codepoint[ref])
+ except KeyError:
+ if keepxmlentities:
+ return '&amp;%s;' % ref
+ else:
+ return ref
+ return _STRIPENTITIES_RE.sub(_replace_entity, text)
+
+
+_STRIPTAGS_RE = re.compile(r'(<!--.*?-->|<[^>]*>)')
+def striptags(text):
+ """Return a copy of the text with any XML/HTML tags removed.
+
+ >>> striptags('<span>Foo</span> bar')
+ 'Foo bar'
+ >>> striptags('<span class="bar">Foo</span>')
+ 'Foo'
+ >>> striptags('Foo<br />')
+ 'Foo'
+
+ HTML/XML comments are stripped, too:
+
+ >>> striptags('<!-- <blub>hehe</blah> -->test')
+ 'test'
+
+ :param text: the string to remove tags from
+ :return: the text with tags removed
+ """
+ return _STRIPTAGS_RE.sub('', text)
+
+
+def stringrepr(string):
+ ascii = string.encode('ascii', 'backslashreplace')
+ quoted = "'" + ascii.replace("'", "\\'") + "'"
+ if len(ascii) > len(string):
+ return 'u' + quoted
+ return quoted
+
+
+# Compatibility fallback implementations for older Python versions
+
+try:
+ all = all
+ any = any
+except NameError:
+ def any(S):
+ for x in S:
+ if x:
+ return True
+ return False
+
+ def all(S):
+ for x in S:
+ if not x:
+ return False
+ return True
diff --git a/icons/activity-debug.svg b/icons/activity-debug.svg
new file mode 100644
index 0000000..32ff193
--- /dev/null
+++ b/icons/activity-debug.svg
@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.0"
+ width="45"
+ height="45"
+ id="svg2">
+ <defs
+ id="defs4" />
+ ` <g
+ transform="matrix(0.81818179,0,0,0.81818179,2.9325954e-7,1.8913349e-6)"
+ id="g2941">
+ <g
+ transform="matrix(1.5,0,0,1.5,-13.078338,-13.158384)"
+ id="g2418">
+ <path
+ d="m 27.377856,20.343519 c 0.943144,-1.32305 2.909066,-2.23143 3.443893,-2.052418 0.816335,0.273235 2.634387,1.285134 2.828305,1.870614 0.363444,1.097313 -0.440961,2.212871 -0.661443,3.319307"
+ id="path3252"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ d="m 31.312226,33.70644 c -3.075459,5.326852 -8.151812,8.153707 -11.338352,6.313958 -3.18654,-1.83975 -3.276587,-7.649428 -0.201128,-12.97628 3.07546,-5.326852 8.151812,-8.153707 11.338352,-6.313958 3.18654,1.83975 3.276588,7.649428 0.201128,12.97628 l 0,0 z"
+ id="path3197"
+ style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:1.51091182;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 30.640788,20.96543 c -3.356038,6.044559 -3.301174,5.949533 -3.301174,5.949533"
+ id="path3199"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ d="m 23.640267,22.485322 c 0.955579,1.892858 2.334494,3.24269 3.831245,4.497123 0.02734,0.02291 3.988592,1.409612 5.914011,1.031489"
+ id="path3201"
+ style="fill:none;stroke:#ffffff;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1" />
+ <path
+ d="m 22.8141,25.484531 c 2.380046,2.460708 2.406938,2.476234 2.406938,2.476234"
+ id="path3203"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 21.785315,26.831706 c 2.380046,2.460709 2.551072,2.661318 2.551072,2.661318"
+ id="path3207"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 20.834693,28.29192 c 2.380046,2.460708 2.617044,2.733363 2.617044,2.733363"
+ id="path3209"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 20.016015,29.896225 c 2.380045,2.460709 2.551071,2.661318 2.551071,2.661318"
+ id="path3211"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 19.370877,31.63468 c 2.380046,2.460709 2.311559,2.455123 2.311559,2.455123"
+ id="path3213"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 19.111902,33.697954 c 2.252745,2.556991 1.685883,1.92411 1.685883,1.92411"
+ id="path3215"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 19.133652,36.330772 c 0.569377,0.566421 0.779483,0.823549 0.779483,0.823549"
+ id="path3217"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="M 30.786085,30.087159 C 27.465026,29.256333 27.438134,29.240807 27.438134,29.240807"
+ id="path3230"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="M 30.133789,31.6517 C 26.81273,30.820874 26.553484,30.773066 26.553484,30.773066"
+ id="path3232"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="M 29.344518,33.20507 C 26.023459,32.374244 25.668834,32.305325 25.668834,32.305325"
+ id="path3234"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="M 28.364488,34.716219 C 25.043429,33.885393 24.784183,33.837585 24.784183,33.837585"
+ id="path3236"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="M 27.18151,36.144152 C 23.860451,35.313326 23.899532,35.369845 23.899532,35.369845"
+ id="path3238"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 25.524151,37.400068 c -3.340792,-0.672439 -2.50927,-0.497963 -2.50927,-0.497963"
+ id="path3240"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="M 23.233189,38.697641 C 22.457965,38.487756 22.130232,38.434363 22.130232,38.434363"
+ id="path3242"
+ style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 38.499043,20.363658 c -0.698249,1.209401 -2.299567,1.592097 -3.576644,0.854776 -1.277077,-0.73732 -1.746312,-2.315451 -1.048064,-3.524852 0.698249,-1.209402 2.299567,-1.592098 3.576644,-0.854777 1.277077,0.73732 1.746312,2.315451 1.048064,3.524853 z"
+ id="path3244"
+ style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.84691685;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 36.397626,17.843099 c -0.197275,0.341691 -0.649694,0.449815 -1.010506,0.2415 -0.360812,-0.208315 -0.493384,-0.654183 -0.296109,-0.995875 0.197276,-0.341691 0.649695,-0.449814 1.010507,-0.241499 0.360812,0.208315 0.493384,0.654183 0.296108,0.995874 z"
+ id="path3246"
+ style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.79813904;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 32.748117,16.754734 c -0.698248,1.209401 -2.299567,1.592097 -3.576644,0.854776 -1.277077,-0.73732 -1.746311,-2.315451 -1.048063,-3.524852 0.698249,-1.209402 2.299567,-1.592098 3.576644,-0.854777 1.277077,0.73732 1.746312,2.315451 1.048063,3.524853 z"
+ id="path3248"
+ style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.84691685;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 30.781161,14.311805 c -0.197275,0.341691 -0.649694,0.449815 -1.010506,0.2415 -0.360813,-0.208315 -0.493385,-0.654183 -0.296109,-0.995875 0.197275,-0.341691 0.649695,-0.449814 1.010507,-0.241499 0.360812,0.208315 0.493384,0.654183 0.296108,0.995874 z"
+ id="path3250"
+ style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.79813904;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 32.6771,18.762402 c 1.915624,-3.421466 5.072352,-4.541771 5.072352,-4.541771 l 0,0"
+ id="path3254"
+ style="fill:none;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="m 32.234792,18.337256 c 2.005265,-3.369712 1.397113,-6.663671 1.397113,-6.663671 l 0,0"
+ id="path3258"
+ style="fill:none;stroke:#ffffff;stroke-width:0.75545591;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="M 26.988213,20.231746 L 25.8722,15.229742 22.062204,17.274526"
+ id="path3260"
+ style="fill:none;stroke:#ffffff;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ d="m 33.361036,23.684723 l 3.584614,-0.929858 2.882681,3.70167"
+ id="path3262"
+ style="fill:none;stroke:#ffffff;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ d="m 23.851158,22.042526 l -1.097622,-3.067217 -8.164934,2.756268"
+ id="path3264"
+ style="fill:none;stroke:#ffffff;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ d="m 33.777334,27.207476 l 3.47402,-0.42778 0.497902,7.4182"
+ id="path3266"
+ style="fill:none;stroke:#ffffff;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ d="M 20.388485,25.668558 L 19.349188,23.00868 12.485021,29.515384"
+ id="path3268"
+ style="fill:none;stroke:#ffffff;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ <path
+ d="m 32.410841,31.851473 l 2.920057,-0.91739 -2.027798,9.412169"
+ id="path3270"
+ style="fill:none;stroke:#ffffff;stroke-width:1.51091182px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ </g>
+ </g>
+</svg>
diff --git a/icons/document-generic.svg b/icons/document-generic.svg
new file mode 100644
index 0000000..a724766
--- /dev/null
+++ b/icons/document-generic.svg
@@ -0,0 +1,25 @@
+<?xml version="1.0" ?>
+<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN'
+ 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+ ]>
+
+<svg enable-background="new 0 0 55 55" height="55px" version="1.1"
+ viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink" y="0px">
+
+ <g display="block" id="activity-write">
+ <g display="inline">
+ <g>
+ <path
+ d="M 43.82,6.088 L 22.876,6.088 L 10.932,18.027 L
+ 10.932,48.914 L 43.819,48.914 L 43.82,6.088 z"
+ fill="&fill_color;" stroke="&stroke_color;" stroke-width="3.5" />
+ </g>
+ <polyline fill="none" points="10.932,18.027 22.876,18.027 22.876,6.088"
+ stroke="&stroke_color;" stroke-width="3.5"/>
+ </g>
+ </g>
+</svg>
diff --git a/icons/folder.svg b/icons/folder.svg
new file mode 100644
index 0000000..c3f6dc8
--- /dev/null
+++ b/icons/folder.svg
@@ -0,0 +1,149 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ height="60"
+ width="60"
+ _SVGFile__filename="oldscale/filesystems/folder.svg"
+ version="1.0"
+ y="0"
+ x="0"
+ id="svg1"
+ sodipodi:version="0.32"
+ inkscape:version="0.48.1 r9760"
+ sodipodi:docname="folder.svg">
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#494949"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="8.7000021"
+ inkscape:cx="21.97746"
+ inkscape:cy="29.627911"
+ inkscape:window-width="1024"
+ inkscape:window-height="548"
+ inkscape:window-x="0"
+ inkscape:window-y="28"
+ inkscape:current-layer="svg1"
+ showgrid="false"
+ inkscape:window-maximized="1" />
+ <defs
+ id="defs3">
+ <linearGradient
+ id="linearGradient900">
+ <stop
+ style="stop-color: #000000;stop-opacity: 1.0;"
+ id="stop901"
+ offset="0" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ id="stop902"
+ offset="1" />
+ </linearGradient>
+ <linearGradient
+ xlink:href="#linearGradient900"
+ id="linearGradient903" />
+ <linearGradient
+ xlink:href="#linearGradient900"
+ id="linearGradient904" />
+ <linearGradient
+ xlink:href="#linearGradient900"
+ id="linearGradient905" />
+ <linearGradient
+ xlink:href="#linearGradient900"
+ id="linearGradient906" />
+ <linearGradient
+ xlink:href="#linearGradient900"
+ id="linearGradient907" />
+ </defs>
+ <path
+ style="font-size:12.000000;fill:#ffffff;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#000000;stroke-width:3.2209184;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1.0000000;"
+ d="M 8.2323190,8.4866780 C 7.7060710,8.4866780 7.2635690,8.9291800 7.2635690,9.4554280 L 7.2323190,51.392930 C 7.2323190,52.276720 7.9422750,52.986680 8.8260690,52.986680 L 46.107320,52.986680 C 46.991110,52.986680 47.701070,52.276720 47.701070,51.392930 L 47.701070,14.392930 C 47.701070,13.509130 46.991110,12.799180 46.107320,12.799180 L 31.388570,12.799180 L 31.388570,9.4554280 C 31.388570,8.9291800 30.946070,8.4866780 30.419820,8.4866780 L 8.2323190,8.4866780 z "
+ transform="matrix(0.970220,0.000000,0.000000,0.970220,0.519335,0.150613)"
+ id="path938" />
+ <path
+ style="font-size:12.000000;fill:#ffffff;fill-opacity:1.0000000;fill-rule:evenodd;stroke:#000000;stroke-width:3.3344083;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1.0000000;"
+ d="M 26.627560,23.398020 C 25.743760,23.398020 25.032260,24.109520 25.032260,24.993320 L 25.032260,55.253090 C 25.032260,56.136880 25.743760,56.848390 26.627560,56.848390 L 63.913400,56.848390 C 64.797190,56.848390 65.508690,56.136880 65.508690,55.253090 L 65.508690,24.993320 C 65.508690,24.109520 64.797190,23.398020 63.913400,23.398020 L 26.627560,23.398020 z "
+ id="rect922"
+ transform="matrix(0.970220,0.000000,-0.163982,0.905300,-7.018060,0.150613)" />
+ <metadata
+ id="metadata15">
+ <RDF
+ id="RDF16">
+ <Work
+ about=""
+ id="Work17">
+ <title
+ id="title18">Part of the Flat Icon Collection (Thu Aug 26 14:40:13 2004)</title>
+ <description
+ id="description20" />
+ <subject
+ id="subject21">
+ <Bag
+ id="Bag22">
+ <li
+ id="li23" />
+ </Bag>
+ </subject>
+ <publisher
+ id="publisher24">
+ <Agent
+ about=""
+ id="Agent25">
+ <title
+ id="title26" />
+ </Agent>
+ </publisher>
+ <creator
+ id="creator27">
+ <Agent
+ about=""
+ id="Agent28">
+ <title
+ id="title29">Danny Allen</title>
+ </Agent>
+ </creator>
+ <rights
+ id="rights31">
+ <Agent
+ about=""
+ id="Agent32">
+ <title
+ id="title33">Danny Allen</title>
+ </Agent>
+ </rights>
+ <date
+ id="date35" />
+ <format
+ id="format36">image/svg+xml</format>
+ <type
+ id="type38"
+ resource="http://purl.org/dc/dcmitype/StillImage" />
+ <license
+ id="license39"
+ resource="http://creativecommons.org/licenses/LGPL/2.1/">
+ <date
+ id="date40" />
+ </license>
+ <language
+ id="language41">en</language>
+ </Work>
+ </RDF>
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+</svg>
diff --git a/icons/image-x-generic.svg b/icons/image-x-generic.svg
new file mode 100644
index 0000000..ac387f2
--- /dev/null
+++ b/icons/image-x-generic.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="clipping-image">
+ <g display="inline">
+ <g>
+ <polygon fill="&fill_color;" points="48.788,43.944 48.788,23.002 36.849,11.058 5.962,11.058 5.962,43.944 " stroke="&stroke_color;" stroke-width="3.5"/>
+ <polyline fill="none" points="36.849,11.058 36.849,23.002 48.788,23.002 " stroke="&stroke_color;" stroke-width="3.5"/>
+ </g>
+ </g>
+ <path d="M27.504,23.342c-6.258,0-11.471,6.241-11.471,6.241s5.213,6.271,11.471,6.267 c6.259-0.005,11.475-6.274,11.475-6.274S33.763,23.338,27.504,23.342z M27.504,33.984c-2.423,0-4.387-1.966-4.387-4.389 c0-2.419,1.964-4.388,4.387-4.388c2.42,0,4.386,1.969,4.386,4.388C31.89,32.019,29.924,33.984,27.504,33.984z" display="inline" fill="&stroke_color;"/>
+ <circle cx="27.504" cy="29.597" display="inline" fill="&stroke_color;" r="1.991"/>
+</g></svg> \ No newline at end of file
diff --git a/icons/text-uri-list.svg b/icons/text-uri-list.svg
new file mode 100644
index 0000000..fbd5276
--- /dev/null
+++ b/icons/text-uri-list.svg
@@ -0,0 +1,22 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="clipping-url_1_">
+ <g display="inline">
+ <g>
+ <polygon fill="&fill_color;" points="10.932,6.088 31.874,6.088 43.818,18.027 43.818,48.914 10.932,48.914 " stroke="&stroke_color;" stroke-width="3.5"/>
+ <polyline fill="none" points="43.818,18.027 31.874,18.027 31.874,6.088 " stroke="&stroke_color;" stroke-width="3.5"/>
+ </g>
+ </g>
+ <g display="inline">
+ <circle cx="27.375" cy="33.5" fill="&stroke_color;" r="9.951"/>
+ <g>
+ <path d="M27.376,23.549c0,0-5.603,4.197-5.603,9.988s5.603,9.914,5.603,9.914" fill="none" stroke="&fill_color;" stroke-width="1.5"/>
+ <path d="M27.376,23.549c0,0,5.532,4.57,5.532,9.988 c0,5.419-5.532,9.914-5.532,9.914" fill="none" stroke="&fill_color;" stroke-width="1.5"/>
+ <line fill="none" stroke="&fill_color;" stroke-width="1.5" x1="27.376" x2="27.376" y1="23.549" y2="43.451"/>
+ <line fill="none" stroke="&fill_color;" stroke-width="1.5" x1="27.376" x2="27.376" y1="23.549" y2="43.451"/>
+ <line fill="none" stroke="&fill_color;" stroke-width="1.5" x1="27.376" x2="27.376" y1="23.549" y2="43.451"/>
+ <line fill="none" stroke="&fill_color;" stroke-width="1.5" x1="17.423" x2="37.326" y1="33.5" y2="33.5"/>
+ </g>
+ </g>
+</g></svg> \ No newline at end of file
diff --git a/icons/text-x-generic.svg b/icons/text-x-generic.svg
new file mode 100644
index 0000000..ae37e77
--- /dev/null
+++ b/icons/text-x-generic.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' [
+ <!ENTITY stroke_color "#010101">
+ <!ENTITY fill_color "#FFFFFF">
+]><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="clipping-text">
+ <g display="inline">
+ <g>
+ <polygon fill="&fill_color;" points="10.932,6.088 31.874,6.088 43.818,18.027 43.818,48.914 10.932,48.914 " stroke="&stroke_color;" stroke-width="3.5"/>
+ <polyline fill="none" points="43.818,18.027 31.874,18.027 31.874,6.088 " stroke="&stroke_color;" stroke-width="3.5"/>
+ </g>
+ </g>
+ <line display="inline" fill="none" stroke="&stroke_color;" stroke-width="3.5" x1="17.875" x2="36.875" y1="26.25" y2="26.25"/>
+ <line display="inline" fill="none" stroke="&stroke_color;" stroke-width="3.5" x1="17.875" x2="36.875" y1="33.25" y2="33.25"/>
+ <line display="inline" fill="none" stroke="&stroke_color;" stroke-width="3.5" x1="17.875" x2="36.875" y1="40.25" y2="40.25"/>
+</g></svg> \ No newline at end of file
diff --git a/icons/text-x-python.svg b/icons/text-x-python.svg
new file mode 100644
index 0000000..5629940
--- /dev/null
+++ b/icons/text-x-python.svg
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="55px"
+ version="1.1"
+ viewBox="0 0 55 55"
+ width="55px"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.48.1 r9760"
+ sodipodi:docname="text-x-python.svg"><metadata
+ id="metadata17"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs15" /><sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1024"
+ inkscape:window-height="548"
+ id="namedview13"
+ showgrid="false"
+ inkscape:zoom="5.2545455"
+ inkscape:cx="30.735294"
+ inkscape:cy="27.690312"
+ inkscape:window-x="0"
+ inkscape:window-y="28"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg2"
+ inkscape:snap-global="true" />
+
+ <g
+ display="block"
+ id="activity-write">
+ <g
+ display="inline"
+ id="g5">
+ <g
+ id="g7">
+ <path
+ d="M 43.82,6.088 L 22.876,6.088 L 10.932,18.027 L 10.932,48.914 L 43.819,48.914 L 43.82,6.088 z"
+ id="path9"
+ stroke-width="3.5"
+ stroke="#010101"
+ fill="#FFFFFF" />
+ </g>
+ <polyline
+ points="10.932,18.027 22.876,18.027 22.876,6.088"
+ id="polyline11"
+ stroke-width="3.5"
+ stroke="#010101"
+ fill="none" />
+ </g>
+ </g>
+<g
+ id="g2993"><path
+ id="path2209"
+ d="m 16.384116,30.072698 c 0.804896,-1.395585 4.511037,-3.091831 7.624656,-2.766888 1.62132,0.169206 3.60107,1.698624 3.681698,2.443779 0.07856,0.789396 0.997753,2.858085 2.39703,2.814298 1.145918,-0.03586 1.613231,-0.96864 1.965262,-2.011995 0,0 -0.399105,-4.61965 2.711442,-6.60763 0.87614,-0.559951 3.647707,-0.888457 4.993647,-0.13255 1.34594,0.755905 0.372534,3.988454 -0.432364,5.384038 -0.804897,1.395586 -3.562687,3.723038 -3.562687,3.723038 -1.609796,2.791169 -3.482464,3.284082 -6.18606,2.992418 -2.313896,-0.249623 -3.643598,-1.921753 -4.200724,-3.246916 -0.479047,-1.139449 -1.669854,-2.037274 -3.218506,-2.04021 -1.0913,-0.0021 -3.297534,0.46227 -2.030025,2.891796 0.442705,0.848562 2.525534,3.076584 4.543849,3.640531 1.715033,0.479206 5.255278,1.587899 8.602148,1.769434 -3.293835,1.720751 -7.968917,1.038163 -10.629399,0.224034 -1.444865,-0.719626 -9.214557,-4.671598 -6.259967,-9.077177 z"
+ nodetypes="cscscssscsssssccc"
+ inkscape:connector-curvature="0"
+ style="fill:#ffffff;stroke:#000000;stroke-width:1.86633408" /><path
+ transform-center-x="-0.50601649"
+ transform-center-y="-3.6171625"
+ id="path2211"
+ d="m 40.302025,27.240441 c -1.247703,0.891922 -3.55947,2.987895 -5.11319,2.028822"
+ inkscape:connector-curvature="0"
+ style="fill:#ffffff;stroke:#000000;stroke-width:0.93316704" /><circle
+ transform="matrix(0.2511227,-0.43541349,-0.44123776,-0.24780781,29.730157,42.836365)"
+ id="circle2213"
+ r="1.927"
+ cy="6.073"
+ cx="33.926998"
+ sodipodi:cx="33.926998"
+ sodipodi:cy="6.073"
+ sodipodi:rx="1.927"
+ sodipodi:ry="1.927"
+ style="fill:#000000;stroke:none" /></g></svg> \ No newline at end of file
diff --git a/inspector.py b/inspector.py
new file mode 100644
index 0000000..df3db38
--- /dev/null
+++ b/inspector.py
@@ -0,0 +1,79 @@
+# Copyright (C) 2008 Jan Alonzo <jmalonzo@unpluggable.com>
+#
+# 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 in the hope that it will be useful,
+# but 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gtk
+import webkit
+
+class Inspector (gtk.Window):
+ def __init__ (self, inspector):
+ """initialize the WebInspector class"""
+ gtk.Window.__init__(self)
+ self.set_default_size(600, 480)
+
+ self._web_inspector = inspector
+
+ self._web_inspector.connect("inspect-web-view",
+ self._inspect_web_view_cb)
+ self._web_inspector.connect("show-window",
+ self._show_window_cb)
+ self._web_inspector.connect("attach-window",
+ self._attach_window_cb)
+ self._web_inspector.connect("detach-window",
+ self._detach_window_cb)
+ self._web_inspector.connect("close-window",
+ self._close_window_cb)
+ self._web_inspector.connect("finished",
+ self._finished_cb)
+
+ self.connect("delete-event", self._close_window_cb)
+
+ def _inspect_web_view_cb (self, inspector, web_view):
+ """Called when the 'inspect' menu item is activated"""
+ scrolled_window = gtk.ScrolledWindow()
+ scrolled_window.props.hscrollbar_policy = gtk.POLICY_AUTOMATIC
+ scrolled_window.props.vscrollbar_policy = gtk.POLICY_AUTOMATIC
+ webview = webkit.WebView()
+ scrolled_window.add(webview)
+ scrolled_window.show_all()
+
+ self.add(scrolled_window)
+ return webview
+
+ def _show_window_cb (self, inspector):
+ """Called when the inspector window should be displayed"""
+ self.present()
+ return True
+
+ def _attach_window_cb (self, inspector):
+ """Called when the inspector should displayed in the same
+ window as the WebView being inspected
+ """
+ return False
+
+ def _detach_window_cb (self, inspector):
+ """Called when the inspector should appear in a separate window"""
+ return False
+
+ def _close_window_cb (self, inspector, view):
+ """Called when the inspector window should be closed"""
+ self.hide()
+ return True
+
+ def _finished_cb (self, inspector):
+ """Called when inspection is done"""
+ self._web_inspector = 0
+ self.destroy()
+ return False
diff --git a/templates/editor.html b/templates/editor.html
new file mode 100644
index 0000000..afb5982
--- /dev/null
+++ b/templates/editor.html
@@ -0,0 +1,62 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/">
+<head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+ <title>Editor</title>
+ <link rel="stylesheet" href="/www/css/main.css" type="text/css" />
+<script src="/www/js/jquery-1.6.2.js" type="text/javascript"></script>
+<!-- <script src="js/jquery-plugins/json.js" type="text/javascript"></script>
+ <script src="js/jquery-plugins/jquery.hotkeys-0.7.8.js" type="text/javascript"></script>
+ <script src="js/jquery-plugins/jquery.intercept.js" type="text/javascript"></script> -->
+</head>
+<body>
+<script src="/www/js/ace/ace-uncompressed.js" type="text/javascript" charset="utf-8"></script>
+<script src="/www/js/ace/theme-twilight.js" type="text/javascript" charset="utf-8"></script>
+<script py:if="mode" src="/www/js/ace/mode-${mode}.js" type="text/javascript" charset="utf-8"></script>
+<pre id="editor">$content</pre>
+<div id="editor-sidebar" class="bling">
+<div class="file">
+ <img src="/www/icons/${icon}" /><br/>
+ ${absdir}/<br/>
+ <strong>${basename}</strong><br/>
+</div>
+<hr />
+<form action="/save" method="post">
+<input type="hidden" name="filename" id="filename" value="$filename" />
+<input type="hidden" name="directory" id="directory" value="$directory" />
+<input type="hidden" name="content" id="content" value="ñññ" />
+<input type="button" class="btn" value="Browse directory" onclick='location.href="/browse?directory=${directory}"' />
+<br/>
+<input type="submit" class="btn" value="Save file" /><br/>
+<input type="button" class="btn" value="Delete file" onclick='location.href="/delete?filename=${filename}"' />
+<!-- save a magic cookie for security here TODO -->
+<input type="button" class="btn" value="Return to main" onclick='top.location.href="/www/index.html"' />
+</form>
+</div>
+<script>
+window.onload = function() {
+ var aceEditor = ace.edit("editor");
+ aceEditor.setTheme("ace/theme/twilight");
+ aceEditor.getSession().setUseSoftTabs(true);
+ // we can assume we're on unix
+ aceEditor.getSession().setNewLineMode("unix");
+
+ if ("${mode}"!="") {
+ var thisMode = require("ace/mode/${mode}").Mode;
+ aceEditor.getSession().setMode(new thisMode());
+ }
+
+ function dispatch() {
+ var c = $('#content');
+ c.val(aceEditor.getSession().getValue());
+ }
+ dispatch()
+
+ aceEditor.getSession().on('change', dispatch);
+ $('.bling').slideDown("slow");
+};
+</script>
+</body>
+</html>
diff --git a/templates/filer.html b/templates/filer.html
new file mode 100644
index 0000000..b2f3637
--- /dev/null
+++ b/templates/filer.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:py="http://genshi.edgewall.org/">
+<head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+ <title>Filer</title>
+ <link rel="stylesheet" href="/www/css/main.css" type="text/css" />
+<script src="/www/js/jquery-1.6.2.js" type="text/javascript"></script>
+<script src="/www/js/jquery.corner.js" type="text/javascript"></script>
+<!-- <script src="js/jquery-plugins/json.js" type="text/javascript"></script>
+ <script src="js/jquery-plugins/jquery.hotkeys-0.7.8.js" type="text/javascript"></script>
+ <script src="js/jquery-plugins/jquery.intercept.js" type="text/javascript"></script> -->
+</head>
+<body>
+ <div id="filer-header">
+ <span>${absdir}/</span>
+ </div>
+ <div id="filer" class="bling">
+ <ul>
+ <li py:for="file in files"><a href="${file.href}" class="file"><img src="/www/icons/${file.icon}" /><br/>${file.name}</a></li>
+ </ul>
+ </div>
+<script>
+window.onload = function() {
+ $('.file').corner("15px");
+ $('.bling').show("slow");
+ $('filer-header').slideDown("slow");
+};
+</script>
+</body>
+</html>
diff --git a/templates/skel.html b/templates/skel.html
new file mode 100644
index 0000000..65d351e
--- /dev/null
+++ b/templates/skel.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+ <title>Filer</title>
+ <link rel="stylesheet" href="/www/css/main.css" type="text/css" />
+<script src="/www/js/jquery-1.6.2.js" type="text/javascript"></script>
+<!-- <script src="js/jquery-plugins/json.js" type="text/javascript"></script>
+ <script src="js/jquery-plugins/jquery.hotkeys-0.7.8.js" type="text/javascript"></script>
+ <script src="js/jquery-plugins/jquery.intercept.js" type="text/javascript"></script> -->
+</head>
+<body>
+<script>
+window.onload = function() {
+};
+</script>
+</body>
+</html>
diff --git a/templates/split-view.html b/templates/split-view.html
new file mode 100644
index 0000000..e23b696
--- /dev/null
+++ b/templates/split-view.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+<title>basic frameset</title>
+</head>
+
+<frameset rows="50%, *">
+ <frame src="${frame1}" />
+ <frame src="${frame2}" />
+</frameset>
+</html>
diff --git a/webkit_local/__init__.py b/webkit_local/__init__.py
new file mode 100644
index 0000000..e418374
--- /dev/null
+++ b/webkit_local/__init__.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+
+import webkit
+
+from webkit import *
diff --git a/webkit_local/libWebKitGdk.so.1 b/webkit_local/libWebKitGdk.so.1
new file mode 100644
index 0000000..b2c1f0b
--- /dev/null
+++ b/webkit_local/libWebKitGdk.so.1
Binary files differ
diff --git a/webkit_local/libxslt.so.1 b/webkit_local/libxslt.so.1
new file mode 100644
index 0000000..4dc90cd
--- /dev/null
+++ b/webkit_local/libxslt.so.1
Binary files differ
diff --git a/webkit_local/webkit.so b/webkit_local/webkit.so
new file mode 100644
index 0000000..bc94300
--- /dev/null
+++ b/webkit_local/webkit.so
Binary files differ
diff --git a/www/css/main.css b/www/css/main.css
new file mode 100644
index 0000000..ce18c7b
--- /dev/null
+++ b/www/css/main.css
@@ -0,0 +1,102 @@
+body {
+ background-color: #c0c0c0;
+ height: 100%;
+ margin: 0;
+}
+
+p.subtitle {
+ font-size: 10pt;
+ margin-top: 5px;
+}
+
+li {
+ margin-bottom: 1em
+}
+
+hr {
+ border: 0;
+ color: #9E9E9E;
+ background-color: #9E9E9E;
+ height: 1px;
+ width: 100%;
+ text-align: left;
+}
+
+#content {
+ margin: 40px;
+}
+
+input.btn {
+ color: white;
+ /* background-color: #808080; */
+}
+
+#beta {
+ font-size: 10pt;
+ color: red;
+ display: none;
+}
+
+#editor {
+ margin: 0;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ width: 85%;
+ margin-left: 15%;
+}
+
+#editor-sidebar {
+ width: 15%;
+}
+
+.bling {
+ display: none;
+}
+
+div#filer {
+ display: none;
+}
+
+#filer ul{
+ list-style: none;
+ width: 100%;
+ padding: 0;
+}
+
+#filer ul li {
+ text-align: center;
+}
+
+
+#filer ul li a:hover{
+ color: white;
+ background-color: #808080;
+}
+
+#filer ul li a{
+ color: black;
+ text-decoration: none;
+ font-size: 8pt;
+ width: 50px;
+ height: 70px;
+ float:left;
+ padding:20px;
+ padding-top:5px;
+}
+
+#filer-header {
+ background-color: #808080;
+ color: white;
+ margin: 0;
+ padding: 0;
+ padding-top: 7px;
+ padding-bottom: 7px;
+ width: 100%;
+}
+
+#filer-header span{
+ padding-left: 10px;
+} \ No newline at end of file
diff --git a/www/icons b/www/icons
new file mode 120000
index 0000000..9483777
--- /dev/null
+++ b/www/icons
@@ -0,0 +1 @@
+../icons/ \ No newline at end of file
diff --git a/www/index.html b/www/index.html
new file mode 100644
index 0000000..2d2e9a8
--- /dev/null
+++ b/www/index.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html lang="en-US">
+<head>
+ <meta charset="UTF-8">
+ <title></title>
+ <link rel="stylesheet" href="css/main.css" type="text/css" />
+ <script src="/www/js/jquery-1.6.2.js" type="text/javascript"></script>
+</head>
+<body>
+ <div id="content" class="bling">
+ <header><h1>Sugar WebSDK <span id="beta">beta</span></h1></header>
+ <section>
+ <article><h2>Technology Demonstration</h2></article>
+ <p>The following are examples of things that can be achieved with WebSDK.</p>
+ <ul>
+ <li><input type="button" class="btn"
+ value="Webpy: WebSDK Studio" onclick='location.href="/edit?filename=appservice.py"' />
+ - <input type="button" class="btn"
+ value="Split view" onclick='location.href="/vsplit"' />
+ <p class="subtitle">Featuring Ace Editor and some tricks.</p>
+ </li>
+ <li><input type="button" class="btn"
+ value="Webpy bundle browser" onclick='location.href="/browse"' />
+ <p class="subtitle"><i>"Write less, do more".</i> Jquery is a bag of candy.</p>
+ </li>
+ <li><input type="button" class="btn"
+ value="Call root python controller" onclick='location.href="/"'>
+ </li>
+ <li><input type="button" class="btn"
+ value="Sugar Integration: Journal" onclick='location.href="/journal"' />
+<input type="button" class="btn"
+ value="Sugar Integration: Collaboration" onclick='location.href="/collaboration"' />
+ <p class="subtitle">Not yet implemented.</p>
+ </li>
+ <li><input type="button" class="btn"
+ value="Debugging tools." onclick='location.href="/debug"' />
+ <p class="subtitle">Begin with right click, inspect element.</p>
+ </li>
+ </ul>
+ </section>
+ </div>
+ <script>
+ window.onload = function() {
+ $('#content').fadeIn(700);
+ $('#beta').delay(1500).fadeIn("slow");
+ }
+ </script>
+</body>
+</html>
diff --git a/www/js/ace/ace-uncompressed.js b/www/js/ace/ace-uncompressed.js
new file mode 100644
index 0000000..389daf2
--- /dev/null
+++ b/www/js/ace/ace-uncompressed.js
@@ -0,0 +1,15284 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Define a module along with a payload
+ * @param module a name for the payload
+ * @param payload a function to call with (require, exports, module) params
+ */
+
+(function() {
+
+var global = (function() {
+ return this;
+})();
+
+// if we find an existing require function use it.
+if (global.require && global.define) {
+ require.packaged = true;
+ return;
+}
+
+var _define = function(module, deps, payload) {
+ if (typeof module !== 'string') {
+ if (_define.original)
+ _define.original.apply(window, arguments);
+ else {
+ console.error('dropping module because define wasn\'t a string.');
+ console.trace();
+ }
+ return;
+ }
+
+ if (arguments.length == 2)
+ payload = deps;
+
+ if (!define.modules)
+ define.modules = {};
+
+ define.modules[module] = payload;
+};
+if (global.define)
+ _define.original = global.define;
+
+global.define = _define;
+
+
+/**
+ * Get at functionality define()ed using the function above
+ */
+var _require = function(module, callback) {
+ if (Object.prototype.toString.call(module) === "[object Array]") {
+ var params = [];
+ for (var i = 0, l = module.length; i < l; ++i) {
+ var dep = lookup(module[i]);
+ if (!dep && _require.original)
+ return _require.original.apply(window, arguments);
+ params.push(dep);
+ }
+ if (callback) {
+ callback.apply(null, params);
+ }
+ }
+ else if (typeof module === 'string') {
+ var payload = lookup(module);
+ if (!payload && _require.original)
+ return _require.original.apply(window, arguments);
+
+ if (callback) {
+ callback();
+ }
+
+ return payload;
+ }
+ else {
+ if (_require.original)
+ return _require.original.apply(window, arguments);
+ }
+};
+
+if (global.require)
+ _require.original = global.require;
+
+global.require = _require;
+require.packaged = true;
+
+/**
+ * Internal function to lookup moduleNames and resolve them by calling the
+ * definition function if needed.
+ */
+var lookup = function(moduleName) {
+ var module = define.modules[moduleName];
+ if (module == null) {
+ console.error('Missing module: ' + moduleName);
+ return null;
+ }
+
+ if (typeof module === 'function') {
+ var exports = {};
+ module(require, exports, { id: moduleName, uri: '' });
+ // cache the resulting module object for next time
+ define.modules[moduleName] = exports;
+ return exports;
+ }
+
+ return module;
+};
+
+})();// vim:set ts=4 sts=4 sw=4 st:
+// -- kriskowal Kris Kowal Copyright (C) 2009-2010 MIT License
+// -- tlrobinson Tom Robinson Copyright (C) 2009-2010 MIT License (Narwhal Project)
+// -- dantman Daniel Friesen Copyright(C) 2010 XXX No License Specified
+// -- fschaefer Florian Schäfer Copyright (C) 2010 MIT License
+// -- Irakli Gozalishvili Copyright (C) 2010 MIT License
+
+/*!
+ Copyright (c) 2009, 280 North Inc. http://280north.com/
+ MIT License. http://github.com/280north/narwhal/blob/master/README.md
+*/
+
+define('pilot/fixoldbrowsers', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+/**
+ * Brings an environment as close to ECMAScript 5 compliance
+ * as is possible with the facilities of erstwhile engines.
+ *
+ * ES5 Draft
+ * http://www.ecma-international.org/publications/files/drafts/tc39-2009-050.pdf
+ *
+ * NOTE: this is a draft, and as such, the URL is subject to change. If the
+ * link is broken, check in the parent directory for the latest TC39 PDF.
+ * http://www.ecma-international.org/publications/files/drafts/
+ *
+ * Previous ES5 Draft
+ * http://www.ecma-international.org/publications/files/drafts/tc39-2009-025.pdf
+ * This is a broken link to the previous draft of ES5 on which most of the
+ * numbered specification references and quotes herein were taken. Updating
+ * these references and quotes to reflect the new document would be a welcome
+ * volunteer project.
+ *
+ * @module
+ */
+
+/*whatsupdoc*/
+
+//
+// Function
+// ========
+//
+
+// ES-5 15.3.4.5
+// http://www.ecma-international.org/publications/files/drafts/tc39-2009-025.pdf
+
+if (!Function.prototype.bind) {
+ var slice = Array.prototype.slice;
+ Function.prototype.bind = function bind(that) { // .length is 1
+ // 1. Let Target be the this value.
+ var target = this;
+ // 2. If IsCallable(Target) is false, throw a TypeError exception.
+ // XXX this gets pretty close, for all intents and purposes, letting
+ // some duck-types slide
+ if (typeof target.apply !== "function" || typeof target.call !== "function")
+ return new TypeError();
+ // 3. Let A be a new (possibly empty) internal list of all of the
+ // argument values provided after thisArg (arg1, arg2 etc), in order.
+ var args = slice.call(arguments);
+ // 4. Let F be a new native ECMAScript object.
+ // 9. Set the [[Prototype]] internal property of F to the standard
+ // built-in Function prototype object as specified in 15.3.3.1.
+ // 10. Set the [[Call]] internal property of F as described in
+ // 15.3.4.5.1.
+ // 11. Set the [[Construct]] internal property of F as described in
+ // 15.3.4.5.2.
+ // 12. Set the [[HasInstance]] internal property of F as described in
+ // 15.3.4.5.3.
+ // 13. The [[Scope]] internal property of F is unused and need not
+ // exist.
+ var bound = function bound() {
+
+ if (this instanceof bound) {
+ // 15.3.4.5.2 [[Construct]]
+ // When the [[Construct]] internal method of a function object,
+ // F that was created using the bind function is called with a
+ // list of arguments ExtraArgs the following steps are taken:
+ // 1. Let target be the value of F's [[TargetFunction]]
+ // internal property.
+ // 2. If target has no [[Construct]] internal method, a
+ // TypeError exception is thrown.
+ // 3. Let boundArgs be the value of F's [[BoundArgs]] internal
+ // property.
+ // 4. Let args be a new list containing the same values as the
+ // list boundArgs in the same order followed by the same
+ // values as the list ExtraArgs in the same order.
+
+ var self = Object.create(target.prototype);
+ target.apply(self, args.concat(slice.call(arguments)));
+ return self;
+
+ } else {
+ // 15.3.4.5.1 [[Call]]
+ // When the [[Call]] internal method of a function object, F,
+ // which was created using the bind function is called with a
+ // this value and a list of arguments ExtraArgs the following
+ // steps are taken:
+ // 1. Let boundArgs be the value of F's [[BoundArgs]] internal
+ // property.
+ // 2. Let boundThis be the value of F's [[BoundThis]] internal
+ // property.
+ // 3. Let target be the value of F's [[TargetFunction]] internal
+ // property.
+ // 4. Let args be a new list containing the same values as the list
+ // boundArgs in the same order followed by the same values as
+ // the list ExtraArgs in the same order. 5. Return the
+ // result of calling the [[Call]] internal method of target
+ // providing boundThis as the this value and providing args
+ // as the arguments.
+
+ // equiv: target.call(this, ...boundArgs, ...args)
+ return target.call.apply(
+ target,
+ args.concat(slice.call(arguments))
+ );
+
+ }
+
+ };
+ bound.length = (
+ // 14. If the [[Class]] internal property of Target is "Function", then
+ typeof target === "function" ?
+ // a. Let L be the length property of Target minus the length of A.
+ // b. Set the length own property of F to either 0 or L, whichever is larger.
+ Math.max(target.length - args.length, 0) :
+ // 15. Else set the length own property of F to 0.
+ 0
+ )
+ // 16. The length own property of F is given attributes as specified in
+ // 15.3.5.1.
+ // TODO
+ // 17. Set the [[Extensible]] internal property of F to true.
+ // TODO
+ // 18. Call the [[DefineOwnProperty]] internal method of F with
+ // arguments "caller", PropertyDescriptor {[[Value]]: null,
+ // [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]:
+ // false}, and false.
+ // TODO
+ // 19. Call the [[DefineOwnProperty]] internal method of F with
+ // arguments "arguments", PropertyDescriptor {[[Value]]: null,
+ // [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]:
+ // false}, and false.
+ // TODO
+ // NOTE Function objects created using Function.prototype.bind do not
+ // have a prototype property.
+ // XXX can't delete it in pure-js.
+ return bound;
+ };
+}
+
+// Shortcut to an often accessed properties, in order to avoid multiple
+// dereference that costs universally.
+// _Please note: Shortcuts are defined after `Function.prototype.bind` as we
+// us it in defining shortcuts.
+var call = Function.prototype.call;
+var prototypeOfArray = Array.prototype;
+var prototypeOfObject = Object.prototype;
+var owns = call.bind(prototypeOfObject.hasOwnProperty);
+
+var defineGetter, defineSetter, lookupGetter, lookupSetter, supportsAccessors;
+// If JS engine supports accessors creating shortcuts.
+if ((supportsAccessors = owns(prototypeOfObject, '__defineGetter__'))) {
+ defineGetter = call.bind(prototypeOfObject.__defineGetter__);
+ defineSetter = call.bind(prototypeOfObject.__defineSetter__);
+ lookupGetter = call.bind(prototypeOfObject.__lookupGetter__);
+ lookupSetter = call.bind(prototypeOfObject.__lookupSetter__);
+}
+
+
+//
+// Array
+// =====
+//
+
+// ES5 15.4.3.2
+if (!Array.isArray) {
+ Array.isArray = function isArray(obj) {
+ return Object.prototype.toString.call(obj) === "[object Array]";
+ };
+}
+
+// ES5 15.4.4.18
+if (!Array.prototype.forEach) {
+ Array.prototype.forEach = function forEach(block, thisObject) {
+ var len = +this.length;
+ for (var i = 0; i < len; i++) {
+ if (i in this) {
+ block.call(thisObject, this[i], i, this);
+ }
+ }
+ };
+}
+
+// ES5 15.4.4.19
+// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/map
+if (!Array.prototype.map) {
+ Array.prototype.map = function map(fun /*, thisp*/) {
+ var len = +this.length;
+ if (typeof fun !== "function")
+ throw new TypeError();
+
+ var res = new Array(len);
+ var thisp = arguments[1];
+ for (var i = 0; i < len; i++) {
+ if (i in this)
+ res[i] = fun.call(thisp, this[i], i, this);
+ }
+
+ return res;
+ };
+}
+
+// ES5 15.4.4.20
+if (!Array.prototype.filter) {
+ Array.prototype.filter = function filter(block /*, thisp */) {
+ var values = [];
+ var thisp = arguments[1];
+ for (var i = 0; i < this.length; i++)
+ if (block.call(thisp, this[i]))
+ values.push(this[i]);
+ return values;
+ };
+}
+
+// ES5 15.4.4.16
+if (!Array.prototype.every) {
+ Array.prototype.every = function every(block /*, thisp */) {
+ var thisp = arguments[1];
+ for (var i = 0; i < this.length; i++)
+ if (!block.call(thisp, this[i]))
+ return false;
+ return true;
+ };
+}
+
+// ES5 15.4.4.17
+if (!Array.prototype.some) {
+ Array.prototype.some = function some(block /*, thisp */) {
+ var thisp = arguments[1];
+ for (var i = 0; i < this.length; i++)
+ if (block.call(thisp, this[i]))
+ return true;
+ return false;
+ };
+}
+
+// ES5 15.4.4.21
+// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/reduce
+if (!Array.prototype.reduce) {
+ Array.prototype.reduce = function reduce(fun /*, initial*/) {
+ var len = +this.length;
+ if (typeof fun !== "function")
+ throw new TypeError();
+
+ // no value to return if no initial value and an empty array
+ if (len === 0 && arguments.length === 1)
+ throw new TypeError();
+
+ var i = 0;
+ if (arguments.length >= 2) {
+ var rv = arguments[1];
+ } else {
+ do {
+ if (i in this) {
+ rv = this[i++];
+ break;
+ }
+
+ // if array contains no values, no initial value to return
+ if (++i >= len)
+ throw new TypeError();
+ } while (true);
+ }
+
+ for (; i < len; i++) {
+ if (i in this)
+ rv = fun.call(null, rv, this[i], i, this);
+ }
+
+ return rv;
+ };
+}
+
+// ES5 15.4.4.22
+// https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Objects/Array/reduceRight
+if (!Array.prototype.reduceRight) {
+ Array.prototype.reduceRight = function reduceRight(fun /*, initial*/) {
+ var len = +this.length;
+ if (typeof fun !== "function")
+ throw new TypeError();
+
+ // no value to return if no initial value, empty array
+ if (len === 0 && arguments.length === 1)
+ throw new TypeError();
+
+ var i = len - 1;
+ if (arguments.length >= 2) {
+ var rv = arguments[1];
+ } else {
+ do {
+ if (i in this) {
+ rv = this[i--];
+ break;
+ }
+
+ // if array contains no values, no initial value to return
+ if (--i < 0)
+ throw new TypeError();
+ } while (true);
+ }
+
+ for (; i >= 0; i--) {
+ if (i in this)
+ rv = fun.call(null, rv, this[i], i, this);
+ }
+
+ return rv;
+ };
+}
+
+// ES5 15.4.4.14
+if (!Array.prototype.indexOf) {
+ Array.prototype.indexOf = function indexOf(value /*, fromIndex */ ) {
+ var length = this.length;
+ if (!length)
+ return -1;
+ var i = arguments[1] || 0;
+ if (i >= length)
+ return -1;
+ if (i < 0)
+ i += length;
+ for (; i < length; i++) {
+ if (!owns(this, i))
+ continue;
+ if (value === this[i])
+ return i;
+ }
+ return -1;
+ };
+}
+
+// ES5 15.4.4.15
+if (!Array.prototype.lastIndexOf) {
+ Array.prototype.lastIndexOf = function lastIndexOf(value /*, fromIndex */) {
+ var length = this.length;
+ if (!length)
+ return -1;
+ var i = arguments[1] || length;
+ if (i < 0)
+ i += length;
+ i = Math.min(i, length - 1);
+ for (; i >= 0; i--) {
+ if (!owns(this, i))
+ continue;
+ if (value === this[i])
+ return i;
+ }
+ return -1;
+ };
+}
+
+//
+// Object
+// ======
+//
+
+// ES5 15.2.3.2
+if (!Object.getPrototypeOf) {
+ // https://github.com/kriskowal/es5-shim/issues#issue/2
+ // http://ejohn.org/blog/objectgetprototypeof/
+ // recommended by fschaefer on github
+ Object.getPrototypeOf = function getPrototypeOf(object) {
+ return object.__proto__ || object.constructor.prototype;
+ // or undefined if not available in this engine
+ };
+}
+
+// ES5 15.2.3.3
+if (!Object.getOwnPropertyDescriptor) {
+ var ERR_NON_OBJECT = "Object.getOwnPropertyDescriptor called on a " +
+ "non-object: ";
+ Object.getOwnPropertyDescriptor = function getOwnPropertyDescriptor(object, property) {
+ if ((typeof object !== "object" && typeof object !== "function") || object === null)
+ throw new TypeError(ERR_NON_OBJECT + object);
+ // If object does not owns property return undefined immediately.
+ if (!owns(object, property))
+ return undefined;
+
+ var despriptor, getter, setter;
+
+ // If object has a property then it's for sure both `enumerable` and
+ // `configurable`.
+ despriptor = { enumerable: true, configurable: true };
+
+ // If JS engine supports accessor properties then property may be a
+ // getter or setter.
+ if (supportsAccessors) {
+ // Unfortunately `__lookupGetter__` will return a getter even
+ // if object has own non getter property along with a same named
+ // inherited getter. To avoid misbehavior we temporary remove
+ // `__proto__` so that `__lookupGetter__` will return getter only
+ // if it's owned by an object.
+ var prototype = object.__proto__;
+ object.__proto__ = prototypeOfObject;
+
+ var getter = lookupGetter(object, property);
+ var setter = lookupSetter(object, property);
+
+ // Once we have getter and setter we can put values back.
+ object.__proto__ = prototype;
+
+ if (getter || setter) {
+ if (getter) descriptor.get = getter;
+ if (setter) descriptor.set = setter;
+
+ // If it was accessor property we're done and return here
+ // in order to avoid adding `value` to the descriptor.
+ return descriptor;
+ }
+ }
+
+ // If we got this far we know that object has an own property that is
+ // not an accessor so we set it as a value and return descriptor.
+ descriptor.value = object[property];
+ return descriptor;
+ };
+}
+
+// ES5 15.2.3.4
+if (!Object.getOwnPropertyNames) {
+ Object.getOwnPropertyNames = function getOwnPropertyNames(object) {
+ return Object.keys(object);
+ };
+}
+
+// ES5 15.2.3.5
+if (!Object.create) {
+ Object.create = function create(prototype, properties) {
+ var object;
+ if (prototype === null) {
+ object = { "__proto__": null };
+ } else {
+ if (typeof prototype !== "object")
+ throw new TypeError("typeof prototype["+(typeof prototype)+"] != 'object'");
+ var Type = function () {};
+ Type.prototype = prototype;
+ object = new Type();
+ // IE has no built-in implementation of `Object.getPrototypeOf`
+ // neither `__proto__`, but this manually setting `__proto__` will
+ // guarantee that `Object.getPrototypeOf` will work as expected with
+ // objects created using `Object.create`
+ object.__proto__ = prototype;
+ }
+ if (typeof properties !== "undefined")
+ Object.defineProperties(object, properties);
+ return object;
+ };
+}
+
+// ES5 15.2.3.6
+if (!Object.defineProperty) {
+ var ERR_NON_OBJECT_DESCRIPTOR = "Property description must be an object: ";
+ var ERR_NON_OBJECT_TARGET = "Object.defineProperty called on non-object: "
+ var ERR_ACCESSORS_NOT_SUPPORTED = "getters & setters can not be defined " +
+ "on this javascript engine";
+
+ Object.defineProperty = function defineProperty(object, property, descriptor) {
+ if (typeof object !== "object" && typeof object !== "function")
+ throw new TypeError(ERR_NON_OBJECT_TARGET + object);
+ if (typeof object !== "object" || object === null)
+ throw new TypeError(ERR_NON_OBJECT_DESCRIPTOR + descriptor);
+
+ // If it's a data property.
+ if (owns(descriptor, "value")) {
+ // fail silently if "writable", "enumerable", or "configurable"
+ // are requested but not supported
+ /*
+ // alternate approach:
+ if ( // can't implement these features; allow false but not true
+ !(owns(descriptor, "writable") ? descriptor.writable : true) ||
+ !(owns(descriptor, "enumerable") ? descriptor.enumerable : true) ||
+ !(owns(descriptor, "configurable") ? descriptor.configurable : true)
+ )
+ throw new RangeError(
+ "This implementation of Object.defineProperty does not " +
+ "support configurable, enumerable, or writable."
+ );
+ */
+
+ if (supportsAccessors && (lookupGetter(object, property) ||
+ lookupSetter(object, property)))
+ {
+ // As accessors are supported only on engines implementing
+ // `__proto__` we can safely override `__proto__` while defining
+ // a property to make sure that we don't hit an inherited
+ // accessor.
+ var prototype = object.__proto__;
+ object.__proto__ = prototypeOfObject;
+ // Deleting a property anyway since getter / setter may be
+ // defined on object itself.
+ delete object[property];
+ object[property] = descriptor.value;
+ // Setting original `__proto__` back now.
+ object.prototype;
+ } else {
+ object[property] = descriptor.value;
+ }
+ } else {
+ if (!supportsAccessors)
+ throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED);
+ // If we got that far then getters and setters can be defined !!
+ if (owns(descriptor, "get"))
+ defineGetter(object, property, descriptor.get);
+ if (owns(descriptor, "set"))
+ defineSetter(object, property, descriptor.set);
+ }
+
+ return object;
+ };
+}
+
+// ES5 15.2.3.7
+if (!Object.defineProperties) {
+ Object.defineProperties = function defineProperties(object, properties) {
+ for (var property in properties) {
+ if (owns(properties, property))
+ Object.defineProperty(object, property, properties[property]);
+ }
+ return object;
+ };
+}
+
+// ES5 15.2.3.8
+if (!Object.seal) {
+ Object.seal = function seal(object) {
+ // this is misleading and breaks feature-detection, but
+ // allows "securable" code to "gracefully" degrade to working
+ // but insecure code.
+ return object;
+ };
+}
+
+// ES5 15.2.3.9
+if (!Object.freeze) {
+ Object.freeze = function freeze(object) {
+ // this is misleading and breaks feature-detection, but
+ // allows "securable" code to "gracefully" degrade to working
+ // but insecure code.
+ return object;
+ };
+}
+
+// detect a Rhino bug and patch it
+try {
+ Object.freeze(function () {});
+} catch (exception) {
+ Object.freeze = (function freeze(freezeObject) {
+ return function freeze(object) {
+ if (typeof object === "function") {
+ return object;
+ } else {
+ return freezeObject(object);
+ }
+ };
+ })(Object.freeze);
+}
+
+// ES5 15.2.3.10
+if (!Object.preventExtensions) {
+ Object.preventExtensions = function preventExtensions(object) {
+ // this is misleading and breaks feature-detection, but
+ // allows "securable" code to "gracefully" degrade to working
+ // but insecure code.
+ return object;
+ };
+}
+
+// ES5 15.2.3.11
+if (!Object.isSealed) {
+ Object.isSealed = function isSealed(object) {
+ return false;
+ };
+}
+
+// ES5 15.2.3.12
+if (!Object.isFrozen) {
+ Object.isFrozen = function isFrozen(object) {
+ return false;
+ };
+}
+
+// ES5 15.2.3.13
+if (!Object.isExtensible) {
+ Object.isExtensible = function isExtensible(object) {
+ return true;
+ };
+}
+
+// ES5 15.2.3.14
+// http://whattheheadsaid.com/2010/10/a-safer-object-keys-compatibility-implementation
+if (!Object.keys) {
+
+ var hasDontEnumBug = true,
+ dontEnums = [
+ 'toString',
+ 'toLocaleString',
+ 'valueOf',
+ 'hasOwnProperty',
+ 'isPrototypeOf',
+ 'propertyIsEnumerable',
+ 'constructor'
+ ],
+ dontEnumsLength = dontEnums.length;
+
+ for (var key in {"toString": null})
+ hasDontEnumBug = false;
+
+ Object.keys = function keys(object) {
+
+ if (
+ typeof object !== "object" && typeof object !== "function"
+ || object === null
+ )
+ throw new TypeError("Object.keys called on a non-object");
+
+ var keys = [];
+ for (var name in object) {
+ if (owns(object, name)) {
+ keys.push(name);
+ }
+ }
+
+ if (hasDontEnumBug) {
+ for (var i = 0, ii = dontEnumsLength; i < ii; i++) {
+ var dontEnum = dontEnums[i];
+ if (owns(object, dontEnum)) {
+ keys.push(dontEnum);
+ }
+ }
+ }
+
+ return keys;
+ };
+
+}
+
+//
+// Date
+// ====
+//
+
+// ES5 15.9.5.43
+// Format a Date object as a string according to a subset of the ISO-8601 standard.
+// Useful in Atom, among other things.
+if (!Date.prototype.toISOString) {
+ Date.prototype.toISOString = function toISOString() {
+ return (
+ this.getUTCFullYear() + "-" +
+ (this.getUTCMonth() + 1) + "-" +
+ this.getUTCDate() + "T" +
+ this.getUTCHours() + ":" +
+ this.getUTCMinutes() + ":" +
+ this.getUTCSeconds() + "Z"
+ );
+ }
+}
+
+// ES5 15.9.4.4
+if (!Date.now) {
+ Date.now = function now() {
+ return new Date().getTime();
+ };
+}
+
+// ES5 15.9.5.44
+if (!Date.prototype.toJSON) {
+ Date.prototype.toJSON = function toJSON(key) {
+ // This function provides a String representation of a Date object for
+ // use by JSON.stringify (15.12.3). When the toJSON method is called
+ // with argument key, the following steps are taken:
+
+ // 1. Let O be the result of calling ToObject, giving it the this
+ // value as its argument.
+ // 2. Let tv be ToPrimitive(O, hint Number).
+ // 3. If tv is a Number and is not finite, return null.
+ // XXX
+ // 4. Let toISO be the result of calling the [[Get]] internal method of
+ // O with argument "toISOString".
+ // 5. If IsCallable(toISO) is false, throw a TypeError exception.
+ if (typeof this.toISOString !== "function")
+ throw new TypeError();
+ // 6. Return the result of calling the [[Call]] internal method of
+ // toISO with O as the this value and an empty argument list.
+ return this.toISOString();
+
+ // NOTE 1 The argument is ignored.
+
+ // NOTE 2 The toJSON function is intentionally generic; it does not
+ // require that its this value be a Date object. Therefore, it can be
+ // transferred to other kinds of objects for use as a method. However,
+ // it does require that any such object have a toISOString method. An
+ // object is free to use the argument key to filter its
+ // stringification.
+ };
+}
+
+// 15.9.4.2 Date.parse (string)
+// 15.9.1.15 Date Time String Format
+// Date.parse
+// based on work shared by Daniel Friesen (dantman)
+// http://gist.github.com/303249
+if (isNaN(Date.parse("T00:00"))) {
+ // XXX global assignment won't work in embeddings that use
+ // an alternate object for the context.
+ Date = (function(NativeDate) {
+
+ // Date.length === 7
+ var Date = function(Y, M, D, h, m, s, ms) {
+ var length = arguments.length;
+ if (this instanceof NativeDate) {
+ var date = length === 1 && String(Y) === Y ? // isString(Y)
+ // We explicitly pass it through parse:
+ new NativeDate(Date.parse(Y)) :
+ // We have to manually make calls depending on argument
+ // length here
+ length >= 7 ? new NativeDate(Y, M, D, h, m, s, ms) :
+ length >= 6 ? new NativeDate(Y, M, D, h, m, s) :
+ length >= 5 ? new NativeDate(Y, M, D, h, m) :
+ length >= 4 ? new NativeDate(Y, M, D, h) :
+ length >= 3 ? new NativeDate(Y, M, D) :
+ length >= 2 ? new NativeDate(Y, M) :
+ length >= 1 ? new NativeDate(Y) :
+ new NativeDate();
+ // Prevent mixups with unfixed Date object
+ date.constructor = Date;
+ return date;
+ }
+ return NativeDate.apply(this, arguments);
+ };
+
+ // 15.9.1.15 Date Time String Format
+ var isoDateExpression = new RegExp("^" +
+ "(?:" + // optional year-month-day
+ "(" + // year capture
+ "(?:[+-]\\d\\d)?" + // 15.9.1.15.1 Extended years
+ "\\d\\d\\d\\d" + // four-digit year
+ ")" +
+ "(?:-" + // optional month-day
+ "(\\d\\d)" + // month capture
+ "(?:-" + // optional day
+ "(\\d\\d)" + // day capture
+ ")?" +
+ ")?" +
+ ")?" +
+ "(?:T" + // hour:minute:second.subsecond
+ "(\\d\\d)" + // hour capture
+ ":(\\d\\d)" + // minute capture
+ "(?::" + // optional :second.subsecond
+ "(\\d\\d)" + // second capture
+ "(?:\\.(\\d\\d\\d))?" + // milisecond capture
+ ")?" +
+ ")?" +
+ "(?:" + // time zone
+ "Z|" + // UTC capture
+ "([+-])(\\d\\d):(\\d\\d)" + // timezone offset
+ // capture sign, hour, minute
+ ")?" +
+ "$");
+
+ // Copy any custom methods a 3rd party library may have added
+ for (var key in NativeDate)
+ Date[key] = NativeDate[key];
+
+ // Copy "native" methods explicitly; they may be non-enumerable
+ Date.now = NativeDate.now;
+ Date.UTC = NativeDate.UTC;
+ Date.prototype = NativeDate.prototype;
+ Date.prototype.constructor = Date;
+
+ // Upgrade Date.parse to handle the ISO dates we use
+ // TODO review specification to ascertain whether it is
+ // necessary to implement partial ISO date strings.
+ Date.parse = function parse(string) {
+ var match = isoDateExpression.exec(string);
+ if (match) {
+ match.shift(); // kill match[0], the full match
+ // recognize times without dates before normalizing the
+ // numeric values, for later use
+ var timeOnly = match[0] === undefined;
+ // parse numerics
+ for (var i = 0; i < 10; i++) {
+ // skip + or - for the timezone offset
+ if (i === 7)
+ continue;
+ // Note: parseInt would read 0-prefix numbers as
+ // octal. Number constructor or unary + work better
+ // here:
+ match[i] = +(match[i] || (i < 3 ? 1 : 0));
+ // match[1] is the month. Months are 0-11 in JavaScript
+ // Date objects, but 1-12 in ISO notation, so we
+ // decrement.
+ if (i === 1)
+ match[i]--;
+ }
+ // if no year-month-date is provided, return a milisecond
+ // quantity instead of a UTC date number value.
+ if (timeOnly)
+ return ((match[3] * 60 + match[4]) * 60 + match[5]) * 1000 + match[6];
+
+ // account for an explicit time zone offset if provided
+ var offset = (match[8] * 60 + match[9]) * 60 * 1000;
+ if (match[6] === "-")
+ offset = -offset;
+
+ return NativeDate.UTC.apply(this, match.slice(0, 7)) + offset;
+ }
+ return NativeDate.parse.apply(this, arguments);
+ };
+
+ return Date;
+ })(Date);
+}
+
+//
+// String
+// ======
+//
+
+// ES5 15.5.4.20
+if (!String.prototype.trim) {
+ // http://blog.stevenlevithan.com/archives/faster-trim-javascript
+ var trimBeginRegexp = /^\s\s*/;
+ var trimEndRegexp = /\s\s*$/;
+ String.prototype.trim = function trim() {
+ return String(this).replace(trimBeginRegexp, '').replace(trimEndRegexp, '');
+ };
+}
+
+});/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Kevin Dangoor (kdangoor@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/ace', ['require', 'exports', 'module' , 'pilot/index', 'pilot/fixoldbrowsers', 'pilot/plugin_manager', 'pilot/dom', 'pilot/event', 'ace/editor', 'ace/edit_session', 'ace/undomanager', 'ace/virtual_renderer', 'ace/theme/textmate', 'pilot/environment'], function(require, exports, module) {
+
+ require("pilot/index");
+ require("pilot/fixoldbrowsers");
+ var catalog = require("pilot/plugin_manager").catalog;
+ catalog.registerPlugins([ "pilot/index" ]);
+
+ var Dom = require("pilot/dom");
+ var Event = require("pilot/event");
+
+ var Editor = require("ace/editor").Editor;
+ var EditSession = require("ace/edit_session").EditSession;
+ var UndoManager = require("ace/undomanager").UndoManager;
+ var Renderer = require("ace/virtual_renderer").VirtualRenderer;
+
+ exports.edit = function(el) {
+ if (typeof(el) == "string") {
+ el = document.getElementById(el);
+ }
+
+ var doc = new EditSession(Dom.getInnerText(el));
+ doc.setUndoManager(new UndoManager());
+ el.innerHTML = '';
+
+ var editor = new Editor(new Renderer(el, require("ace/theme/textmate")));
+ editor.setSession(doc);
+
+ var env = require("pilot/environment").create();
+ catalog.startupPlugins({ env: env }).then(function() {
+ env.document = doc;
+ env.editor = editor;
+ editor.resize();
+ Event.addListener(window, "resize", function() {
+ editor.resize();
+ });
+ el.env = env;
+ });
+ // Store env on editor such that it can be accessed later on from
+ // the returned object.
+ editor.env = env;
+ return editor;
+ };
+});/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Kevin Dangoor (kdangoor@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/index', ['require', 'exports', 'module' , 'pilot/fixoldbrowsers', 'pilot/types/basic', 'pilot/types/command', 'pilot/types/settings', 'pilot/commands/settings', 'pilot/commands/basic', 'pilot/settings/canon', 'pilot/canon'], function(require, exports, module) {
+
+exports.startup = function(data, reason) {
+ require('pilot/fixoldbrowsers');
+
+ require('pilot/types/basic').startup(data, reason);
+ require('pilot/types/command').startup(data, reason);
+ require('pilot/types/settings').startup(data, reason);
+ require('pilot/commands/settings').startup(data, reason);
+ require('pilot/commands/basic').startup(data, reason);
+ // require('pilot/commands/history').startup(data, reason);
+ require('pilot/settings/canon').startup(data, reason);
+ require('pilot/canon').startup(data, reason);
+};
+
+exports.shutdown = function(data, reason) {
+ require('pilot/types/basic').shutdown(data, reason);
+ require('pilot/types/command').shutdown(data, reason);
+ require('pilot/types/settings').shutdown(data, reason);
+ require('pilot/commands/settings').shutdown(data, reason);
+ require('pilot/commands/basic').shutdown(data, reason);
+ // require('pilot/commands/history').shutdown(data, reason);
+ require('pilot/settings/canon').shutdown(data, reason);
+ require('pilot/canon').shutdown(data, reason);
+};
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com)
+ * Kevin Dangoor (kdangoor@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/types/basic', ['require', 'exports', 'module' , 'pilot/types'], function(require, exports, module) {
+
+var types = require("pilot/types");
+var Type = types.Type;
+var Conversion = types.Conversion;
+var Status = types.Status;
+
+/**
+ * These are the basic types that we accept. They are vaguely based on the
+ * Jetpack settings system (https://wiki.mozilla.org/Labs/Jetpack/JEP/24)
+ * although clearly more restricted.
+ *
+ * <p>In addition to these types, Jetpack also accepts range, member, password
+ * that we are thinking of adding.
+ *
+ * <p>This module probably should not be accessed directly, but instead used
+ * through types.js
+ */
+
+/**
+ * 'text' is the default if no type is given.
+ */
+var text = new Type();
+
+text.stringify = function(value) {
+ return value;
+};
+
+text.parse = function(value) {
+ if (typeof value != 'string') {
+ throw new Error('non-string passed to text.parse()');
+ }
+ return new Conversion(value);
+};
+
+text.name = 'text';
+
+/**
+ * We don't currently plan to distinguish between integers and floats
+ */
+var number = new Type();
+
+number.stringify = function(value) {
+ if (!value) {
+ return null;
+ }
+ return '' + value;
+};
+
+number.parse = function(value) {
+ if (typeof value != 'string') {
+ throw new Error('non-string passed to number.parse()');
+ }
+
+ if (value.replace(/\s/g, '').length === 0) {
+ return new Conversion(null, Status.INCOMPLETE, '');
+ }
+
+ var reply = new Conversion(parseInt(value, 10));
+ if (isNaN(reply.value)) {
+ reply.status = Status.INVALID;
+ reply.message = 'Can\'t convert "' + value + '" to a number.';
+ }
+
+ return reply;
+};
+
+number.decrement = function(value) {
+ return value - 1;
+};
+
+number.increment = function(value) {
+ return value + 1;
+};
+
+number.name = 'number';
+
+/**
+ * One of a known set of options
+ */
+function SelectionType(typeSpec) {
+ if (!Array.isArray(typeSpec.data) && typeof typeSpec.data !== 'function') {
+ throw new Error('instances of SelectionType need typeSpec.data to be an array or function that returns an array:' + JSON.stringify(typeSpec));
+ }
+ Object.keys(typeSpec).forEach(function(key) {
+ this[key] = typeSpec[key];
+ }, this);
+};
+
+SelectionType.prototype = new Type();
+
+SelectionType.prototype.stringify = function(value) {
+ return value;
+};
+
+SelectionType.prototype.parse = function(str) {
+ if (typeof str != 'string') {
+ throw new Error('non-string passed to parse()');
+ }
+ if (!this.data) {
+ throw new Error('Missing data on selection type extension.');
+ }
+ var data = (typeof(this.data) === 'function') ? this.data() : this.data;
+
+ // The matchedValue could be the boolean value false
+ var hasMatched = false;
+ var matchedValue;
+ var completions = [];
+ data.forEach(function(option) {
+ if (str == option) {
+ matchedValue = this.fromString(option);
+ hasMatched = true;
+ }
+ else if (option.indexOf(str) === 0) {
+ completions.push(this.fromString(option));
+ }
+ }, this);
+
+ if (hasMatched) {
+ return new Conversion(matchedValue);
+ }
+ else {
+ // This is something of a hack it basically allows us to tell the
+ // setting type to forget its last setting hack.
+ if (this.noMatch) {
+ this.noMatch();
+ }
+
+ if (completions.length > 0) {
+ var msg = 'Possibilities' +
+ (str.length === 0 ? '' : ' for \'' + str + '\'');
+ return new Conversion(null, Status.INCOMPLETE, msg, completions);
+ }
+ else {
+ var msg = 'Can\'t use \'' + str + '\'.';
+ return new Conversion(null, Status.INVALID, msg, completions);
+ }
+ }
+};
+
+SelectionType.prototype.fromString = function(str) {
+ return str;
+};
+
+SelectionType.prototype.decrement = function(value) {
+ var data = (typeof this.data === 'function') ? this.data() : this.data;
+ var index;
+ if (value == null) {
+ index = data.length - 1;
+ }
+ else {
+ var name = this.stringify(value);
+ var index = data.indexOf(name);
+ index = (index === 0 ? data.length - 1 : index - 1);
+ }
+ return this.fromString(data[index]);
+};
+
+SelectionType.prototype.increment = function(value) {
+ var data = (typeof this.data === 'function') ? this.data() : this.data;
+ var index;
+ if (value == null) {
+ index = 0;
+ }
+ else {
+ var name = this.stringify(value);
+ var index = data.indexOf(name);
+ index = (index === data.length - 1 ? 0 : index + 1);
+ }
+ return this.fromString(data[index]);
+};
+
+SelectionType.prototype.name = 'selection';
+
+/**
+ * SelectionType is a base class for other types
+ */
+exports.SelectionType = SelectionType;
+
+/**
+ * true/false values
+ */
+var bool = new SelectionType({
+ name: 'bool',
+ data: [ 'true', 'false' ],
+ stringify: function(value) {
+ return '' + value;
+ },
+ fromString: function(str) {
+ return str === 'true' ? true : false;
+ }
+});
+
+
+/**
+ * A we don't know right now, but hope to soon.
+ */
+function DeferredType(typeSpec) {
+ if (typeof typeSpec.defer !== 'function') {
+ throw new Error('Instances of DeferredType need typeSpec.defer to be a function that returns a type');
+ }
+ Object.keys(typeSpec).forEach(function(key) {
+ this[key] = typeSpec[key];
+ }, this);
+};
+
+DeferredType.prototype = new Type();
+
+DeferredType.prototype.stringify = function(value) {
+ return this.defer().stringify(value);
+};
+
+DeferredType.prototype.parse = function(value) {
+ return this.defer().parse(value);
+};
+
+DeferredType.prototype.decrement = function(value) {
+ var deferred = this.defer();
+ return (deferred.decrement ? deferred.decrement(value) : undefined);
+};
+
+DeferredType.prototype.increment = function(value) {
+ var deferred = this.defer();
+ return (deferred.increment ? deferred.increment(value) : undefined);
+};
+
+DeferredType.prototype.name = 'deferred';
+
+/**
+ * DeferredType is a base class for other types
+ */
+exports.DeferredType = DeferredType;
+
+
+/**
+ * A set of objects of the same type
+ */
+function ArrayType(typeSpec) {
+ if (typeSpec instanceof Type) {
+ this.subtype = typeSpec;
+ }
+ else if (typeof typeSpec === 'string') {
+ this.subtype = types.getType(typeSpec);
+ if (this.subtype == null) {
+ throw new Error('Unknown array subtype: ' + typeSpec);
+ }
+ }
+ else {
+ throw new Error('Can\' handle array subtype');
+ }
+};
+
+ArrayType.prototype = new Type();
+
+ArrayType.prototype.stringify = function(values) {
+ // TODO: Check for strings with spaces and add quotes
+ return values.join(' ');
+};
+
+ArrayType.prototype.parse = function(value) {
+ return this.defer().parse(value);
+};
+
+ArrayType.prototype.name = 'array';
+
+/**
+ * Registration and de-registration.
+ */
+var isStarted = false;
+exports.startup = function() {
+ if (isStarted) {
+ return;
+ }
+ isStarted = true;
+ types.registerType(text);
+ types.registerType(number);
+ types.registerType(bool);
+ types.registerType(SelectionType);
+ types.registerType(DeferredType);
+ types.registerType(ArrayType);
+};
+
+exports.shutdown = function() {
+ isStarted = false;
+ types.unregisterType(text);
+ types.unregisterType(number);
+ types.unregisterType(bool);
+ types.unregisterType(SelectionType);
+ types.unregisterType(DeferredType);
+ types.unregisterType(ArrayType);
+};
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/types', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+/**
+ * Some types can detect validity, that is to say they can distinguish between
+ * valid and invalid values.
+ * TODO: Change these constants to be numbers for more performance?
+ */
+var Status = {
+ /**
+ * The conversion process worked without any problem, and the value is
+ * valid. There are a number of failure states, so the best way to check
+ * for failure is (x !== Status.VALID)
+ */
+ VALID: {
+ toString: function() { return 'VALID'; },
+ valueOf: function() { return 0; }
+ },
+
+ /**
+ * A conversion process failed, however it was noted that the string
+ * provided to 'parse()' could be VALID by the addition of more characters,
+ * so the typing may not be actually incorrect yet, just unfinished.
+ * @see Status.INVALID
+ */
+ INCOMPLETE: {
+ toString: function() { return 'INCOMPLETE'; },
+ valueOf: function() { return 1; }
+ },
+
+ /**
+ * The conversion process did not work, the value should be null and a
+ * reason for failure should have been provided. In addition some completion
+ * values may be available.
+ * @see Status.INCOMPLETE
+ */
+ INVALID: {
+ toString: function() { return 'INVALID'; },
+ valueOf: function() { return 2; }
+ },
+
+ /**
+ * A combined status is the worser of the provided statuses
+ */
+ combine: function(statuses) {
+ var combined = Status.VALID;
+ for (var i = 0; i < statuses.length; i++) {
+ if (statuses[i].valueOf() > combined.valueOf()) {
+ combined = statuses[i];
+ }
+ }
+ return combined;
+ }
+};
+exports.Status = Status;
+
+/**
+ * The type.parse() method returns a Conversion to inform the user about not
+ * only the result of a Conversion but also about what went wrong.
+ * We could use an exception, and throw if the conversion failed, but that
+ * seems to violate the idea that exceptions should be exceptional. Typos are
+ * not. Also in order to store both a status and a message we'd still need
+ * some sort of exception type...
+ */
+function Conversion(value, status, message, predictions) {
+ /**
+ * The result of the conversion process. Will be null if status != VALID
+ */
+ this.value = value;
+
+ /**
+ * The status of the conversion.
+ * @see Status
+ */
+ this.status = status || Status.VALID;
+
+ /**
+ * A message to go with the conversion. This could be present for any status
+ * including VALID in the case where we want to note a warning for example.
+ * I18N: On the one hand this nasty and un-internationalized, however with
+ * a command line it is hard to know where to start.
+ */
+ this.message = message;
+
+ /**
+ * A array of strings which are the systems best guess at better inputs than
+ * the one presented.
+ * We generally expect there to be about 7 predictions (to match human list
+ * comprehension ability) however it is valid to provide up to about 20,
+ * or less. It is the job of the predictor to decide a smart cut-off.
+ * For example if there are 4 very good matches and 4 very poor ones,
+ * probably only the 4 very good matches should be presented.
+ */
+ this.predictions = predictions || [];
+}
+exports.Conversion = Conversion;
+
+/**
+ * Most of our types are 'static' e.g. there is only one type of 'text', however
+ * some types like 'selection' and 'deferred' are customizable. The basic
+ * Type type isn't useful, but does provide documentation about what types do.
+ */
+function Type() {
+};
+Type.prototype = {
+ /**
+ * Convert the given <tt>value</tt> to a string representation.
+ * Where possible, there should be round-tripping between values and their
+ * string representations.
+ */
+ stringify: function(value) { throw new Error("not implemented"); },
+
+ /**
+ * Convert the given <tt>str</tt> to an instance of this type.
+ * Where possible, there should be round-tripping between values and their
+ * string representations.
+ * @return Conversion
+ */
+ parse: function(str) { throw new Error("not implemented"); },
+
+ /**
+ * The plug-in system, and other things need to know what this type is
+ * called. The name alone is not enough to fully specify a type. Types like
+ * 'selection' and 'deferred' need extra data, however this function returns
+ * only the name, not the extra data.
+ * <p>In old bespin, equality was based on the name. This may turn out to be
+ * important in Ace too.
+ */
+ name: undefined,
+
+ /**
+ * If there is some concept of a higher value, return it,
+ * otherwise return undefined.
+ */
+ increment: function(value) {
+ return undefined;
+ },
+
+ /**
+ * If there is some concept of a lower value, return it,
+ * otherwise return undefined.
+ */
+ decrement: function(value) {
+ return undefined;
+ },
+
+ /**
+ * There is interesting information (like predictions) in a conversion of
+ * nothing, the output of this can sometimes be customized.
+ * @return Conversion
+ */
+ getDefault: function() {
+ return this.parse('');
+ }
+};
+exports.Type = Type;
+
+/**
+ * Private registry of types
+ * Invariant: types[name] = type.name
+ */
+var types = {};
+
+/**
+ * Add a new type to the list available to the system.
+ * You can pass 2 things to this function - either an instance of Type, in
+ * which case we return this instance when #getType() is called with a 'name'
+ * that matches type.name.
+ * Also you can pass in a constructor (i.e. function) in which case when
+ * #getType() is called with a 'name' that matches Type.prototype.name we will
+ * pass the typeSpec into this constructor. See #reconstituteType().
+ */
+exports.registerType = function(type) {
+ if (typeof type === 'object') {
+ if (type instanceof Type) {
+ if (!type.name) {
+ throw new Error('All registered types must have a name');
+ }
+ types[type.name] = type;
+ }
+ else {
+ throw new Error('Can\'t registerType using: ' + type);
+ }
+ }
+ else if (typeof type === 'function') {
+ if (!type.prototype.name) {
+ throw new Error('All registered types must have a name');
+ }
+ types[type.prototype.name] = type;
+ }
+ else {
+ throw new Error('Unknown type: ' + type);
+ }
+};
+
+exports.registerTypes = function registerTypes(types) {
+ Object.keys(types).forEach(function (name) {
+ var type = types[name];
+ type.name = name;
+ exports.registerType(type);
+ });
+};
+
+/**
+ * Remove a type from the list available to the system
+ */
+exports.deregisterType = function(type) {
+ delete types[type.name];
+};
+
+/**
+ * See description of #exports.registerType()
+ */
+function reconstituteType(name, typeSpec) {
+ if (name.substr(-2) === '[]') { // i.e. endsWith('[]')
+ var subtypeName = name.slice(0, -2);
+ return new types['array'](subtypeName);
+ }
+
+ var type = types[name];
+ if (typeof type === 'function') {
+ type = new type(typeSpec);
+ }
+ return type;
+}
+
+/**
+ * Find a type, previously registered using #registerType()
+ */
+exports.getType = function(typeSpec) {
+ if (typeof typeSpec === 'string') {
+ return reconstituteType(typeSpec);
+ }
+
+ if (typeof typeSpec === 'object') {
+ if (!typeSpec.name) {
+ throw new Error('Missing \'name\' member to typeSpec');
+ }
+ return reconstituteType(typeSpec.name, typeSpec);
+ }
+
+ throw new Error('Can\'t extract type from ' + typeSpec);
+};
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com)
+ * Kevin Dangoor (kdangoor@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/types/command', ['require', 'exports', 'module' , 'pilot/canon', 'pilot/types/basic', 'pilot/types'], function(require, exports, module) {
+
+var canon = require("pilot/canon");
+var SelectionType = require("pilot/types/basic").SelectionType;
+var types = require("pilot/types");
+
+
+/**
+ * Select from the available commands
+ */
+var command = new SelectionType({
+ name: 'command',
+ data: function() {
+ return canon.getCommandNames();
+ },
+ stringify: function(command) {
+ return command.name;
+ },
+ fromString: function(str) {
+ return canon.getCommand(str);
+ }
+});
+
+
+/**
+ * Registration and de-registration.
+ */
+exports.startup = function() {
+ types.registerType(command);
+};
+
+exports.shutdown = function() {
+ types.unregisterType(command);
+};
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/canon', ['require', 'exports', 'module' , 'pilot/console', 'pilot/stacktrace', 'pilot/oop', 'pilot/useragent', 'pilot/keys', 'pilot/event_emitter', 'pilot/typecheck', 'pilot/catalog', 'pilot/types', 'pilot/lang'], function(require, exports, module) {
+
+var console = require('pilot/console');
+var Trace = require('pilot/stacktrace').Trace;
+var oop = require('pilot/oop');
+var useragent = require('pilot/useragent');
+var keyUtil = require('pilot/keys');
+var EventEmitter = require('pilot/event_emitter').EventEmitter;
+var typecheck = require('pilot/typecheck');
+var catalog = require('pilot/catalog');
+var Status = require('pilot/types').Status;
+var types = require('pilot/types');
+var lang = require('pilot/lang');
+
+/*
+// TODO: this doesn't belong here - or maybe anywhere?
+var dimensionsChangedExtensionSpec = {
+ name: 'dimensionsChanged',
+ description: 'A dimensionsChanged is a way to be notified of ' +
+ 'changes to the dimension of Skywriter'
+};
+exports.startup = function(data, reason) {
+ catalog.addExtensionSpec(commandExtensionSpec);
+};
+exports.shutdown = function(data, reason) {
+ catalog.removeExtensionSpec(commandExtensionSpec);
+};
+*/
+
+var commandExtensionSpec = {
+ name: 'command',
+ description: 'A command is a bit of functionality with optional ' +
+ 'typed arguments which can do something small like moving ' +
+ 'the cursor around the screen, or large like cloning a ' +
+ 'project from VCS.',
+ indexOn: 'name'
+};
+
+exports.startup = function(data, reason) {
+ // TODO: this is probably all kinds of evil, but we need something working
+ catalog.addExtensionSpec(commandExtensionSpec);
+};
+
+exports.shutdown = function(data, reason) {
+ catalog.removeExtensionSpec(commandExtensionSpec);
+};
+
+/**
+ * Manage a list of commands in the current canon
+ */
+
+/**
+ * A Command is a discrete action optionally with a set of ways to customize
+ * how it happens. This is here for documentation purposes.
+ * TODO: Document better
+ */
+var thingCommand = {
+ name: 'thing',
+ description: 'thing is an example command',
+ params: [{
+ name: 'param1',
+ description: 'an example parameter',
+ type: 'text',
+ defaultValue: null
+ }],
+ exec: function(env, args, request) {
+ thing();
+ }
+};
+
+/**
+ * A lookup hash of our registered commands
+ */
+var commands = {};
+
+/**
+ * A lookup has for command key bindings that use a string as sender.
+ */
+var commmandKeyBinding = {};
+
+/**
+ * Array with command key bindings that use a function to determ the sender.
+ */
+var commandKeyBindingFunc = { };
+
+function splitSafe(s, separator, limit, bLowerCase) {
+ return (bLowerCase && s.toLowerCase() || s)
+ .replace(/(?:^\s+|\n|\s+$)/g, "")
+ .split(new RegExp("[\\s ]*" + separator + "[\\s ]*", "g"), limit || 999);
+}
+
+function parseKeys(keys, val, ret) {
+ var key,
+ hashId = 0,
+ parts = splitSafe(keys, "\\-", null, true),
+ i = 0,
+ l = parts.length;
+
+ for (; i < l; ++i) {
+ if (keyUtil.KEY_MODS[parts[i]])
+ hashId = hashId | keyUtil.KEY_MODS[parts[i]];
+ else
+ key = parts[i] || "-"; //when empty, the splitSafe removed a '-'
+ }
+
+ if (ret == null) {
+ return {
+ key: key,
+ hashId: hashId
+ }
+ } else {
+ (ret[hashId] || (ret[hashId] = {}))[key] = val;
+ }
+}
+
+var platform = useragent.isMac ? "mac" : "win";
+function buildKeyHash(command) {
+ var binding = command.bindKey,
+ key = binding[platform],
+ ckb = commmandKeyBinding,
+ ckbf = commandKeyBindingFunc
+
+ if (!binding.sender) {
+ throw new Error('All key bindings must have a sender');
+ }
+ if (!binding.mac && binding.mac !== null) {
+ throw new Error('All key bindings must have a mac key binding');
+ }
+ if (!binding.win && binding.win !== null) {
+ throw new Error('All key bindings must have a windows key binding');
+ }
+ if(!binding[platform]) {
+ // No keymapping for this platform.
+ return;
+ }
+ if (typeof binding.sender == 'string') {
+ var targets = splitSafe(binding.sender, "\\|", null, true);
+ targets.forEach(function(target) {
+ if (!ckb[target]) {
+ ckb[target] = { };
+ }
+ key.split("|").forEach(function(keyPart) {
+ parseKeys(keyPart, command, ckb[target]);
+ });
+ });
+ } else if (typecheck.isFunction(binding.sender)) {
+ var val = {
+ command: command,
+ sender: binding.sender
+ };
+
+ keyData = parseKeys(key);
+ if (!ckbf[keyData.hashId]) {
+ ckbf[keyData.hashId] = { };
+ }
+ if (!ckbf[keyData.hashId][keyData.key]) {
+ ckbf[keyData.hashId][keyData.key] = [ val ];
+ } else {
+ ckbf[keyData.hashId][keyData.key].push(val);
+ }
+ } else {
+ throw new Error('Key binding must have a sender that is a string or function');
+ }
+}
+
+function findKeyCommand(env, sender, hashId, textOrKey) {
+ // Convert keyCode to the string representation.
+ if (typecheck.isNumber(textOrKey)) {
+ textOrKey = keyUtil.keyCodeToString(textOrKey);
+ }
+
+ // Check bindings with functions as sender first.
+ var bindFuncs = (commandKeyBindingFunc[hashId] || {})[textOrKey] || [];
+ for (var i = 0; i < bindFuncs.length; i++) {
+ if (bindFuncs[i].sender(env, sender, hashId, textOrKey)) {
+ return bindFuncs[i].command;
+ }
+ }
+
+ var ckbr = commmandKeyBinding[sender]
+ return ckbr && ckbr[hashId] && ckbr[hashId][textOrKey];
+}
+
+function execKeyCommand(env, sender, hashId, textOrKey) {
+ var command = findKeyCommand(env, sender, hashId, textOrKey);
+ if (command) {
+ return exec(command, env, sender, { });
+ } else {
+ return false;
+ }
+}
+
+/**
+ * A sorted list of command names, we regularly want them in order, so pre-sort
+ */
+var commandNames = [];
+
+/**
+ * This registration method isn't like other Ace registration methods because
+ * it doesn't return a decorated command because there is no functional
+ * decoration to be done.
+ * TODO: Are we sure that in the future there will be no such decoration?
+ */
+function addCommand(command) {
+ if (!command.name) {
+ throw new Error('All registered commands must have a name');
+ }
+ if (command.params == null) {
+ command.params = [];
+ }
+ if (!Array.isArray(command.params)) {
+ throw new Error('command.params must be an array in ' + command.name);
+ }
+ // Replace the type
+ command.params.forEach(function(param) {
+ if (!param.name) {
+ throw new Error('In ' + command.name + ': all params must have a name');
+ }
+ upgradeType(command.name, param);
+ }, this);
+ commands[command.name] = command;
+
+ if (command.bindKey) {
+ buildKeyHash(command);
+ }
+
+ commandNames.push(command.name);
+ commandNames.sort();
+};
+
+function upgradeType(name, param) {
+ var lookup = param.type;
+ param.type = types.getType(lookup);
+ if (param.type == null) {
+ throw new Error('In ' + name + '/' + param.name +
+ ': can\'t find type for: ' + JSON.stringify(lookup));
+ }
+}
+
+function removeCommand(command) {
+ var name = (typeof command === 'string' ? command : command.name);
+ delete commands[name];
+ lang.arrayRemove(commandNames, name);
+};
+
+function getCommand(name) {
+ return commands[name];
+};
+
+function getCommandNames() {
+ return commandNames;
+};
+
+/**
+ * Default ArgumentProvider that is used if no ArgumentProvider is provided
+ * by the command's sender.
+ */
+function defaultArgsProvider(request, callback) {
+ var args = request.args,
+ params = request.command.params;
+
+ for (var i = 0; i < params.length; i++) {
+ var param = params[i];
+
+ // If the parameter is already valid, then don't ask for it anymore.
+ if (request.getParamStatus(param) != Status.VALID ||
+ // Ask for optional parameters as well.
+ param.defaultValue === null)
+ {
+ var paramPrompt = param.description;
+ if (param.defaultValue === null) {
+ paramPrompt += " (optional)";
+ }
+ var value = prompt(paramPrompt, param.defaultValue || "");
+ // No value but required -> nope.
+ if (!value) {
+ callback();
+ return;
+ } else {
+ args[param.name] = value;
+ }
+ }
+ }
+ callback();
+}
+
+/**
+ * Entry point for keyboard accelerators or anything else that wants to execute
+ * a command. A new request object is created and a check performed, if the
+ * passed in arguments are VALID/INVALID or INCOMPLETE. If they are INCOMPLETE
+ * the ArgumentProvider on the sender is called or otherwise the default
+ * ArgumentProvider to get the still required arguments.
+ * If they are valid (or valid after the ArgumentProvider is done), the command
+ * is executed.
+ *
+ * @param command Either a command, or the name of one
+ * @param env Current environment to execute the command in
+ * @param sender String that should be the same as the senderObject stored on
+ * the environment in env[sender]
+ * @param args Arguments for the command
+ * @param typed (Optional)
+ */
+function exec(command, env, sender, args, typed) {
+ if (typeof command === 'string') {
+ command = commands[command];
+ }
+ if (!command) {
+ // TODO: Should we complain more than returning false?
+ return false;
+ }
+
+ var request = new Request({
+ sender: sender,
+ command: command,
+ args: args || {},
+ typed: typed
+ });
+
+ /**
+ * Executes the command and ensures request.done is called on the request in
+ * case it's not marked to be done already or async.
+ */
+ function execute() {
+ command.exec(env, request.args, request);
+
+ // If the request isn't asnync and isn't done, then make it done.
+ if (!request.isAsync && !request.isDone) {
+ request.done();
+ }
+ }
+
+
+ if (request.getStatus() == Status.INVALID) {
+ console.error("Canon.exec: Invalid parameter(s) passed to " +
+ command.name);
+ return false;
+ }
+ // If the request isn't complete yet, try to complete it.
+ else if (request.getStatus() == Status.INCOMPLETE) {
+ // Check if the sender has a ArgsProvider, otherwise use the default
+ // build in one.
+ var argsProvider;
+ var senderObj = env[sender];
+ if (!senderObj || !senderObj.getArgsProvider ||
+ !(argsProvider = senderObj.getArgsProvider()))
+ {
+ argsProvider = defaultArgsProvider;
+ }
+
+ // Ask the paramProvider to complete the request.
+ argsProvider(request, function() {
+ if (request.getStatus() == Status.VALID) {
+ execute();
+ }
+ });
+ return true;
+ } else {
+ execute();
+ return true;
+ }
+};
+
+exports.removeCommand = removeCommand;
+exports.addCommand = addCommand;
+exports.getCommand = getCommand;
+exports.getCommandNames = getCommandNames;
+exports.findKeyCommand = findKeyCommand;
+exports.exec = exec;
+exports.execKeyCommand = execKeyCommand;
+exports.upgradeType = upgradeType;
+
+
+/**
+ * We publish a 'output' event whenever new command begins output
+ * TODO: make this more obvious
+ */
+oop.implement(exports, EventEmitter);
+
+
+/**
+ * Current requirements are around displaying the command line, and provision
+ * of a 'history' command and cursor up|down navigation of history.
+ * <p>Future requirements could include:
+ * <ul>
+ * <li>Multiple command lines
+ * <li>The ability to recall key presses (i.e. requests with no output) which
+ * will likely be needed for macro recording or similar
+ * <li>The ability to store the command history either on the server or in the
+ * browser local storage.
+ * </ul>
+ * <p>The execute() command doesn't really live here, except as part of that
+ * last future requirement, and because it doesn't really have anywhere else to
+ * live.
+ */
+
+/**
+ * The array of requests that wish to announce their presence
+ */
+var requests = [];
+
+/**
+ * How many requests do we store?
+ */
+var maxRequestLength = 100;
+
+/**
+ * To create an invocation, you need to do something like this (all the ctor
+ * args are optional):
+ * <pre>
+ * var request = new Request({
+ * command: command,
+ * args: args,
+ * typed: typed
+ * });
+ * </pre>
+ * @constructor
+ */
+function Request(options) {
+ options = options || {};
+
+ // Will be used in the keyboard case and the cli case
+ this.command = options.command;
+
+ // Will be used only in the cli case
+ this.args = options.args;
+ this.typed = options.typed;
+
+ // Have we been initialized?
+ this._begunOutput = false;
+
+ this.start = new Date();
+ this.end = null;
+ this.completed = false;
+ this.error = false;
+};
+
+oop.implement(Request.prototype, EventEmitter);
+
+/**
+ * Return the status of a parameter on the request object.
+ */
+Request.prototype.getParamStatus = function(param) {
+ var args = this.args || {};
+
+ // Check if there is already a value for this parameter.
+ if (param.name in args) {
+ // If there is no value set and then the value is VALID if it's not
+ // required or INCOMPLETE if not set yet.
+ if (args[param.name] == null) {
+ if (param.defaultValue === null) {
+ return Status.VALID;
+ } else {
+ return Status.INCOMPLETE;
+ }
+ }
+
+ // Check if the parameter value is valid.
+ var reply,
+ // The passed in value when parsing a type is a string.
+ argsValue = args[param.name].toString();
+
+ // Type.parse can throw errors.
+ try {
+ reply = param.type.parse(argsValue);
+ } catch (e) {
+ return Status.INVALID;
+ }
+
+ if (reply.status != Status.VALID) {
+ return reply.status;
+ }
+ }
+ // Check if the param is marked as required.
+ else if (param.defaultValue === undefined) {
+ // The parameter is not set on the args object but it's required,
+ // which means, things are invalid.
+ return Status.INCOMPLETE;
+ }
+
+ return Status.VALID;
+}
+
+/**
+ * Return the status of a parameter name on the request object.
+ */
+Request.prototype.getParamNameStatus = function(paramName) {
+ var params = this.command.params || [];
+
+ for (var i = 0; i < params.length; i++) {
+ if (params[i].name == paramName) {
+ return this.getParamStatus(params[i]);
+ }
+ }
+
+ throw "Parameter '" + paramName +
+ "' not defined on command '" + this.command.name + "'";
+}
+
+/**
+ * Checks if all required arguments are set on the request such that it can
+ * get executed.
+ */
+Request.prototype.getStatus = function() {
+ var args = this.args || {},
+ params = this.command.params;
+
+ // If there are not parameters, then it's valid.
+ if (!params || params.length == 0) {
+ return Status.VALID;
+ }
+
+ var status = [];
+ for (var i = 0; i < params.length; i++) {
+ status.push(this.getParamStatus(params[i]));
+ }
+
+ return Status.combine(status);
+}
+
+/**
+ * Lazy init to register with the history should only be done on output.
+ * init() is expensive, and won't be used in the majority of cases
+ */
+Request.prototype._beginOutput = function() {
+ this._begunOutput = true;
+ this.outputs = [];
+
+ requests.push(this);
+ // This could probably be optimized with some maths, but 99.99% of the
+ // time we will only be off by one, and I'm feeling lazy.
+ while (requests.length > maxRequestLength) {
+ requests.shiftObject();
+ }
+
+ exports._dispatchEvent('output', { requests: requests, request: this });
+};
+
+/**
+ * Sugar for:
+ * <pre>request.error = true; request.done(output);</pre>
+ */
+Request.prototype.doneWithError = function(content) {
+ this.error = true;
+ this.done(content);
+};
+
+/**
+ * Declares that this function will not be automatically done when
+ * the command exits
+ */
+Request.prototype.async = function() {
+ this.isAsync = true;
+ if (!this._begunOutput) {
+ this._beginOutput();
+ }
+};
+
+/**
+ * Complete the currently executing command with successful output.
+ * @param output Either DOM node, an SproutCore element or something that
+ * can be used in the content of a DIV to create a DOM node.
+ */
+Request.prototype.output = function(content) {
+ if (!this._begunOutput) {
+ this._beginOutput();
+ }
+
+ if (typeof content !== 'string' && !(content instanceof Node)) {
+ content = content.toString();
+ }
+
+ this.outputs.push(content);
+ this.isDone = true;
+ this._dispatchEvent('output', {});
+
+ return this;
+};
+
+/**
+ * All commands that do output must call this to indicate that the command
+ * has finished execution.
+ */
+Request.prototype.done = function(content) {
+ this.completed = true;
+ this.end = new Date();
+ this.duration = this.end.getTime() - this.start.getTime();
+
+ if (content) {
+ this.output(content);
+ }
+
+ // Ensure to finish the request only once.
+ if (!this.isDone) {
+ this.isDone = true;
+ this._dispatchEvent('output', {});
+ }
+};
+exports.Request = Request;
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com)
+ * Patrick Walton (pwalton@mozilla.com)
+ * Julian Viereck (jviereck@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+define('pilot/console', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+/**
+ * This object represents a "safe console" object that forwards debugging
+ * messages appropriately without creating a dependency on Firebug in Firefox.
+ */
+
+var noop = function() {};
+
+// These are the functions that are available in Chrome 4/5, Safari 4
+// and Firefox 3.6. Don't add to this list without checking browser support
+var NAMES = [
+ "assert", "count", "debug", "dir", "dirxml", "error", "group", "groupEnd",
+ "info", "log", "profile", "profileEnd", "time", "timeEnd", "trace", "warn"
+];
+
+if (typeof(window) === 'undefined') {
+ // We're in a web worker. Forward to the main thread so the messages
+ // will show up.
+ NAMES.forEach(function(name) {
+ exports[name] = function() {
+ var args = Array.prototype.slice.call(arguments);
+ var msg = { op: 'log', method: name, args: args };
+ postMessage(JSON.stringify(msg));
+ };
+ });
+} else {
+ // For each of the console functions, copy them if they exist, stub if not
+ NAMES.forEach(function(name) {
+ if (window.console && window.console[name]) {
+ exports[name] = Function.prototype.bind.call(window.console[name], window.console);
+ } else {
+ exports[name] = noop;
+ }
+ });
+}
+
+});
+define('pilot/stacktrace', ['require', 'exports', 'module' , 'pilot/useragent', 'pilot/console'], function(require, exports, module) {
+
+var ua = require("pilot/useragent");
+var console = require('pilot/console');
+
+// Changed to suit the specific needs of running within Skywriter
+
+// Domain Public by Eric Wendelin http://eriwen.com/ (2008)
+// Luke Smith http://lucassmith.name/ (2008)
+// Loic Dachary <loic@dachary.org> (2008)
+// Johan Euphrosine <proppy@aminche.com> (2008)
+// Øyvind Sean Kinsey http://kinsey.no/blog
+//
+// Information and discussions
+// http://jspoker.pokersource.info/skin/test-printstacktrace.html
+// http://eriwen.com/javascript/js-stack-trace/
+// http://eriwen.com/javascript/stacktrace-update/
+// http://pastie.org/253058
+// http://browsershots.org/http://jspoker.pokersource.info/skin/test-printstacktrace.html
+//
+
+//
+// guessFunctionNameFromLines comes from firebug
+//
+// Software License Agreement (BSD License)
+//
+// Copyright (c) 2007, Parakey Inc.
+// All rights reserved.
+//
+// Redistribution and use of this software in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// * Redistributions of source code must retain the above
+// copyright notice, this list of conditions and the
+// following disclaimer.
+//
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the
+// following disclaimer in the documentation and/or other
+// materials provided with the distribution.
+//
+// * Neither the name of Parakey Inc. nor the names of its
+// contributors may be used to endorse or promote products
+// derived from this software without specific prior
+// written permission of Parakey Inc.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+
+/**
+ * Different browsers create stack traces in different ways.
+ * <strike>Feature</strike> Browser detection baby ;).
+ */
+var mode = (function() {
+
+ // We use SC's browser detection here to avoid the "break on error"
+ // functionality provided by Firebug. Firebug tries to do the right
+ // thing here and break, but it happens every time you load the page.
+ // bug 554105
+ if (ua.isGecko) {
+ return 'firefox';
+ } else if (ua.isOpera) {
+ return 'opera';
+ } else {
+ return 'other';
+ }
+
+ // SC doesn't do any detection of Chrome at this time.
+
+ // this is the original feature detection code that is used as a
+ // fallback.
+ try {
+ (0)();
+ } catch (e) {
+ if (e.arguments) {
+ return 'chrome';
+ }
+ if (e.stack) {
+ return 'firefox';
+ }
+ if (window.opera && !('stacktrace' in e)) { //Opera 9-
+ return 'opera';
+ }
+ }
+ return 'other';
+})();
+
+/**
+ *
+ */
+function stringifyArguments(args) {
+ for (var i = 0; i < args.length; ++i) {
+ var argument = args[i];
+ if (typeof argument == 'object') {
+ args[i] = '#object';
+ } else if (typeof argument == 'function') {
+ args[i] = '#function';
+ } else if (typeof argument == 'string') {
+ args[i] = '"' + argument + '"';
+ }
+ }
+ return args.join(',');
+}
+
+/**
+ * Extract a stack trace from the format emitted by each browser.
+ */
+var decoders = {
+ chrome: function(e) {
+ var stack = e.stack;
+ if (!stack) {
+ console.log(e);
+ return [];
+ }
+ return stack.replace(/^.*?\n/, '').
+ replace(/^.*?\n/, '').
+ replace(/^.*?\n/, '').
+ replace(/^[^\(]+?[\n$]/gm, '').
+ replace(/^\s+at\s+/gm, '').
+ replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@').
+ split('\n');
+ },
+
+ firefox: function(e) {
+ var stack = e.stack;
+ if (!stack) {
+ console.log(e);
+ return [];
+ }
+ // stack = stack.replace(/^.*?\n/, '');
+ stack = stack.replace(/(?:\n@:0)?\s+$/m, '');
+ stack = stack.replace(/^\(/gm, '{anonymous}(');
+ return stack.split('\n');
+ },
+
+ // Opera 7.x and 8.x only!
+ opera: function(e) {
+ var lines = e.message.split('\n'), ANON = '{anonymous}',
+ lineRE = /Line\s+(\d+).*?script\s+(http\S+)(?:.*?in\s+function\s+(\S+))?/i, i, j, len;
+
+ for (i = 4, j = 0, len = lines.length; i < len; i += 2) {
+ if (lineRE.test(lines[i])) {
+ lines[j++] = (RegExp.$3 ? RegExp.$3 + '()@' + RegExp.$2 + RegExp.$1 : ANON + '()@' + RegExp.$2 + ':' + RegExp.$1) +
+ ' -- ' +
+ lines[i + 1].replace(/^\s+/, '');
+ }
+ }
+
+ lines.splice(j, lines.length - j);
+ return lines;
+ },
+
+ // Safari, Opera 9+, IE, and others
+ other: function(curr) {
+ var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, stack = [], j = 0, fn, args;
+
+ var maxStackSize = 10;
+ while (curr && stack.length < maxStackSize) {
+ fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON;
+ args = Array.prototype.slice.call(curr['arguments']);
+ stack[j++] = fn + '(' + stringifyArguments(args) + ')';
+
+ //Opera bug: if curr.caller does not exist, Opera returns curr (WTF)
+ if (curr === curr.caller && window.opera) {
+ //TODO: check for same arguments if possible
+ break;
+ }
+ curr = curr.caller;
+ }
+ return stack;
+ }
+};
+
+/**
+ *
+ */
+function NameGuesser() {
+}
+
+NameGuesser.prototype = {
+
+ sourceCache: {},
+
+ ajax: function(url) {
+ var req = this.createXMLHTTPObject();
+ if (!req) {
+ return;
+ }
+ req.open('GET', url, false);
+ req.setRequestHeader('User-Agent', 'XMLHTTP/1.0');
+ req.send('');
+ return req.responseText;
+ },
+
+ createXMLHTTPObject: function() {
+ // Try XHR methods in order and store XHR factory
+ var xmlhttp, XMLHttpFactories = [
+ function() {
+ return new XMLHttpRequest();
+ }, function() {
+ return new ActiveXObject('Msxml2.XMLHTTP');
+ }, function() {
+ return new ActiveXObject('Msxml3.XMLHTTP');
+ }, function() {
+ return new ActiveXObject('Microsoft.XMLHTTP');
+ }
+ ];
+ for (var i = 0; i < XMLHttpFactories.length; i++) {
+ try {
+ xmlhttp = XMLHttpFactories[i]();
+ // Use memoization to cache the factory
+ this.createXMLHTTPObject = XMLHttpFactories[i];
+ return xmlhttp;
+ } catch (e) {}
+ }
+ },
+
+ getSource: function(url) {
+ if (!(url in this.sourceCache)) {
+ this.sourceCache[url] = this.ajax(url).split('\n');
+ }
+ return this.sourceCache[url];
+ },
+
+ guessFunctions: function(stack) {
+ for (var i = 0; i < stack.length; ++i) {
+ var reStack = /{anonymous}\(.*\)@(\w+:\/\/([-\w\.]+)+(:\d+)?[^:]+):(\d+):?(\d+)?/;
+ var frame = stack[i], m = reStack.exec(frame);
+ if (m) {
+ var file = m[1], lineno = m[4]; //m[7] is character position in Chrome
+ if (file && lineno) {
+ var functionName = this.guessFunctionName(file, lineno);
+ stack[i] = frame.replace('{anonymous}', functionName);
+ }
+ }
+ }
+ return stack;
+ },
+
+ guessFunctionName: function(url, lineNo) {
+ try {
+ return this.guessFunctionNameFromLines(lineNo, this.getSource(url));
+ } catch (e) {
+ return 'getSource failed with url: ' + url + ', exception: ' + e.toString();
+ }
+ },
+
+ guessFunctionNameFromLines: function(lineNo, source) {
+ var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/;
+ var reGuessFunction = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(function|eval|new Function)/;
+ // Walk backwards from the first line in the function until we find the line which
+ // matches the pattern above, which is the function definition
+ var line = '', maxLines = 10;
+ for (var i = 0; i < maxLines; ++i) {
+ line = source[lineNo - i] + line;
+ if (line !== undefined) {
+ var m = reGuessFunction.exec(line);
+ if (m) {
+ return m[1];
+ }
+ else {
+ m = reFunctionArgNames.exec(line);
+ }
+ if (m && m[1]) {
+ return m[1];
+ }
+ }
+ }
+ return '(?)';
+ }
+};
+
+var guesser = new NameGuesser();
+
+var frameIgnorePatterns = [
+ /http:\/\/localhost:4020\/sproutcore.js:/
+];
+
+exports.ignoreFramesMatching = function(regex) {
+ frameIgnorePatterns.push(regex);
+};
+
+/**
+ * Create a stack trace from an exception
+ * @param ex {Error} The error to create a stacktrace from (optional)
+ * @param guess {Boolean} If we should try to resolve the names of anonymous functions
+ */
+exports.Trace = function Trace(ex, guess) {
+ this._ex = ex;
+ this._stack = decoders[mode](ex);
+
+ if (guess) {
+ this._stack = guesser.guessFunctions(this._stack);
+ }
+};
+
+/**
+ * Log to the console a number of lines (default all of them)
+ * @param lines {number} Maximum number of lines to wrote to console
+ */
+exports.Trace.prototype.log = function(lines) {
+ if (lines <= 0) {
+ // You aren't going to have more lines in your stack trace than this
+ // and it still fits in a 32bit integer
+ lines = 999999999;
+ }
+
+ var printed = 0;
+ for (var i = 0; i < this._stack.length && printed < lines; i++) {
+ var frame = this._stack[i];
+ var display = true;
+ frameIgnorePatterns.forEach(function(regex) {
+ if (regex.test(frame)) {
+ display = false;
+ }
+ });
+ if (display) {
+ console.debug(frame);
+ printed++;
+ }
+ }
+};
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/useragent', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+var os = (navigator.platform.match(/mac|win|linux/i) || ["other"])[0].toLowerCase();
+var ua = navigator.userAgent;
+var av = navigator.appVersion;
+
+/** Is the user using a browser that identifies itself as Windows */
+exports.isWin = (os == "win");
+
+/** Is the user using a browser that identifies itself as Mac OS */
+exports.isMac = (os == "mac");
+
+/** Is the user using a browser that identifies itself as Linux */
+exports.isLinux = (os == "linux");
+
+exports.isIE = ! + "\v1";
+
+/** Is this Firefox or related? */
+exports.isGecko = exports.isMozilla = window.controllers && window.navigator.product === "Gecko";
+
+/** oldGecko == rev < 2.0 **/
+exports.isOldGecko = exports.isGecko && /rv\:1/.test(navigator.userAgent);
+
+/** Is this Opera */
+exports.isOpera = window.opera && Object.prototype.toString.call(window.opera) == "[object Opera]";
+
+/** Is the user using a browser that identifies itself as WebKit */
+exports.isWebKit = parseFloat(ua.split("WebKit/")[1]) || undefined;
+
+exports.isAIR = ua.indexOf("AdobeAIR") >= 0;
+
+exports.isIPad = ua.indexOf("iPad") >= 0;
+
+/**
+ * I hate doing this, but we need some way to determine if the user is on a Mac
+ * The reason is that users have different expectations of their key combinations.
+ *
+ * Take copy as an example, Mac people expect to use CMD or APPLE + C
+ * Windows folks expect to use CTRL + C
+ */
+exports.OS = {
+ LINUX: 'LINUX',
+ MAC: 'MAC',
+ WINDOWS: 'WINDOWS'
+};
+
+/**
+ * Return an exports.OS constant
+ */
+exports.getOS = function() {
+ if (exports.isMac) {
+ return exports.OS['MAC'];
+ } else if (exports.isLinux) {
+ return exports.OS['LINUX'];
+ } else {
+ return exports.OS['WINDOWS'];
+ }
+};
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/oop', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+exports.inherits = (function() {
+ var tempCtor = function() {};
+ return function(ctor, superCtor) {
+ tempCtor.prototype = superCtor.prototype;
+ ctor.super_ = superCtor.prototype;
+ ctor.prototype = new tempCtor();
+ ctor.prototype.constructor = ctor;
+ }
+}());
+
+exports.mixin = function(obj, mixin) {
+ for (var key in mixin) {
+ obj[key] = mixin[key];
+ }
+};
+
+exports.implement = function(proto, mixin) {
+ exports.mixin(proto, mixin);
+};
+
+});
+/*! @license
+==========================================================================
+SproutCore -- JavaScript Application Framework
+copyright 2006-2009, Sprout Systems Inc., Apple Inc. and contributors.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+
+SproutCore and the SproutCore logo are trademarks of Sprout Systems, Inc.
+
+For more information about SproutCore, visit http://www.sproutcore.com
+
+
+==========================================================================
+@license */
+
+// Most of the following code is taken from SproutCore with a few changes.
+
+define('pilot/keys', ['require', 'exports', 'module' , 'pilot/oop'], function(require, exports, module) {
+
+var oop = require("pilot/oop");
+
+/**
+ * Helper functions and hashes for key handling.
+ */
+var Keys = (function() {
+ var ret = {
+ MODIFIER_KEYS: {
+ 16: 'Shift', 17: 'Ctrl', 18: 'Alt', 224: 'Meta'
+ },
+
+ KEY_MODS: {
+ "ctrl": 1, "alt": 2, "option" : 2,
+ "shift": 4, "meta": 8, "command": 8
+ },
+
+ FUNCTION_KEYS : {
+ 8 : "Backspace",
+ 9 : "Tab",
+ 13 : "Return",
+ 19 : "Pause",
+ 27 : "Esc",
+ 32 : "Space",
+ 33 : "PageUp",
+ 34 : "PageDown",
+ 35 : "End",
+ 36 : "Home",
+ 37 : "Left",
+ 38 : "Up",
+ 39 : "Right",
+ 40 : "Down",
+ 44 : "Print",
+ 45 : "Insert",
+ 46 : "Delete",
+ 112: "F1",
+ 113: "F2",
+ 114: "F3",
+ 115: "F4",
+ 116: "F5",
+ 117: "F6",
+ 118: "F7",
+ 119: "F8",
+ 120: "F9",
+ 121: "F10",
+ 122: "F11",
+ 123: "F12",
+ 144: "Numlock",
+ 145: "Scrolllock"
+ },
+
+ PRINTABLE_KEYS: {
+ 32: ' ', 48: '0', 49: '1', 50: '2', 51: '3', 52: '4', 53: '5',
+ 54: '6', 55: '7', 56: '8', 57: '9', 59: ';', 61: '=', 65: 'a',
+ 66: 'b', 67: 'c', 68: 'd', 69: 'e', 70: 'f', 71: 'g', 72: 'h',
+ 73: 'i', 74: 'j', 75: 'k', 76: 'l', 77: 'm', 78: 'n', 79: 'o',
+ 80: 'p', 81: 'q', 82: 'r', 83: 's', 84: 't', 85: 'u', 86: 'v',
+ 87: 'w', 88: 'x', 89: 'y', 90: 'z', 107: '+', 109: '-', 110: '.',
+ 188: ',', 190: '.', 191: '/', 192: '`', 219: '[', 220: '\\',
+ 221: ']', 222: '\"'
+ }
+ };
+
+ // A reverse map of FUNCTION_KEYS
+ for (i in ret.FUNCTION_KEYS) {
+ var name = ret.FUNCTION_KEYS[i].toUpperCase();
+ ret[name] = parseInt(i, 10);
+ }
+
+ // Add the MODIFIER_KEYS, FUNCTION_KEYS and PRINTABLE_KEYS to the KEY
+ // variables as well.
+ oop.mixin(ret, ret.MODIFIER_KEYS);
+ oop.mixin(ret, ret.PRINTABLE_KEYS);
+ oop.mixin(ret, ret.FUNCTION_KEYS);
+
+ return ret;
+})();
+oop.mixin(exports, Keys);
+
+exports.keyCodeToString = function(keyCode) {
+ return (Keys[keyCode] || String.fromCharCode(keyCode)).toLowerCase();
+}
+
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/event_emitter', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+var EventEmitter = {};
+
+EventEmitter._emit =
+EventEmitter._dispatchEvent = function(eventName, e) {
+ this._eventRegistry = this._eventRegistry || {};
+
+ var listeners = this._eventRegistry[eventName];
+ if (!listeners || !listeners.length) return;
+
+ var e = e || {};
+ e.type = eventName;
+
+ for (var i=0; i<listeners.length; i++) {
+ listeners[i](e);
+ }
+};
+
+EventEmitter.on =
+EventEmitter.addEventListener = function(eventName, callback) {
+ this._eventRegistry = this._eventRegistry || {};
+
+ var listeners = this._eventRegistry[eventName];
+ if (!listeners) {
+ var listeners = this._eventRegistry[eventName] = [];
+ }
+ if (listeners.indexOf(callback) == -1) {
+ listeners.push(callback);
+ }
+};
+
+EventEmitter.removeListener =
+EventEmitter.removeEventListener = function(eventName, callback) {
+ this._eventRegistry = this._eventRegistry || {};
+
+ var listeners = this._eventRegistry[eventName];
+ if (!listeners) {
+ return;
+ }
+ var index = listeners.indexOf(callback);
+ if (index !== -1) {
+ listeners.splice(index, 1);
+ }
+};
+
+EventEmitter.removeAllListeners = function(eventName) {
+ if (this._eventRegistry) this._eventRegistry[eventName] = [];
+}
+
+exports.EventEmitter = EventEmitter;
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/typecheck', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+var objectToString = Object.prototype.toString;
+
+/**
+ * Return true if it is a String
+ */
+exports.isString = function(it) {
+ return it && objectToString.call(it) === "[object String]";
+};
+
+/**
+ * Returns true if it is a Boolean.
+ */
+exports.isBoolean = function(it) {
+ return it && objectToString.call(it) === "[object Boolean]";
+};
+
+/**
+ * Returns true if it is a Number.
+ */
+exports.isNumber = function(it) {
+ return it && objectToString.call(it) === "[object Number]" && isFinite(it);
+};
+
+/**
+ * Hack copied from dojo.
+ */
+exports.isObject = function(it) {
+ return it !== undefined &&
+ (it === null || typeof it == "object" ||
+ Array.isArray(it) || exports.isFunction(it));
+};
+
+/**
+ * Is the passed object a function?
+ * From dojo.isFunction()
+ */
+exports.isFunction = function(it) {
+ return it && objectToString.call(it) === "[object Function]";
+};
+
+});/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Julian Viereck (jviereck@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/catalog', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+
+var extensionSpecs = {};
+
+exports.addExtensionSpec = function(extensionSpec) {
+ extensionSpecs[extensionSpec.name] = extensionSpec;
+};
+
+exports.removeExtensionSpec = function(extensionSpec) {
+ if (typeof extensionSpec === "string") {
+ delete extensionSpecs[extensionSpec];
+ }
+ else {
+ delete extensionSpecs[extensionSpec.name];
+ }
+};
+
+exports.getExtensionSpec = function(name) {
+ return extensionSpecs[name];
+};
+
+exports.getExtensionSpecs = function() {
+ return Object.keys(extensionSpecs);
+};
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/lang', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+exports.stringReverse = function(string) {
+ return string.split("").reverse().join("");
+};
+
+exports.stringRepeat = function (string, count) {
+ return new Array(count + 1).join(string);
+};
+
+var trimBeginRegexp = /^\s\s*/;
+var trimEndRegexp = /\s\s*$/;
+
+exports.stringTrimLeft = function (string) {
+ return string.replace(trimBeginRegexp, '')
+};
+
+exports.stringTrimRight = function (string) {
+ return string.replace(trimEndRegexp, '');
+};
+
+exports.copyObject = function(obj) {
+ var copy = {};
+ for (var key in obj) {
+ copy[key] = obj[key];
+ }
+ return copy;
+};
+
+exports.copyArray = function(array){
+ var copy = [];
+ for (i=0, l=array.length; i<l; i++) {
+ if (array[i] && typeof array[i] == "object")
+ copy[i] = this.copyObject( array[i] );
+ else
+ copy[i] = array[i]
+ }
+ return copy;
+};
+
+exports.deepCopy = function (obj) {
+ if (typeof obj != "object") {
+ return obj;
+ }
+
+ var copy = obj.constructor();
+ for (var key in obj) {
+ if (typeof obj[key] == "object") {
+ copy[key] = this.deepCopy(obj[key]);
+ } else {
+ copy[key] = obj[key];
+ }
+ }
+ return copy;
+}
+
+exports.arrayToMap = function(arr) {
+ var map = {};
+ for (var i=0; i<arr.length; i++) {
+ map[arr[i]] = 1;
+ }
+ return map;
+
+};
+
+/**
+ * splice out of 'array' anything that === 'value'
+ */
+exports.arrayRemove = function(array, value) {
+ for (var i = 0; i <= array.length; i++) {
+ if (value === array[i]) {
+ array.splice(i, 1);
+ }
+ }
+};
+
+exports.escapeRegExp = function(str) {
+ return str.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1');
+};
+
+exports.deferredCall = function(fcn) {
+
+ var timer = null;
+ var callback = function() {
+ timer = null;
+ fcn();
+ };
+
+ var deferred = function(timeout) {
+ if (!timer) {
+ timer = setTimeout(callback, timeout || 0);
+ }
+ return deferred;
+ }
+
+ deferred.schedule = deferred;
+
+ deferred.call = function() {
+ this.cancel();
+ fcn();
+ return deferred;
+ };
+
+ deferred.cancel = function() {
+ clearTimeout(timer);
+ timer = null;
+ return deferred;
+ };
+
+ return deferred;
+};
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com)
+ * Kevin Dangoor (kdangoor@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/types/settings', ['require', 'exports', 'module' , 'pilot/types/basic', 'pilot/types', 'pilot/settings'], function(require, exports, module) {
+
+var SelectionType = require('pilot/types/basic').SelectionType;
+var DeferredType = require('pilot/types/basic').DeferredType;
+var types = require('pilot/types');
+var settings = require('pilot/settings').settings;
+
+
+/**
+ * EVIL: This relies on us using settingValue in the same event as setting
+ * The alternative is to have some central place where we store the current
+ * command line, but this might be a lesser evil for now.
+ */
+var lastSetting;
+
+/**
+ * Select from the available settings
+ */
+var setting = new SelectionType({
+ name: 'setting',
+ data: function() {
+ return env.settings.getSettingNames();
+ },
+ stringify: function(setting) {
+ lastSetting = setting;
+ return setting.name;
+ },
+ fromString: function(str) {
+ lastSetting = settings.getSetting(str);
+ return lastSetting;
+ },
+ noMatch: function() {
+ lastSetting = null;
+ }
+});
+
+/**
+ * Something of a hack to allow the set command to give a clearer definition
+ * of the type to the command line.
+ */
+var settingValue = new DeferredType({
+ name: 'settingValue',
+ defer: function() {
+ if (lastSetting) {
+ return lastSetting.type;
+ }
+ else {
+ return types.getType('text');
+ }
+ },
+ /**
+ * Promote the current value in any list of predictions, and add it if
+ * there are none.
+ */
+ getDefault: function() {
+ var conversion = this.parse('');
+ if (lastSetting) {
+ var current = lastSetting.get();
+ if (conversion.predictions.length === 0) {
+ conversion.predictions.push(current);
+ }
+ else {
+ // Remove current from predictions
+ var removed = false;
+ while (true) {
+ var index = conversion.predictions.indexOf(current);
+ if (index === -1) {
+ break;
+ }
+ conversion.predictions.splice(index, 1);
+ removed = true;
+ }
+ // If the current value wasn't something we would predict, leave it
+ if (removed) {
+ conversion.predictions.push(current);
+ }
+ }
+ }
+ return conversion;
+ }
+});
+
+var env;
+
+/**
+ * Registration and de-registration.
+ */
+exports.startup = function(data, reason) {
+ // TODO: this is probably all kinds of evil, but we need something working
+ env = data.env;
+ types.registerType(setting);
+ types.registerType(settingValue);
+};
+
+exports.shutdown = function(data, reason) {
+ types.unregisterType(setting);
+ types.unregisterType(settingValue);
+};
+
+
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com)
+ * Julian Viereck (jviereck@mozilla.com)
+ * Kevin Dangoor (kdangoor@mozilla.com)
+ * Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/settings', ['require', 'exports', 'module' , 'pilot/console', 'pilot/oop', 'pilot/types', 'pilot/event_emitter', 'pilot/catalog'], function(require, exports, module) {
+
+/**
+ * This plug-in manages settings.
+ */
+
+var console = require('pilot/console');
+var oop = require('pilot/oop');
+var types = require('pilot/types');
+var EventEmitter = require('pilot/event_emitter').EventEmitter;
+var catalog = require('pilot/catalog');
+
+var settingExtensionSpec = {
+ name: 'setting',
+ description: 'A setting is something that the application offers as a ' +
+ 'way to customize how it works',
+ register: 'env.settings.addSetting',
+ indexOn: 'name'
+};
+
+exports.startup = function(data, reason) {
+ catalog.addExtensionSpec(settingExtensionSpec);
+};
+
+exports.shutdown = function(data, reason) {
+ catalog.removeExtensionSpec(settingExtensionSpec);
+};
+
+
+/**
+ * Create a new setting.
+ * @param settingSpec An object literal that looks like this:
+ * {
+ * name: 'thing',
+ * description: 'Thing is an example setting',
+ * type: 'string',
+ * defaultValue: 'something'
+ * }
+ */
+function Setting(settingSpec, settings) {
+ this._settings = settings;
+
+ Object.keys(settingSpec).forEach(function(key) {
+ this[key] = settingSpec[key];
+ }, this);
+
+ this.type = types.getType(this.type);
+ if (this.type == null) {
+ throw new Error('In ' + this.name +
+ ': can\'t find type for: ' + JSON.stringify(settingSpec.type));
+ }
+
+ if (!this.name) {
+ throw new Error('Setting.name == undefined. Ignoring.', this);
+ }
+
+ if (!this.defaultValue === undefined) {
+ throw new Error('Setting.defaultValue == undefined', this);
+ }
+
+ if (this.onChange) {
+ this.on('change', this.onChange.bind(this))
+ }
+
+ this.set(this.defaultValue);
+}
+Setting.prototype = {
+ get: function() {
+ return this.value;
+ },
+
+ set: function(value) {
+ if (this.value === value) {
+ return;
+ }
+
+ this.value = value;
+ if (this._settings.persister) {
+ this._settings.persister.persistValue(this._settings, this.name, value);
+ }
+
+ this._dispatchEvent('change', { setting: this, value: value });
+ },
+
+ /**
+ * Reset the value of the <code>key</code> setting to it's default
+ */
+ resetValue: function() {
+ this.set(this.defaultValue);
+ }
+};
+oop.implement(Setting.prototype, EventEmitter);
+
+
+/**
+ * A base class for all the various methods of storing settings.
+ * <p>Usage:
+ * <pre>
+ * // Create manually, or require 'settings' from the container.
+ * // This is the manual version:
+ * var settings = plugins.catalog.getObject('settings');
+ * // Add a new setting
+ * settings.addSetting({ name:'foo', ... });
+ * // Display the default value
+ * alert(settings.get('foo'));
+ * // Alter the value, which also publishes the change etc.
+ * settings.set('foo', 'bar');
+ * // Reset the value to the default
+ * settings.resetValue('foo');
+ * </pre>
+ * @constructor
+ */
+function Settings(persister) {
+ // Storage for deactivated values
+ this._deactivated = {};
+
+ // Storage for the active settings
+ this._settings = {};
+ // We often want sorted setting names. Cache
+ this._settingNames = [];
+
+ if (persister) {
+ this.setPersister(persister);
+ }
+};
+
+Settings.prototype = {
+ /**
+ * Function to add to the list of available settings.
+ * <p>Example usage:
+ * <pre>
+ * var settings = plugins.catalog.getObject('settings');
+ * settings.addSetting({
+ * name: 'tabsize', // For use in settings.get('X')
+ * type: 'number', // To allow value checking.
+ * defaultValue: 4 // Default value for use when none is directly set
+ * });
+ * </pre>
+ * @param {object} settingSpec Object containing name/type/defaultValue members.
+ */
+ addSetting: function(settingSpec) {
+ var setting = new Setting(settingSpec, this);
+ this._settings[setting.name] = setting;
+ this._settingNames.push(setting.name);
+ this._settingNames.sort();
+ },
+
+ addSettings: function addSettings(settings) {
+ Object.keys(settings).forEach(function (name) {
+ var setting = settings[name];
+ if (!('name' in setting)) setting.name = name;
+ this.addSetting(setting);
+ }, this);
+ },
+
+ removeSetting: function(setting) {
+ var name = (typeof setting === 'string' ? setting : setting.name);
+ setting = this._settings[name];
+ delete this._settings[name];
+ util.arrayRemove(this._settingNames, name);
+ settings.removeAllListeners('change');
+ },
+
+ removeSettings: function removeSettings(settings) {
+ Object.keys(settings).forEach(function(name) {
+ var setting = settings[name];
+ if (!('name' in setting)) setting.name = name;
+ this.removeSettings(setting);
+ }, this);
+ },
+
+ getSettingNames: function() {
+ return this._settingNames;
+ },
+
+ getSetting: function(name) {
+ return this._settings[name];
+ },
+
+ /**
+ * A Persister is able to store settings. It is an object that defines
+ * two functions:
+ * loadInitialValues(settings) and persistValue(settings, key, value).
+ */
+ setPersister: function(persister) {
+ this._persister = persister;
+ if (persister) {
+ persister.loadInitialValues(this);
+ }
+ },
+
+ resetAll: function() {
+ this.getSettingNames().forEach(function(key) {
+ this.resetValue(key);
+ }, this);
+ },
+
+ /**
+ * Retrieve a list of the known settings and their values
+ */
+ _list: function() {
+ var reply = [];
+ this.getSettingNames().forEach(function(setting) {
+ reply.push({
+ 'key': setting,
+ 'value': this.getSetting(setting).get()
+ });
+ }, this);
+ return reply;
+ },
+
+ /**
+ * Prime the local cache with the defaults.
+ */
+ _loadDefaultValues: function() {
+ this._loadFromObject(this._getDefaultValues());
+ },
+
+ /**
+ * Utility to load settings from an object
+ */
+ _loadFromObject: function(data) {
+ // We iterate over data rather than keys so we don't forget values
+ // which don't have a setting yet.
+ for (var key in data) {
+ if (data.hasOwnProperty(key)) {
+ var setting = this._settings[key];
+ if (setting) {
+ var value = setting.type.parse(data[key]);
+ this.set(key, value);
+ } else {
+ this.set(key, data[key]);
+ }
+ }
+ }
+ },
+
+ /**
+ * Utility to grab all the settings and export them into an object
+ */
+ _saveToObject: function() {
+ return this.getSettingNames().map(function(key) {
+ return this._settings[key].type.stringify(this.get(key));
+ }.bind(this));
+ },
+
+ /**
+ * The default initial settings
+ */
+ _getDefaultValues: function() {
+ return this.getSettingNames().map(function(key) {
+ return this._settings[key].spec.defaultValue;
+ }.bind(this));
+ }
+};
+exports.settings = new Settings();
+
+/**
+ * Save the settings in a cookie
+ * This code has not been tested since reboot
+ * @constructor
+ */
+function CookiePersister() {
+};
+
+CookiePersister.prototype = {
+ loadInitialValues: function(settings) {
+ settings._loadDefaultValues();
+ var data = cookie.get('settings');
+ settings._loadFromObject(JSON.parse(data));
+ },
+
+ persistValue: function(settings, key, value) {
+ try {
+ var stringData = JSON.stringify(settings._saveToObject());
+ cookie.set('settings', stringData);
+ } catch (ex) {
+ console.error('Unable to JSONify the settings! ' + ex);
+ return;
+ }
+ }
+};
+
+exports.CookiePersister = CookiePersister;
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Skywriter Team (skywriter@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/commands/settings', ['require', 'exports', 'module' , 'pilot/canon'], function(require, exports, module) {
+
+
+var setCommandSpec = {
+ name: 'set',
+ params: [
+ {
+ name: 'setting',
+ type: 'setting',
+ description: 'The name of the setting to display or alter',
+ defaultValue: null
+ },
+ {
+ name: 'value',
+ type: 'settingValue',
+ description: 'The new value for the chosen setting',
+ defaultValue: null
+ }
+ ],
+ description: 'define and show settings',
+ exec: function(env, args, request) {
+ var html;
+ if (!args.setting) {
+ // 'set' by itself lists all the settings
+ var names = env.settings.getSettingNames();
+ html = '';
+ // first sort the settingsList based on the name
+ names.sort(function(name1, name2) {
+ return name1.localeCompare(name2);
+ });
+
+ names.forEach(function(name) {
+ var setting = env.settings.getSetting(name);
+ var url = 'https://wiki.mozilla.org/Labs/Skywriter/Settings#' +
+ setting.name;
+ html += '<a class="setting" href="' + url +
+ '" title="View external documentation on setting: ' +
+ setting.name +
+ '" target="_blank">' +
+ setting.name +
+ '</a> = ' +
+ setting.value +
+ '<br/>';
+ });
+ } else {
+ // set with only a setting, shows the value for that setting
+ if (args.value === undefined) {
+ html = '<strong>' + setting.name + '</strong> = ' +
+ setting.get();
+ } else {
+ // Actually change the setting
+ args.setting.set(args.value);
+ html = 'Setting: <strong>' + args.setting.name + '</strong> = ' +
+ args.setting.get();
+ }
+ }
+ request.done(html);
+ }
+};
+
+var unsetCommandSpec = {
+ name: 'unset',
+ params: [
+ {
+ name: 'setting',
+ type: 'setting',
+ description: 'The name of the setting to return to defaults'
+ }
+ ],
+ description: 'unset a setting entirely',
+ exec: function(env, args, request) {
+ var setting = env.settings.get(args.setting);
+ if (!setting) {
+ request.doneWithError('No setting with the name <strong>' +
+ args.setting + '</strong>.');
+ return;
+ }
+
+ setting.reset();
+ request.done('Reset ' + setting.name + ' to default: ' +
+ env.settings.get(args.setting));
+ }
+};
+
+var canon = require('pilot/canon');
+
+exports.startup = function(data, reason) {
+ canon.addCommand(setCommandSpec);
+ canon.addCommand(unsetCommandSpec);
+};
+
+exports.shutdown = function(data, reason) {
+ canon.removeCommand(setCommandSpec);
+ canon.removeCommand(unsetCommandSpec);
+};
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Skywriter Team (skywriter@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/commands/basic', ['require', 'exports', 'module' , 'pilot/typecheck', 'pilot/canon'], function(require, exports, module) {
+
+
+var checks = require("pilot/typecheck");
+var canon = require('pilot/canon');
+
+/**
+ *
+ */
+var helpMessages = {
+ plainPrefix:
+ '<h2>Welcome to Skywriter - Code in the Cloud</h2><ul>' +
+ '<li><a href="http://labs.mozilla.com/projects/skywriter" target="_blank">Home Page</a></li>' +
+ '<li><a href="https://wiki.mozilla.org/Labs/Skywriter" target="_blank">Wiki</a></li>' +
+ '<li><a href="https://wiki.mozilla.org/Labs/Skywriter/UserGuide" target="_blank">User Guide</a></li>' +
+ '<li><a href="https://wiki.mozilla.org/Labs/Skywriter/Tips" target="_blank">Tips and Tricks</a></li>' +
+ '<li><a href="https://wiki.mozilla.org/Labs/Skywriter/FAQ" target="_blank">FAQ</a></li>' +
+ '<li><a href="https://wiki.mozilla.org/Labs/Skywriter/DeveloperGuide" target="_blank">Developers Guide</a></li>' +
+ '</ul>',
+ plainSuffix:
+ 'For more information, see the <a href="https://wiki.mozilla.org/Labs/Skywriter">Skywriter Wiki</a>.'
+};
+
+/**
+ * 'help' command
+ */
+var helpCommandSpec = {
+ name: 'help',
+ params: [
+ {
+ name: 'search',
+ type: 'text',
+ description: 'Search string to narrow the output.',
+ defaultValue: null
+ }
+ ],
+ description: 'Get help on the available commands.',
+ exec: function(env, args, request) {
+ var output = [];
+
+ var command = canon.getCommand(args.search);
+ if (command && command.exec) {
+ // caught a real command
+ output.push(command.description ?
+ command.description :
+ 'No description for ' + args.search);
+ } else {
+ var showHidden = false;
+
+ if (!args.search && helpMessages.plainPrefix) {
+ output.push(helpMessages.plainPrefix);
+ }
+
+ if (command) {
+ // We must be looking at sub-commands
+ output.push('<h2>Sub-Commands of ' + command.name + '</h2>');
+ output.push('<p>' + command.description + '</p>');
+ }
+ else if (args.search) {
+ if (args.search == 'hidden') { // sneaky, sneaky.
+ args.search = '';
+ showHidden = true;
+ }
+ output.push('<h2>Commands starting with \'' + args.search + '\':</h2>');
+ }
+ else {
+ output.push('<h2>Available Commands:</h2>');
+ }
+
+ var commandNames = canon.getCommandNames();
+ commandNames.sort();
+
+ output.push('<table>');
+ for (var i = 0; i < commandNames.length; i++) {
+ command = canon.getCommand(commandNames[i]);
+ if (!showHidden && command.hidden) {
+ continue;
+ }
+ if (command.description === undefined) {
+ // Ignore editor actions
+ continue;
+ }
+ if (args.search && command.name.indexOf(args.search) !== 0) {
+ // Filtered out by the user
+ continue;
+ }
+ if (!args.search && command.name.indexOf(' ') != -1) {
+ // sub command
+ continue;
+ }
+ if (command && command.name == args.search) {
+ // sub command, and we've already given that help
+ continue;
+ }
+
+ // todo add back a column with parameter information, perhaps?
+
+ output.push('<tr>');
+ output.push('<th class="right">' + command.name + '</th>');
+ output.push('<td>' + command.description + '</td>');
+ output.push('</tr>');
+ }
+ output.push('</table>');
+
+ if (!args.search && helpMessages.plainSuffix) {
+ output.push(helpMessages.plainSuffix);
+ }
+ }
+
+ request.done(output.join(''));
+ }
+};
+
+/**
+ * 'eval' command
+ */
+var evalCommandSpec = {
+ name: 'eval',
+ params: [
+ {
+ name: 'javascript',
+ type: 'text',
+ description: 'The JavaScript to evaluate'
+ }
+ ],
+ description: 'evals given js code and show the result',
+ hidden: true,
+ exec: function(env, args, request) {
+ var result;
+ var javascript = args.javascript;
+ try {
+ result = eval(javascript);
+ } catch (e) {
+ result = '<b>Error: ' + e.message + '</b>';
+ }
+
+ var msg = '';
+ var type = '';
+ var x;
+
+ if (checks.isFunction(result)) {
+ // converts the function to a well formated string
+ msg = (result + '').replace(/\n/g, '<br>').replace(/ /g, '&#160');
+ type = 'function';
+ } else if (checks.isObject(result)) {
+ if (Array.isArray(result)) {
+ type = 'array';
+ } else {
+ type = 'object';
+ }
+
+ var items = [];
+ var value;
+
+ for (x in result) {
+ if (result.hasOwnProperty(x)) {
+ if (checks.isFunction(result[x])) {
+ value = '[function]';
+ } else if (checks.isObject(result[x])) {
+ value = '[object]';
+ } else {
+ value = result[x];
+ }
+
+ items.push({name: x, value: value});
+ }
+ }
+
+ items.sort(function(a,b) {
+ return (a.name.toLowerCase() < b.name.toLowerCase()) ? -1 : 1;
+ });
+
+ for (x = 0; x < items.length; x++) {
+ msg += '<b>' + items[x].name + '</b>: ' + items[x].value + '<br>';
+ }
+
+ } else {
+ msg = result;
+ type = typeof result;
+ }
+
+ request.done('Result for eval <b>\'' + javascript + '\'</b>' +
+ ' (type: '+ type+'): <br><br>'+ msg);
+ }
+};
+
+/**
+ * 'version' command
+ */
+var versionCommandSpec = {
+ name: 'version',
+ description: 'show the Skywriter version',
+ hidden: true,
+ exec: function(env, args, request) {
+ var version = 'Skywriter ' + skywriter.versionNumber + ' (' +
+ skywriter.versionCodename + ')';
+ request.done(version);
+ }
+};
+
+/**
+ * 'skywriter' command
+ */
+var skywriterCommandSpec = {
+ name: 'skywriter',
+ hidden: true,
+ exec: function(env, args, request) {
+ var index = Math.floor(Math.random() * messages.length);
+ request.done('Skywriter ' + messages[index]);
+ }
+};
+var messages = [
+ 'really wants you to trick it out in some way.',
+ 'is your Web editor.',
+ 'would love to be like Emacs on the Web.',
+ 'is written on the Web platform, so you can tweak it.'
+];
+
+
+var canon = require('pilot/canon');
+
+exports.startup = function(data, reason) {
+ canon.addCommand(helpCommandSpec);
+ canon.addCommand(evalCommandSpec);
+ // canon.addCommand(versionCommandSpec);
+ canon.addCommand(skywriterCommandSpec);
+};
+
+exports.shutdown = function(data, reason) {
+ canon.removeCommand(helpCommandSpec);
+ canon.removeCommand(evalCommandSpec);
+ // canon.removeCommand(versionCommandSpec);
+ canon.removeCommand(skywriterCommandSpec);
+};
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/settings/canon', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+
+var historyLengthSetting = {
+ name: "historyLength",
+ description: "How many typed commands do we recall for reference?",
+ type: "number",
+ defaultValue: 50
+};
+
+exports.startup = function(data, reason) {
+ data.env.settings.addSetting(historyLengthSetting);
+};
+
+exports.shutdown = function(data, reason) {
+ data.env.settings.removeSetting(historyLengthSetting);
+};
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Kevin Dangoor (kdangoor@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/plugin_manager', ['require', 'exports', 'module' , 'pilot/promise'], function(require, exports, module) {
+
+var Promise = require("pilot/promise").Promise;
+
+exports.REASONS = {
+ APP_STARTUP: 1,
+ APP_SHUTDOWN: 2,
+ PLUGIN_ENABLE: 3,
+ PLUGIN_DISABLE: 4,
+ PLUGIN_INSTALL: 5,
+ PLUGIN_UNINSTALL: 6,
+ PLUGIN_UPGRADE: 7,
+ PLUGIN_DOWNGRADE: 8
+};
+
+exports.Plugin = function(name) {
+ this.name = name;
+ this.status = this.INSTALLED;
+};
+
+exports.Plugin.prototype = {
+ /**
+ * constants for the state
+ */
+ NEW: 0,
+ INSTALLED: 1,
+ REGISTERED: 2,
+ STARTED: 3,
+ UNREGISTERED: 4,
+ SHUTDOWN: 5,
+
+ install: function(data, reason) {
+ var pr = new Promise();
+ if (this.status > this.NEW) {
+ pr.resolve(this);
+ return pr;
+ }
+ require([this.name], function(pluginModule) {
+ if (pluginModule.install) {
+ pluginModule.install(data, reason);
+ }
+ this.status = this.INSTALLED;
+ pr.resolve(this);
+ }.bind(this));
+ return pr;
+ },
+
+ register: function(data, reason) {
+ var pr = new Promise();
+ if (this.status != this.INSTALLED) {
+ pr.resolve(this);
+ return pr;
+ }
+ require([this.name], function(pluginModule) {
+ if (pluginModule.register) {
+ pluginModule.register(data, reason);
+ }
+ this.status = this.REGISTERED;
+ pr.resolve(this);
+ }.bind(this));
+ return pr;
+ },
+
+ startup: function(data, reason) {
+ reason = reason || exports.REASONS.APP_STARTUP;
+ var pr = new Promise();
+ if (this.status != this.REGISTERED) {
+ pr.resolve(this);
+ return pr;
+ }
+ require([this.name], function(pluginModule) {
+ if (pluginModule.startup) {
+ pluginModule.startup(data, reason);
+ }
+ this.status = this.STARTED;
+ pr.resolve(this);
+ }.bind(this));
+ return pr;
+ },
+
+ shutdown: function(data, reason) {
+ if (this.status != this.STARTED) {
+ return;
+ }
+ pluginModule = require(this.name);
+ if (pluginModule.shutdown) {
+ pluginModule.shutdown(data, reason);
+ }
+ }
+};
+
+exports.PluginCatalog = function() {
+ this.plugins = {};
+};
+
+exports.PluginCatalog.prototype = {
+ registerPlugins: function(pluginList, data, reason) {
+ var registrationPromises = [];
+ pluginList.forEach(function(pluginName) {
+ var plugin = this.plugins[pluginName];
+ if (plugin === undefined) {
+ plugin = new exports.Plugin(pluginName);
+ this.plugins[pluginName] = plugin;
+ registrationPromises.push(plugin.register(data, reason));
+ }
+ }.bind(this));
+ return Promise.group(registrationPromises);
+ },
+
+ startupPlugins: function(data, reason) {
+ var startupPromises = [];
+ for (var pluginName in this.plugins) {
+ var plugin = this.plugins[pluginName];
+ startupPromises.push(plugin.startup(data, reason));
+ }
+ return Promise.group(startupPromises);
+ }
+};
+
+exports.catalog = new exports.PluginCatalog();
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/promise', ['require', 'exports', 'module' , 'pilot/console', 'pilot/stacktrace'], function(require, exports, module) {
+
+var console = require("pilot/console");
+var Trace = require('pilot/stacktrace').Trace;
+
+/**
+ * A promise can be in one of 2 states.
+ * The ERROR and SUCCESS states are terminal, the PENDING state is the only
+ * start state.
+ */
+var ERROR = -1;
+var PENDING = 0;
+var SUCCESS = 1;
+
+/**
+ * We give promises and ID so we can track which are outstanding
+ */
+var _nextId = 0;
+
+/**
+ * Debugging help if 2 things try to complete the same promise.
+ * This can be slow (especially on chrome due to the stack trace unwinding) so
+ * we should leave this turned off in normal use.
+ */
+var _traceCompletion = false;
+
+/**
+ * Outstanding promises. Handy list for debugging only.
+ */
+var _outstanding = [];
+
+/**
+ * Recently resolved promises. Also for debugging only.
+ */
+var _recent = [];
+
+/**
+ * Create an unfulfilled promise
+ */
+Promise = function () {
+ this._status = PENDING;
+ this._value = undefined;
+ this._onSuccessHandlers = [];
+ this._onErrorHandlers = [];
+
+ // Debugging help
+ this._id = _nextId++;
+ //this._createTrace = new Trace(new Error());
+ _outstanding[this._id] = this;
+};
+
+/**
+ * Yeay for RTTI.
+ */
+Promise.prototype.isPromise = true;
+
+/**
+ * Have we either been resolve()ed or reject()ed?
+ */
+Promise.prototype.isComplete = function() {
+ return this._status != PENDING;
+};
+
+/**
+ * Have we resolve()ed?
+ */
+Promise.prototype.isResolved = function() {
+ return this._status == SUCCESS;
+};
+
+/**
+ * Have we reject()ed?
+ */
+Promise.prototype.isRejected = function() {
+ return this._status == ERROR;
+};
+
+/**
+ * Take the specified action of fulfillment of a promise, and (optionally)
+ * a different action on promise rejection.
+ */
+Promise.prototype.then = function(onSuccess, onError) {
+ if (typeof onSuccess === 'function') {
+ if (this._status === SUCCESS) {
+ onSuccess.call(null, this._value);
+ } else if (this._status === PENDING) {
+ this._onSuccessHandlers.push(onSuccess);
+ }
+ }
+
+ if (typeof onError === 'function') {
+ if (this._status === ERROR) {
+ onError.call(null, this._value);
+ } else if (this._status === PENDING) {
+ this._onErrorHandlers.push(onError);
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Like then() except that rather than returning <tt>this</tt> we return
+ * a promise which
+ */
+Promise.prototype.chainPromise = function(onSuccess) {
+ var chain = new Promise();
+ chain._chainedFrom = this;
+ this.then(function(data) {
+ try {
+ chain.resolve(onSuccess(data));
+ } catch (ex) {
+ chain.reject(ex);
+ }
+ }, function(ex) {
+ chain.reject(ex);
+ });
+ return chain;
+};
+
+/**
+ * Supply the fulfillment of a promise
+ */
+Promise.prototype.resolve = function(data) {
+ return this._complete(this._onSuccessHandlers, SUCCESS, data, 'resolve');
+};
+
+/**
+ * Renege on a promise
+ */
+Promise.prototype.reject = function(data) {
+ return this._complete(this._onErrorHandlers, ERROR, data, 'reject');
+};
+
+/**
+ * Internal method to be called on resolve() or reject().
+ * @private
+ */
+Promise.prototype._complete = function(list, status, data, name) {
+ // Complain if we've already been completed
+ if (this._status != PENDING) {
+ console.group('Promise already closed');
+ console.error('Attempted ' + name + '() with ', data);
+ console.error('Previous status = ', this._status,
+ ', previous value = ', this._value);
+ console.trace();
+
+ if (this._completeTrace) {
+ console.error('Trace of previous completion:');
+ this._completeTrace.log(5);
+ }
+ console.groupEnd();
+ return this;
+ }
+
+ if (_traceCompletion) {
+ this._completeTrace = new Trace(new Error());
+ }
+
+ this._status = status;
+ this._value = data;
+
+ // Call all the handlers, and then delete them
+ list.forEach(function(handler) {
+ handler.call(null, this._value);
+ }, this);
+ this._onSuccessHandlers.length = 0;
+ this._onErrorHandlers.length = 0;
+
+ // Remove the given {promise} from the _outstanding list, and add it to the
+ // _recent list, pruning more than 20 recent promises from that list.
+ delete _outstanding[this._id];
+ _recent.push(this);
+ while (_recent.length > 20) {
+ _recent.shift();
+ }
+
+ return this;
+};
+
+/**
+ * Takes an array of promises and returns a promise that that is fulfilled once
+ * all the promises in the array are fulfilled
+ * @param group The array of promises
+ * @return the promise that is fulfilled when all the array is fulfilled
+ */
+Promise.group = function(promiseList) {
+ if (!(promiseList instanceof Array)) {
+ promiseList = Array.prototype.slice.call(arguments);
+ }
+
+ // If the original array has nothing in it, return now to avoid waiting
+ if (promiseList.length === 0) {
+ return new Promise().resolve([]);
+ }
+
+ var groupPromise = new Promise();
+ var results = [];
+ var fulfilled = 0;
+
+ var onSuccessFactory = function(index) {
+ return function(data) {
+ results[index] = data;
+ fulfilled++;
+ // If the group has already failed, silently drop extra results
+ if (groupPromise._status !== ERROR) {
+ if (fulfilled === promiseList.length) {
+ groupPromise.resolve(results);
+ }
+ }
+ };
+ };
+
+ promiseList.forEach(function(promise, index) {
+ var onSuccess = onSuccessFactory(index);
+ var onError = groupPromise.reject.bind(groupPromise);
+ promise.then(onSuccess, onError);
+ });
+
+ return groupPromise;
+};
+
+exports.Promise = Promise;
+exports._outstanding = _outstanding;
+exports._recent = _recent;
+
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Mihai Sucan <mihai AT sucan AT gmail ODT com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/dom', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+var XHTML_NS = "http://www.w3.org/1999/xhtml";
+
+exports.createElement = function(tag, ns) {
+ return document.createElementNS ?
+ document.createElementNS(ns || XHTML_NS, tag) :
+ document.createElement(tag);
+};
+
+exports.setText = function(elem, text) {
+ if (elem.innerText !== undefined) {
+ elem.innerText = text;
+ }
+ if (elem.textContent !== undefined) {
+ elem.textContent = text;
+ }
+};
+
+if (!document.documentElement.classList) {
+ exports.hasCssClass = function(el, name) {
+ var classes = el.className.split(/\s+/g);
+ return classes.indexOf(name) !== -1;
+ };
+
+ /**
+ * Add a CSS class to the list of classes on the given node
+ */
+ exports.addCssClass = function(el, name) {
+ if (!exports.hasCssClass(el, name)) {
+ el.className += " " + name;
+ }
+ };
+
+ /**
+ * Remove a CSS class from the list of classes on the given node
+ */
+ exports.removeCssClass = function(el, name) {
+ var classes = el.className.split(/\s+/g);
+ while (true) {
+ var index = classes.indexOf(name);
+ if (index == -1) {
+ break;
+ }
+ classes.splice(index, 1);
+ }
+ el.className = classes.join(" ");
+ };
+
+ exports.toggleCssClass = function(el, name) {
+ var classes = el.className.split(/\s+/g), add = true;
+ while (true) {
+ var index = classes.indexOf(name);
+ if (index == -1) {
+ break;
+ }
+ add = false;
+ classes.splice(index, 1);
+ }
+ if(add)
+ classes.push(name);
+
+ el.className = classes.join(" ");
+ return add;
+ };
+} else {
+ exports.hasCssClass = function(el, name) {
+ return el.classList.contains(name);
+ };
+
+ exports.addCssClass = function(el, name) {
+ el.classList.add(name);
+ };
+
+ exports.removeCssClass = function(el, name) {
+ el.classList.remove(name);
+ };
+
+ exports.toggleCssClass = function(el, name) {
+ return el.classList.toggle(name);
+ };
+}
+
+/**
+ * Add or remove a CSS class from the list of classes on the given node
+ * depending on the value of <tt>include</tt>
+ */
+exports.setCssClass = function(node, className, include) {
+ if (include) {
+ exports.addCssClass(node, className);
+ } else {
+ exports.removeCssClass(node, className);
+ }
+};
+
+exports.importCssString = function(cssText, doc){
+ doc = doc || document;
+
+ if (doc.createStyleSheet) {
+ var sheet = doc.createStyleSheet();
+ sheet.cssText = cssText;
+ }
+ else {
+ var style = doc.createElementNS ?
+ doc.createElementNS(XHTML_NS, "style") :
+ doc.createElement("style");
+
+ style.appendChild(doc.createTextNode(cssText));
+
+ var head = doc.getElementsByTagName("head")[0] || doc.documentElement;
+ head.appendChild(style);
+ }
+};
+
+exports.getInnerWidth = function(element) {
+ return (parseInt(exports.computedStyle(element, "paddingLeft"))
+ + parseInt(exports.computedStyle(element, "paddingRight")) + element.clientWidth);
+};
+
+exports.getInnerHeight = function(element) {
+ return (parseInt(exports.computedStyle(element, "paddingTop"))
+ + parseInt(exports.computedStyle(element, "paddingBottom")) + element.clientHeight);
+};
+
+if (window.pageYOffset !== undefined) {
+ exports.getPageScrollTop = function() {
+ return window.pageYOffset;
+ };
+
+ exports.getPageScrollLeft = function() {
+ return window.pageXOffset;
+ };
+}
+else {
+ exports.getPageScrollTop = function() {
+ return document.body.scrollTop;
+ };
+
+ exports.getPageScrollLeft = function() {
+ return document.body.scrollLeft;
+ };
+}
+
+if (window.getComputedStyle)
+ exports.computedStyle = function(element, style) {
+ if (style)
+ return (window.getComputedStyle(element, "") || {})[style] || "";
+ return window.getComputedStyle(element, "") || {}
+ };
+else
+ exports.computedStyle = function(element, style) {
+ if (style)
+ return element.currentStyle[style];
+ return element.currentStyle
+ };
+
+exports.scrollbarWidth = function() {
+
+ var inner = exports.createElement("p");
+ inner.style.width = "100%";
+ inner.style.height = "200px";
+
+ var outer = exports.createElement("div");
+ var style = outer.style;
+
+ style.position = "absolute";
+ style.left = "-10000px";
+ style.overflow = "hidden";
+ style.width = "200px";
+ style.height = "150px";
+
+ outer.appendChild(inner);
+
+ var body = document.body || document.documentElement;
+ body.appendChild(outer);
+
+ var noScrollbar = inner.offsetWidth;
+
+ style.overflow = "scroll";
+ var withScrollbar = inner.offsetWidth;
+
+ if (noScrollbar == withScrollbar) {
+ withScrollbar = outer.clientWidth;
+ }
+
+ body.removeChild(outer);
+
+ return noScrollbar-withScrollbar;
+};
+
+/**
+ * Optimized set innerHTML. This is faster than plain innerHTML if the element
+ * already contains a lot of child elements.
+ *
+ * See http://blog.stevenlevithan.com/archives/faster-than-innerhtml for details
+ */
+exports.setInnerHtml = function(el, innerHtml) {
+ var element = el.cloneNode(false);//document.createElement("div");
+ element.innerHTML = innerHtml;
+ el.parentNode.replaceChild(element, el);
+ return element;
+};
+
+exports.setInnerText = function(el, innerText) {
+ if (document.body && "textContent" in document.body)
+ el.textContent = innerText;
+ else
+ el.innerText = innerText;
+
+};
+
+exports.getInnerText = function(el) {
+ if (document.body && "textContent" in document.body)
+ return el.textContent;
+ else
+ return el.innerText || el.textContent || "";
+};
+
+exports.getParentWindow = function(document) {
+ return document.defaultView || document.parentWindow;
+};
+
+exports.getSelectionStart = function(textarea) {
+ // TODO IE
+ var start;
+ try {
+ start = textarea.selectionStart || 0;
+ } catch (e) {
+ start = 0;
+ }
+ return start;
+};
+
+exports.setSelectionStart = function(textarea, start) {
+ // TODO IE
+ return textarea.selectionStart = start;
+};
+
+exports.getSelectionEnd = function(textarea) {
+ // TODO IE
+ var end;
+ try {
+ end = textarea.selectionEnd || 0;
+ } catch (e) {
+ end = 0;
+ }
+ return end;
+};
+
+exports.setSelectionEnd = function(textarea, end) {
+ // TODO IE
+ return textarea.selectionEnd = end;
+};
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/event', ['require', 'exports', 'module' , 'pilot/keys', 'pilot/useragent', 'pilot/dom'], function(require, exports, module) {
+
+var keys = require("pilot/keys");
+var useragent = require("pilot/useragent");
+var dom = require("pilot/dom");
+
+exports.addListener = function(elem, type, callback) {
+ if (elem.addEventListener) {
+ return elem.addEventListener(type, callback, false);
+ }
+ if (elem.attachEvent) {
+ var wrapper = function() {
+ callback(window.event);
+ };
+ callback._wrapper = wrapper;
+ elem.attachEvent("on" + type, wrapper);
+ }
+};
+
+exports.removeListener = function(elem, type, callback) {
+ if (elem.removeEventListener) {
+ return elem.removeEventListener(type, callback, false);
+ }
+ if (elem.detachEvent) {
+ elem.detachEvent("on" + type, callback._wrapper || callback);
+ }
+};
+
+/**
+* Prevents propagation and clobbers the default action of the passed event
+*/
+exports.stopEvent = function(e) {
+ exports.stopPropagation(e);
+ exports.preventDefault(e);
+ return false;
+};
+
+exports.stopPropagation = function(e) {
+ if (e.stopPropagation)
+ e.stopPropagation();
+ else
+ e.cancelBubble = true;
+};
+
+exports.preventDefault = function(e) {
+ if (e.preventDefault)
+ e.preventDefault();
+ else
+ e.returnValue = false;
+};
+
+exports.getDocumentX = function(e) {
+ if (e.clientX) {
+ return e.clientX + dom.getPageScrollLeft();
+ } else {
+ return e.pageX;
+ }
+};
+
+exports.getDocumentY = function(e) {
+ if (e.clientY) {
+ return e.clientY + dom.getPageScrollTop();
+ } else {
+ return e.pageY;
+ }
+};
+
+/**
+ * @return {Number} 0 for left button, 1 for middle button, 2 for right button
+ */
+exports.getButton = function(e) {
+ if (e.type == "dblclick")
+ return 0;
+ else if (e.type == "contextmenu")
+ return 2;
+
+ // DOM Event
+ if (e.preventDefault) {
+ return e.button;
+ }
+ // old IE
+ else {
+ return {1:0, 2:2, 4:1}[e.button];
+ }
+};
+
+if (document.documentElement.setCapture) {
+ exports.capture = function(el, eventHandler, releaseCaptureHandler) {
+ function onMouseMove(e) {
+ eventHandler(e);
+ return exports.stopPropagation(e);
+ }
+
+ function onReleaseCapture(e) {
+ eventHandler && eventHandler(e);
+ releaseCaptureHandler && releaseCaptureHandler();
+
+ exports.removeListener(el, "mousemove", eventHandler);
+ exports.removeListener(el, "mouseup", onReleaseCapture);
+ exports.removeListener(el, "losecapture", onReleaseCapture);
+
+ el.releaseCapture();
+ }
+
+ exports.addListener(el, "mousemove", eventHandler);
+ exports.addListener(el, "mouseup", onReleaseCapture);
+ exports.addListener(el, "losecapture", onReleaseCapture);
+ el.setCapture();
+ };
+}
+else {
+ exports.capture = function(el, eventHandler, releaseCaptureHandler) {
+ function onMouseMove(e) {
+ eventHandler(e);
+ e.stopPropagation();
+ }
+
+ function onMouseUp(e) {
+ eventHandler && eventHandler(e);
+ releaseCaptureHandler && releaseCaptureHandler();
+
+ document.removeEventListener("mousemove", onMouseMove, true);
+ document.removeEventListener("mouseup", onMouseUp, true);
+
+ e.stopPropagation();
+ }
+
+ document.addEventListener("mousemove", onMouseMove, true);
+ document.addEventListener("mouseup", onMouseUp, true);
+ };
+}
+
+exports.addMouseWheelListener = function(el, callback) {
+ var listener = function(e) {
+ if (e.wheelDelta !== undefined) {
+ if (e.wheelDeltaX !== undefined) {
+ e.wheelX = -e.wheelDeltaX / 8;
+ e.wheelY = -e.wheelDeltaY / 8;
+ } else {
+ e.wheelX = 0;
+ e.wheelY = -e.wheelDelta / 8;
+ }
+ }
+ else {
+ if (e.axis && e.axis == e.HORIZONTAL_AXIS) {
+ e.wheelX = (e.detail || 0) * 5;
+ e.wheelY = 0;
+ } else {
+ e.wheelX = 0;
+ e.wheelY = (e.detail || 0) * 5;
+ }
+ }
+ callback(e);
+ };
+ exports.addListener(el, "DOMMouseScroll", listener);
+ exports.addListener(el, "mousewheel", listener);
+};
+
+exports.addMultiMouseDownListener = function(el, button, count, timeout, callback) {
+ var clicks = 0;
+ var startX, startY;
+
+ var listener = function(e) {
+ clicks += 1;
+ if (clicks == 1) {
+ startX = e.clientX;
+ startY = e.clientY;
+
+ setTimeout(function() {
+ clicks = 0;
+ }, timeout || 600);
+ }
+
+ var isButton = exports.getButton(e) == button;
+ if (!isButton || Math.abs(e.clientX - startX) > 5 || Math.abs(e.clientY - startY) > 5)
+ clicks = 0;
+
+ if (clicks == count) {
+ clicks = 0;
+ callback(e);
+ }
+
+ if (isButton)
+ return exports.preventDefault(e);
+ };
+
+ exports.addListener(el, "mousedown", listener);
+ useragent.isIE && exports.addListener(el, "dblclick", listener);
+};
+
+function normalizeCommandKeys(callback, e, keyCode) {
+ var hashId = 0;
+ if (useragent.isOpera && useragent.isMac) {
+ hashId = 0 | (e.metaKey ? 1 : 0) | (e.altKey ? 2 : 0)
+ | (e.shiftKey ? 4 : 0) | (e.ctrlKey ? 8 : 0);
+ } else {
+ hashId = 0 | (e.ctrlKey ? 1 : 0) | (e.altKey ? 2 : 0)
+ | (e.shiftKey ? 4 : 0) | (e.metaKey ? 8 : 0);
+ }
+
+ if (keyCode in keys.MODIFIER_KEYS) {
+ switch (keys.MODIFIER_KEYS[keyCode]) {
+ case "Alt":
+ hashId = 2;
+ break;
+ case "Shift":
+ hashId = 4;
+ break
+ case "Ctrl":
+ hashId = 1;
+ break;
+ default:
+ hashId = 8;
+ break;
+ }
+ keyCode = 0;
+ }
+
+ if (hashId & 8 && (keyCode == 91 || keyCode == 93)) {
+ keyCode = 0;
+ }
+
+ // If there is no hashID and the keyCode is not a function key, then
+ // we don't call the callback as we don't handle a command key here
+ // (it's a normal key/character input).
+ if (hashId == 0 && !(keyCode in keys.FUNCTION_KEYS)) {
+ return false;
+ }
+
+ return callback(e, hashId, keyCode);
+}
+
+exports.addCommandKeyListener = function(el, callback) {
+ var addListener = exports.addListener;
+ if (useragent.isOldGecko) {
+ // Old versions of Gecko aka. Firefox < 4.0 didn't repeat the keydown
+ // event if the user pressed the key for a longer time. Instead, the
+ // keydown event was fired once and later on only the keypress event.
+ // To emulate the 'right' keydown behavior, the keyCode of the initial
+ // keyDown event is stored and in the following keypress events the
+ // stores keyCode is used to emulate a keyDown event.
+ var lastKeyDownKeyCode = null;
+ addListener(el, "keydown", function(e) {
+ lastKeyDownKeyCode = e.keyCode;
+ });
+ addListener(el, "keypress", function(e) {
+ return normalizeCommandKeys(callback, e, lastKeyDownKeyCode);
+ });
+ } else {
+ var lastDown = null;
+
+ addListener(el, "keydown", function(e) {
+ lastDown = e.keyIdentifier || e.keyCode;
+ return normalizeCommandKeys(callback, e, e.keyCode);
+ });
+
+ // repeated keys are fired as keypress and not keydown events
+ if (useragent.isMac && useragent.isOpera) {
+ addListener(el, "keypress", function(e) {
+ var keyId = e.keyIdentifier || e.keyCode;
+ if (lastDown !== keyId) {
+ return normalizeCommandKeys(callback, e, e.keyCode);
+ } else {
+ lastDown = null;
+ }
+ });
+ }
+ }
+};
+
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)
+ * Julian Viereck <julian.viereck@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/editor', ['require', 'exports', 'module' , 'pilot/fixoldbrowsers', 'pilot/oop', 'pilot/event', 'pilot/lang', 'pilot/useragent', 'ace/keyboard/textinput', 'ace/mouse_handler', 'ace/keyboard/keybinding', 'ace/edit_session', 'ace/search', 'ace/range', 'pilot/event_emitter'], function(require, exports, module) {
+
+require("pilot/fixoldbrowsers");
+
+var oop = require("pilot/oop");
+var event = require("pilot/event");
+var lang = require("pilot/lang");
+var useragent = require("pilot/useragent");
+var TextInput = require("ace/keyboard/textinput").TextInput;
+var MouseHandler = require("ace/mouse_handler").MouseHandler;
+//var TouchHandler = require("ace/touch_handler").TouchHandler;
+var KeyBinding = require("ace/keyboard/keybinding").KeyBinding;
+var EditSession = require("ace/edit_session").EditSession;
+var Search = require("ace/search").Search;
+var Range = require("ace/range").Range;
+var EventEmitter = require("pilot/event_emitter").EventEmitter;
+
+var Editor =function(renderer, session) {
+ var container = renderer.getContainerElement();
+ this.container = container;
+ this.renderer = renderer;
+
+ this.textInput = new TextInput(renderer.getTextAreaContainer(), this);
+ this.keyBinding = new KeyBinding(this);
+
+ // TODO detect touch event support
+ if (useragent.isIPad) {
+ //this.$mouseHandler = new TouchHandler(this);
+ } else {
+ this.$mouseHandler = new MouseHandler(this);
+ }
+
+ this.$blockScrolling = 0;
+ this.$search = new Search().set({
+ wrap: true
+ });
+
+ this.setSession(session || new EditSession(""));
+};
+
+(function(){
+
+ oop.implement(this, EventEmitter);
+
+ this.$forwardEvents = {
+ gutterclick: 1,
+ gutterdblclick: 1
+ };
+
+ this.$originalAddEventListener = this.addEventListener;
+ this.$originalRemoveEventListener = this.removeEventListener;
+
+ this.addEventListener = function(eventName, callback) {
+ if (this.$forwardEvents[eventName]) {
+ return this.renderer.addEventListener(eventName, callback);
+ } else {
+ return this.$originalAddEventListener(eventName, callback);
+ }
+ };
+
+ this.removeEventListener = function(eventName, callback) {
+ if (this.$forwardEvents[eventName]) {
+ return this.renderer.removeEventListener(eventName, callback);
+ } else {
+ return this.$originalRemoveEventListener(eventName, callback);
+ }
+ };
+
+ this.setKeyboardHandler = function(keyboardHandler) {
+ this.keyBinding.setKeyboardHandler(keyboardHandler);
+ };
+
+ this.getKeyboardHandler = function() {
+ return this.keyBinding.getKeyboardHandler();
+ };
+
+ this.setSession = function(session) {
+ if (this.session == session) return;
+
+ if (this.session) {
+ var oldSession = this.session;
+ this.session.removeEventListener("change", this.$onDocumentChange);
+ this.session.removeEventListener("changeMode", this.$onChangeMode);
+ this.session.removeEventListener("tokenizerUpdate", this.$onTokenizerUpdate);
+ this.session.removeEventListener("changeTabSize", this.$onChangeTabSize);
+ this.session.removeEventListener("changeWrapLimit", this.$onChangeWrapLimit);
+ this.session.removeEventListener("changeWrapMode", this.$onChangeWrapMode);
+ this.session.removeEventListener("onChangeFold", this.$onChangeFold);
+ this.session.removeEventListener("changeFrontMarker", this.$onChangeFrontMarker);
+ this.session.removeEventListener("changeBackMarker", this.$onChangeBackMarker);
+ this.session.removeEventListener("changeBreakpoint", this.$onChangeBreakpoint);
+ this.session.removeEventListener("changeAnnotation", this.$onChangeAnnotation);
+ this.session.removeEventListener("changeOverwrite", this.$onCursorChange);
+
+ var selection = this.session.getSelection();
+ selection.removeEventListener("changeCursor", this.$onCursorChange);
+ selection.removeEventListener("changeSelection", this.$onSelectionChange);
+
+ this.session.setScrollTopRow(this.renderer.getScrollTopRow());
+ }
+
+ this.session = session;
+
+ this.$onDocumentChange = this.onDocumentChange.bind(this);
+ session.addEventListener("change", this.$onDocumentChange);
+ this.renderer.setSession(session);
+
+ this.$onChangeMode = this.onChangeMode.bind(this);
+ session.addEventListener("changeMode", this.$onChangeMode);
+
+ this.$onTokenizerUpdate = this.onTokenizerUpdate.bind(this);
+ session.addEventListener("tokenizerUpdate", this.$onTokenizerUpdate);
+
+ this.$onChangeTabSize = this.renderer.updateText.bind(this.renderer);
+ session.addEventListener("changeTabSize", this.$onChangeTabSize);
+
+ this.$onChangeWrapLimit = this.onChangeWrapLimit.bind(this);
+ session.addEventListener("changeWrapLimit", this.$onChangeWrapLimit);
+
+ this.$onChangeWrapMode = this.onChangeWrapMode.bind(this);
+ session.addEventListener("changeWrapMode", this.$onChangeWrapMode);
+
+ this.$onChangeFold = this.onChangeFold.bind(this);
+ session.addEventListener("changeFold", this.$onChangeFold);
+
+ this.$onChangeFrontMarker = this.onChangeFrontMarker.bind(this);
+ this.session.addEventListener("changeFrontMarker", this.$onChangeFrontMarker);
+
+ this.$onChangeBackMarker = this.onChangeBackMarker.bind(this);
+ this.session.addEventListener("changeBackMarker", this.$onChangeBackMarker);
+
+ this.$onChangeBreakpoint = this.onChangeBreakpoint.bind(this);
+ this.session.addEventListener("changeBreakpoint", this.$onChangeBreakpoint);
+
+ this.$onChangeAnnotation = this.onChangeAnnotation.bind(this);
+ this.session.addEventListener("changeAnnotation", this.$onChangeAnnotation);
+
+ this.$onCursorChange = this.onCursorChange.bind(this);
+ this.session.addEventListener("changeOverwrite", this.$onCursorChange);
+
+ this.selection = session.getSelection();
+ this.selection.addEventListener("changeCursor", this.$onCursorChange);
+
+ this.$onSelectionChange = this.onSelectionChange.bind(this);
+ this.selection.addEventListener("changeSelection", this.$onSelectionChange);
+
+ this.onChangeMode();
+
+ this.onCursorChange();
+ this.onSelectionChange();
+ this.onChangeFrontMarker();
+ this.onChangeBackMarker();
+ this.onChangeBreakpoint();
+ this.onChangeAnnotation();
+ this.renderer.scrollToRow(session.getScrollTopRow());
+ this.renderer.updateFull();
+
+ this._dispatchEvent("changeSession", {
+ session: session,
+ oldSession: oldSession
+ });
+ };
+
+ this.getSession = function() {
+ return this.session;
+ };
+
+ this.getSelection = function() {
+ return this.selection;
+ };
+
+ this.resize = function() {
+ this.renderer.onResize();
+ };
+
+ this.setTheme = function(theme) {
+ this.renderer.setTheme(theme);
+ };
+
+ this.getTheme = function() {
+ return this.renderer.getTheme();
+ }
+
+ this.setStyle = function(style) {
+ this.renderer.setStyle(style)
+ };
+
+ this.unsetStyle = function(style) {
+ this.renderer.unsetStyle(style)
+ }
+
+ this.$highlightBrackets = function() {
+ if (this.session.$bracketHighlight) {
+ this.session.removeMarker(this.session.$bracketHighlight);
+ this.session.$bracketHighlight = null;
+ }
+
+ if (this.$highlightPending) {
+ return;
+ }
+
+ // perform highlight async to not block the browser during navigation
+ var self = this;
+ this.$highlightPending = true;
+ setTimeout(function() {
+ self.$highlightPending = false;
+
+ var pos = self.session.findMatchingBracket(self.getCursorPosition());
+ if (pos) {
+ var range = new Range(pos.row, pos.column, pos.row, pos.column+1);
+ self.session.$bracketHighlight = self.session.addMarker(range, "ace_bracket");
+ }
+ }, 10);
+ };
+
+ this.focus = function() {
+ // Safari needs the timeout
+ // iOS and Firefox need it called immediately
+ // to be on the save side we do both
+ // except for IE
+ var _self = this;
+ if (!useragent.isIE) {
+ setTimeout(function() {
+ _self.textInput.focus();
+ });
+ }
+ this.textInput.focus();
+ };
+
+ this.blur = function() {
+ this.textInput.blur();
+ };
+
+ this.onFocus = function() {
+ this.renderer.showCursor();
+ this.renderer.visualizeFocus();
+ this._dispatchEvent("focus");
+ };
+
+ this.onBlur = function() {
+ this.renderer.hideCursor();
+ this.renderer.visualizeBlur();
+ this._dispatchEvent("blur");
+ };
+
+ this.onDocumentChange = function(e) {
+ var delta = e.data;
+ var range = delta.range;
+
+ if (range.start.row == range.end.row && delta.action != "insertLines" && delta.action != "removeLines")
+ var lastRow = range.end.row;
+ else
+ lastRow = Infinity;
+ this.renderer.updateLines(range.start.row, lastRow);
+
+ // update cursor because tab characters can influence the cursor position
+ this.renderer.updateCursor();
+ };
+
+ this.onTokenizerUpdate = function(e) {
+ var rows = e.data;
+ this.renderer.updateLines(rows.first, rows.last);
+ };
+
+ this.onCursorChange = function(e) {
+ this.renderer.updateCursor();
+
+ if (!this.$blockScrolling) {
+ this.renderer.scrollCursorIntoView();
+ }
+
+ // move text input over the cursor
+ // this is required for iOS and IME
+ this.renderer.moveTextAreaToCursor(this.textInput.getElement());
+
+ this.$highlightBrackets();
+ this.$updateHighlightActiveLine();
+ };
+
+ this.$updateHighlightActiveLine = function() {
+ var session = this.getSession();
+
+ if (session.$highlightLineMarker) {
+ session.removeMarker(session.$highlightLineMarker);
+ }
+ session.$highlightLineMarker = null;
+
+ if (this.getHighlightActiveLine() && (this.getSelectionStyle() != "line" || !this.selection.isMultiLine())) {
+ var cursor = this.getCursorPosition(),
+ foldLine = this.session.getFoldLine(cursor.row);
+ var range;
+ if (foldLine) {
+ range = new Range(foldLine.start.row, 0, foldLine.end.row + 1, 0);
+ } else {
+ range = new Range(cursor.row, 0, cursor.row+1, 0);
+ }
+ session.$highlightLineMarker = session.addMarker(range, "ace_active_line", "line");
+ }
+ };
+
+ this.onSelectionChange = function(e) {
+ var session = this.getSession();
+
+ if (session.$selectionMarker) {
+ session.removeMarker(session.$selectionMarker);
+ }
+ session.$selectionMarker = null;
+
+ if (!this.selection.isEmpty()) {
+ var range = this.selection.getRange();
+ var style = this.getSelectionStyle();
+ session.$selectionMarker = session.addMarker(range, "ace_selection", style);
+ } else {
+ this.$updateHighlightActiveLine();
+ }
+
+ if (this.$highlightSelectedWord)
+ this.session.getMode().highlightSelection(this);
+ };
+
+ this.onChangeFrontMarker = function() {
+ this.renderer.updateFrontMarkers();
+ };
+
+ this.onChangeBackMarker = function() {
+ this.renderer.updateBackMarkers();
+ };
+
+ this.onChangeBreakpoint = function() {
+ this.renderer.setBreakpoints(this.session.getBreakpoints());
+ };
+
+ this.onChangeAnnotation = function() {
+ this.renderer.setAnnotations(this.session.getAnnotations());
+ };
+
+ this.onChangeMode = function() {
+ this.renderer.updateText()
+ };
+
+ this.onChangeWrapLimit = function() {
+ this.renderer.updateFull();
+ };
+
+ this.onChangeWrapMode = function() {
+ this.renderer.onResize(true);
+ };
+
+ this.onChangeFold = function() {
+ // Update the active line marker as due to folding changes the current
+ // line range on the screen might have changed.
+ this.$updateHighlightActiveLine();
+ // TODO: This might be too much updating. Okay for now.
+ this.renderer.updateFull();
+ };
+
+ this.getCopyText = function() {
+ if (!this.selection.isEmpty()) {
+ return this.session.getTextRange(this.getSelectionRange());
+ }
+ else {
+ return "";
+ }
+ };
+
+ this.onCut = function() {
+ if (this.$readOnly)
+ return;
+
+ if (!this.selection.isEmpty()) {
+ this.session.remove(this.getSelectionRange())
+ this.clearSelection();
+ }
+ };
+
+ this.insert = function(text) {
+ if (this.$readOnly)
+ return;
+
+ var session = this.session;
+ var mode = session.getMode();
+
+ var cursor = this.getCursorPosition();
+
+ if (this.getBehavioursEnabled()) {
+ // Get a transform if the current mode wants one.
+ var transform = mode.transformAction(session.getState(cursor.row), 'insertion', this, session, text);
+ if (transform)
+ text = transform.text;
+ }
+
+ text = text.replace("\t", this.session.getTabString());
+
+ // remove selected text
+ if (!this.selection.isEmpty()) {
+ var cursor = this.session.remove(this.getSelectionRange());
+ this.clearSelection();
+ }
+ else if (this.session.getOverwrite()) {
+ var range = new Range.fromPoints(cursor, cursor);
+ range.end.column += text.length;
+ this.session.remove(range);
+ }
+
+ this.clearSelection();
+
+ var start = cursor.column;
+ var lineState = session.getState(cursor.row);
+ var shouldOutdent = mode.checkOutdent(lineState, session.getLine(cursor.row), text);
+ var line = session.getLine(cursor.row);
+ var lineIndent = mode.getNextLineIndent(lineState, line.slice(0, cursor.column), session.getTabString());
+ var end = session.insert(cursor, text);
+
+ if (transform && transform.selection) {
+ if (transform.selection.length == 2) { // Transform relative to the current column
+ this.selection.setSelectionRange(
+ new Range(cursor.row, start + transform.selection[0],
+ cursor.row, start + transform.selection[1]));
+ } else { // Transform relative to the current row.
+ this.selection.setSelectionRange(
+ new Range(cursor.row + transform.selection[0],
+ transform.selection[1],
+ cursor.row + transform.selection[2],
+ transform.selection[3]));
+ }
+ }
+
+ var lineState = session.getState(cursor.row);
+
+ // TODO disabled multiline auto indent
+ // possibly doing the indent before inserting the text
+ // if (cursor.row !== end.row) {
+ if (session.getDocument().isNewLine(text)) {
+ this.moveCursorTo(cursor.row+1, 0);
+
+ var size = session.getTabSize();
+ var minIndent = Number.MAX_VALUE;
+
+ for (var row = cursor.row + 1; row <= end.row; ++row) {
+ var indent = 0;
+
+ line = session.getLine(row);
+ for (var i = 0; i < line.length; ++i)
+ if (line.charAt(i) == '\t')
+ indent += size;
+ else if (line.charAt(i) == ' ')
+ indent += 1;
+ else
+ break;
+ if (/[^\s]/.test(line))
+ minIndent = Math.min(indent, minIndent);
+ }
+
+ for (var row = cursor.row + 1; row <= end.row; ++row) {
+ var outdent = minIndent;
+
+ line = session.getLine(row);
+ for (var i = 0; i < line.length && outdent > 0; ++i)
+ if (line.charAt(i) == '\t')
+ outdent -= size;
+ else if (line.charAt(i) == ' ')
+ outdent -= 1;
+ session.remove(new Range(row, 0, row, i));
+ }
+ session.indentRows(cursor.row + 1, end.row, lineIndent);
+ } else {
+ if (shouldOutdent) {
+ mode.autoOutdent(lineState, session, cursor.row);
+ }
+ }
+ };
+
+ this.onTextInput = function(text) {
+ this.keyBinding.onTextInput(text);
+ };
+
+ this.onCommandKey = function(e, hashId, keyCode) {
+ this.keyBinding.onCommandKey(e, hashId, keyCode);
+ };
+
+ this.setOverwrite = function(overwrite) {
+ this.session.setOverwrite(overwrite);
+ };
+
+ this.getOverwrite = function() {
+ return this.session.getOverwrite();
+ };
+
+ this.toggleOverwrite = function() {
+ this.session.toggleOverwrite();
+ };
+
+ this.setScrollSpeed = function(speed) {
+ this.$mouseHandler.setScrollSpeed(speed);
+ };
+
+ this.getScrollSpeed = function() {
+ return this.$mouseHandler.getScrollSpeed()
+ };
+
+ this.$selectionStyle = "line";
+ this.setSelectionStyle = function(style) {
+ if (this.$selectionStyle == style) return;
+
+ this.$selectionStyle = style;
+ this.onSelectionChange();
+ this._dispatchEvent("changeSelectionStyle", {data: style});
+ };
+
+ this.getSelectionStyle = function() {
+ return this.$selectionStyle;
+ };
+
+ this.$highlightActiveLine = true;
+ this.setHighlightActiveLine = function(shouldHighlight) {
+ if (this.$highlightActiveLine == shouldHighlight) return;
+
+ this.$highlightActiveLine = shouldHighlight;
+ this.$updateHighlightActiveLine();
+ };
+
+ this.getHighlightActiveLine = function() {
+ return this.$highlightActiveLine;
+ };
+
+ this.$highlightSelectedWord = true;
+ this.setHighlightSelectedWord = function(shouldHighlight) {
+ if (this.$highlightSelectedWord == shouldHighlight)
+ return;
+
+ this.$highlightSelectedWord = shouldHighlight;
+ if (shouldHighlight)
+ this.session.getMode().highlightSelection(this);
+ else
+ this.session.getMode().clearSelectionHighlight(this);
+ };
+
+ this.getHighlightSelectedWord = function() {
+ return this.$highlightSelectedWord;
+ };
+
+ this.setShowInvisibles = function(showInvisibles) {
+ if (this.getShowInvisibles() == showInvisibles)
+ return;
+
+ this.renderer.setShowInvisibles(showInvisibles);
+ };
+
+ this.getShowInvisibles = function() {
+ return this.renderer.getShowInvisibles();
+ };
+
+ this.setShowPrintMargin = function(showPrintMargin) {
+ this.renderer.setShowPrintMargin(showPrintMargin);
+ };
+
+ this.getShowPrintMargin = function() {
+ return this.renderer.getShowPrintMargin();
+ };
+
+ this.setPrintMarginColumn = function(showPrintMargin) {
+ this.renderer.setPrintMarginColumn(showPrintMargin);
+ };
+
+ this.getPrintMarginColumn = function() {
+ return this.renderer.getPrintMarginColumn();
+ };
+
+ this.$readOnly = false;
+ this.setReadOnly = function(readOnly) {
+ this.$readOnly = readOnly;
+ };
+
+ this.getReadOnly = function() {
+ return this.$readOnly;
+ };
+
+ this.$modeBehaviours = false;
+ this.setBehavioursEnabled = function (enabled) {
+ this.$modeBehaviours = enabled;
+ }
+
+ this.getBehavioursEnabled = function () {
+ return this.$modeBehaviours;
+ }
+
+ this.removeRight = function() {
+ if (this.$readOnly)
+ return;
+
+ if (this.selection.isEmpty()) {
+ this.selection.selectRight();
+ }
+ this.session.remove(this.getSelectionRange())
+ this.clearSelection();
+ };
+
+ this.removeLeft = function() {
+ if (this.$readOnly)
+ return;
+
+ if (this.selection.isEmpty())
+ this.selection.selectLeft();
+
+ var range = this.getSelectionRange();
+ if (this.getBehavioursEnabled()) {
+ var session = this.session;
+ var state = session.getState(range.start.row);
+ var new_range = session.getMode().transformAction(state, 'deletion', this, session, range);
+ if (new_range !== false) {
+ range = new_range;
+ }
+ }
+
+ this.session.remove(range);
+ this.clearSelection();
+ };
+
+ this.removeWordRight = function() {
+ if (this.$readOnly)
+ return;
+
+ if (this.selection.isEmpty())
+ this.selection.selectWordRight();
+
+ this.session.remove(this.getSelectionRange());
+ this.clearSelection();
+ };
+
+ this.removeWordLeft = function() {
+ if (this.$readOnly)
+ return;
+
+ if (this.selection.isEmpty())
+ this.selection.selectWordLeft();
+
+ this.session.remove(this.getSelectionRange());
+ this.clearSelection();
+ };
+
+ this.removeToLineStart = function() {
+ if (this.$readOnly)
+ return;
+
+ if (this.selection.isEmpty())
+ this.selection.selectLineStart();
+
+ this.session.remove(this.getSelectionRange());
+ this.clearSelection();
+ };
+
+ this.removeToLineEnd = function() {
+ if (this.$readOnly)
+ return;
+
+ if (this.selection.isEmpty())
+ this.selection.selectLineEnd();
+
+ var range = this.getSelectionRange();
+ if (range.start.column == range.end.column && range.start.row == range.end.row) {
+ range.end.column = 0;
+ range.end.row++;
+ }
+
+ this.session.remove(range);
+ this.clearSelection();
+ };
+
+ this.splitLine = function() {
+ if (this.$readOnly)
+ return;
+
+ if (!this.selection.isEmpty()) {
+ this.session.remove(this.getSelectionRange());
+ this.clearSelection();
+ }
+
+ var cursor = this.getCursorPosition();
+ this.insert("\n");
+ this.moveCursorToPosition(cursor);
+ };
+
+ this.transposeLetters = function() {
+ if (this.$readOnly)
+ return;
+
+ if (!this.selection.isEmpty()) {
+ return;
+ }
+
+ var cursor = this.getCursorPosition();
+ var column = cursor.column;
+ if (column == 0)
+ return;
+
+ var line = this.session.getLine(cursor.row);
+ if (column < line.length) {
+ var swap = line.charAt(column) + line.charAt(column-1);
+ var range = new Range(cursor.row, column-1, cursor.row, column+1)
+ }
+ else {
+ var swap = line.charAt(column-1) + line.charAt(column-2);
+ var range = new Range(cursor.row, column-2, cursor.row, column)
+ }
+ this.session.replace(range, swap);
+ };
+
+ this.indent = function() {
+ if (this.$readOnly)
+ return;
+
+ var session = this.session;
+ var range = this.getSelectionRange();
+
+ if (range.start.row < range.end.row || range.start.column < range.end.column) {
+ var rows = this.$getSelectedRows();
+ session.indentRows(rows.first, rows.last, "\t");
+ } else {
+ var indentString;
+
+ if (this.session.getUseSoftTabs()) {
+ var size = session.getTabSize(),
+ position = this.getCursorPosition(),
+ column = session.documentToScreenColumn(position.row, position.column),
+ count = (size - column % size);
+
+ indentString = lang.stringRepeat(" ", count);
+ } else
+ indentString = "\t";
+ return this.onTextInput(indentString);
+ }
+ };
+
+ this.blockOutdent = function() {
+ if (this.$readOnly)
+ return;
+
+ var selection = this.session.getSelection();
+ this.session.outdentRows(selection.getRange());
+ };
+
+ this.toggleCommentLines = function() {
+ if (this.$readOnly)
+ return;
+
+ var state = this.session.getState(this.getCursorPosition().row);
+ var rows = this.$getSelectedRows()
+ this.session.getMode().toggleCommentLines(state, this.session, rows.first, rows.last);
+ };
+
+ this.removeLines = function() {
+ if (this.$readOnly)
+ return;
+
+ var rows = this.$getSelectedRows();
+ this.session.remove(new Range(rows.first, 0, rows.last+1, 0));
+ this.clearSelection();
+ };
+
+ this.moveLinesDown = function() {
+ if (this.$readOnly)
+ return;
+
+ this.$moveLines(function(firstRow, lastRow) {
+ return this.session.moveLinesDown(firstRow, lastRow);
+ });
+ };
+
+ this.moveLinesUp = function() {
+ if (this.$readOnly)
+ return;
+
+ this.$moveLines(function(firstRow, lastRow) {
+ return this.session.moveLinesUp(firstRow, lastRow);
+ });
+ };
+
+ this.moveText = function(range, toPosition) {
+ if (this.$readOnly)
+ return null;
+
+ return this.session.moveText(range, toPosition);
+ };
+
+ this.copyLinesUp = function() {
+ if (this.$readOnly)
+ return;
+
+ this.$moveLines(function(firstRow, lastRow) {
+ this.session.duplicateLines(firstRow, lastRow);
+ return 0;
+ });
+ };
+
+ this.copyLinesDown = function() {
+ if (this.$readOnly)
+ return;
+
+ this.$moveLines(function(firstRow, lastRow) {
+ return this.session.duplicateLines(firstRow, lastRow);
+ });
+ };
+
+
+ this.$moveLines = function(mover) {
+ var rows = this.$getSelectedRows();
+
+ var linesMoved = mover.call(this, rows.first, rows.last);
+
+ var selection = this.selection;
+ selection.setSelectionAnchor(rows.last+linesMoved+1, 0);
+ selection.$moveSelection(function() {
+ selection.moveCursorTo(rows.first+linesMoved, 0);
+ });
+ };
+
+ this.$getSelectedRows = function() {
+ var range = this.getSelectionRange().collapseRows();
+
+ return {
+ first: range.start.row,
+ last: range.end.row
+ };
+ };
+
+ this.onCompositionStart = function(text) {
+ this.renderer.showComposition(this.getCursorPosition());
+ };
+
+ this.onCompositionUpdate = function(text) {
+ this.renderer.setCompositionText(text);
+ };
+
+ this.onCompositionEnd = function() {
+ this.renderer.hideComposition();
+ };
+
+
+ this.getFirstVisibleRow = function() {
+ return this.renderer.getFirstVisibleRow();
+ };
+
+ this.getLastVisibleRow = function() {
+ return this.renderer.getLastVisibleRow();
+ };
+
+ this.isRowVisible = function(row) {
+ return (row >= this.getFirstVisibleRow() && row <= this.getLastVisibleRow());
+ };
+
+ this.$getVisibleRowCount = function() {
+ return this.renderer.getScrollBottomRow() - this.renderer.getScrollTopRow() + 1;
+ };
+
+ this.$getPageDownRow = function() {
+ return this.renderer.getScrollBottomRow();
+ };
+
+ this.$getPageUpRow = function() {
+ var firstRow = this.renderer.getScrollTopRow();
+ var lastRow = this.renderer.getScrollBottomRow();
+
+ return firstRow - (lastRow - firstRow);
+ };
+
+ this.selectPageDown = function() {
+ var row = this.$getPageDownRow() + Math.floor(this.$getVisibleRowCount() / 2);
+
+ this.scrollPageDown();
+
+ var selection = this.getSelection();
+ var leadScreenPos = this.session.documentToScreenPosition(selection.getSelectionLead());
+ var dest = this.session.screenToDocumentPosition(row, leadScreenPos.column);
+ selection.selectTo(dest.row, dest.column);
+ };
+
+ this.selectPageUp = function() {
+ var visibleRows = this.renderer.getScrollTopRow() - this.renderer.getScrollBottomRow();
+ var row = this.$getPageUpRow() + Math.round(visibleRows / 2);
+
+ this.scrollPageUp();
+
+ var selection = this.getSelection();
+ var leadScreenPos = this.session.documentToScreenPosition(selection.getSelectionLead());
+ var dest = this.session.screenToDocumentPosition(row, leadScreenPos.column);
+ selection.selectTo(dest.row, dest.column);
+ };
+
+ this.gotoPageDown = function() {
+ var row = this.$getPageDownRow();
+ var column = this.getCursorPositionScreen().column;
+
+ this.scrollToRow(row);
+ this.getSelection().moveCursorToScreen(row, column);
+ };
+
+ this.gotoPageUp = function() {
+ var row = this.$getPageUpRow();
+ var column = this.getCursorPositionScreen().column;
+
+ this.scrollToRow(row);
+ this.getSelection().moveCursorToScreen(row, column);
+ };
+
+ this.scrollPageDown = function() {
+ this.scrollToRow(this.$getPageDownRow());
+ };
+
+ this.scrollPageUp = function() {
+ this.renderer.scrollToRow(this.$getPageUpRow());
+ };
+
+ this.scrollToRow = function(row) {
+ this.renderer.scrollToRow(row);
+ };
+
+ this.scrollToLine = function(line, center) {
+ this.renderer.scrollToLine(line, center);
+ };
+
+ this.centerSelection = function() {
+ var range = this.getSelectionRange();
+ var line = Math.floor(range.start.row + (range.end.row - range.start.row) / 2);
+ this.renderer.scrollToLine(line, true);
+ };
+
+ this.getCursorPosition = function() {
+ return this.selection.getCursor();
+ };
+
+ this.getCursorPositionScreen = function() {
+ return this.session.documentToScreenPosition(this.getCursorPosition());
+ };
+
+ this.getSelectionRange = function() {
+ return this.selection.getRange();
+ };
+
+
+ this.selectAll = function() {
+ this.$blockScrolling += 1;
+ this.selection.selectAll();
+ this.$blockScrolling -= 1;
+ };
+
+ this.clearSelection = function() {
+ this.selection.clearSelection();
+ };
+
+ this.moveCursorTo = function(row, column) {
+ this.selection.moveCursorTo(row, column);
+ };
+
+ this.moveCursorToPosition = function(pos) {
+ this.selection.moveCursorToPosition(pos);
+ };
+
+
+ this.gotoLine = function(lineNumber, row) {
+ this.selection.clearSelection();
+
+ this.$blockScrolling += 1;
+ this.moveCursorTo(lineNumber-1, row || 0);
+ this.$blockScrolling -= 1;
+
+ if (!this.isRowVisible(this.getCursorPosition().row)) {
+ this.scrollToLine(lineNumber, true);
+ }
+ },
+
+ this.navigateTo = function(row, column) {
+ this.clearSelection();
+ this.moveCursorTo(row, column);
+ };
+
+ this.navigateUp = function(times) {
+ this.selection.clearSelection();
+ times = times || 1;
+ this.selection.moveCursorBy(-times, 0);
+ };
+
+ this.navigateDown = function(times) {
+ this.selection.clearSelection();
+ times = times || 1;
+ this.selection.moveCursorBy(times, 0);
+ };
+
+ this.navigateLeft = function(times) {
+ if (!this.selection.isEmpty()) {
+ var selectionStart = this.getSelectionRange().start;
+ this.moveCursorToPosition(selectionStart);
+ }
+ else {
+ times = times || 1;
+ while (times--) {
+ this.selection.moveCursorLeft();
+ }
+ }
+ this.clearSelection();
+ };
+
+ this.navigateRight = function(times) {
+ if (!this.selection.isEmpty()) {
+ var selectionEnd = this.getSelectionRange().end;
+ this.moveCursorToPosition(selectionEnd);
+ }
+ else {
+ times = times || 1;
+ while (times--) {
+ this.selection.moveCursorRight();
+ }
+ }
+ this.clearSelection();
+ };
+
+ this.navigateLineStart = function() {
+ this.selection.moveCursorLineStart();
+ this.clearSelection();
+ };
+
+ this.navigateLineEnd = function() {
+ this.selection.moveCursorLineEnd();
+ this.clearSelection();
+ };
+
+ this.navigateFileEnd = function() {
+ this.selection.moveCursorFileEnd();
+ this.clearSelection();
+ };
+
+ this.navigateFileStart = function() {
+ this.selection.moveCursorFileStart();
+ this.clearSelection();
+ };
+
+ this.navigateWordRight = function() {
+ this.selection.moveCursorWordRight();
+ this.clearSelection();
+ };
+
+ this.navigateWordLeft = function() {
+ this.selection.moveCursorWordLeft();
+ this.clearSelection();
+ };
+
+ this.replace = function(replacement, options) {
+ if (options)
+ this.$search.set(options);
+
+ var range = this.$search.find(this.session);
+ this.$tryReplace(range, replacement);
+ if (range !== null)
+ this.selection.setSelectionRange(range);
+ },
+
+ this.replaceAll = function(replacement, options) {
+ if (options) {
+ this.$search.set(options);
+ }
+
+ var ranges = this.$search.findAll(this.session);
+ if (!ranges.length)
+ return;
+
+ var selection = this.getSelectionRange();
+ this.clearSelection();
+ this.selection.moveCursorTo(0, 0);
+
+ this.$blockScrolling += 1;
+ for (var i = ranges.length - 1; i >= 0; --i)
+ this.$tryReplace(ranges[i], replacement);
+
+ this.selection.setSelectionRange(selection);
+ this.$blockScrolling -= 1;
+ },
+
+ this.$tryReplace = function(range, replacement) {
+ var input = this.session.getTextRange(range);
+ var replacement = this.$search.replace(input, replacement);
+ if (replacement !== null) {
+ range.end = this.session.replace(range, replacement);
+ return range;
+ } else {
+ return null;
+ }
+ };
+
+ this.getLastSearchOptions = function() {
+ return this.$search.getOptions();
+ };
+
+ this.find = function(needle, options) {
+ this.clearSelection();
+ options = options || {};
+ options.needle = needle;
+ this.$search.set(options);
+ this.$find();
+ },
+
+ this.findNext = function(options) {
+ options = options || {};
+ if (typeof options.backwards == "undefined")
+ options.backwards = false;
+ this.$search.set(options);
+ this.$find();
+ };
+
+ this.findPrevious = function(options) {
+ options = options || {};
+ if (typeof options.backwards == "undefined")
+ options.backwards = true;
+ this.$search.set(options);
+ this.$find();
+ };
+
+ this.$find = function(backwards) {
+ if (!this.selection.isEmpty()) {
+ this.$search.set({needle: this.session.getTextRange(this.getSelectionRange())});
+ }
+
+ if (typeof backwards != "undefined")
+ this.$search.set({backwards: backwards});
+
+ var range = this.$search.find(this.session);
+ if (range) {
+ this.gotoLine(range.end.row+1, range.end.column);
+ this.selection.setSelectionRange(range);
+ }
+ };
+
+ this.undo = function() {
+ this.session.getUndoManager().undo();
+ };
+
+ this.redo = function() {
+ this.session.getUndoManager().redo();
+ };
+
+ this.destroy = function() {
+ this.renderer.destroy();
+ }
+
+}).call(Editor.prototype);
+
+
+exports.Editor = Editor;
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Mihai Sucan <mihai DOT sucan AT gmail DOT com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/keyboard/textinput', ['require', 'exports', 'module' , 'pilot/event', 'pilot/useragent', 'pilot/dom'], function(require, exports, module) {
+
+var event = require("pilot/event");
+var useragent = require("pilot/useragent");
+var dom = require("pilot/dom");
+
+var TextInput = function(parentNode, host) {
+
+ var text = dom.createElement("textarea");
+ text.style.left = "-10000px";
+ parentNode.appendChild(text);
+
+ var PLACEHOLDER = String.fromCharCode(0);
+ sendText();
+
+ var inCompostion = false;
+ var copied = false;
+ var tempStyle = '';
+
+ function sendText(valueToSend) {
+ if (!copied) {
+ var value = valueToSend || text.value;
+ if (value) {
+ if (value.charCodeAt(value.length-1) == PLACEHOLDER.charCodeAt(0)) {
+ value = value.slice(0, -1);
+ if (value)
+ host.onTextInput(value);
+ } else
+ host.onTextInput(value);
+ }
+ }
+ copied = false;
+
+ // Safari doesn't fire copy events if no text is selected
+ text.value = PLACEHOLDER;
+ text.select();
+ }
+
+ var onTextInput = function(e) {
+ if (useragent.isIE && text.value.charCodeAt(0) > 128) return;
+ setTimeout(function() {
+ if (!inCompostion)
+ sendText();
+ }, 0);
+ };
+
+ var onCompositionStart = function(e) {
+ inCompostion = true;
+ if (!useragent.isIE) {
+ sendText();
+ text.value = "";
+ };
+ host.onCompositionStart();
+ if (!useragent.isGecko) setTimeout(onCompositionUpdate, 0);
+ };
+
+ var onCompositionUpdate = function() {
+ if (!inCompostion) return;
+ host.onCompositionUpdate(text.value);
+ };
+
+ var onCompositionEnd = function(e) {
+ inCompostion = false;
+ host.onCompositionEnd();
+ if (useragent.isGecko) {
+ sendText();
+ } else {
+ setTimeout(function () {
+ if (!inCompostion)
+ sendText();
+ }, 0);
+ }
+ };
+
+ var onCopy = function(e) {
+ copied = true;
+ var copyText = host.getCopyText();
+ if(copyText)
+ text.value = copyText;
+ else
+ e.preventDefault();
+ text.select();
+ setTimeout(function () {
+ sendText();
+ }, 0);
+ };
+
+ var onCut = function(e) {
+ copied = true;
+ var copyText = host.getCopyText();
+ if(copyText) {
+ text.value = copyText;
+ host.onCut();
+ } else
+ e.preventDefault();
+ text.select();
+ setTimeout(function () {
+ sendText();
+ }, 0);
+ };
+
+ event.addCommandKeyListener(text, host.onCommandKey.bind(host));
+ event.addListener(text, "keypress", onTextInput);
+ if (useragent.isIE) {
+ var keytable = { 13:1, 27:1 };
+ event.addListener(text, "keyup", function (e) {
+ if (inCompostion && (!text.value || keytable[e.keyCode]))
+ setTimeout(onCompositionEnd, 0);
+ if ((text.value.charCodeAt(0)|0) < 129) {
+ return;
+ };
+ inCompostion ? onCompositionUpdate() : onCompositionStart();
+ });
+ };
+ event.addListener(text, "textInput", onTextInput);
+ event.addListener(text, "paste", function(e) {
+ // Some browsers support the event.clipboardData API. Use this to get
+ // the pasted content which increases speed if pasting a lot of lines.
+ if (e.clipboardData && e.clipboardData.getData) {
+ sendText(e.clipboardData.getData("text/plain"));
+ e.preventDefault();
+ } else
+ // If a browser doesn't support any of the things above, use the regular
+ // method to detect the pasted input.
+ {
+ onTextInput();
+ }
+ });
+ if (!useragent.isIE) {
+ event.addListener(text, "propertychange", onTextInput);
+ };
+
+ if (useragent.isIE) {
+ event.addListener(text, "beforecopy", function(e) {
+ var copyText = host.getCopyText();
+ if(copyText)
+ clipboardData.setData("Text", copyText);
+ else
+ e.preventDefault();
+ });
+ event.addListener(parentNode, "keydown", function(e) {
+ if (e.ctrlKey && e.keyCode == 88) {
+ var copyText = host.getCopyText();
+ if (copyText) {
+ clipboardData.setData("Text", copyText);
+ host.onCut();
+ }
+ event.preventDefault(e)
+ }
+ });
+ }
+ else {
+ event.addListener(text, "copy", onCopy);
+ event.addListener(text, "cut", onCut);
+ }
+
+ event.addListener(text, "compositionstart", onCompositionStart);
+ if (useragent.isGecko) {
+ event.addListener(text, "text", onCompositionUpdate);
+ };
+ if (useragent.isWebKit) {
+ event.addListener(text, "keyup", onCompositionUpdate);
+ };
+ event.addListener(text, "compositionend", onCompositionEnd);
+
+ event.addListener(text, "blur", function() {
+ host.onBlur();
+ });
+
+ event.addListener(text, "focus", function() {
+ host.onFocus();
+ text.select();
+ });
+
+ this.focus = function() {
+ host.onFocus();
+ text.select();
+ text.focus();
+ };
+
+ this.blur = function() {
+ text.blur();
+ };
+
+ this.getElement = function() {
+ return text;
+ };
+
+ this.onContextMenu = function(mousePos, isEmpty){
+ if (mousePos) {
+ if(!tempStyle)
+ tempStyle = text.style.cssText;
+ text.style.cssText = 'position:fixed; z-index:1000;' +
+ 'left:' + (mousePos.x - 2) + 'px; top:' + (mousePos.y - 2) + 'px;'
+
+ }
+ if (isEmpty)
+ text.value='';
+ }
+
+ this.onContextMenuClose = function(){
+ setTimeout(function () {
+ if (tempStyle) {
+ text.style.cssText = tempStyle;
+ tempStyle = '';
+ }
+ sendText();
+ }, 0);
+ }
+};
+
+exports.TextInput = TextInput;
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Mihai Sucan <mihai DOT sucan AT gmail DOT com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/mouse_handler', ['require', 'exports', 'module' , 'pilot/event', 'pilot/dom'], function(require, exports, module) {
+
+var event = require("pilot/event");
+var dom = require("pilot/dom");
+
+var STATE_UNKNOWN = 0;
+var STATE_SELECT = 1;
+var STATE_DRAG = 2;
+
+var DRAG_TIMER = 250; // milliseconds
+var DRAG_OFFSET = 5; // pixels
+
+var MouseHandler = function(editor) {
+ this.editor = editor;
+ event.addListener(editor.container, "mousedown", function(e) {
+ editor.focus();
+ return event.preventDefault(e);
+ });
+ event.addListener(editor.container, "selectstart", function(e) {
+ return event.preventDefault(e);
+ });
+
+ var mouseTarget = editor.renderer.getMouseEventTarget();
+ event.addListener(mouseTarget, "mousedown", this.onMouseDown.bind(this));
+ event.addMultiMouseDownListener(mouseTarget, 0, 2, 500, this.onMouseDoubleClick.bind(this));
+ event.addMultiMouseDownListener(mouseTarget, 0, 3, 600, this.onMouseTripleClick.bind(this));
+ event.addMultiMouseDownListener(mouseTarget, 0, 4, 600, this.onMouseQuadClick.bind(this));
+ event.addMouseWheelListener(mouseTarget, this.onMouseWheel.bind(this));
+};
+
+(function() {
+
+ this.$scrollSpeed = 1;
+ this.setScrollSpeed = function(speed) {
+ this.$scrollSpeed = speed;
+ };
+
+ this.getScrollSpeed = function() {
+ return this.$scrollSpeed;
+ };
+
+ this.$getEventPosition = function(e) {
+ var pageX = event.getDocumentX(e);
+ var pageY = event.getDocumentY(e);
+ var pos = this.editor.renderer.screenToTextCoordinates(pageX, pageY);
+ pos.row = Math.max(0, Math.min(pos.row, this.editor.session.getLength()-1));
+ return pos;
+ };
+
+ this.$distance = function(ax, ay, bx, by) {
+ return Math.sqrt(Math.pow(bx - ax, 2) + Math.pow(by - ay, 2));
+ };
+
+ this.onMouseDown = function(e) {
+ var pageX = event.getDocumentX(e);
+ var pageY = event.getDocumentY(e);
+ var pos = this.$getEventPosition(e);
+ var editor = this.editor;
+ var self = this;
+ var selectionRange = editor.getSelectionRange();
+ var selectionEmpty = selectionRange.isEmpty();
+ var state = STATE_UNKNOWN;
+ var inSelection = false;
+
+ var button = event.getButton(e);
+ if (button !== 0) {
+ if (selectionEmpty) {
+ editor.moveCursorToPosition(pos);
+ }
+ if(button == 2) {
+ editor.textInput.onContextMenu({x: pageX, y: pageY}, selectionEmpty);
+ event.capture(editor.container, function(){}, editor.textInput.onContextMenuClose);
+ }
+ return;
+ } else {
+ // Select the fold as the user clicks it.
+ var fold = editor.session.getFoldAt(pos.row, pos.column, 1);
+ if (fold) {
+ editor.selection.setSelectionRange(fold.range);
+ return;
+ }
+
+ inSelection = !editor.getReadOnly()
+ && !selectionEmpty
+ && selectionRange.contains(pos.row, pos.column);
+ }
+
+ if (!inSelection) {
+ // Directly pick STATE_SELECT, since the user is not clicking inside
+ // a selection.
+ onStartSelect(pos);
+ }
+
+ var mousePageX, mousePageY;
+ var overwrite = editor.getOverwrite();
+ var mousedownTime = (new Date()).getTime();
+ var dragCursor, dragRange;
+
+ var onMouseSelection = function(e) {
+ mousePageX = event.getDocumentX(e);
+ mousePageY = event.getDocumentY(e);
+ };
+
+ var onMouseSelectionEnd = function() {
+ clearInterval(timerId);
+ if (state == STATE_UNKNOWN)
+ onStartSelect(pos);
+ else if (state == STATE_DRAG)
+ onMouseDragSelectionEnd();
+
+ self.$clickSelection = null;
+ state = STATE_UNKNOWN;
+ };
+
+ var onMouseDragSelectionEnd = function() {
+ dom.removeCssClass(editor.container, "ace_dragging");
+ editor.session.removeMarker(dragSelectionMarker);
+
+ if (!self.$clickSelection) {
+ if (!dragCursor) {
+ editor.moveCursorToPosition(pos);
+ editor.selection.clearSelection(pos.row, pos.column);
+ }
+ }
+
+ if (!dragCursor)
+ return;
+
+ if (dragRange.contains(dragCursor.row, dragCursor.column)) {
+ dragCursor = null;
+ return;
+ }
+
+ editor.clearSelection();
+ var newRange = editor.moveText(dragRange, dragCursor);
+ if (!newRange) {
+ dragCursor = null;
+ return;
+ }
+
+ editor.selection.setSelectionRange(newRange);
+ };
+
+ var onSelectionInterval = function() {
+ if (mousePageX === undefined || mousePageY === undefined)
+ return;
+
+ if (state == STATE_UNKNOWN) {
+ var distance = self.$distance(pageX, pageY, mousePageX, mousePageY);
+ var time = (new Date()).getTime();
+
+
+ if (distance > DRAG_OFFSET) {
+ state = STATE_SELECT;
+ var cursor = editor.renderer.screenToTextCoordinates(mousePageX, mousePageY);
+ cursor.row = Math.max(0, Math.min(cursor.row, editor.session.getLength()-1));
+ onStartSelect(cursor);
+ } else if ((time - mousedownTime) > DRAG_TIMER) {
+ state = STATE_DRAG;
+ dragRange = editor.getSelectionRange();
+ var style = editor.getSelectionStyle();
+ dragSelectionMarker = editor.session.addMarker(dragRange, "ace_selection", style);
+ editor.clearSelection();
+ dom.addCssClass(editor.container, "ace_dragging");
+ }
+
+ }
+
+ if (state == STATE_DRAG)
+ onDragSelectionInterval();
+ else if (state == STATE_SELECT)
+ onUpdateSelectionInterval();
+ };
+
+ function onStartSelect(pos) {
+ if (e.shiftKey)
+ editor.selection.selectToPosition(pos)
+ else {
+ if (!self.$clickSelection) {
+ editor.moveCursorToPosition(pos);
+ editor.selection.clearSelection(pos.row, pos.column);
+ }
+ }
+ state = STATE_SELECT;
+ }
+
+ var onUpdateSelectionInterval = function() {
+ var cursor = editor.renderer.screenToTextCoordinates(mousePageX, mousePageY);
+ cursor.row = Math.max(0, Math.min(cursor.row, editor.session.getLength()-1));
+
+ if (self.$clickSelection) {
+ if (self.$clickSelection.contains(cursor.row, cursor.column)) {
+ editor.selection.setSelectionRange(self.$clickSelection);
+ } else {
+ if (self.$clickSelection.compare(cursor.row, cursor.column) == -1) {
+ var anchor = self.$clickSelection.end;
+ } else {
+ var anchor = self.$clickSelection.start;
+ }
+ editor.selection.setSelectionAnchor(anchor.row, anchor.column);
+ editor.selection.selectToPosition(cursor);
+ }
+ }
+ else {
+ editor.selection.selectToPosition(cursor);
+ }
+
+ editor.renderer.scrollCursorIntoView();
+ };
+
+ var onDragSelectionInterval = function() {
+ dragCursor = editor.renderer.screenToTextCoordinates(mousePageX, mousePageY);
+ dragCursor.row = Math.max(0, Math.min(dragCursor.row,
+ editor.session.getLength() - 1));
+
+ editor.moveCursorToPosition(dragCursor);
+ };
+
+ event.capture(editor.container, onMouseSelection, onMouseSelectionEnd);
+ var timerId = setInterval(onSelectionInterval, 20);
+
+ return event.preventDefault(e);
+ };
+
+ this.onMouseDoubleClick = function(e) {
+ var editor = this.editor;
+ var pos = this.$getEventPosition(e);
+
+ // If the user dclicked on a fold, then expand it.
+ var fold = editor.session.getFoldAt(pos.row, pos.column, 1);
+ if (fold) {
+ editor.session.expandFold(fold);
+ } else {
+ editor.moveCursorToPosition(pos);
+ editor.selection.selectWord();
+ this.$clickSelection = editor.getSelectionRange();
+ }
+ };
+
+ this.onMouseTripleClick = function(e) {
+ var pos = this.$getEventPosition(e);
+ this.editor.moveCursorToPosition(pos);
+ this.editor.selection.selectLine();
+ this.$clickSelection = this.editor.getSelectionRange();
+ };
+
+ this.onMouseQuadClick = function(e) {
+ this.editor.selectAll();
+ this.$clickSelection = this.editor.getSelectionRange();
+ };
+
+ this.onMouseWheel = function(e) {
+ var speed = this.$scrollSpeed * 2;
+
+ this.editor.renderer.scrollBy(e.wheelX * speed, e.wheelY * speed);
+ return event.preventDefault(e);
+ };
+
+
+}).call(MouseHandler.prototype);
+
+exports.MouseHandler = MouseHandler;
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Julian Viereck <julian.viereck@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/keyboard/keybinding', ['require', 'exports', 'module' , 'pilot/useragent', 'pilot/keys', 'pilot/event', 'pilot/settings', 'pilot/canon', 'ace/commands/default_commands'], function(require, exports, module) {
+
+var useragent = require("pilot/useragent");
+var keyUtil = require("pilot/keys");
+var event = require("pilot/event");
+var settings = require("pilot/settings").settings;
+var canon = require("pilot/canon");
+require("ace/commands/default_commands");
+
+var KeyBinding = function(editor) {
+ this.$editor = editor;
+ this.$data = { };
+ this.$keyboardHandler = null;
+};
+
+(function() {
+ this.setKeyboardHandler = function(keyboardHandler) {
+ if (this.$keyboardHandler != keyboardHandler) {
+ this.$data = { };
+ this.$keyboardHandler = keyboardHandler;
+ }
+ };
+
+ this.getKeyboardHandler = function() {
+ return this.$keyboardHandler;
+ };
+
+ this.$callKeyboardHandler = function (e, hashId, keyOrText, keyCode) {
+ var env = {editor: this.$editor},
+ toExecute;
+
+ if (this.$keyboardHandler) {
+ toExecute =
+ this.$keyboardHandler.handleKeyboard(this.$data, hashId, keyOrText, keyCode, e);
+ }
+
+ // If there is nothing to execute yet, then use the default keymapping.
+ if (!toExecute || !toExecute.command) {
+ if (hashId != 0 || keyCode != 0) {
+ toExecute = {
+ command: canon.findKeyCommand(env, "editor", hashId, keyOrText)
+ }
+ } else {
+ toExecute = {
+ command: "inserttext",
+ args: {
+ text: keyOrText
+ }
+ }
+ }
+ }
+
+ if (toExecute) {
+ var success = canon.exec(toExecute.command,
+ env, "editor", toExecute.args);
+ if (success) {
+ return event.stopEvent(e);
+ }
+ }
+ };
+
+ this.onCommandKey = function(e, hashId, keyCode) {
+ var keyString = keyUtil.keyCodeToString(keyCode);
+ this.$callKeyboardHandler(e, hashId, keyString, keyCode);
+ };
+
+ this.onTextInput = function(text) {
+ this.$callKeyboardHandler({}, 0, text, 0);
+ }
+
+}).call(KeyBinding.prototype);
+
+exports.KeyBinding = KeyBinding;
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Julian Viereck <julian.viereck@gmail.com>
+ * Mihai Sucan <mihai.sucan@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/commands/default_commands', ['require', 'exports', 'module' , 'pilot/lang', 'pilot/canon'], function(require, exports, module) {
+
+var lang = require("pilot/lang");
+var canon = require("pilot/canon");
+
+function bindKey(win, mac) {
+ return {
+ win: win,
+ mac: mac,
+ sender: "editor"
+ };
+}
+
+canon.addCommand({
+ name: "null",
+ exec: function(env, args, request) { }
+});
+
+canon.addCommand({
+ name: "selectall",
+ bindKey: bindKey("Ctrl-A", "Command-A"),
+ exec: function(env, args, request) { env.editor.selectAll(); }
+});
+canon.addCommand({
+ name: "removeline",
+ bindKey: bindKey("Ctrl-D", "Command-D"),
+ exec: function(env, args, request) { env.editor.removeLines(); }
+});
+canon.addCommand({
+ name: "gotoline",
+ bindKey: bindKey("Ctrl-L", "Command-L"),
+ exec: function(env, args, request) {
+ var line = parseInt(prompt("Enter line number:"));
+ if (!isNaN(line)) {
+ env.editor.gotoLine(line);
+ }
+ }
+});
+canon.addCommand({
+ name: "togglecomment",
+ bindKey: bindKey("Ctrl-7", "Command-7"),
+ exec: function(env, args, request) { env.editor.toggleCommentLines(); }
+});
+canon.addCommand({
+ name: "findnext",
+ bindKey: bindKey("Ctrl-K", "Command-G"),
+ exec: function(env, args, request) { env.editor.findNext(); }
+});
+canon.addCommand({
+ name: "findprevious",
+ bindKey: bindKey("Ctrl-Shift-K", "Command-Shift-G"),
+ exec: function(env, args, request) { env.editor.findPrevious(); }
+});
+canon.addCommand({
+ name: "find",
+ bindKey: bindKey("Ctrl-F", "Command-F"),
+ exec: function(env, args, request) {
+ var needle = prompt("Find:");
+ env.editor.find(needle);
+ }
+});
+canon.addCommand({
+ name: "replace",
+ bindKey: bindKey("Ctrl-R", "Command-Option-F"),
+ exec: function(env, args, request) {
+ var needle = prompt("Find:");
+ if (!needle)
+ return;
+ var replacement = prompt("Replacement:");
+ if (!replacement)
+ return;
+ env.editor.replace(replacement, {needle: needle});
+ }
+});
+canon.addCommand({
+ name: "replaceall",
+ bindKey: bindKey("Ctrl-Shift-R", "Command-Shift-Option-F"),
+ exec: function(env, args, request) {
+ var needle = prompt("Find:");
+ if (!needle)
+ return;
+ var replacement = prompt("Replacement:");
+ if (!replacement)
+ return;
+ env.editor.replaceAll(replacement, {needle: needle});
+ }
+});
+canon.addCommand({
+ name: "undo",
+ bindKey: bindKey("Ctrl-Z", "Command-Z"),
+ exec: function(env, args, request) { env.editor.undo(); }
+});
+canon.addCommand({
+ name: "redo",
+ bindKey: bindKey("Ctrl-Shift-Z|Ctrl-Y", "Command-Shift-Z|Command-Y"),
+ exec: function(env, args, request) { env.editor.redo(); }
+});
+canon.addCommand({
+ name: "overwrite",
+ bindKey: bindKey("Insert", "Insert"),
+ exec: function(env, args, request) { env.editor.toggleOverwrite(); }
+});
+canon.addCommand({
+ name: "copylinesup",
+ bindKey: bindKey("Ctrl-Alt-Up", "Command-Option-Up"),
+ exec: function(env, args, request) { env.editor.copyLinesUp(); }
+});
+canon.addCommand({
+ name: "movelinesup",
+ bindKey: bindKey("Alt-Up", "Option-Up"),
+ exec: function(env, args, request) { env.editor.moveLinesUp(); }
+});
+canon.addCommand({
+ name: "selecttostart",
+ bindKey: bindKey("Ctrl-Shift-Home|Alt-Shift-Up", "Command-Shift-Up"),
+ exec: function(env, args, request) { env.editor.getSelection().selectFileStart(); }
+});
+canon.addCommand({
+ name: "gotostart",
+ bindKey: bindKey("Ctrl-Home|Ctrl-Up", "Command-Home|Command-Up"),
+ exec: function(env, args, request) { env.editor.navigateFileStart(); }
+});
+canon.addCommand({
+ name: "selectup",
+ bindKey: bindKey("Shift-Up", "Shift-Up"),
+ exec: function(env, args, request) { env.editor.getSelection().selectUp(); }
+});
+canon.addCommand({
+ name: "golineup",
+ bindKey: bindKey("Up", "Up|Ctrl-P"),
+ exec: function(env, args, request) { env.editor.navigateUp(args.times); }
+});
+canon.addCommand({
+ name: "copylinesdown",
+ bindKey: bindKey("Ctrl-Alt-Down", "Command-Option-Down"),
+ exec: function(env, args, request) { env.editor.copyLinesDown(); }
+});
+canon.addCommand({
+ name: "movelinesdown",
+ bindKey: bindKey("Alt-Down", "Option-Down"),
+ exec: function(env, args, request) { env.editor.moveLinesDown(); }
+});
+canon.addCommand({
+ name: "selecttoend",
+ bindKey: bindKey("Ctrl-Shift-End|Alt-Shift-Down", "Command-Shift-Down"),
+ exec: function(env, args, request) { env.editor.getSelection().selectFileEnd(); }
+});
+canon.addCommand({
+ name: "gotoend",
+ bindKey: bindKey("Ctrl-End|Ctrl-Down", "Command-End|Command-Down"),
+ exec: function(env, args, request) { env.editor.navigateFileEnd(); }
+});
+canon.addCommand({
+ name: "selectdown",
+ bindKey: bindKey("Shift-Down", "Shift-Down"),
+ exec: function(env, args, request) { env.editor.getSelection().selectDown(); }
+});
+canon.addCommand({
+ name: "golinedown",
+ bindKey: bindKey("Down", "Down|Ctrl-N"),
+ exec: function(env, args, request) { env.editor.navigateDown(args.times); }
+});
+canon.addCommand({
+ name: "selectwordleft",
+ bindKey: bindKey("Ctrl-Shift-Left", "Option-Shift-Left"),
+ exec: function(env, args, request) { env.editor.getSelection().selectWordLeft(); }
+});
+canon.addCommand({
+ name: "gotowordleft",
+ bindKey: bindKey("Ctrl-Left", "Option-Left"),
+ exec: function(env, args, request) { env.editor.navigateWordLeft(); }
+});
+canon.addCommand({
+ name: "selecttolinestart",
+ bindKey: bindKey("Alt-Shift-Left", "Command-Shift-Left"),
+ exec: function(env, args, request) { env.editor.getSelection().selectLineStart(); }
+});
+canon.addCommand({
+ name: "gotolinestart",
+ bindKey: bindKey("Alt-Left|Home", "Command-Left|Home|Ctrl-A"),
+ exec: function(env, args, request) { env.editor.navigateLineStart(); }
+});
+canon.addCommand({
+ name: "selectleft",
+ bindKey: bindKey("Shift-Left", "Shift-Left"),
+ exec: function(env, args, request) { env.editor.getSelection().selectLeft(); }
+});
+canon.addCommand({
+ name: "gotoleft",
+ bindKey: bindKey("Left", "Left|Ctrl-B"),
+ exec: function(env, args, request) { env.editor.navigateLeft(args.times); }
+});
+canon.addCommand({
+ name: "selectwordright",
+ bindKey: bindKey("Ctrl-Shift-Right", "Option-Shift-Right"),
+ exec: function(env, args, request) { env.editor.getSelection().selectWordRight(); }
+});
+canon.addCommand({
+ name: "gotowordright",
+ bindKey: bindKey("Ctrl-Right", "Option-Right"),
+ exec: function(env, args, request) { env.editor.navigateWordRight(); }
+});
+canon.addCommand({
+ name: "selecttolineend",
+ bindKey: bindKey("Alt-Shift-Right", "Command-Shift-Right"),
+ exec: function(env, args, request) { env.editor.getSelection().selectLineEnd(); }
+});
+canon.addCommand({
+ name: "gotolineend",
+ bindKey: bindKey("Alt-Right|End", "Command-Right|End|Ctrl-E"),
+ exec: function(env, args, request) { env.editor.navigateLineEnd(); }
+});
+canon.addCommand({
+ name: "selectright",
+ bindKey: bindKey("Shift-Right", "Shift-Right"),
+ exec: function(env, args, request) { env.editor.getSelection().selectRight(); }
+});
+canon.addCommand({
+ name: "gotoright",
+ bindKey: bindKey("Right", "Right|Ctrl-F"),
+ exec: function(env, args, request) { env.editor.navigateRight(args.times); }
+});
+canon.addCommand({
+ name: "selectpagedown",
+ bindKey: bindKey("Shift-PageDown", "Shift-PageDown"),
+ exec: function(env, args, request) { env.editor.selectPageDown(); }
+});
+canon.addCommand({
+ name: "pagedown",
+ bindKey: bindKey(null, "PageDown"),
+ exec: function(env, args, request) { env.editor.scrollPageDown(); }
+});
+canon.addCommand({
+ name: "gotopagedown",
+ bindKey: bindKey("PageDown", "Option-PageDown|Ctrl-V"),
+ exec: function(env, args, request) { env.editor.gotoPageDown(); }
+});
+canon.addCommand({
+ name: "selectpageup",
+ bindKey: bindKey("Shift-PageUp", "Shift-PageUp"),
+ exec: function(env, args, request) { env.editor.selectPageUp(); }
+});
+canon.addCommand({
+ name: "pageup",
+ bindKey: bindKey(null, "PageUp"),
+ exec: function(env, args, request) { env.editor.scrollPageUp(); }
+});
+canon.addCommand({
+ name: "gotopageup",
+ bindKey: bindKey("PageUp", "Option-PageUp"),
+ exec: function(env, args, request) { env.editor.gotoPageUp(); }
+});
+canon.addCommand({
+ name: "selectlinestart",
+ bindKey: bindKey("Shift-Home", "Shift-Home"),
+ exec: function(env, args, request) { env.editor.getSelection().selectLineStart(); }
+});
+canon.addCommand({
+ name: "selectlineend",
+ bindKey: bindKey("Shift-End", "Shift-End"),
+ exec: function(env, args, request) { env.editor.getSelection().selectLineEnd(); }
+});
+canon.addCommand({
+ name: "del",
+ bindKey: bindKey("Delete", "Delete|Ctrl-D"),
+ exec: function(env, args, request) { env.editor.removeRight(); }
+});
+canon.addCommand({
+ name: "backspace",
+ bindKey: bindKey(
+ "Ctrl-Backspace|Command-Backspace|Option-Backspace|Shift-Backspace|Backspace",
+ "Ctrl-Backspace|Command-Backspace|Shift-Backspace|Backspace|Ctrl-H"
+ ),
+ exec: function(env, args, request) { env.editor.removeLeft(); }
+});
+canon.addCommand({
+ name: "removetolinestart",
+ bindKey: bindKey(null, "Option-Backspace"),
+ exec: function(env, args, request) { env.editor.removeToLineStart(); }
+});
+canon.addCommand({
+ name: "removetolineend",
+ bindKey: bindKey(null, "Ctrl-K"),
+ exec: function(env, args, request) { env.editor.removeToLineEnd(); }
+});
+canon.addCommand({
+ name: "removewordleft",
+ bindKey: bindKey(null, "Alt-Backspace|Ctrl-Alt-Backspace"),
+ exec: function(env, args, request) { env.editor.removeWordLeft(); }
+});
+canon.addCommand({
+ name: "removewordright",
+ bindKey: bindKey(null, "Alt-Delete"),
+ exec: function(env, args, request) { env.editor.removeWordRight(); }
+});
+canon.addCommand({
+ name: "outdent",
+ bindKey: bindKey("Shift-Tab", "Shift-Tab"),
+ exec: function(env, args, request) { env.editor.blockOutdent(); }
+});
+canon.addCommand({
+ name: "indent",
+ bindKey: bindKey("Tab", "Tab"),
+ exec: function(env, args, request) { env.editor.indent(); }
+});
+canon.addCommand({
+ name: "inserttext",
+ exec: function(env, args, request) {
+ env.editor.insert(lang.stringRepeat(args.text || "", args.times || 1));
+ }
+});
+canon.addCommand({
+ name: "centerselection",
+ bindKey: bindKey(null, "Ctrl-L"),
+ exec: function(env, args, request) { env.editor.centerSelection(); }
+});
+canon.addCommand({
+ name: "splitline",
+ bindKey: bindKey(null, "Ctrl-O"),
+ exec: function(env, args, request) { env.editor.splitLine(); }
+});
+canon.addCommand({
+ name: "transposeletters",
+ bindKey: bindKey("Ctrl-T", "Ctrl-T"),
+ exec: function(env, args, request) { env.editor.transposeLetters(); }
+});
+
+});/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Mihai Sucan <mihai DOT sucan AT gmail DOT com>
+ * Julian Viereck <julian DOT viereck AT gmail DOT com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/edit_session', ['require', 'exports', 'module' , 'pilot/oop', 'pilot/lang', 'pilot/event_emitter', 'ace/selection', 'ace/mode/text', 'ace/range', 'ace/document', 'ace/background_tokenizer', 'ace/edit_session/folding'], function(require, exports, module) {
+
+var oop = require("pilot/oop");
+var lang = require("pilot/lang");
+var EventEmitter = require("pilot/event_emitter").EventEmitter;
+var Selection = require("ace/selection").Selection;
+var TextMode = require("ace/mode/text").Mode;
+var Range = require("ace/range").Range;
+var Document = require("ace/document").Document;
+var BackgroundTokenizer = require("ace/background_tokenizer").BackgroundTokenizer;
+
+var EditSession = function(text, mode) {
+ this.$modified = true;
+ this.$breakpoints = [];
+ this.$frontMarkers = {};
+ this.$backMarkers = {};
+ this.$markerId = 1;
+ this.$rowCache = [];
+ this.$rowCacheSize = 1000;
+ this.$wrapData = [];
+ this.$foldData = [];
+ this.$foldData.toString = function() {
+ var str = "";
+ this.forEach(function(foldLine) {
+ str += "\n" + foldLine.toString();
+ });
+ return str;
+ }
+ this.$docChangeCounter = 0;
+
+ if (text instanceof Document) {
+ this.setDocument(text);
+ } else {
+ this.setDocument(new Document(text));
+ }
+
+ this.selection = new Selection(this);
+ if (mode)
+ this.setMode(mode);
+ else
+ this.setMode(new TextMode());
+};
+
+
+(function() {
+
+ oop.implement(this, EventEmitter);
+
+ this.setDocument = function(doc) {
+ if (this.doc)
+ throw new Error("Document is already set");
+
+ this.doc = doc;
+ doc.on("change", this.onChange.bind(this));
+ doc.on("changeStart", this.onChangeStart.bind(this));
+ doc.on("changeEnd", this.onChangeEnd.bind(this));
+ this.on("changeFold", this.onChangeFold.bind(this));
+ };
+
+ this.getDocument = function() {
+ return this.doc;
+ };
+
+ this.onChangeStart = function() {
+ this.$docChangeCounter ++;
+ };
+
+ this.$resetRowCache = function(row) {
+ if (row == 0) {
+ this.$rowCache = [];
+ return;
+ }
+ var rowCache = this.$rowCache;
+ for (var i = 0; i < rowCache.length; i++) {
+ if (rowCache[i].docRow >= row) {
+ rowCache.splice(i, rowCache.length);
+ return;
+ }
+ }
+ }
+
+ this.onChangeEnd = function() {
+ this.$docChangeCounter --;
+ if (this.$docChangeCounter == 0
+ && !this.$fromUndo && this.$undoManager)
+ {
+ if (this.$deltasFold.length) {
+ this.$deltas.push({
+ group: "fold",
+ deltas: this.$deltasFold
+ });
+ this.$deltasFold = [];
+ }
+ if (this.$deltasDoc) {
+ this.$deltas.push({
+ group: "doc",
+ deltas: this.$deltasDoc
+ });
+ this.$deltasDoc = [];
+ }
+ this.$informUndoManager.schedule();
+ }
+ };
+
+ this.onChangeFold = function(e) {
+ var fold = e.data;
+ this.$resetRowCache(fold.start.row);
+ };
+
+ this.onChange = function(e) {
+ var delta = e.data;
+ this.$modified = true;
+
+ this.$resetRowCache(delta.range.start.row);
+
+ var removedFolds = this.$updateInternalDataOnChange(e);
+ if (!this.$fromUndo && this.$undoManager && !delta.ignore) {
+ this.$deltasDoc.push(delta);
+ if (removedFolds && removedFolds.length != 0) {
+ this.$deltasFold.push({
+ action: "removeFolds",
+ folds: removedFolds
+ });
+ }
+ }
+
+ this.bgTokenizer.start(delta.range.start.row);
+ this._dispatchEvent("change", e);
+ };
+
+ this.setValue = function(text) {
+ this.doc.setValue(text);
+ this.$resetRowCache(0);
+ this.$deltas = [];
+ this.$deltasDoc = [];
+ this.$deltasFold = [];
+ this.getUndoManager().reset();
+ };
+
+ this.getValue =
+ this.toString = function() {
+ return this.doc.getValue();
+ };
+
+ this.getSelection = function() {
+ return this.selection;
+ };
+
+ this.getState = function(row) {
+ return this.bgTokenizer.getState(row);
+ };
+
+ this.getTokens = function(firstRow, lastRow) {
+ return this.bgTokenizer.getTokens(firstRow, lastRow);
+ };
+
+ this.setUndoManager = function(undoManager) {
+ this.$undoManager = undoManager;
+ this.$resetRowCache(0);
+ this.$deltas = [];
+ this.$deltasDoc = [];
+ this.$deltasFold = [];
+
+ if (this.$informUndoManager) {
+ this.$informUndoManager.cancel();
+ }
+
+ if (undoManager) {
+ var self = this;
+ this.$syncInformUndoManager = function() {
+ self.$informUndoManager.cancel();
+ if (self.$deltas.length > 0)
+ undoManager.execute({
+ action : "aceupdate",
+ args : [self.$deltas, self]
+ });
+ self.$deltas = [];
+ }
+ this.$informUndoManager =
+ lang.deferredCall(this.$syncInformUndoManager);
+ }
+ };
+
+ this.$defaultUndoManager = {
+ undo: function() {},
+ redo: function() {},
+ reset: function() {}
+ };
+
+ this.getUndoManager = function() {
+ return this.$undoManager || this.$defaultUndoManager;
+ },
+
+ this.getTabString = function() {
+ if (this.getUseSoftTabs()) {
+ return lang.stringRepeat(" ", this.getTabSize());
+ } else {
+ return "\t";
+ }
+ };
+
+ this.$useSoftTabs = true;
+ this.setUseSoftTabs = function(useSoftTabs) {
+ if (this.$useSoftTabs === useSoftTabs) return;
+
+ this.$useSoftTabs = useSoftTabs;
+ };
+
+ this.getUseSoftTabs = function() {
+ return this.$useSoftTabs;
+ };
+
+ this.$tabSize = 4;
+ this.setTabSize = function(tabSize) {
+ if (isNaN(tabSize) || this.$tabSize === tabSize) return;
+
+ this.$modified = true;
+ this.$tabSize = tabSize;
+ this._dispatchEvent("changeTabSize");
+ };
+
+ this.getTabSize = function() {
+ return this.$tabSize;
+ };
+
+ this.isTabStop = function(position) {
+ return this.$useSoftTabs && (position.column % this.$tabSize == 0);
+ };
+
+ this.$overwrite = false;
+ this.setOverwrite = function(overwrite) {
+ if (this.$overwrite == overwrite) return;
+
+ this.$overwrite = overwrite;
+ this._dispatchEvent("changeOverwrite");
+ };
+
+ this.getOverwrite = function() {
+ return this.$overwrite;
+ };
+
+ this.toggleOverwrite = function() {
+ this.setOverwrite(!this.$overwrite);
+ };
+
+ this.getBreakpoints = function() {
+ return this.$breakpoints;
+ };
+
+ this.setBreakpoints = function(rows) {
+ this.$breakpoints = [];
+ for (var i=0; i<rows.length; i++) {
+ this.$breakpoints[rows[i]] = true;
+ }
+ this._dispatchEvent("changeBreakpoint", {});
+ };
+
+ this.clearBreakpoints = function() {
+ this.$breakpoints = [];
+ this._dispatchEvent("changeBreakpoint", {});
+ };
+
+ this.setBreakpoint = function(row) {
+ this.$breakpoints[row] = true;
+ this._dispatchEvent("changeBreakpoint", {});
+ };
+
+ this.clearBreakpoint = function(row) {
+ delete this.$breakpoints[row];
+ this._dispatchEvent("changeBreakpoint", {});
+ };
+
+ this.getBreakpoints = function() {
+ return this.$breakpoints;
+ };
+
+ this.addMarker = function(range, clazz, type, inFront) {
+ var id = this.$markerId++;
+
+ var marker = {
+ range : range,
+ type : type || "line",
+ renderer: typeof type == "function" ? type : null,
+ clazz : clazz,
+ inFront: !!inFront
+ }
+
+ if (inFront) {
+ this.$frontMarkers[id] = marker;
+ this._dispatchEvent("changeFrontMarker")
+ } else {
+ this.$backMarkers[id] = marker;
+ this._dispatchEvent("changeBackMarker")
+ }
+
+ return id;
+ };
+
+ this.removeMarker = function(markerId) {
+ var marker = this.$frontMarkers[markerId] || this.$backMarkers[markerId];
+ if (!marker)
+ return;
+
+ var markers = marker.inFront ? this.$frontMarkers : this.$backMarkers;
+ if (marker) {
+ delete (markers[markerId]);
+ this._dispatchEvent(marker.inFront ? "changeFrontMarker" : "changeBackMarker");
+ }
+ };
+
+ this.getMarkers = function(inFront) {
+ return inFront ? this.$frontMarkers : this.$backMarkers;
+ };
+
+ /**
+ * Error:
+ * {
+ * row: 12,
+ * column: 2, //can be undefined
+ * text: "Missing argument",
+ * type: "error" // or "warning" or "info"
+ * }
+ */
+ this.setAnnotations = function(annotations) {
+ this.$annotations = {};
+ for (var i=0; i<annotations.length; i++) {
+ var annotation = annotations[i];
+ var row = annotation.row;
+ if (this.$annotations[row])
+ this.$annotations[row].push(annotation);
+ else
+ this.$annotations[row] = [annotation];
+ }
+ this._dispatchEvent("changeAnnotation", {});
+ };
+
+ this.getAnnotations = function() {
+ return this.$annotations;
+ };
+
+ this.clearAnnotations = function() {
+ this.$annotations = {};
+ this._dispatchEvent("changeAnnotation", {});
+ };
+
+ this.$detectNewLine = function(text) {
+ var match = text.match(/^.*?(\r?\n)/m);
+ if (match) {
+ this.$autoNewLine = match[1];
+ } else {
+ this.$autoNewLine = "\n";
+ }
+ };
+
+ this.tokenRe = /^[\w\d]+/g;
+ this.nonTokenRe = /^(?:[^\w\d]|[\u3040-\u309F]|[\u30A0-\u30FF]|[\u4E00-\u9FFF\uF900-\uFAFF\u3400-\u4DBF])+/g;
+
+ this.getWordRange = function(row, column) {
+ var line = this.getLine(row);
+
+ var inToken = false;
+ if (column > 0) {
+ inToken = !!line.charAt(column - 1).match(this.tokenRe);
+ }
+
+ if (!inToken) {
+ inToken = !!line.charAt(column).match(this.tokenRe);
+ }
+
+ var re = inToken ? this.tokenRe : this.nonTokenRe;
+
+ var start = column;
+ if (start > 0) {
+ do {
+ start--;
+ }
+ while (start >= 0 && line.charAt(start).match(re));
+ start++;
+ }
+
+ var end = column;
+ while (end < line.length && line.charAt(end).match(re)) {
+ end++;
+ }
+
+ return new Range(row, start, row, end);
+ };
+
+ this.setNewLineMode = function(newLineMode) {
+ this.doc.setNewLineMode(newLineMode);
+ };
+
+ this.getNewLineMode = function() {
+ return this.doc.getNewLineMode();
+ };
+
+ this.$useWorker = true;
+ this.setUseWorker = function(useWorker) {
+ if (this.$useWorker == useWorker)
+ return;
+
+ this.$useWorker = useWorker;
+
+ this.$stopWorker();
+ if (useWorker)
+ this.$startWorker();
+ };
+
+ this.getUseWorker = function() {
+ return this.$useWorker;
+ };
+
+ this.onReloadTokenizer = function(e) {
+ var rows = e.data;
+ this.bgTokenizer.start(rows.first);
+ this._dispatchEvent("tokenizerUpdate", e);
+ };
+
+ this.$mode = null;
+ this.setMode = function(mode) {
+ if (this.$mode === mode) return;
+ this.$mode = mode;
+
+ this.$stopWorker();
+
+ if (this.$useWorker)
+ this.$startWorker();
+
+ var tokenizer = mode.getTokenizer();
+
+ if(tokenizer.addEventListener !== undefined) {
+ var onReloadTokenizer = this.onReloadTokenizer.bind(this);
+ tokenizer.addEventListener("update", onReloadTokenizer);
+ }
+
+ if (!this.bgTokenizer) {
+ this.bgTokenizer = new BackgroundTokenizer(tokenizer);
+ var _self = this;
+ this.bgTokenizer.addEventListener("update", function(e) {
+ _self._dispatchEvent("tokenizerUpdate", e);
+ });
+ } else {
+ this.bgTokenizer.setTokenizer(tokenizer);
+ }
+
+ this.bgTokenizer.setDocument(this.getDocument());
+ this.bgTokenizer.start(0);
+
+ this._dispatchEvent("changeMode");
+ };
+
+ this.$stopWorker = function() {
+ if (this.$worker)
+ this.$worker.terminate();
+
+ this.$worker = null;
+ };
+
+ this.$startWorker = function() {
+ if (typeof Worker !== "undefined" && !require.noWorker) {
+ try {
+ this.$worker = this.$mode.createWorker(this);
+ } catch (e) {
+ console.log("Could not load worker");
+ console.log(e);
+ this.$worker = null;
+ }
+ }
+ else
+ this.$worker = null;
+ };
+
+ this.getMode = function() {
+ return this.$mode;
+ };
+
+ this.$scrollTop = 0;
+ this.setScrollTopRow = function(scrollTopRow) {
+ if (this.$scrollTop === scrollTopRow) return;
+
+ this.$scrollTop = scrollTopRow;
+ this._dispatchEvent("changeScrollTop");
+ };
+
+ this.getScrollTopRow = function() {
+ return this.$scrollTop;
+ };
+
+ this.getWidth = function() {
+ this.$computeWidth();
+ return this.width;
+ };
+
+ this.getScreenWidth = function() {
+ this.$computeWidth();
+ return this.screenWidth;
+ };
+
+ this.$computeWidth = function(force) {
+ if (this.$modified || force) {
+ this.$modified = false;
+
+ var lines = this.doc.getAllLines();
+ var longestLine = 0;
+ var longestScreenLine = 0;
+
+ for ( var i = 0; i < lines.length; i++) {
+ var foldLine = this.getFoldLine(i),
+ line, len;
+
+ line = lines[i];
+ if (foldLine) {
+ var end = foldLine.range.end;
+ line = this.getFoldDisplayLine(foldLine);
+ // Continue after the foldLine.end.row. All the lines in
+ // between are folded.
+ i = end.row;
+ }
+ len = line.length;
+ longestLine = Math.max(longestLine, len);
+ if (!this.$useWrapMode) {
+ longestScreenLine = Math.max(
+ longestScreenLine,
+ this.$getStringScreenWidth(line)[0]
+ );
+ }
+ }
+ this.width = longestLine;
+
+ if (this.$useWrapMode) {
+ this.screenWidth = this.$wrapLimit;
+ } else {
+ this.screenWidth = longestScreenLine;
+ }
+ }
+ };
+
+ /**
+ * Get a verbatim copy of the given line as it is in the document
+ */
+ this.getLine = function(row) {
+ return this.doc.getLine(row);
+ };
+
+ this.getLines = function(firstRow, lastRow) {
+ return this.doc.getLines(firstRow, lastRow);
+ };
+
+ this.getLength = function() {
+ return this.doc.getLength();
+ };
+
+ this.getTextRange = function(range) {
+ return this.doc.getTextRange(range);
+ };
+
+ this.findMatchingBracket = function(position) {
+ if (position.column == 0) return null;
+
+ var charBeforeCursor = this.getLine(position.row).charAt(position.column-1);
+ if (charBeforeCursor == "") return null;
+
+ var match = charBeforeCursor.match(/([\(\[\{])|([\)\]\}])/);
+ if (!match) {
+ return null;
+ }
+
+ if (match[1]) {
+ return this.$findClosingBracket(match[1], position);
+ } else {
+ return this.$findOpeningBracket(match[2], position);
+ }
+ };
+
+ this.$brackets = {
+ ")": "(",
+ "(": ")",
+ "]": "[",
+ "[": "]",
+ "{": "}",
+ "}": "{"
+ };
+
+ this.$findOpeningBracket = function(bracket, position) {
+ var openBracket = this.$brackets[bracket];
+
+ var column = position.column - 2;
+ var row = position.row;
+ var depth = 1;
+
+ var line = this.getLine(row);
+
+ while (true) {
+ while(column >= 0) {
+ var ch = line.charAt(column);
+ if (ch == openBracket) {
+ depth -= 1;
+ if (depth == 0) {
+ return {row: row, column: column};
+ }
+ }
+ else if (ch == bracket) {
+ depth +=1;
+ }
+ column -= 1;
+ }
+ row -=1;
+ if (row < 0) break;
+
+ var line = this.getLine(row);
+ var column = line.length-1;
+ }
+ return null;
+ };
+
+ this.$findClosingBracket = function(bracket, position) {
+ var closingBracket = this.$brackets[bracket];
+
+ var column = position.column;
+ var row = position.row;
+ var depth = 1;
+
+ var line = this.getLine(row);
+ var lineCount = this.getLength();
+
+ while (true) {
+ while(column < line.length) {
+ var ch = line.charAt(column);
+ if (ch == closingBracket) {
+ depth -= 1;
+ if (depth == 0) {
+ return {row: row, column: column};
+ }
+ }
+ else if (ch == bracket) {
+ depth +=1;
+ }
+ column += 1;
+ }
+ row +=1;
+ if (row >= lineCount) break;
+
+ var line = this.getLine(row);
+ var column = 0;
+ }
+ return null;
+ };
+
+ this.insert = function(position, text) {
+ return this.doc.insert(position, text);
+ };
+
+ this.remove = function(range) {
+ return this.doc.remove(range);
+ };
+
+ this.undoChanges = function(deltas, dontSelect) {
+ if (!deltas.length)
+ return;
+
+ this.$fromUndo = true;
+ var lastUndoRange = null;
+ for (var i = deltas.length - 1; i != -1; i--) {
+ delta = deltas[i];
+ if (delta.group == "doc") {
+ this.doc.revertDeltas(delta.deltas);
+ lastUndoRange =
+ this.$getUndoSelection(delta.deltas, true, lastUndoRange);
+ } else {
+ delta.deltas.forEach(function(foldDelta) {
+ this.addFolds(foldDelta.folds);
+ }, this);
+ }
+ }
+ this.$fromUndo = false;
+ lastUndoRange &&
+ !dontSelect &&
+ this.selection.setSelectionRange(lastUndoRange);
+ return lastUndoRange;
+ },
+
+ this.redoChanges = function(deltas, dontSelect) {
+ if (!deltas.length)
+ return;
+
+ this.$fromUndo = true;
+ var lastUndoRange = null;
+ for (var i = 0; i < deltas.length; i++) {
+ delta = deltas[i];
+ if (delta.group == "doc") {
+ this.doc.applyDeltas(delta.deltas);
+ lastUndoRange =
+ this.$getUndoSelection(delta.deltas, false, lastUndoRange);
+ }
+ }
+ this.$fromUndo = false;
+ lastUndoRange &&
+ !dontSelect &&
+ this.selection.setSelectionRange(lastUndoRange);
+ return lastUndoRange;
+ },
+
+ this.$getUndoSelection = function(deltas, isUndo, lastUndoRange) {
+ function isInsert(delta) {
+ var insert =
+ delta.action == "insertText" || delta.action == "insertLines";
+ return isUndo ? !insert : insert;
+ }
+
+ var delta = deltas[0];
+ var range, point;
+ var lastDeltaIsInsert = false;
+ if (isInsert(delta)) {
+ range = delta.range.clone();
+ lastDeltaIsInsert = true;
+ } else {
+ range = Range.fromPoints(delta.range.start, delta.range.start);
+ lastDeltaIsInsert = false;
+ }
+
+ for (var i = 1; i < deltas.length; i++) {
+ delta = deltas[i];
+ if (isInsert(delta)) {
+ point = delta.range.start;
+ if (range.compare(point.row, point.column) == -1) {
+ range.setStart(delta.range.start);
+ }
+ point = delta.range.end;
+ if (range.compare(point.row, point.column) == 1) {
+ range.setEnd(delta.range.end);
+ }
+ lastDeltaIsInsert = true;
+ } else {
+ point = delta.range.start;
+ if (range.compare(point.row, point.column) == -1) {
+ range =
+ Range.fromPoints(delta.range.start, delta.range.start);
+ }
+ lastDeltaIsInsert = false;
+ }
+ }
+
+ // Check if this range and the last undo range has something in common.
+ // If true, merge the ranges.
+ if (lastUndoRange != null) {
+ var cmp = lastUndoRange.compareRange(range);
+ if (cmp == 1) {
+ range.setStart(lastUndoRange.start);
+ } else if (cmp == -1) {
+ range.setEnd(lastUndoRange.end);
+ }
+ }
+
+ return range;
+ },
+
+ this.replace = function(range, text) {
+ return this.doc.replace(range, text);
+ };
+
+ /**
+ * Move a range of text from the given range to the given position.
+ *
+ * @param fromRange {Range} The range of text you want moved within the
+ * document.
+ * @param toPosition {Object} The location (row and column) where you want
+ * to move the text to.
+ * @return {Range} The new range where the text was moved to.
+ */
+ this.moveText = function(fromRange, toPosition) {
+ var text = this.getTextRange(fromRange);
+ this.remove(fromRange);
+
+ var toRow = toPosition.row;
+ var toColumn = toPosition.column;
+
+ // Make sure to update the insert location, when text is removed in
+ // front of the chosen point of insertion.
+ if (!fromRange.isMultiLine() && fromRange.start.row == toRow &&
+ fromRange.end.column < toColumn)
+ toColumn -= text.length;
+
+ if (fromRange.isMultiLine() && fromRange.end.row < toRow) {
+ var lines = this.doc.$split(text);
+ toRow -= lines.length - 1;
+ }
+
+ var endRow = toRow + fromRange.end.row - fromRange.start.row;
+ var endColumn = fromRange.isMultiLine() ?
+ fromRange.end.column :
+ toColumn + fromRange.end.column - fromRange.start.column;
+
+ var toRange = new Range(toRow, toColumn, endRow, endColumn);
+
+ this.insert(toRange.start, text);
+
+ return toRange;
+ };
+
+ this.indentRows = function(startRow, endRow, indentString) {
+ indentString = indentString.replace(/\t/g, this.getTabString());
+ for (var row=startRow; row<=endRow; row++) {
+ this.insert({row: row, column:0}, indentString);
+ }
+ };
+
+ this.outdentRows = function (range) {
+ var rowRange = range.collapseRows();
+ var deleteRange = new Range(0, 0, 0, 0);
+ var size = this.getTabSize();
+
+ for (var i = rowRange.start.row; i <= rowRange.end.row; ++i) {
+ var line = this.getLine(i);
+
+ deleteRange.start.row = i;
+ deleteRange.end.row = i;
+ for (var j = 0; j < size; ++j)
+ if (line.charAt(j) != ' ')
+ break;
+ if (j < size && line.charAt(j) == '\t') {
+ deleteRange.start.column = j;
+ deleteRange.end.column = j + 1;
+ } else {
+ deleteRange.start.column = 0;
+ deleteRange.end.column = j;
+ }
+ this.remove(deleteRange);
+ }
+ };
+
+ this.moveLinesUp = function(firstRow, lastRow) {
+ if (firstRow <= 0) return 0;
+
+ var removed = this.doc.removeLines(firstRow, lastRow);
+ this.doc.insertLines(firstRow - 1, removed);
+ return -1;
+ };
+
+ this.moveLinesDown = function(firstRow, lastRow) {
+ if (lastRow >= this.doc.getLength()-1) return 0;
+
+ var removed = this.doc.removeLines(firstRow, lastRow);
+ this.doc.insertLines(firstRow+1, removed);
+ return 1;
+ };
+
+ this.duplicateLines = function(firstRow, lastRow) {
+ var firstRow = this.$clipRowToDocument(firstRow);
+ var lastRow = this.$clipRowToDocument(lastRow);
+
+ var lines = this.getLines(firstRow, lastRow);
+ this.doc.insertLines(firstRow, lines);
+
+ var addedRows = lastRow - firstRow + 1;
+ return addedRows;
+ };
+
+ this.$clipRowToDocument = function(row) {
+ return Math.max(0, Math.min(row, this.doc.getLength()-1));
+ };
+
+ // WRAPMODE
+ this.$wrapLimit = 80;
+ this.$useWrapMode = false;
+ this.$wrapLimitRange = {
+ min : null,
+ max : null
+ };
+
+ this.setUseWrapMode = function(useWrapMode) {
+ if (useWrapMode != this.$useWrapMode) {
+ this.$useWrapMode = useWrapMode;
+ this.$modified = true;
+ this.$resetRowCache(0);
+
+ // If wrapMode is activaed, the wrapData array has to be initialized.
+ if (useWrapMode) {
+ var len = this.getLength();
+ this.$wrapData = [];
+ for (i = 0; i < len; i++) {
+ this.$wrapData.push([]);
+ }
+ this.$updateWrapData(0, len - 1);
+ }
+
+ this._dispatchEvent("changeWrapMode");
+ }
+ };
+
+ this.getUseWrapMode = function() {
+ return this.$useWrapMode;
+ };
+
+ // Allow the wrap limit to move freely between min and max. Either
+ // parameter can be null to allow the wrap limit to be unconstrained
+ // in that direction. Or set both parameters to the same number to pin
+ // the limit to that value.
+ this.setWrapLimitRange = function(min, max) {
+ if (this.$wrapLimitRange.min !== min || this.$wrapLimitRange.max !== max) {
+ this.$wrapLimitRange.min = min;
+ this.$wrapLimitRange.max = max;
+ this.$modified = true;
+ // This will force a recalculation of the wrap limit
+ this._dispatchEvent("changeWrapMode");
+ }
+ };
+
+ // This should generally only be called by the renderer when a resize
+ // is detected.
+ this.adjustWrapLimit = function(desiredLimit) {
+ var wrapLimit = this.$constrainWrapLimit(desiredLimit);
+ if (wrapLimit != this.$wrapLimit && wrapLimit > 0) {
+ this.$wrapLimit = wrapLimit;
+ this.$modified = true;
+ if (this.$useWrapMode) {
+ this.$updateWrapData(0, this.getLength() - 1);
+ this.$resetRowCache(0)
+ this._dispatchEvent("changeWrapLimit");
+ }
+ return true;
+ }
+ return false;
+ };
+
+ this.$constrainWrapLimit = function(wrapLimit) {
+ var min = this.$wrapLimitRange.min;
+ if (min)
+ wrapLimit = Math.max(min, wrapLimit);
+
+ var max = this.$wrapLimitRange.max;
+ if (max)
+ wrapLimit = Math.min(max, wrapLimit);
+
+ // What would a limit of 0 even mean?
+ return Math.max(1, wrapLimit);
+ };
+
+ this.getWrapLimit = function() {
+ return this.$wrapLimit;
+ };
+
+ this.getWrapLimitRange = function() {
+ // Avoid unexpected mutation by returning a copy
+ return {
+ min : this.$wrapLimitRange.min,
+ max : this.$wrapLimitRange.max
+ };
+ };
+
+ this.$updateInternalDataOnChange = function(e) {
+ var useWrapMode = this.$useWrapMode;
+ var len;
+ var action = e.data.action;
+ var firstRow = e.data.range.start.row,
+ lastRow = e.data.range.end.row,
+ start = e.data.range.start,
+ end = e.data.range.end;
+ var removedFolds = null;
+
+ if (action.indexOf("Lines") != -1) {
+ if (action == "insertLines") {
+ lastRow = firstRow + (e.data.lines.length);
+ } else {
+ lastRow = firstRow;
+ }
+ len = e.data.lines.length;
+ } else {
+ len = lastRow - firstRow;
+ }
+
+ if (len != 0) {
+ if (action.indexOf("remove") != -1) {
+ useWrapMode && this.$wrapData.splice(firstRow, len);
+
+ var foldLines = this.$foldData;
+ removedFolds = this.getFoldsInRange(e.data.range);
+ this.removeFolds(removedFolds);
+
+ var foldLine = this.getFoldLine(end.row);
+ var idx = 0;
+ if (foldLine) {
+ foldLine.addRemoveChars(end.row, end.column, start.column - end.column);
+ foldLine.shiftRow(-len);
+
+ var foldLineBefore = this.getFoldLine(firstRow);
+ if (foldLineBefore && foldLineBefore !== foldLine) {
+ foldLineBefore.merge(foldLine);
+ foldLine = foldLineBefore;
+ }
+ idx = foldLines.indexOf(foldLine) + 1;
+ }
+
+ for (idx; idx < foldLines.length; idx++) {
+ var foldLine = foldLines[idx];
+ if (foldLine.start.row >= end.row) {
+ foldLine.shiftRow(-len);
+ }
+ }
+
+ lastRow = firstRow;
+ } else {
+ var args;
+ if (useWrapMode) {
+ args = [firstRow, 0];
+ for (var i = 0; i < len; i++) args.push([]);
+ this.$wrapData.splice.apply(this.$wrapData, args);
+ }
+
+ // If some new line is added inside of a foldLine, then split
+ // the fold line up.
+ var foldLines = this.$foldData;
+ var foldLine = this.getFoldLine(firstRow);
+ var idx = 0;
+ if (foldLine) {
+ var cmp = foldLine.range.compareInside(start.row, start.column)
+ // Inside of the foldLine range. Need to split stuff up.
+ if (cmp == 0) {
+ foldLine = foldLine.split(start.row, start.column);
+ foldLine.shiftRow(len);
+ foldLine.addRemoveChars(
+ lastRow, 0, end.column - start.column);
+ } else
+ // Infront of the foldLine but same row. Need to shift column.
+ if (cmp == -1) {
+ foldLine.addRemoveChars(firstRow, 0, end.column - start.column);
+ foldLine.shiftRow(len);
+ }
+ // Nothing to do if the insert is after the foldLine.
+ idx = foldLines.indexOf(foldLine) + 1;
+ }
+
+ for (idx; idx < foldLines.length; idx++) {
+ var foldLine = foldLines[idx];
+ if (foldLine.start.row >= firstRow) {
+ foldLine.shiftRow(len);
+ }
+ }
+ }
+ } else {
+ // Realign folds. E.g. if you add some new chars before a fold, the
+ // fold should "move" to the right.
+ var column;
+ len = Math.abs(e.data.range.start.column - e.data.range.end.column);
+ if (action.indexOf("remove") != -1) {
+ // Get all the folds in the change range and remove them.
+ removedFolds = this.getFoldsInRange(e.data.range);
+ this.removeFolds(removedFolds);
+
+ len = -len;
+ }
+ var foldLine = this.getFoldLine(firstRow);
+ if (foldLine) {
+ foldLine.addRemoveChars(firstRow, start.column, len);
+ }
+ }
+
+ if (useWrapMode && this.$wrapData.length != this.doc.getLength()) {
+ console.error("doc.getLength() and $wrapData.length have to be the same!");
+ }
+
+ useWrapMode && this.$updateWrapData(firstRow, lastRow);
+
+ return removedFolds;
+ };
+
+ this.$updateWrapData = function(firstRow, lastRow) {
+ var lines = this.doc.getAllLines();
+ var tabSize = this.getTabSize();
+ var wrapData = this.$wrapData;
+ var wrapLimit = this.$wrapLimit;
+ var tokens;
+ var foldLine;
+
+ var row = firstRow;
+ lastRow = Math.min(lastRow, lines.length - 1);
+ while (row <= lastRow) {
+ foldLine = this.getFoldLine(row);
+ if (!foldLine) {
+ tokens = this.$getDisplayTokens(lang.stringTrimRight(lines[row]));
+ } else {
+ tokens = [];
+ foldLine.walk(
+ function(placeholder, row, column, lastColumn) {
+ var walkTokens;
+ if (placeholder) {
+ walkTokens = this.$getDisplayTokens(
+ placeholder, tokens.length);
+ walkTokens[0] = PLACEHOLDER_START;
+ for (var i = 1; i < walkTokens.length; i++) {
+ walkTokens[i] = PLACEHOLDER_BODY;
+ }
+ } else {
+ walkTokens = this.$getDisplayTokens(
+ lines[row].substring(lastColumn, column),
+ tokens.length);
+ }
+ tokens = tokens.concat(walkTokens);
+ }.bind(this),
+ foldLine.end.row,
+ lines[foldLine.end.row].length + 1
+ );
+ // Remove spaces/tabs from the back of the token array.
+ while (tokens.length != 0
+ && tokens[tokens.length - 1] >= SPACE)
+ {
+ tokens.pop();
+ }
+ }
+ wrapData[row] =
+ this.$computeWrapSplits(tokens, wrapLimit, tabSize);
+
+ row = this.getRowFoldEnd(row) + 1;
+ }
+ };
+
+ // "Tokens"
+ var CHAR = 1,
+ CHAR_EXT = 2,
+ PLACEHOLDER_START = 3,
+ PLACEHOLDER_BODY = 4,
+ SPACE = 10,
+ TAB = 11,
+ TAB_SPACE = 12;
+
+ this.$computeWrapSplits = function(tokens, wrapLimit, tabSize) {
+ if (tokens.length == 0) {
+ return [];
+ }
+
+ var tabSize = this.getTabSize();
+ var splits = [];
+ var displayLength = tokens.length;
+ var lastSplit = 0, lastDocSplit = 0;
+
+ function addSplit(screenPos) {
+ var displayed = tokens.slice(lastSplit, screenPos);
+
+ // The document size is the current size - the extra width for tabs
+ // and multipleWidth characters.
+ var len = displayed.length;
+ displayed.join("").
+ // Get all the TAB_SPACEs.
+ replace(/12/g, function(m) {
+ len -= 1;
+ }).
+ // Get all the CHAR_EXT/multipleWidth characters.
+ replace(/2/g, function(m) {
+ len -= 1;
+ });
+
+ lastDocSplit += len;
+ splits.push(lastDocSplit);
+ lastSplit = screenPos;
+ }
+
+ while (displayLength - lastSplit > wrapLimit) {
+ // This is, where the split should be.
+ var split = lastSplit + wrapLimit;
+
+ // If there is a space or tab at this split position, then making
+ // a split is simple.
+ if (tokens[split] >= SPACE) {
+ // Include all following spaces + tabs in this split as well.
+ while (tokens[split] >= SPACE) {
+ split ++;
+ }
+ addSplit(split);
+ continue;
+ }
+
+ // === ELSE ===
+ // Check if split is inside of a placeholder. Placeholder are
+ // not splitable. Therefore, seek the beginning of the placeholder
+ // and try to place the split beofre the placeholder's start.
+ if (tokens[split] == PLACEHOLDER_START
+ || tokens[split] == PLACEHOLDER_BODY)
+ {
+ // Seek the start of the placeholder and do the split
+ // before the placeholder. By definition there always
+ // a PLACEHOLDER_START between split and lastSplit.
+ for (split; split != lastSplit - 1; split--) {
+ if (tokens[split] == PLACEHOLDER_START) {
+ // split++; << No incremental here as we want to
+ // have the position before the Placeholder.
+ break;
+ }
+ }
+
+ // If the PLACEHOLDER_START is not the index of the
+ // last split, then we can do the split
+ if (split > lastSplit) {
+ addSplit(split);
+ continue;
+ }
+
+ // If the PLACEHOLDER_START IS the index of the last
+ // split, then we have to place the split after the
+ // placeholder. So, let's seek for the end of the placeholder.
+ split = lastSplit + wrapLimit;
+ for (split; split < tokens.length; split++) {
+ if (tokens[split] != PLACEHOLDER_BODY)
+ {
+ break;
+ }
+ }
+
+ // If spilt == tokens.length, then the placeholder is the last
+ // thing in the line and adding a new split doesn't make sense.
+ if (split == tokens.length) {
+ break; // Breaks the while-loop.
+ }
+
+ // Finally, add the split...
+ addSplit(split);
+ continue;
+ }
+
+ // === ELSE ===
+ // Search for the first non space/tab/placeholder token backwards.
+ for (split; split != lastSplit - 1; split--) {
+ if (tokens[split] >= PLACEHOLDER_START) {
+ split++;
+ break;
+ }
+ }
+ // If we found one, then add the split.
+ if (split > lastSplit) {
+ addSplit(split);
+ continue;
+ }
+
+ // === ELSE ===
+ split = lastSplit + wrapLimit;
+ // The split is inside of a CHAR or CHAR_EXT token and no space
+ // around -> force a split.
+ addSplit(lastSplit + wrapLimit);
+ }
+ return splits;
+ }
+
+ /**
+ * @param
+ * offset: The offset in screenColumn at which position str starts.
+ * Important for calculating the realTabSize.
+ */
+ this.$getDisplayTokens = function(str, offset) {
+ var arr = [];
+ var tabSize;
+ offset = offset || 0;
+
+ for (var i = 0; i < str.length; i++) {
+ var c = str.charCodeAt(i);
+ // Tab
+ if (c == 9) {
+ tabSize = this.getScreenTabSize(arr.length + offset);
+ arr.push(TAB);
+ for (var n = 1; n < tabSize; n++) {
+ arr.push(TAB_SPACE);
+ }
+ }
+ // Space
+ else if(c == 32) {
+ arr.push(SPACE);
+ }
+ // full width characters
+ else if (isFullWidth(c)) {
+ arr.push(CHAR, CHAR_EXT);
+ } else {
+ arr.push(CHAR);
+ }
+ }
+ return arr;
+ }
+
+ /**
+ * Calculates the width of the a string on the screen while assuming that
+ * the string starts at the first column on the screen.
+ *
+ * @param string str String to calculate the screen width of
+ * @return array
+ * [0]: number of columns for str on screen.
+ * [1]: docColumn position that was read until (useful with screenColumn)
+ */
+ this.$getStringScreenWidth = function(str, maxScreenColumn, screenColumn) {
+ if (maxScreenColumn == 0) {
+ return [0, 0];
+ }
+ if (maxScreenColumn == null) {
+ maxScreenColumn = screenColumn +
+ str.length * Math.max(this.getTabSize(), 2);
+ }
+ screenColumn = screenColumn || 0;
+
+ var c, column;
+ for (column = 0; column < str.length; column++) {
+ c = str.charCodeAt(column);
+ // tab
+ if (c == 9) {
+ screenColumn += this.getScreenTabSize(screenColumn);
+ }
+ // full width characters
+ else if (isFullWidth(c)) {
+ screenColumn += 2;
+ } else {
+ screenColumn += 1;
+ }
+ if (screenColumn > maxScreenColumn) {
+ break
+ }
+ }
+
+ return [screenColumn, column];
+ }
+
+ this.getRowLength = function(row) {
+ if (!this.$useWrapMode || !this.$wrapData[row]) {
+ return 1;
+ } else {
+ return this.$wrapData[row].length + 1;
+ }
+ }
+
+ this.getRowHeight = function(config, row) {
+ return this.getRowLength(row) * config.lineHeight;
+ }
+
+ this.getScreenLastRowColumn = function(screenRow) {
+ // Note: This won't work if someone has more then
+ // 1.7976931348623158e+307 characters in one row. But I think we can
+ // live with this limitation ;)
+ return this.screenToDocumentColumn(screenRow, Number.MAX_VALUE / 10)
+ };
+
+ this.getDocumentLastRowColumn = function(docRow, docColumn) {
+ var screenRow = this.documentToScreenRow(docRow, docColumn);
+ return this.getScreenLastRowColumn(screenRow);
+ };
+
+ this.getDocumentLastRowColumnPosition = function(docRow, docColumn) {
+ var screenRow = this.documentToScreenRow(docRow, docColumn);
+ return this.screenToDocumentPosition(screenRow, Number.MAX_VALUE / 10);
+ };
+
+ this.getRowSplitData = function(row) {
+ if (!this.$useWrapMode) {
+ return undefined;
+ } else {
+ return this.$wrapData[row];
+ }
+ };
+
+ /**
+ * Returns the width of a tab character at screenColumn.
+ */
+ this.getScreenTabSize = function(screenColumn) {
+ return this.$tabSize - screenColumn % this.$tabSize;
+ };
+
+ this.screenToDocumentRow = function(screenRow, screenColumn) {
+ return this.screenToDocumentPosition(screenRow, screenColumn).row;
+ };
+
+ this.screenToDocumentColumn = function(screenRow, screenColumn) {
+ return this.screenToDocumentPosition(screenRow, screenColumn).column;
+ };
+
+ this.screenToDocumentPosition = function(screenRow, screenColumn) {
+ var line;
+ var docRow = 0;
+ var docColumn = 0;
+ var column;
+ var foldLineRowLength;
+ var row = 0;
+ var rowLength = 0;
+ var splits = null;
+
+ var rowCache = this.$rowCache;
+ var doCache = !rowCache.length;
+ for (var i = 0; i < rowCache.length; i++) {
+ if (rowCache[i].screenRow < screenRow) {
+ row = rowCache[i].screenRow;
+ docRow = rowCache[i].docRow;
+ doCache = i == rowCache.length - 1;
+ }
+ }
+ var docRowCacheLast = docRow;
+ // clamp row before clamping column, for selection on last line
+ var maxRow = this.getLength() - 1;
+
+ var foldLine = this.getNextFold(docRow);
+ var foldStart = foldLine ?foldLine.start.row :Infinity;
+
+ while (row <= screenRow) {
+ if (doCache
+ && docRow - docRowCacheLast > this.$rowCacheSize) {
+ rowCache.push({
+ docRow: docRow,
+ screenRow: row
+ });
+ docRowCacheLast = docRow;
+ }
+ rowLength = this.getRowLength(docRow);
+ if (row + rowLength - 1 >= screenRow || docRow >= maxRow) {
+ break;
+ } else {
+ row += rowLength;
+ docRow++;
+ if(docRow > foldStart) {
+ docRow = foldLine.end.row+1;
+ foldLine = this.getNextFold(docRow);
+ foldStart = foldLine ?foldLine.start.row :Infinity;
+ }
+ }
+ }
+
+ if (foldLine && foldLine.start.row <= docRow)
+ line = this.getFoldDisplayLine(foldLine);
+ else {
+ line = this.getLine(docRow);
+ foldLine = null;
+ }
+
+ if (this.$useWrapMode) {
+ splits = this.$wrapData[docRow];
+ if (splits) {
+ column = splits[screenRow - row]
+ if(screenRow > row && splits.length) {
+ docColumn = splits[screenRow - row - 1] || splits[splits.length - 1];
+ line = line.substring(docColumn);
+ }
+ }
+ }
+
+ docColumn += this.$getStringScreenWidth(line, screenColumn)[1];
+
+ // Need to do some clamping action here.
+ if (this.$useWrapMode) {
+ if (docColumn >= column) {
+ // We remove one character at the end such that the docColumn
+ // position returned is not associated to the next row on the
+ // screen.
+ docColumn = column - 1;
+ }
+ } else {
+ docColumn = Math.min(docColumn, line.length);
+ }
+
+ if (foldLine) {
+ return foldLine.idxToPosition(docColumn);
+ }
+
+ return {
+ row: docRow,
+ column: docColumn
+ }
+ };
+
+ this.documentToScreenPosition = function(docRow, docColumn) {
+ // Normalize the passed in arguments.
+ if (docColumn == null) {
+ docColumn = docRow.column;
+ docRow = docRow.row;
+ }
+
+ var wrapData;
+ // Special case in wrapMode if the doc is at the end of the document.
+ if (this.$useWrapMode) {
+ wrapData = this.$wrapData;
+ if (docRow > wrapData.length - 1) {
+ return {
+ row: this.getScreenLength(),
+ column: wrapData.length == 0
+ ? 0
+ : (wrapData[wrapData.length - 1].length - 1)
+ };
+ }
+ }
+
+ var screenRow = 0;
+ var screenColumn = 0;
+ var foldStartRow = null;
+ var fold = null;
+
+ // Clamp the docRow position in case it's inside of a folded block.
+ fold = this.getFoldAt(docRow, docColumn, 1);
+ if (fold) {
+ docRow = fold.start.row;
+ docColumn = fold.start.column;
+ }
+
+ var rowEnd, row = 0;
+ var rowCache = this.$rowCache;
+ //
+ var doCache = !rowCache.length;
+ for (var i = 0; i < rowCache.length; i++) {
+ if (rowCache[i].docRow < docRow) {
+ screenRow = rowCache[i].screenRow;
+ row = rowCache[i].docRow;
+ doCache = i == rowCache.length - 1;
+ }
+ }
+ var docRowCacheLast = row;
+
+ var foldLine = this.getNextFold(row);
+ var foldStart = foldLine ?foldLine.start.row :Infinity;
+
+ while (row < docRow) {
+ if (row >= foldStart) {
+ rowEnd = foldLine.end.row + 1;
+ if (rowEnd > docRow)
+ break;
+ foldLine = this.getNextFold(rowEnd);
+ foldStart = foldLine ?foldLine.start.row :Infinity;
+ } else {
+ rowEnd = row + 1;
+ }
+ if (doCache
+ && row - docRowCacheLast > this.$rowCacheSize) {
+ rowCache.push({
+ docRow: row,
+ screenRow: screenRow
+ });
+ docRowCacheLast = row;
+ }
+
+ screenRow += this.getRowLength(row);
+ row = rowEnd;
+ }
+
+ // Calculate the text line that is displayed in docRow on the screen.
+ var textLine = "";
+ // Check if the final row we want to reach is inside of a fold.
+ if (foldLine && row >= foldStart) {
+ textLine = this.getFoldDisplayLine(foldLine, docRow, docColumn);
+ foldStartRow = foldLine.start.row;
+ } else {
+ textLine = this.getLine(docRow).substring(0, docColumn);
+ foldStartRow = docRow;
+ }
+ // Clamp textLine if in wrapMode.
+ if (this.$useWrapMode) {
+ var wrapRow = wrapData[foldStartRow];
+ var screenRowOffset = 0;
+ while (textLine.length >= wrapRow[screenRowOffset]) {
+ screenRow ++;
+ screenRowOffset++;
+ }
+ textLine = textLine.substring(
+ wrapRow[screenRowOffset - 1] || 0, textLine.length);
+ }
+
+ return {
+ row: screenRow,
+ column: this.$getStringScreenWidth(textLine)[0]
+ };
+ };
+
+ this.documentToScreenColumn = function(row, docColumn) {
+ return this.documentToScreenPosition(row, docColumn).column;
+ };
+
+ this.documentToScreenRow = function(docRow, docColumn) {
+ return this.documentToScreenPosition(docRow, docColumn).row;
+ };
+
+ this.getScreenLength = function() {
+ var screenRows = 0;
+ var lastFoldLine = null;
+ var foldLine = null;
+ if (!this.$useWrapMode) {
+ screenRows = this.getLength();
+
+ // Remove the folded lines again.
+ var foldData = this.$foldData;
+ for (var i = 0; i < foldData.length; i++) {
+ foldLine = foldData[i];
+ screenRows -= foldLine.end.row - foldLine.start.row;
+ }
+ } else {
+ for (var row = 0; row < this.$wrapData.length; row++) {
+ if (foldLine = this.getFoldLine(row, lastFoldLine)) {
+ row = foldLine.end.row;
+ screenRows += 1;
+ } else {
+ screenRows += this.$wrapData[row].length + 1;
+ }
+ }
+ }
+
+ return screenRows;
+ }
+
+ // For every keystroke this gets called once per char in the whole doc!!
+ // Wouldn't hurt to make it a bit faster for c >= 0x1100
+ function isFullWidth(c) {
+ if (c < 0x1100)
+ return false;
+ return c >= 0x1100 && c <= 0x115F ||
+ c >= 0x11A3 && c <= 0x11A7 ||
+ c >= 0x11FA && c <= 0x11FF ||
+ c >= 0x2329 && c <= 0x232A ||
+ c >= 0x2E80 && c <= 0x2E99 ||
+ c >= 0x2E9B && c <= 0x2EF3 ||
+ c >= 0x2F00 && c <= 0x2FD5 ||
+ c >= 0x2FF0 && c <= 0x2FFB ||
+ c >= 0x3000 && c <= 0x303E ||
+ c >= 0x3041 && c <= 0x3096 ||
+ c >= 0x3099 && c <= 0x30FF ||
+ c >= 0x3105 && c <= 0x312D ||
+ c >= 0x3131 && c <= 0x318E ||
+ c >= 0x3190 && c <= 0x31BA ||
+ c >= 0x31C0 && c <= 0x31E3 ||
+ c >= 0x31F0 && c <= 0x321E ||
+ c >= 0x3220 && c <= 0x3247 ||
+ c >= 0x3250 && c <= 0x32FE ||
+ c >= 0x3300 && c <= 0x4DBF ||
+ c >= 0x4E00 && c <= 0xA48C ||
+ c >= 0xA490 && c <= 0xA4C6 ||
+ c >= 0xA960 && c <= 0xA97C ||
+ c >= 0xAC00 && c <= 0xD7A3 ||
+ c >= 0xD7B0 && c <= 0xD7C6 ||
+ c >= 0xD7CB && c <= 0xD7FB ||
+ c >= 0xF900 && c <= 0xFAFF ||
+ c >= 0xFE10 && c <= 0xFE19 ||
+ c >= 0xFE30 && c <= 0xFE52 ||
+ c >= 0xFE54 && c <= 0xFE66 ||
+ c >= 0xFE68 && c <= 0xFE6B ||
+ c >= 0xFF01 && c <= 0xFF60 ||
+ c >= 0xFFE0 && c <= 0xFFE6;
+ };
+
+}).call(EditSession.prototype);
+
+require("ace/edit_session/folding").Folding.call(EditSession.prototype);
+
+exports.EditSession = EditSession;
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Julian Viereck <julian.viereck@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/selection', ['require', 'exports', 'module' , 'pilot/oop', 'pilot/lang', 'pilot/event_emitter', 'ace/range'], function(require, exports, module) {
+
+var oop = require("pilot/oop");
+var lang = require("pilot/lang");
+var EventEmitter = require("pilot/event_emitter").EventEmitter;
+var Range = require("ace/range").Range;
+
+var Selection = function(session) {
+ this.session = session;
+ this.doc = session.getDocument();
+
+ this.clearSelection();
+ this.selectionLead = this.doc.createAnchor(0, 0);
+ this.selectionAnchor = this.doc.createAnchor(0, 0);
+
+ var _self = this;
+ this.selectionLead.on("change", function(e) {
+ _self._dispatchEvent("changeCursor");
+ if (!_self.$isEmpty)
+ _self._dispatchEvent("changeSelection");
+ if (e.old.row == e.value.row)
+ _self.$updateDesiredColumn();
+ });
+
+ this.selectionAnchor.on("change", function() {
+ if (!_self.$isEmpty)
+ _self._dispatchEvent("changeSelection");
+ });
+};
+
+(function() {
+
+ oop.implement(this, EventEmitter);
+
+ this.isEmpty = function() {
+ return (this.$isEmpty || (
+ this.selectionAnchor.row == this.selectionLead.row &&
+ this.selectionAnchor.column == this.selectionLead.column
+ ));
+ };
+
+ this.isMultiLine = function() {
+ if (this.isEmpty()) {
+ return false;
+ }
+
+ return this.getRange().isMultiLine();
+ };
+
+ this.getCursor = function() {
+ return this.selectionLead.getPosition();
+ };
+
+ this.setSelectionAnchor = function(row, column) {
+ this.selectionAnchor.setPosition(row, column);
+
+ if (this.$isEmpty) {
+ this.$isEmpty = false;
+ this._dispatchEvent("changeSelection");
+ }
+ };
+
+ this.getSelectionAnchor = function() {
+ if (this.$isEmpty)
+ return this.getSelectionLead()
+ else
+ return this.selectionAnchor.getPosition();
+ };
+
+ this.getSelectionLead = function() {
+ return this.selectionLead.getPosition();
+ };
+
+ this.shiftSelection = function(columns) {
+ if (this.$isEmpty) {
+ this.moveCursorTo(this.selectionLead.row, this.selectionLead.column + columns);
+ return;
+ };
+
+ var anchor = this.getSelectionAnchor();
+ var lead = this.getSelectionLead();
+
+ var isBackwards = this.isBackwards();
+
+ if (!isBackwards || anchor.column !== 0)
+ this.setSelectionAnchor(anchor.row, anchor.column + columns);
+
+ if (isBackwards || lead.column !== 0) {
+ this.$moveSelection(function() {
+ this.moveCursorTo(lead.row, lead.column + columns);
+ });
+ }
+ };
+
+ this.isBackwards = function() {
+ var anchor = this.selectionAnchor;
+ var lead = this.selectionLead;
+ return (anchor.row > lead.row || (anchor.row == lead.row && anchor.column > lead.column));
+ };
+
+ this.getRange = function() {
+ var anchor = this.selectionAnchor;
+ var lead = this.selectionLead;
+
+ if (this.isEmpty())
+ return Range.fromPoints(lead, lead);
+
+ if (this.isBackwards()) {
+ return Range.fromPoints(lead, anchor);
+ }
+ else {
+ return Range.fromPoints(anchor, lead);
+ }
+ };
+
+ this.clearSelection = function() {
+ if (!this.$isEmpty) {
+ this.$isEmpty = true;
+ this._dispatchEvent("changeSelection");
+ }
+ };
+
+ this.selectAll = function() {
+ var lastRow = this.doc.getLength() - 1;
+ this.setSelectionAnchor(lastRow, this.doc.getLine(lastRow).length);
+ this.moveCursorTo(0, 0);
+ };
+
+ this.setSelectionRange = function(range, reverse) {
+ if (reverse) {
+ this.setSelectionAnchor(range.end.row, range.end.column);
+ this.selectTo(range.start.row, range.start.column);
+ } else {
+ this.setSelectionAnchor(range.start.row, range.start.column);
+ this.selectTo(range.end.row, range.end.column);
+ }
+ this.$updateDesiredColumn();
+ };
+
+ this.$updateDesiredColumn = function() {
+ var cursor = this.getCursor();
+ this.$desiredColumn = this.session.documentToScreenColumn(cursor.row, cursor.column);
+ };
+
+ this.$moveSelection = function(mover) {
+ var lead = this.selectionLead;
+ if (this.$isEmpty)
+ this.setSelectionAnchor(lead.row, lead.column);
+
+ mover.call(this);
+ };
+
+ this.selectTo = function(row, column) {
+ this.$moveSelection(function() {
+ this.moveCursorTo(row, column);
+ });
+ };
+
+ this.selectToPosition = function(pos) {
+ this.$moveSelection(function() {
+ this.moveCursorToPosition(pos);
+ });
+ };
+
+ this.selectUp = function() {
+ this.$moveSelection(this.moveCursorUp);
+ };
+
+ this.selectDown = function() {
+ this.$moveSelection(this.moveCursorDown);
+ };
+
+ this.selectRight = function() {
+ this.$moveSelection(this.moveCursorRight);
+ };
+
+ this.selectLeft = function() {
+ this.$moveSelection(this.moveCursorLeft);
+ };
+
+ this.selectLineStart = function() {
+ this.$moveSelection(this.moveCursorLineStart);
+ };
+
+ this.selectLineEnd = function() {
+ this.$moveSelection(this.moveCursorLineEnd);
+ };
+
+ this.selectFileEnd = function() {
+ this.$moveSelection(this.moveCursorFileEnd);
+ };
+
+ this.selectFileStart = function() {
+ this.$moveSelection(this.moveCursorFileStart);
+ };
+
+ this.selectWordRight = function() {
+ this.$moveSelection(this.moveCursorWordRight);
+ };
+
+ this.selectWordLeft = function() {
+ this.$moveSelection(this.moveCursorWordLeft);
+ };
+
+ this.selectWord = function() {
+ var cursor = this.getCursor();
+ var range = this.session.getWordRange(cursor.row, cursor.column);
+ this.setSelectionRange(range);
+ };
+
+ this.selectLine = function() {
+ var rowStart = this.selectionLead.row;
+ var rowEnd;
+
+ var foldLine = this.session.getFoldLine(rowStart);
+ if (foldLine) {
+ rowStart = foldLine.start.row;
+ rowEnd = foldLine.end.row;
+ } else {
+ rowEnd = rowStart;
+ }
+ this.setSelectionAnchor(rowStart, 0);
+ this.$moveSelection(function() {
+ this.moveCursorTo(rowEnd + 1, 0);
+ });
+ };
+
+ this.moveCursorUp = function() {
+ this.moveCursorBy(-1, 0);
+ };
+
+ this.moveCursorDown = function() {
+ this.moveCursorBy(1, 0);
+ };
+
+ this.moveCursorLeft = function() {
+ var cursor = this.selectionLead.getPosition(),
+ fold;
+
+ if (fold = this.session.getFoldAt(cursor.row, cursor.column, -1)) {
+ this.moveCursorTo(fold.start.row, fold.start.column);
+ } else if (cursor.column == 0) {
+ // cursor is a line (start
+ if (cursor.row > 0) {
+ this.moveCursorTo(cursor.row - 1, this.doc.getLine(cursor.row - 1).length);
+ }
+ }
+ else {
+ var tabSize = this.session.getTabSize();
+ if (this.session.isTabStop(cursor) && this.doc.getLine(cursor.row).slice(cursor.column-tabSize, cursor.column).split(" ").length-1 == tabSize)
+ this.moveCursorBy(0, -tabSize);
+ else
+ this.moveCursorBy(0, -1);
+ }
+ };
+
+ this.moveCursorRight = function() {
+ var cursor = this.selectionLead.getPosition(),
+ fold;
+ if (fold = this.session.getFoldAt(cursor.row, cursor.column, 1)) {
+ this.moveCursorTo(fold.end.row, fold.end.column);
+ } else if (this.selectionLead.column == this.doc.getLine(this.selectionLead.row).length) {
+ if (this.selectionLead.row < this.doc.getLength() - 1) {
+ this.moveCursorTo(this.selectionLead.row + 1, 0);
+ }
+ }
+ else {
+ var tabSize = this.session.getTabSize();
+ var cursor = this.selectionLead;
+ if (this.session.isTabStop(cursor) && this.doc.getLine(cursor.row).slice(cursor.column, cursor.column+tabSize).split(" ").length-1 == tabSize)
+ this.moveCursorBy(0, tabSize);
+ else
+ this.moveCursorBy(0, 1);
+ }
+ };
+
+ this.moveCursorLineStart = function() {
+ var row = this.selectionLead.row;
+ var column = this.selectionLead.column;
+ var screenRow = this.session.documentToScreenRow(row, column);
+
+ // Determ the doc-position of the first character at the screen line.
+ var firstColumnPosition =
+ this.session.screenToDocumentPosition(screenRow, 0);
+
+ // Determ the string "before" the cursor.
+ var beforeCursor = this.session.getDisplayLine(
+ row, column,
+ firstColumnPosition.row, firstColumnPosition.column);
+
+ //
+ var leadingSpace = beforeCursor.match(/^\s*/);
+ if (leadingSpace[0].length == 0
+ || leadingSpace[0].length >= column - firstColumnPosition.column)
+ {
+ this.moveCursorTo(
+ firstColumnPosition.row, firstColumnPosition.column);
+ } else {
+ this.moveCursorTo(
+ firstColumnPosition.row,
+ firstColumnPosition.column + leadingSpace[0].length);
+ }
+ };
+
+ this.moveCursorLineEnd = function() {
+ var lead = this.selectionLead;
+ var lastRowColumnPosition =
+ this.session.getDocumentLastRowColumnPosition(lead.row, lead.column);
+ this.moveCursorTo(
+ lastRowColumnPosition.row,
+ lastRowColumnPosition.column
+ );
+ };
+
+ this.moveCursorFileEnd = function() {
+ var row = this.doc.getLength() - 1;
+ var column = this.doc.getLine(row).length;
+ this.moveCursorTo(row, column);
+ };
+
+ this.moveCursorFileStart = function() {
+ this.moveCursorTo(0, 0);
+ };
+
+ this.moveCursorWordRight = function() {
+ var row = this.selectionLead.row;
+ var column = this.selectionLead.column;
+ var line = this.doc.getLine(row);
+ var rightOfCursor = line.substring(column);
+
+ var match;
+ this.session.nonTokenRe.lastIndex = 0;
+ this.session.tokenRe.lastIndex = 0;
+
+ var fold;
+ if (fold = this.session.getFoldAt(row, column, 1)) {
+ this.moveCursorTo(fold.end.row, fold.end.column);
+ return;
+ } else if (column == line.length) {
+ this.moveCursorRight();
+ return;
+ }
+ else if (match = this.session.nonTokenRe.exec(rightOfCursor)) {
+ column += this.session.nonTokenRe.lastIndex;
+ this.session.nonTokenRe.lastIndex = 0;
+ }
+ else if (match = this.session.tokenRe.exec(rightOfCursor)) {
+ column += this.session.tokenRe.lastIndex;
+ this.session.tokenRe.lastIndex = 0;
+ }
+
+ this.moveCursorTo(row, column);
+ };
+
+ this.moveCursorWordLeft = function() {
+ var row = this.selectionLead.row;
+ var column = this.selectionLead.column;
+
+ var fold;
+ if (fold = this.session.getFoldAt(row, column, -1)) {
+ this.moveCursorTo(fold.start.row, fold.start.column);
+ return;
+ }
+
+ if (column == 0) {
+ this.moveCursorLeft();
+ return;
+ }
+
+ var str = this.session.getFoldStringAt(row, column, -1);
+ if (str == null) {
+ str = this.doc.getLine(row).substring(0, column)
+ }
+ var leftOfCursor = lang.stringReverse(str);
+
+ var match;
+ this.session.nonTokenRe.lastIndex = 0;
+ this.session.tokenRe.lastIndex = 0;
+
+ if (match = this.session.nonTokenRe.exec(leftOfCursor)) {
+ column -= this.session.nonTokenRe.lastIndex;
+ this.session.nonTokenRe.lastIndex = 0;
+ }
+ else if (match = this.session.tokenRe.exec(leftOfCursor)) {
+ column -= this.session.tokenRe.lastIndex;
+ this.session.tokenRe.lastIndex = 0;
+ }
+
+ this.moveCursorTo(row, column);
+ };
+
+ this.moveCursorBy = function(rows, chars) {
+ var screenPos = this.session.documentToScreenPosition(
+ this.selectionLead.row,
+ this.selectionLead.column
+ );
+ var screenCol = (chars == 0 && this.$desiredColumn) || screenPos.column;
+ var docPos = this.session.screenToDocumentPosition(screenPos.row + rows, screenCol);
+
+ this.moveCursorTo(docPos.row, docPos.column + chars, chars == 0);
+ };
+
+ this.moveCursorToPosition = function(position) {
+ this.moveCursorTo(position.row, position.column);
+ };
+
+ this.moveCursorTo = function(row, column, preventUpdateDesiredColumn) {
+ // Ensure the row/column is not inside of a fold.
+ var fold = this.session.getFoldAt(row, column, 1);
+ if (fold) {
+ row = fold.start.row;
+ column = fold.start.column;
+ }
+ this.selectionLead.setPosition(row, column);
+ if (!preventUpdateDesiredColumn)
+ this.$updateDesiredColumn(this.selectionLead.column);
+ };
+
+ this.moveCursorToScreen = function(row, column, preventUpdateDesiredColumn) {
+ var pos = this.session.screenToDocumentPosition(row, column);
+ row = pos.row;
+ column = pos.column;
+ this.moveCursorTo(row, column, preventUpdateDesiredColumn);
+ };
+
+}).call(Selection.prototype);
+
+exports.Selection = Selection;
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/range', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+var Range = function(startRow, startColumn, endRow, endColumn) {
+ this.start = {
+ row: startRow,
+ column: startColumn
+ };
+
+ this.end = {
+ row: endRow,
+ column: endColumn
+ };
+};
+
+(function() {
+
+ this.toString = function() {
+ return ("Range: [" + this.start.row + "/" + this.start.column +
+ "] -> [" + this.end.row + "/" + this.end.column + "]");
+ };
+
+ this.contains = function(row, column) {
+ return this.compare(row, column) == 0;
+ };
+
+ /**
+ * Compares this range (A) with another range (B), where B is the passed in
+ * range.
+ *
+ * Return values:
+ * -2: (B) is infront of (A) and doesn't intersect with (A)
+ * -1: (B) begins before (A) but ends inside of (A)
+ * 0: (B) is completly inside of (A) OR (A) is complety inside of (B)
+ * +1: (B) begins inside of (A) but ends outside of (A)
+ * +2: (B) is after (A) and doesn't intersect with (A)
+ *
+ * 42: FTW state: (B) ends in (A) but starts outside of (A)
+ */
+ this.compareRange = function(range) {
+ var cmp,
+ end = range.end,
+ start = range.start;
+
+ cmp = this.compare(end.row, end.column);
+ if (cmp == 1) {
+ cmp = this.compare(start.row, start.column);
+ if (cmp == 1) {
+ return 2;
+ } else if (cmp == 0) {
+ return 1;
+ } else {
+ return 0;
+ }
+ } else if (cmp == -1) {
+ return -2;
+ } else {
+ cmp = this.compare(start.row, start.column);
+ if (cmp == -1) {
+ return -1;
+ } else if (cmp == 1) {
+ return 42;
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ this.containsRange = function(range) {
+ var cmp = this.compareRange(range);
+ return (cmp == -1 || cmp == 0 || cmp == 1);
+ }
+
+ this.isEnd = function(row, column) {
+ return this.end.row == row && this.end.column == column;
+ }
+
+ this.isStart = function(row, column) {
+ return this.start.row == row && this.start.column == column;
+ }
+
+ this.setStart = function(row, column) {
+ if (typeof row == "object") {
+ this.start.column = row.column;
+ this.start.row = row.row;
+ } else {
+ this.start.row = row;
+ this.start.column = column;
+ }
+ }
+
+ this.setEnd = function(row, column) {
+ if (typeof row == "object") {
+ this.end.column = row.column;
+ this.end.row = row.row;
+ } else {
+ this.end.row = row;
+ this.end.column = column;
+ }
+ }
+
+ this.inside = function(row, column) {
+ if (this.compare(row, column) == 0) {
+ if (this.isEnd(row, column) || this.isStart(row, column)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ this.insideStart = function(row, column) {
+ if (this.compare(row, column) == 0) {
+ if (this.isEnd(row, column)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ this.insideEnd = function(row, column) {
+ if (this.compare(row, column) == 0) {
+ if (this.isStart(row, column)) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ this.compare = function(row, column) {
+ if (!this.isMultiLine()) {
+ if (row === this.start.row) {
+ return column < this.start.column ? -1 : (column > this.end.column ? 1 : 0);
+ };
+ }
+
+ if (row < this.start.row)
+ return -1;
+
+ if (row > this.end.row)
+ return 1;
+
+ if (this.start.row === row)
+ return column >= this.start.column ? 0 : -1;
+
+ if (this.end.row === row)
+ return column <= this.end.column ? 0 : 1;
+
+ return 0;
+ };
+
+ /**
+ * Like .compare(), but if isStart is true, return -1;
+ */
+ this.compareStart = function(row, column) {
+ if (this.start.row == row && this.start.column == column) {
+ return -1;
+ } else {
+ return this.compare(row, column);
+ }
+ }
+
+ /**
+ * Like .compare(), but if isEnd is true, return 1;
+ */
+ this.compareEnd = function(row, column) {
+ if (this.end.row == row && this.end.column == column) {
+ return 1;
+ } else {
+ return this.compare(row, column);
+ }
+ }
+
+ this.compareInside = function(row, column) {
+ if (this.end.row == row && this.end.column == column) {
+ return 1;
+ } else if (this.start.row == row && this.start.column == column) {
+ return -1;
+ } else {
+ return this.compare(row, column);
+ }
+ }
+
+ this.clipRows = function(firstRow, lastRow) {
+ if (this.end.row > lastRow) {
+ var end = {
+ row: lastRow+1,
+ column: 0
+ };
+ }
+
+ if (this.start.row > lastRow) {
+ var start = {
+ row: lastRow+1,
+ column: 0
+ };
+ }
+
+ if (this.start.row < firstRow) {
+ var start = {
+ row: firstRow,
+ column: 0
+ };
+ }
+
+ if (this.end.row < firstRow) {
+ var end = {
+ row: firstRow,
+ column: 0
+ };
+ }
+ return Range.fromPoints(start || this.start, end || this.end);
+ };
+
+ this.extend = function(row, column) {
+ var cmp = this.compare(row, column);
+
+ if (cmp == 0)
+ return this;
+ else if (cmp == -1)
+ var start = {row: row, column: column};
+ else
+ var end = {row: row, column: column};
+
+ return Range.fromPoints(start || this.start, end || this.end);
+ };
+
+ this.isEmpty = function() {
+ return (this.start.row == this.end.row && this.start.column == this.end.column);
+ };
+
+ this.isMultiLine = function() {
+ return (this.start.row !== this.end.row);
+ };
+
+ this.clone = function() {
+ return Range.fromPoints(this.start, this.end);
+ };
+
+ this.collapseRows = function() {
+ if (this.end.column == 0)
+ return new Range(this.start.row, 0, Math.max(this.start.row, this.end.row-1), 0)
+ else
+ return new Range(this.start.row, 0, this.end.row, 0)
+ };
+
+ this.toScreenRange = function(session) {
+ var screenPosStart =
+ session.documentToScreenPosition(this.start);
+ var screenPosEnd =
+ session.documentToScreenPosition(this.end);
+ return new Range(
+ screenPosStart.row, screenPosStart.column,
+ screenPosEnd.row, screenPosEnd.column
+ );
+ };
+
+}).call(Range.prototype);
+
+
+Range.fromPoints = function(start, end) {
+ return new Range(start.row, start.column, end.row, end.column);
+};
+
+exports.Range = Range;
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Mihai Sucan <mihai DOT sucan AT gmail DOT com>
+ * Chris Spencer <chris.ag.spencer AT googlemail DOT com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/mode/text', ['require', 'exports', 'module' , 'ace/tokenizer', 'ace/mode/text_highlight_rules', 'ace/mode/behaviour'], function(require, exports, module) {
+
+var Tokenizer = require("ace/tokenizer").Tokenizer;
+var TextHighlightRules = require("ace/mode/text_highlight_rules").TextHighlightRules;
+var Behaviour = require("ace/mode/behaviour").Behaviour;
+
+var Mode = function() {
+ this.$tokenizer = new Tokenizer(new TextHighlightRules().getRules());
+ this.$behaviour = new Behaviour();
+};
+
+(function() {
+
+ this.getTokenizer = function() {
+ return this.$tokenizer;
+ };
+
+ this.toggleCommentLines = function(state, doc, startRow, endRow) {
+ };
+
+ this.getNextLineIndent = function(state, line, tab) {
+ return "";
+ };
+
+ this.checkOutdent = function(state, line, input) {
+ return false;
+ };
+
+ this.autoOutdent = function(state, doc, row) {
+ };
+
+ this.$getIndent = function(line) {
+ var match = line.match(/^(\s+)/);
+ if (match) {
+ return match[1];
+ }
+
+ return "";
+ };
+
+ this.createWorker = function(session) {
+ return null;
+ };
+
+ this.highlightSelection = function(editor) {
+ var session = editor.session;
+ if (!session.$selectionOccurrences)
+ session.$selectionOccurrences = [];
+
+ if (session.$selectionOccurrences.length)
+ this.clearSelectionHighlight(editor);
+
+ var selection = editor.getSelectionRange();
+ if (selection.isEmpty() || selection.isMultiLine())
+ return;
+
+ var startOuter = selection.start.column - 1;
+ var endOuter = selection.end.column + 1;
+ var line = session.getLine(selection.start.row);
+ var lineCols = line.length;
+ var needle = line.substring(Math.max(startOuter, 0),
+ Math.min(endOuter, lineCols));
+
+ // Make sure the outer characters are not part of the word.
+ if ((startOuter >= 0 && /^[\w\d]/.test(needle)) ||
+ (endOuter <= lineCols && /[\w\d]$/.test(needle)))
+ return;
+
+ needle = line.substring(selection.start.column, selection.end.column);
+ if (!/^[\w\d]+$/.test(needle))
+ return;
+
+ var cursor = editor.getCursorPosition();
+
+ var newOptions = {
+ wrap: true,
+ wholeWord: true,
+ caseSensitive: true,
+ needle: needle
+ };
+
+ var currentOptions = editor.$search.getOptions();
+ editor.$search.set(newOptions);
+
+ var ranges = editor.$search.findAll(session);
+ ranges.forEach(function(range) {
+ if (!range.contains(cursor.row, cursor.column)) {
+ var marker = session.addMarker(range, "ace_selected_word");
+ session.$selectionOccurrences.push(marker);
+ }
+ });
+
+ editor.$search.set(currentOptions);
+ };
+
+ this.clearSelectionHighlight = function(editor) {
+ if (!editor.session.$selectionOccurrences)
+ return;
+
+ editor.session.$selectionOccurrences.forEach(function(marker) {
+ editor.session.removeMarker(marker);
+ });
+
+ editor.session.$selectionOccurrences = [];
+ };
+
+ this.createModeDelegates = function (mapping) {
+ if (!this.$embeds) {
+ return;
+ }
+ this.$modes = {};
+ for (var i = 0; i < this.$embeds.length; i++) {
+ if (mapping[this.$embeds[i]]) {
+ this.$modes[this.$embeds[i]] = new mapping[this.$embeds[i]]();
+ }
+ }
+
+ var delegations = ['toggleCommentLines', 'getNextLineIndent', 'checkOutdent', 'autoOutdent', 'transformAction'];
+
+ for (var i = 0; i < delegations.length; i++) {
+ (function(scope) {
+ var functionName = delegations[i];
+ var defaultHandler = scope[functionName];
+ scope[delegations[i]] = function() {
+ return this.$delegator(functionName, arguments, defaultHandler);
+ }
+ } (this));
+ }
+ }
+
+ this.$delegator = function(method, args, defaultHandler) {
+ var state = args[0];
+
+ for (var i = 0; i < this.$embeds.length; i++) {
+ if (!this.$modes[this.$embeds[i]]) continue;
+
+ var split = state.split(this.$embeds[i]);
+ if (!split[0] && split[1]) {
+ args[0] = split[1];
+ var mode = this.$modes[this.$embeds[i]];
+ return mode[method].apply(mode, args);
+ }
+ }
+ var ret = defaultHandler.apply(this, args);
+ return defaultHandler ? ret : undefined;
+ };
+
+ this.transformAction = function(state, action, editor, session, param) {
+ if (this.$behaviour) {
+ var behaviours = this.$behaviour.getBehaviours();
+ for (var key in behaviours) {
+ if (behaviours[key][action]) {
+ var ret = behaviours[key][action].apply(this, arguments);
+ if (ret !== false) {
+ return ret;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+}).call(Mode.prototype);
+
+exports.Mode = Mode;
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/tokenizer', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+var Tokenizer = function(rules) {
+ this.rules = rules;
+
+ this.regExps = {};
+ this.matchMappings = {};
+ for ( var key in this.rules) {
+ var rule = this.rules[key];
+ var state = rule;
+ var ruleRegExps = [];
+ var matchTotal = 0;
+ var mapping = this.matchMappings[key] = {};
+
+ for ( var i = 0; i < state.length; i++) {
+ // Count number of matching groups. 2 extra groups from the full match
+ // And the catch-all on the end (used to force a match);
+ var matchcount = new RegExp("(?:(" + state[i].regex + ")|(.))").exec("a").length - 2;
+
+ // Replace any backreferences and offset appropriately.
+ var adjustedregex = state[i].regex.replace(/\\([0-9]+)/g, function (match, digit) {
+ return "\\" + (parseInt(digit, 10) + matchTotal + 1);
+ });
+
+ mapping[matchTotal] = {
+ rule: i,
+ len: matchcount
+ };
+ matchTotal += matchcount;
+
+ ruleRegExps.push(adjustedregex);
+ }
+
+ this.regExps[key] = new RegExp("(?:(" + ruleRegExps.join(")|(") + ")|(.))", "g");
+
+ }
+};
+
+(function() {
+
+ this.getLineTokens = function(line, startState) {
+ var currentState = startState;
+ var state = this.rules[currentState];
+ var mapping = this.matchMappings[currentState];
+ var re = this.regExps[currentState];
+ re.lastIndex = 0;
+
+ var match, tokens = [];
+
+ var lastIndex = 0;
+
+ var token = {
+ type: null,
+ value: ""
+ };
+
+ while (match = re.exec(line)) {
+ var type = "text";
+ var value = [match[0]];
+
+ for ( var i = 0; i < match.length-2; i++) {
+ if (match[i + 1] !== undefined) {
+ var rule = state[mapping[i].rule];
+
+ if (mapping[i].len > 1) {
+ value = match.slice(i+2, i+1+mapping[i].len);
+ }
+
+ if (typeof rule.token == "function")
+ type = rule.token.apply(this, value);
+ else
+ type = rule.token;
+
+ if (rule.next && rule.next !== currentState) {
+ currentState = rule.next;
+ state = this.rules[currentState];
+ mapping = this.matchMappings[currentState];
+ lastIndex = re.lastIndex;
+
+ re = this.regExps[currentState];
+ re.lastIndex = lastIndex;
+ }
+ break;
+ }
+ };
+
+ if (typeof type == "string") {
+ if (typeof value != "string") {
+ value = [value.join("")];
+ }
+ type = [type];
+ }
+
+ for ( var i = 0; i < value.length; i++) {
+ if (token.type !== type[i]) {
+ if (token.type) {
+ tokens.push(token);
+ }
+
+ token = {
+ type: type[i],
+ value: value[i]
+ }
+ } else {
+ token.value += value[i];
+ }
+ }
+
+ if (lastIndex == line.length)
+ break;
+
+ lastIndex = re.lastIndex;
+ };
+
+ if (token.type)
+ tokens.push(token);
+
+ return {
+ tokens : tokens,
+ state : currentState
+ };
+ };
+
+}).call(Tokenizer.prototype);
+
+exports.Tokenizer = Tokenizer;
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/mode/text_highlight_rules', ['require', 'exports', 'module' , 'pilot/lang'], function(require, exports, module) {
+
+var lang = require("pilot/lang");
+
+var TextHighlightRules = function() {
+
+ // regexp must not have capturing parentheses
+ // regexps are ordered -> the first match is used
+
+ this.$rules = {
+ "start" : [ {
+ token : "empty_line",
+ regex : '^$'
+ }, {
+ token : "text",
+ regex : ".+"
+ } ]
+ };
+};
+
+(function() {
+
+ this.addRules = function(rules, prefix) {
+ for (var key in rules) {
+ var state = rules[key];
+ for (var i=0; i<state.length; i++) {
+ var rule = state[i];
+ if (rule.next) {
+ rule.next = prefix + rule.next;
+ } else {
+ rule.next = prefix + key;
+ }
+ }
+ this.$rules[prefix + key] = state;
+ }
+ };
+
+ this.getRules = function() {
+ return this.$rules;
+ };
+
+ this.embedRules = function (HighlightRules, prefix, escapeRules, states) {
+ var embedRules = new HighlightRules().getRules();
+ if (states) {
+ for (var i = 0; i < states.length; i++) {
+ states[i] = prefix + states[i];
+ }
+ } else {
+ states = [];
+ for (var key in embedRules) {
+ states.push(prefix + key);
+ }
+ }
+ this.addRules(embedRules, prefix);
+
+ for (var i = 0; i < states.length; i++) {
+ Array.prototype.unshift.apply(this.$rules[states[i]], lang.deepCopy(escapeRules));
+ }
+
+ if (!this.$embeds) {
+ this.$embeds = [];
+ }
+ this.$embeds.push(prefix);
+ }
+
+ this.getEmbeds = function() {
+ return this.$embeds;
+ }
+
+}).call(TextHighlightRules.prototype);
+
+exports.TextHighlightRules = TextHighlightRules;
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Chris Spencer <chris.ag.spencer AT googlemail DOT com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/mode/behaviour', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+var Behaviour = function() {
+ this.$behaviours = {};
+};
+
+(function () {
+
+ this.add = function (name, action, callback) {
+ switch (undefined) {
+ case this.$behaviours:
+ this.$behaviours = {};
+ case this.$behaviours[name]:
+ this.$behaviours[name] = {};
+ }
+ this.$behaviours[name][action] = callback;
+ }
+
+ this.addBehaviours = function (behaviours) {
+ for (var key in behaviours) {
+ for (var action in behaviours[key]) {
+ this.add(key, action, behaviours[key][action]);
+ }
+ }
+ }
+
+ this.remove = function (name) {
+ if (this.$behaviours && this.$behaviours[name]) {
+ delete this.$behaviours[name];
+ }
+ }
+
+ this.inherit = function (mode, filter) {
+ if (typeof mode === "function") {
+ var behaviours = new mode().getBehaviours(filter);
+ } else {
+ var behaviours = mode.getBehaviours(filter);
+ }
+ this.addBehaviours(behaviours);
+ }
+
+ this.getBehaviours = function (filter) {
+ if (!filter) {
+ return this.$behaviours;
+ } else {
+ var ret = {}
+ for (var i = 0; i < filter.length; i++) {
+ if (this.$behaviours[filter[i]]) {
+ ret[filter[i]] = this.$behaviours[filter[i]];
+ }
+ }
+ return ret;
+ }
+ }
+
+}).call(Behaviour.prototype);
+
+exports.Behaviour = Behaviour;
+});/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/document', ['require', 'exports', 'module' , 'pilot/oop', 'pilot/event_emitter', 'ace/range', 'ace/anchor'], function(require, exports, module) {
+
+var oop = require("pilot/oop");
+var EventEmitter = require("pilot/event_emitter").EventEmitter;
+var Range = require("ace/range").Range;
+var Anchor = require("ace/anchor").Anchor;
+
+var Document = function(text) {
+ this.$lines = [];
+
+ if (Array.isArray(text)) {
+ this.insertLines(0, text);
+ }
+ // There has to be one line at least in the document. If you pass an empty
+ // string to the insert function, nothing will happen. Workaround.
+ else if (text.length == 0) {
+ this.$lines = [""];
+ } else {
+ this.insert({row: 0, column:0}, text);
+ }
+};
+
+(function() {
+
+ oop.implement(this, EventEmitter);
+
+ this.setValue = function(text) {
+ var len = this.getLength();
+ this.remove(new Range(0, 0, len, this.getLine(len-1).length));
+ this.insert({row: 0, column:0}, text);
+ };
+
+ this.getValue = function() {
+ return this.getAllLines().join(this.getNewLineCharacter());
+ };
+
+ this.createAnchor = function(row, column) {
+ return new Anchor(this, row, column);
+ };
+
+ // check for IE split bug
+ if ("aaa".split(/a/).length == 0)
+ this.$split = function(text) {
+ return text.replace(/\r\n|\r/g, "\n").split("\n");
+ }
+ else
+ this.$split = function(text) {
+ return text.split(/\r\n|\r|\n/);
+ };
+
+
+ this.$detectNewLine = function(text) {
+ var match = text.match(/^.*?(\r?\n)/m);
+ if (match) {
+ this.$autoNewLine = match[1];
+ } else {
+ this.$autoNewLine = "\n";
+ }
+ };
+
+ this.getNewLineCharacter = function() {
+ switch (this.$newLineMode) {
+ case "windows":
+ return "\r\n";
+
+ case "unix":
+ return "\n";
+
+ case "auto":
+ return this.$autoNewLine;
+ }
+ },
+
+ this.$autoNewLine = "\n";
+ this.$newLineMode = "auto";
+ this.setNewLineMode = function(newLineMode) {
+ if (this.$newLineMode === newLineMode) return;
+
+ this.$newLineMode = newLineMode;
+ };
+
+ this.getNewLineMode = function() {
+ return this.$newLineMode;
+ };
+
+ this.isNewLine = function(text) {
+ return (text == "\r\n" || text == "\r" || text == "\n");
+ };
+
+ /**
+ * Get a verbatim copy of the given line as it is in the document
+ */
+ this.getLine = function(row) {
+ return this.$lines[row] || "";
+ };
+
+ this.getLines = function(firstRow, lastRow) {
+ return this.$lines.slice(firstRow, lastRow + 1);
+ };
+
+ /**
+ * Returns all lines in the document as string array. Warning: The caller
+ * should not modify this array!
+ */
+ this.getAllLines = function() {
+ return this.getLines(0, this.getLength());
+ };
+
+ this.getLength = function() {
+ return this.$lines.length;
+ };
+
+ this.getTextRange = function(range) {
+ if (range.start.row == range.end.row) {
+ return this.$lines[range.start.row].substring(range.start.column,
+ range.end.column);
+ }
+ else {
+ var lines = [];
+ lines.push(this.$lines[range.start.row].substring(range.start.column));
+ lines.push.apply(lines, this.getLines(range.start.row+1, range.end.row-1));
+ lines.push(this.$lines[range.end.row].substring(0, range.end.column));
+ return lines.join(this.getNewLineCharacter());
+ }
+ };
+
+ this.$clipPosition = function(position) {
+ var length = this.getLength();
+ if (position.row >= length) {
+ position.row = Math.max(0, length - 1);
+ position.column = this.getLine(length-1).length;
+ }
+ return position;
+ }
+
+ this.insert = function(position, text) {
+ if (text.length == 0)
+ return position;
+
+ position = this.$clipPosition(position);
+
+ if (this.getLength() <= 1)
+ this.$detectNewLine(text);
+
+ var lines = this.$split(text);
+ var firstLine = lines.splice(0, 1)[0];
+ var lastLine = lines.length == 0 ? null : lines.splice(lines.length - 1, 1)[0];
+
+ this._dispatchEvent("changeStart");
+ position = this.insertInLine(position, firstLine);
+ if (lastLine !== null) {
+ position = this.insertNewLine(position); // terminate first line
+ position = this.insertLines(position.row, lines);
+ position = this.insertInLine(position, lastLine || "");
+ }
+ this._dispatchEvent("changeEnd");
+ return position;
+ };
+
+ this.insertLines = function(row, lines) {
+ if (lines.length == 0)
+ return {row: row, column: 0};
+
+ var args = [row, 0];
+ args.push.apply(args, lines);
+ this.$lines.splice.apply(this.$lines, args);
+
+ this._dispatchEvent("changeStart");
+ var range = new Range(row, 0, row + lines.length, 0);
+ var delta = {
+ action: "insertLines",
+ range: range,
+ lines: lines
+ };
+ this._dispatchEvent("change", { data: delta });
+ this._dispatchEvent("changeEnd");
+ return range.end;
+ },
+
+ this.insertNewLine = function(position) {
+ position = this.$clipPosition(position);
+ var line = this.$lines[position.row] || "";
+
+ this._dispatchEvent("changeStart");
+ this.$lines[position.row] = line.substring(0, position.column);
+ this.$lines.splice(position.row + 1, 0, line.substring(position.column, line.length));
+
+ var end = {
+ row : position.row + 1,
+ column : 0
+ };
+
+ var delta = {
+ action: "insertText",
+ range: Range.fromPoints(position, end),
+ text: this.getNewLineCharacter()
+ };
+ this._dispatchEvent("change", { data: delta });
+ this._dispatchEvent("changeEnd");
+
+ return end;
+ };
+
+ this.insertInLine = function(position, text) {
+ if (text.length == 0)
+ return position;
+
+ var line = this.$lines[position.row] || "";
+
+ this._dispatchEvent("changeStart");
+ this.$lines[position.row] = line.substring(0, position.column) + text
+ + line.substring(position.column);
+
+ var end = {
+ row : position.row,
+ column : position.column + text.length
+ };
+
+ var delta = {
+ action: "insertText",
+ range: Range.fromPoints(position, end),
+ text: text
+ };
+ this._dispatchEvent("change", { data: delta });
+ this._dispatchEvent("changeEnd");
+
+ return end;
+ };
+
+ this.remove = function(range) {
+ // clip to document
+ range.start = this.$clipPosition(range.start);
+ range.end = this.$clipPosition(range.end);
+
+ if (range.isEmpty())
+ return range.start;
+
+ var firstRow = range.start.row;
+ var lastRow = range.end.row;
+
+ this._dispatchEvent("changeStart");
+ if (range.isMultiLine()) {
+ var firstFullRow = range.start.column == 0 ? firstRow : firstRow + 1;
+ var lastFullRow = lastRow - 1;
+
+ if (range.end.column > 0)
+ this.removeInLine(lastRow, 0, range.end.column);
+
+ if (lastFullRow >= firstFullRow)
+ this.removeLines(firstFullRow, lastFullRow);
+
+ if (firstFullRow != firstRow) {
+ this.removeInLine(firstRow, range.start.column, this.getLine(firstRow).length);
+ this.removeNewLine(range.start.row);
+ }
+ }
+ else {
+ this.removeInLine(firstRow, range.start.column, range.end.column);
+ }
+ this._dispatchEvent("changeEnd");
+ return range.start;
+ };
+
+ this.removeInLine = function(row, startColumn, endColumn) {
+ if (startColumn == endColumn)
+ return;
+
+ var range = new Range(row, startColumn, row, endColumn);
+ var line = this.getLine(row);
+ var removed = line.substring(startColumn, endColumn);
+ var newLine = line.substring(0, startColumn) + line.substring(endColumn, line.length);
+ this._dispatchEvent("changeStart");
+ this.$lines.splice(row, 1, newLine);
+
+ var delta = {
+ action: "removeText",
+ range: range,
+ text: removed
+ };
+ this._dispatchEvent("change", { data: delta });
+ this._dispatchEvent("changeEnd");
+ return range.start;
+ };
+
+ /**
+ * Removes a range of full lines
+ *
+ * @param firstRow {Integer} The first row to be removed
+ * @param lastRow {Integer} The last row to be removed
+ * @return {String[]} The removed lines
+ */
+ this.removeLines = function(firstRow, lastRow) {
+ this._dispatchEvent("changeStart");
+ var range = new Range(firstRow, 0, lastRow + 1, 0);
+ var removed = this.$lines.splice(firstRow, lastRow - firstRow + 1);
+
+ var delta = {
+ action: "removeLines",
+ range: range,
+ nl: this.getNewLineCharacter(),
+ lines: removed
+ };
+ this._dispatchEvent("change", { data: delta });
+ this._dispatchEvent("changeEnd");
+ return removed;
+ };
+
+ this.removeNewLine = function(row) {
+ var firstLine = this.getLine(row);
+ var secondLine = this.getLine(row+1);
+
+ var range = new Range(row, firstLine.length, row+1, 0);
+ var line = firstLine + secondLine;
+
+ this._dispatchEvent("changeStart");
+ this.$lines.splice(row, 2, line);
+
+ var delta = {
+ action: "removeText",
+ range: range,
+ text: this.getNewLineCharacter()
+ };
+ this._dispatchEvent("change", { data: delta });
+ this._dispatchEvent("changeEnd");
+ };
+
+ this.replace = function(range, text) {
+ if (text.length == 0 && range.isEmpty())
+ return range.start;
+
+ // Shortcut: If the text we want to insert is the same as it is already
+ // in the document, we don't have to replace anything.
+ if (text == this.getTextRange(range))
+ return range.end;
+
+ this._dispatchEvent("changeStart");
+ this.remove(range);
+ if (text) {
+ var end = this.insert(range.start, text);
+ }
+ else {
+ end = range.start;
+ }
+ this._dispatchEvent("changeEnd");
+
+ return end;
+ };
+
+ this.applyDeltas = function(deltas) {
+ for (var i=0; i<deltas.length; i++) {
+ var delta = deltas[i];
+ var range = Range.fromPoints(delta.range.start, delta.range.end);
+
+ if (delta.action == "insertLines")
+ this.insertLines(range.start.row, delta.lines)
+ else if (delta.action == "insertText")
+ this.insert(range.start, delta.text)
+ else if (delta.action == "removeLines")
+ this.removeLines(range.start.row, range.end.row - 1)
+ else if (delta.action == "removeText")
+ this.remove(range)
+ }
+ };
+
+ this.revertDeltas = function(deltas) {
+ for (var i=deltas.length-1; i>=0; i--) {
+ var delta = deltas[i];
+
+ var range = Range.fromPoints(delta.range.start, delta.range.end);
+
+ if (delta.action == "insertLines")
+ this.removeLines(range.start.row, range.end.row - 1)
+ else if (delta.action == "insertText")
+ this.remove(range)
+ else if (delta.action == "removeLines")
+ this.insertLines(range.start.row, delta.lines)
+ else if (delta.action == "removeText")
+ this.insert(range.start, delta.text)
+ }
+ };
+
+}).call(Document.prototype);
+
+exports.Document = Document;
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/anchor', ['require', 'exports', 'module' , 'pilot/oop', 'pilot/event_emitter'], function(require, exports, module) {
+
+var oop = require("pilot/oop");
+var EventEmitter = require("pilot/event_emitter").EventEmitter;
+
+/**
+ * An Anchor is a floating pointer in the document. Whenever text is inserted or
+ * deleted before the cursor, the position of the cursor is updated
+ */
+var Anchor = exports.Anchor = function(doc, row, column) {
+ this.document = doc;
+
+ if (typeof column == "undefined")
+ this.setPosition(row.row, row.column);
+ else
+ this.setPosition(row, column);
+
+ this.$onChange = this.onChange.bind(this);
+ doc.on("change", this.$onChange);
+};
+
+(function() {
+
+ oop.implement(this, EventEmitter);
+
+ this.getPosition = function() {
+ return this.$clipPositionToDocument(this.row, this.column);
+ };
+
+ this.getDocument = function() {
+ return this.document;
+ };
+
+ this.onChange = function(e) {
+ var delta = e.data;
+ var range = delta.range;
+
+ if (range.start.row == range.end.row && range.start.row != this.row)
+ return;
+
+ if (range.start.row > this.row)
+ return;
+
+ if (range.start.row == this.row && range.start.column > this.column)
+ return;
+
+ var row = this.row;
+ var column = this.column;
+
+ if (delta.action === "insertText") {
+ if (range.start.row === row && range.start.column <= column) {
+ if (range.start.row === range.end.row) {
+ column += range.end.column - range.start.column;
+ }
+ else {
+ column -= range.start.column;
+ row += range.end.row - range.start.row;
+ }
+ }
+ else if (range.start.row !== range.end.row && range.start.row < row) {
+ row += range.end.row - range.start.row;
+ }
+ } else if (delta.action === "insertLines") {
+ if (range.start.row <= row) {
+ row += range.end.row - range.start.row;
+ }
+ }
+ else if (delta.action == "removeText") {
+ if (range.start.row == row && range.start.column < column) {
+ if (range.end.column >= column)
+ column = range.start.column;
+ else
+ column = Math.max(0, column - (range.end.column - range.start.column));
+
+ } else if (range.start.row !== range.end.row && range.start.row < row) {
+ if (range.end.row == row) {
+ column = Math.max(0, column - range.end.column) + range.start.column;
+ }
+ row -= (range.end.row - range.start.row);
+ }
+ else if (range.end.row == row) {
+ row -= range.end.row - range.start.row;
+ column = Math.max(0, column - range.end.column) + range.start.column;
+ }
+ } else if (delta.action == "removeLines") {
+ if (range.start.row <= row) {
+ if (range.end.row <= row)
+ row -= range.end.row - range.start.row;
+ else {
+ row = range.start.row;
+ column = 0;
+ }
+ }
+ }
+
+ this.setPosition(row, column, true);
+ };
+
+ this.setPosition = function(row, column, noClip) {
+ if (noClip) {
+ pos = {
+ row: row,
+ column: column
+ };
+ }
+ else {
+ pos = this.$clipPositionToDocument(row, column);
+ }
+
+ if (this.row == pos.row && this.column == pos.column)
+ return;
+
+ var old = {
+ row: this.row,
+ column: this.column
+ };
+
+ this.row = pos.row;
+ this.column = pos.column;
+ this._dispatchEvent("change", {
+ old: old,
+ value: pos
+ });
+ };
+
+ this.detach = function() {
+ this.document.removeEventListener("change", this.$onChange);
+ };
+
+ this.$clipPositionToDocument = function(row, column) {
+ var pos = {};
+
+ if (row >= this.document.getLength()) {
+ pos.row = Math.max(0, this.document.getLength() - 1);
+ pos.column = this.document.getLine(pos.row).length;
+ }
+ else if (row < 0) {
+ pos.row = 0;
+ pos.column = 0;
+ }
+ else {
+ pos.row = row;
+ pos.column = Math.min(this.document.getLine(pos.row).length, Math.max(0, column));
+ }
+
+ if (column < 0)
+ pos.column = 0;
+
+ return pos;
+ };
+
+}).call(Anchor.prototype);
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/background_tokenizer', ['require', 'exports', 'module' , 'pilot/oop', 'pilot/event_emitter'], function(require, exports, module) {
+
+var oop = require("pilot/oop");
+var EventEmitter = require("pilot/event_emitter").EventEmitter;
+
+var BackgroundTokenizer = function(tokenizer, editor) {
+ this.running = false;
+ this.lines = [];
+ this.currentLine = 0;
+ this.tokenizer = tokenizer;
+
+ var self = this;
+
+ this.$worker = function() {
+ if (!self.running) { return; }
+
+ var workerStart = new Date();
+ var startLine = self.currentLine;
+ var doc = self.doc;
+
+ var processedLines = 0;
+
+ var len = doc.getLength();
+ while (self.currentLine < len) {
+ self.lines[self.currentLine] = self.$tokenizeRows(self.currentLine, self.currentLine)[0];
+ self.currentLine++;
+
+ // only check every 5 lines
+ processedLines += 1;
+ if ((processedLines % 5 == 0) && (new Date() - workerStart) > 20) {
+ self.fireUpdateEvent(startLine, self.currentLine-1);
+ self.running = setTimeout(self.$worker, 20);
+ return;
+ }
+ }
+
+ self.running = false;
+
+ self.fireUpdateEvent(startLine, len - 1);
+ };
+};
+
+(function(){
+
+ oop.implement(this, EventEmitter);
+
+ this.setTokenizer = function(tokenizer) {
+ this.tokenizer = tokenizer;
+ this.lines = [];
+
+ this.start(0);
+ };
+
+ this.setDocument = function(doc) {
+ this.doc = doc;
+ this.lines = [];
+
+ this.stop();
+ };
+
+ this.fireUpdateEvent = function(firstRow, lastRow) {
+ var data = {
+ first: firstRow,
+ last: lastRow
+ };
+ this._dispatchEvent("update", {data: data});
+ };
+
+ this.start = function(startRow) {
+ this.currentLine = Math.min(startRow || 0, this.currentLine,
+ this.doc.getLength());
+
+ // remove all cached items below this line
+ this.lines.splice(this.currentLine, this.lines.length);
+
+ this.stop();
+ // pretty long delay to prevent the tokenizer from interfering with the user
+ this.running = setTimeout(this.$worker, 700);
+ };
+
+ this.stop = function() {
+ if (this.running)
+ clearTimeout(this.running);
+ this.running = false;
+ };
+
+ this.getTokens = function(firstRow, lastRow) {
+ return this.$tokenizeRows(firstRow, lastRow);
+ };
+
+ this.getState = function(row) {
+ return this.$tokenizeRows(row, row)[0].state;
+ };
+
+ this.$tokenizeRows = function(firstRow, lastRow) {
+ if (!this.doc)
+ return [];
+
+ var rows = [];
+
+ // determine start state
+ var state = "start";
+ var doCache = false;
+ if (firstRow > 0 && this.lines[firstRow - 1]) {
+ state = this.lines[firstRow - 1].state;
+ doCache = true;
+ } else if (firstRow == 0) {
+ state = "start";
+ doCache = true;
+ } else if (this.lines.length > 0) {
+ // Guess that we haven't changed state.
+ state = this.lines[this.lines.length-1].state;
+ }
+
+ var lines = this.doc.getLines(firstRow, lastRow);
+ for (var row=firstRow; row<=lastRow; row++) {
+ if (!this.lines[row]) {
+ var tokens = this.tokenizer.getLineTokens(lines[row-firstRow] || "", state);
+ var state = tokens.state;
+ rows.push(tokens);
+
+ if (doCache) {
+ this.lines[row] = tokens;
+ }
+ }
+ else {
+ var tokens = this.lines[row];
+ state = tokens.state;
+ rows.push(tokens);
+ }
+ }
+ return rows;
+ };
+
+}).call(BackgroundTokenizer.prototype);
+
+exports.BackgroundTokenizer = BackgroundTokenizer;
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Julian Viereck <julian DOT viereck AT gmail DOT com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/edit_session/folding', ['require', 'exports', 'module' , 'ace/range', 'ace/edit_session/fold_line'], function(require, exports, module) {
+
+var Range = require("ace/range").Range;
+var FoldLine = require("ace/edit_session/fold_line").FoldLine;
+
+/**
+ * Simple fold-data struct.
+ **/
+function Fold(range, placeholder) {
+ this.foldLine = null;
+ this.placeholder = placeholder;
+ this.range = range;
+ this.start = range.start;
+ this.end = range.end;
+
+ this.sameRow = range.start.row == range.end.row;
+ this.subFolds = [];
+}
+
+Fold.prototype.toString = function() {
+ return '"' + this.placeholder + '" ' + this.range.toString();
+}
+
+Fold.prototype.setFoldLine = function(foldLine) {
+ this.foldLine = foldLine;
+ this.subFolds.forEach(function(fold) {
+ fold.setFoldLine(foldLine);
+ });
+}
+
+Fold.prototype.clone = function() {
+ var range = this.range.clone();
+ var fold = new Fold(range, this.placeholder);
+ this.subFolds.forEach(function(subFold) {
+ fold.subFolds.push(subFold.clone());
+ });
+ return fold;
+}
+
+function Folding() {
+ /**
+ * Looks up a fold at a given row/column. Possible values for side:
+ * -1: ignore a fold if fold.start = row/column
+ * +1: ignore a fold if fold.end = row/column
+ */
+ this.getFoldAt = function(row, column, side) {
+ var foldLine = this.getFoldLine(row);
+ if (foldLine) {
+ var folds = foldLine.folds,
+ fold;
+
+ for (var i = 0; i < folds.length; i++) {
+ fold = folds[i];
+ if (fold.range.contains(row, column)) {
+ if (side == 1 && fold.range.isEnd(row, column)) {
+ continue;
+ } else if (side == -1 && fold.range.isStart(row, column)) {
+ continue;
+ }
+ return fold;
+ }
+ }
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns all folds in the given range. Note, that this will return folds
+ *
+ */
+ this.getFoldsInRange = function(range) {
+ range = range.clone();
+ var start = range.start,
+ end = range.end;
+ var foldLines = this.$foldData,
+ folds,
+ fold;
+ var cmp,
+ foundFolds = [];
+
+ start.column += 1;
+ end.column -= 1;
+
+ for (var i = 0; i < foldLines.length; i++) {
+ cmp = foldLines[i].range.compareRange(range);
+ // Range is before foldLine. No intersection. This means,
+ // there might be other foldLines that intersect.
+ if (cmp == 2) {
+ continue;
+ } else
+ // Range is after foldLine. There can't be any other foldLines then,
+ // so let's give up.
+ if (cmp == -2) {
+ break;
+ }
+
+ folds = foldLines[i].folds;
+ for (var j = 0; j < folds.length; j++) {
+ fold = folds[j];
+ cmp = fold.range.compareRange(range);
+ if (cmp == -2) {
+ break;
+ } else if (cmp == 2) {
+ continue;
+ } else
+ // WTF-state: Can happen due to -1/+1 to start/end column.
+ if (cmp == 42) {
+ break;
+ }
+ foundFolds.push(fold);
+ }
+ }
+ return foundFolds;
+ }
+
+ /**
+ * Returns the string between folds at the given position.
+ * E.g.
+ * foo<fold>b|ar<fold>wolrd -> "bar"
+ * foo<fold>bar<fold>wol|rd -> "world"
+ * foo<fold>bar<fo|ld>wolrd -> <null>
+ *
+ * where | means the position of row/column
+ *
+ * The trim option determs if the return string should be trimed according
+ * to the "side" passed with the trim value:
+ *
+ * E.g.
+ * foo<fold>b|ar<fold>wolrd -trim=-1> "b"
+ * foo<fold>bar<fold>wol|rd -trim=+1> "rld"
+ * fo|o<fold>bar<fold>wolrd -trim=00> "foo"
+ */
+ this.getFoldStringAt = function(row, column, trim, foldLine) {
+ var foldLine = foldLine || this.getFoldLine(row);
+ if (!foldLine) {
+ return null;
+ } else {
+ var fold, lastFold, cmp, str;
+ lastFold = {
+ end: { column: 0 }
+ };
+ // TODO: Refactor to use getNextFoldTo function.
+ for (var i = 0; i < foldLine.folds.length; i++) {
+ fold = foldLine.folds[i];
+ cmp = fold.range.compareEnd(row, column);
+ if (cmp == -1) {
+ str = this.getLine(fold.start.row).
+ substring(lastFold.end.column, fold.start.column);
+ break;
+ } else if (cmp == 0) {
+ return null;
+ }
+ lastFold = fold;
+ }
+ if (!str) {
+ str = this.getLine(fold.start.row).
+ substring(lastFold.end.column);
+ }
+ if (trim == -1) {
+ return str.substring(0, column - lastFold.end.column);
+ } else if (trim == 1) {
+ return str.substring(column - lastFold.end.column)
+ } else {
+ return str;
+ }
+ }
+ }
+
+ this.getFoldLine = function(docRow, startFoldLine) {
+ var foldData = this.$foldData;
+ var i = 0;
+ if(startFoldLine)
+ i = foldData.indexOf(startFoldLine);
+ if(i == -1)
+ i = 0;
+ for (i; i < foldData.length; i++) {
+ var foldLine = foldData[i];
+ if (foldLine.start.row <= docRow && foldLine.end.row >= docRow) {
+ return foldLine;
+ } else if (foldLine.end.row > docRow) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ // returns the fold which starts after or contains docRow
+ this.getNextFold = function(docRow, startFoldLine) {
+ var foldData = this.$foldData, ans;
+ var i = 0;
+ if(startFoldLine)
+ i = foldData.indexOf(startFoldLine);
+ if(i == -1)
+ i = 0;
+ for (i; i < foldData.length; i++) {
+ var foldLine = foldData[i];
+ if (foldLine.end.row >= docRow) {
+ return foldLine;
+ }
+ }
+ return null;
+ }
+
+ this.getFoldedRowCount = function(first, last) {
+ var foldData = this.$foldData, rowCount = last-first+1;
+ for (var i = 0; i < foldData.length; i++) {
+ var foldLine = foldData[i],
+ end = foldLine.end.row,
+ start = foldLine.start.row;
+ if(end >= last) {
+ if(start < last) {
+ if(start >= first)
+ rowCount -= last-start;
+ else
+ rowCount = 0;//in one fold
+ }
+ break;
+ } else if(end >= first){
+ if (start >= first) //fold inside range
+ rowCount -= end-start;
+ else
+ rowCount -= end-first+1;
+ }
+ }
+ return rowCount;
+ }
+
+ this.$addFoldLine = function(foldLine) {
+ this.$foldData.push(foldLine);
+ this.$foldData.sort(function(a, b) {
+ return a.start.row - b.start.row;
+ });
+ return foldLine;
+ }
+
+ /**
+ * Adds a new fold.
+ *
+ * @returns
+ * The new created Fold object or an existing fold object in case the
+ * passed in range fits an existing fold exactly.
+ */
+ this.addFold = function(placeholder, startRow, startColumn, endRow, endColumn) {
+ var range;
+ var foldData = this.$foldData;
+ var foldRow = null;
+ var foldLine;
+ var fold;
+ var argsFold;
+ var folds;
+ var added = false;
+
+ if (placeholder instanceof Fold) {
+ argsFold = placeholder;
+ startRow = argsFold.range;
+ placeholder = argsFold.placeholder;
+ }
+
+ // Normalize parameters.
+ if (!(startRow instanceof Range)) {
+ range = new Range(startRow, startColumn, endRow, endColumn);
+ } else {
+ range = startRow;
+ startRow = range.start.row;
+ startColumn = range.start.column;
+ endRow = range.end.row;
+ endColumn = range.end.column;
+ }
+
+ // --- Some checking ---
+ if (placeholder.length < 2) {
+ throw "Placeholder has to be at least 2 characters";
+ }
+
+ if (startRow == endRow && endColumn - startColumn < 2) {
+ throw "The range has to be at least 2 characters width";
+ }
+
+ fold = this.getFoldAt(startRow, startColumn, 1);
+ if (fold
+ && fold.range.isEnd(endRow, endColumn)
+ && fold.range.isStart(startRow, startColumn))
+ {
+ return fold;
+ }
+
+ fold = this.getFoldAt(startRow, startColumn, 1);
+ if (fold && !fold.range.isStart(startRow, startColumn)) {
+ throw "A fold can't start inside of an already existing fold";
+ }
+
+ fold = this.getFoldAt(endRow, endColumn, -1);
+ if (fold && !fold.range.isEnd(endRow, endColumn)) {
+ throw "A fold can't end inside of an already existing fold";
+ }
+
+ if (endRow >= this.doc.getLength()) {
+ throw "End of fold is outside of the document.";
+ }
+
+ if (endColumn > this.getLine(endRow).length
+ || startColumn > this.getLine(startRow).length)
+ {
+ throw "End of fold is outside of the document.";
+ }
+
+ // --- Start adding the fold ---
+ // Use the passed in fold or create a new one.
+ fold = argsFold || new Fold(range, placeholder);
+
+ // Check if there are folds in the range we create the new fold for.
+ folds = this.getFoldsInRange(range);
+ if (folds.length > 0) {
+ // Remove the folds from fold data.
+ this.removeFolds(folds);
+ // Add the removed folds as subfolds on the new fold.
+ fold.subFolds = folds;
+ }
+
+ for (var i = 0; i < foldData.length; i++) {
+ foldLine = foldData[i];
+ if (endRow == foldLine.start.row) {
+ foldLine.addFold(fold);
+ added = true;
+ break;
+ } else if (startRow == foldLine.end.row) {
+ foldLine.addFold(fold);
+ added = true;
+ if (!fold.sameRow) {
+ // Check if we might have to merge two FoldLines.
+ foldLineNext = foldData[i + 1];
+ if (foldLineNext && foldLineNext.start.row == endRow) {
+ // We need to merge!
+ foldLine.merge(foldLineNext);
+ break;
+ }
+ }
+ break;
+ } else if (endRow <= foldLine.start.row) {
+ break;
+ }
+ }
+
+ if (!added) {
+ foldLine = this.$addFoldLine(new FoldLine(this.$foldData, fold));
+ }
+
+ if (this.$useWrapMode) {
+ this.$updateWrapData(foldLine.start.row, foldLine.start.row);
+ }
+
+ // Notify that fold data has changed.
+ this.$modified = true;
+ this._dispatchEvent("changeFold", { data: fold });
+
+ return fold;
+ };
+
+ this.addFolds = function(folds) {
+ folds.forEach(function(fold) {
+ this.addFold(fold);
+ }, this);
+ };
+
+ this.removeFold = function(fold) {
+ var foldLine = fold.foldLine;
+ var startRow = foldLine.start.row;
+ var endRow = foldLine.end.row;
+
+ var foldLines = this.$foldData,
+ folds = foldLine.folds;
+ // Simple case where there is only one fold in the FoldLine such that
+ // the entire fold line can get removed directly.
+ if (folds.length == 1) {
+ foldLines.splice(foldLines.indexOf(foldLine), 1);
+ } else
+ // If the fold is the last fold of the foldLine, just remove it.
+ if (foldLine.range.isEnd(fold.end.row, fold.end.column)) {
+ folds.pop();
+ foldLine.end.row = folds[folds.length - 1].end.row;
+ foldLine.end.column = folds[folds.length - 1].end.column;
+ } else
+ // If the fold is the first fold of the foldLine, just remove it.
+ if (foldLine.range.isStart(fold.start.row, fold.start.column)) {
+ folds.shift();
+ foldLine.start.row = folds[0].start.row;
+ foldLine.start.column = folds[0].start.column;
+ } else
+ // We know there are more then 2 folds and the fold is not at the edge.
+ // This means, the fold is somewhere in between.
+ //
+ // If the fold is in one row, we just can remove it.
+ if (fold.sameRow) {
+ folds.splice(folds.indexOf(fold), 1);
+ } else
+ // The fold goes over more then one row. This means remvoing this fold
+ // will cause the fold line to get splitted up.
+ {
+ var newFoldLine = foldLine.split(fold.start.row, fold.start.column);
+ newFoldLine.folds.shift();
+ foldLine.start.row = folds[0].start.row;
+ foldLine.start.column = folds[0].start.column;
+ this.$addFoldLine(newFoldLine);
+ }
+
+ if (this.$useWrapMode) {
+ this.$updateWrapData(startRow, endRow);
+ }
+
+ // Notify that fold data has changed.
+ this.$modified = true;
+ this._dispatchEvent("changeFold", { data: fold });
+ }
+
+ this.removeFolds = function(folds) {
+ // We need to clone the folds array passed in as it might be the folds
+ // array of a fold line and as we call this.removeFold(fold), folds
+ // are removed from folds and changes the current index.
+ var cloneFolds = [];
+ for (var i = 0; i < folds.length; i++) {
+ cloneFolds.push(folds[i]);
+ }
+
+ cloneFolds.forEach(function(fold) {
+ this.removeFold(fold);
+ }, this);
+ this.$modified = true;
+ }
+
+ this.expandFold = function(fold) {
+ this.removeFold(fold);
+ fold.subFolds.forEach(function(fold) {
+ this.addFold(fold);
+ }, this);
+ fold.subFolds = [];
+ }
+
+ this.expandFolds = function(folds) {
+ folds.forEach(function(fold) {
+ this.expandFold(fold);
+ }, this);
+ }
+
+ /**
+ * Checks if a given documentRow is folded. This is true if there are some
+ * folded parts such that some parts of the line is still visible.
+ **/
+ this.isRowFolded = function(docRow, startFoldRow) {
+ return !!this.getFoldLine(docRow, startFoldRow);
+ };
+
+ this.getRowFoldEnd = function(docRow, startFoldRow) {
+ var foldLine = this.getFoldLine(docRow, startFoldRow);
+ return (foldLine
+ ? foldLine.end.row
+ : docRow)
+ };
+
+ this.getFoldDisplayLine = function(foldLine, endRow, endColumn, startRow, startColumn) {
+ if (startRow == null) {
+ startRow = foldLine.start.row;
+ startColumn = 0;
+ }
+
+ if (endRow == null) {
+ endRow = foldLine.end.row;
+ endColumn = this.getLine(endRow).length;
+ }
+
+ // Build the textline using the FoldLine walker.
+ var line = "";
+ var doc = this.doc;
+ var textLine = "";
+
+ foldLine.walk(function(placeholder, row, column, lastColumn, isNewRow) {
+ if (row < startRow) {
+ return;
+ } else if (row == startRow) {
+ if (column < startColumn) {
+ return;
+ }
+ lastColumn = Math.max(startColumn, lastColumn);
+ }
+ if (placeholder) {
+ textLine += placeholder;
+ } else {
+ textLine += doc.getLine(row).substring(lastColumn, column);
+ }
+ }.bind(this), endRow, endColumn);
+ return textLine;
+ };
+
+ this.getDisplayLine = function(row, endColumn, startRow, startColumn) {
+ var foldLine = this.getFoldLine(row);
+
+ if (!foldLine) {
+ var line;
+ line = this.doc.getLine(row);
+ return line.substring(startColumn || 0, endColumn || line.length);
+ } else {
+ return this.getFoldDisplayLine(
+ foldLine, row, endColumn, startRow, startColumn);
+ }
+ };
+
+ this.$cloneFoldData = function() {
+ var foldData = this.$foldData;
+ var fd = [];
+ fd = this.$foldData.map(function(foldLine) {
+ var folds = foldLine.folds.map(function(fold) {
+ return fold.clone();
+ });
+ return new FoldLine(fd, folds);
+ });
+
+ return fd;
+ };
+}
+
+exports.Folding = Folding;
+
+});/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Julian Viereck <julian DOT viereck AT gmail DOT com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/edit_session/fold_line', ['require', 'exports', 'module' , 'ace/range'], function(require, exports, module) {
+
+var Range = require("ace/range").Range;
+
+/**
+ * If the an array is passed in, the folds are expected to be sorted already.
+ */
+function FoldLine(foldData, folds) {
+ this.foldData = foldData;
+ if (Array.isArray(folds)) {
+ this.folds = folds;
+ } else {
+ folds = this.folds = [ folds ];
+ }
+
+ var last = folds[folds.length - 1]
+ this.range = new Range(folds[0].start.row, folds[0].start.column,
+ last.end.row, last.end.column);
+ this.start = this.range.start;
+ this.end = this.range.end;
+
+ this.folds.forEach(function(fold) {
+ fold.setFoldLine(this);
+ }, this);
+}
+
+(function() {
+ /**
+ * Note: This doesn't update wrapData!
+ */
+ this.shiftRow = function(shift) {
+ this.start.row += shift;
+ this.end.row += shift;
+ this.folds.forEach(function(fold) {
+ fold.start.row += shift;
+ fold.end.row += shift;
+ });
+ }
+
+ this.addFold = function(fold) {
+ if (fold.sameRow) {
+ if (fold.start.row < this.startRow || fold.endRow > this.endRow) {
+ throw "Can't add a fold to this FoldLine as it has no connection";
+ }
+ this.folds.push(fold);
+ this.folds.sort(function(a, b) {
+ return -a.range.compareEnd(b.start.row, b.start.column);
+ });
+ if (this.range.compareEnd(fold.start.row, fold.start.column) > 0) {
+ this.end.row = fold.end.row;
+ this.end.column = fold.end.column;
+ } else if (this.range.compareStart(fold.end.row, fold.end.column) < 0) {
+ this.start.row = fold.start.row;
+ this.start.column = fold.start.column;
+ }
+ } else if (fold.start.row == this.end.row) {
+ this.folds.push(fold);
+ this.end.row = fold.end.row;
+ this.end.column = fold.end.column;
+ } else if (fold.end.row == this.start.row) {
+ this.folds.unshift(fold);
+ this.start.row = fold.start.row;
+ this.start.column = fold.start.column;
+ } else {
+ throw "Trying to add fold to FoldRow that doesn't have a matching row";
+ }
+ fold.foldLine = this;
+ }
+
+ this.containsRow = function(row) {
+ return row >= this.start.row && row <= this.end.row;
+ }
+
+ this.walk = function(callback, endRow, endColumn) {
+ var lastEnd = 0,
+ folds = this.folds,
+ fold,
+ comp, stop, isNewRow = true;
+
+ if (endRow == null) {
+ endRow = this.end.row;
+ endColumn = this.end.column;
+ }
+
+ for (var i = 0; i < folds.length; i++) {
+ fold = folds[i];
+
+ comp = fold.range.compareStart(endRow, endColumn);
+ // This fold is after the endRow/Column.
+ if (comp == -1) {
+ callback(null, endRow, endColumn, lastEnd, isNewRow);
+ return;
+ }
+
+ stop = callback(null, fold.start.row, fold.start.column, lastEnd, isNewRow);
+ stop = !stop && callback(fold.placeholder, fold.start.row, fold.start.column, lastEnd);
+
+ // If the user requested to stop the walk or endRow/endColumn is
+ // inside of this fold (comp == 0), then end here.
+ if (stop || comp == 0) {
+ return;
+ }
+
+ // Note the new lastEnd might not be on the same line. However,
+ // it's the callback's job to recognize this.
+ isNewRow = !fold.sameRow;
+ lastEnd = fold.end.column;
+ }
+ callback(null, endRow, endColumn, lastEnd, isNewRow);
+ }
+
+ this.getNextFoldTo = function(row, column) {
+ var fold, cmp;
+ for (var i = 0; i < this.folds.length; i++) {
+ fold = this.folds[i];
+ cmp = fold.range.compareEnd(row, column);
+ if (cmp == -1) {
+ return {
+ fold: fold,
+ kind: "after"
+ };
+ } else if (cmp == 0) {
+ return {
+ fold: fold,
+ kind: "inside"
+ }
+ }
+ }
+ return null;
+ }
+
+ this.addRemoveChars = function(row, column, len) {
+ var ret = this.getNextFoldTo(row, column),
+ fold, folds;
+ if (ret) {
+ fold = ret.fold;
+ if (ret.kind == "inside"
+ && fold.start.column != column
+ && fold.start.row != row)
+ {
+ throw "Moving characters inside of a fold should never be reached";
+ } else if (fold.start.row == row) {
+ folds = this.folds;
+ var i = folds.indexOf(fold);
+ if (i == 0) {
+ this.start.column += len;
+ }
+ for (i; i < folds.length; i++) {
+ fold = folds[i];
+ fold.start.column += len;
+ if (!fold.sameRow) {
+ return;
+ }
+ fold.end.column += len;
+ }
+ this.end.column += len;
+ }
+ }
+ }
+
+ this.split = function(row, column) {
+ var fold = this.getNextFoldTo(row, column).fold,
+ folds = this.folds;
+ var foldData = this.foldData;
+
+ if (!fold) {
+ return null;
+ }
+ var i = folds.indexOf(fold);
+ var foldBefore = folds[i - 1];
+ this.end.row = foldBefore.end.row;
+ this.end.column = foldBefore.end.column;
+
+ // Remove the folds after row/column and create a new FoldLine
+ // containing these removed folds.
+ folds = folds.splice(i, folds.length - i);
+
+ var newFoldLine = new FoldLine(foldData, folds);
+ foldData.splice(foldData.indexOf(this) + 1, 0, newFoldLine);
+ return newFoldLine;
+ }
+
+ this.merge = function(foldLineNext) {
+ var folds = foldLineNext.folds;
+ for (var i = 0; i < folds.length; i++) {
+ this.addFold(folds[i]);
+ }
+ // Remove the foldLineNext - no longer needed, as
+ // it's merged now with foldLineNext.
+ var foldData = this.foldData;
+ foldData.splice(foldData.indexOf(foldLineNext), 1);
+ }
+
+ this.toString = function() {
+ var ret = [this.range.toString() + ": [" ];
+
+ this.folds.forEach(function(fold) {
+ ret.push(" " + fold.toString());
+ });
+ ret.push("]")
+ return ret.join("\n");
+ }
+
+ this.idxToPosition = function(idx) {
+ var lastFoldEndColumn = 0;
+ var fold;
+
+ for (var i = 0; i < this.folds.length; i++) {
+ var fold = this.folds[i];
+
+ idx -= fold.start.column - lastFoldEndColumn;
+ if (idx < 0) {
+ return {
+ row: fold.start.row,
+ column: fold.start.column + idx
+ };
+ }
+
+ idx -= fold.placeholder.length;
+ if (idx < 0) {
+ return fold.start;
+ }
+
+ lastFoldEndColumn = fold.end.column;
+ }
+
+ return {
+ row: this.end.row,
+ column: this.end.column + idx
+ };
+ }
+}).call(FoldLine.prototype);
+
+exports.FoldLine = FoldLine;
+});/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Mihai Sucan <mihai DOT sucan AT gmail DOT com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/search', ['require', 'exports', 'module' , 'pilot/lang', 'pilot/oop', 'ace/range'], function(require, exports, module) {
+
+var lang = require("pilot/lang");
+var oop = require("pilot/oop");
+var Range = require("ace/range").Range;
+
+var Search = function() {
+ this.$options = {
+ needle: "",
+ backwards: false,
+ wrap: false,
+ caseSensitive: false,
+ wholeWord: false,
+ scope: Search.ALL,
+ regExp: false
+ };
+};
+
+Search.ALL = 1;
+Search.SELECTION = 2;
+
+(function() {
+
+ this.set = function(options) {
+ oop.mixin(this.$options, options);
+ return this;
+ };
+
+ this.getOptions = function() {
+ return lang.copyObject(this.$options);
+ };
+
+ this.find = function(session) {
+ if (!this.$options.needle)
+ return null;
+
+ if (this.$options.backwards) {
+ var iterator = this.$backwardMatchIterator(session);
+ } else {
+ iterator = this.$forwardMatchIterator(session);
+ }
+
+ var firstRange = null;
+ iterator.forEach(function(range) {
+ firstRange = range;
+ return true;
+ });
+
+ return firstRange;
+ };
+
+ this.findAll = function(session) {
+ if (!this.$options.needle)
+ return [];
+
+ if (this.$options.backwards) {
+ var iterator = this.$backwardMatchIterator(session);
+ } else {
+ iterator = this.$forwardMatchIterator(session);
+ }
+
+ var ranges = [];
+ iterator.forEach(function(range) {
+ ranges.push(range);
+ });
+
+ return ranges;
+ };
+
+ this.replace = function(input, replacement) {
+ var re = this.$assembleRegExp();
+ var match = re.exec(input);
+ if (match && match[0].length == input.length) {
+ if (this.$options.regExp) {
+ return input.replace(re, replacement);
+ } else {
+ return replacement;
+ }
+ } else {
+ return null;
+ }
+ };
+
+ this.$forwardMatchIterator = function(session) {
+ var re = this.$assembleRegExp();
+ var self = this;
+
+ return {
+ forEach: function(callback) {
+ self.$forwardLineIterator(session).forEach(function(line, startIndex, row) {
+ if (startIndex) {
+ line = line.substring(startIndex);
+ }
+
+ var matches = [];
+
+ line.replace(re, function(str) {
+ var offset = arguments[arguments.length-2];
+ matches.push({
+ str: str,
+ offset: startIndex + offset
+ });
+ return str;
+ });
+
+ for (var i=0; i<matches.length; i++) {
+ var match = matches[i];
+ var range = self.$rangeFromMatch(row, match.offset, match.str.length);
+ if (callback(range))
+ return true;
+ }
+
+ });
+ }
+ };
+ };
+
+ this.$backwardMatchIterator = function(session) {
+ var re = this.$assembleRegExp();
+ var self = this;
+
+ return {
+ forEach: function(callback) {
+ self.$backwardLineIterator(session).forEach(function(line, startIndex, row) {
+ if (startIndex) {
+ line = line.substring(startIndex);
+ }
+
+ var matches = [];
+
+ line.replace(re, function(str, offset) {
+ matches.push({
+ str: str,
+ offset: startIndex + offset
+ });
+ return str;
+ });
+
+ for (var i=matches.length-1; i>= 0; i--) {
+ var match = matches[i];
+ var range = self.$rangeFromMatch(row, match.offset, match.str.length);
+ if (callback(range))
+ return true;
+ }
+ });
+ }
+ };
+ };
+
+ this.$rangeFromMatch = function(row, column, length) {
+ return new Range(row, column, row, column+length);
+ };
+
+ this.$assembleRegExp = function() {
+ if (this.$options.regExp) {
+ var needle = this.$options.needle;
+ } else {
+ needle = lang.escapeRegExp(this.$options.needle);
+ }
+
+ if (this.$options.wholeWord) {
+ needle = "\\b" + needle + "\\b";
+ }
+
+ var modifier = "g";
+ if (!this.$options.caseSensitive) {
+ modifier += "i";
+ }
+
+ var re = new RegExp(needle, modifier);
+ return re;
+ };
+
+ this.$forwardLineIterator = function(session) {
+ var searchSelection = this.$options.scope == Search.SELECTION;
+
+ var range = session.getSelection().getRange();
+ var start = session.getSelection().getCursor();
+
+ var firstRow = searchSelection ? range.start.row : 0;
+ var firstColumn = searchSelection ? range.start.column : 0;
+ var lastRow = searchSelection ? range.end.row : session.getLength() - 1;
+
+ var wrap = this.$options.wrap;
+ var inWrap = false;
+
+ function getLine(row) {
+ var line = session.getLine(row);
+ if (searchSelection && row == range.end.row) {
+ line = line.substring(0, range.end.column);
+ }
+ if (inWrap && row == start.row) {
+ line = line.substring(0, start.column);
+ }
+ return line;
+ }
+
+ return {
+ forEach: function(callback) {
+ var row = start.row;
+
+ var line = getLine(row);
+ var startIndex = start.column;
+
+ var stop = false;
+ inWrap = false;
+
+ while (!callback(line, startIndex, row)) {
+
+ if (stop) {
+ return;
+ }
+
+ row++;
+ startIndex = 0;
+
+ if (row > lastRow) {
+ if (wrap) {
+ row = firstRow;
+ startIndex = firstColumn;
+ inWrap = true;
+ } else {
+ return;
+ }
+ }
+
+ if (row == start.row)
+ stop = true;
+
+ line = getLine(row);
+ }
+ }
+ };
+ };
+
+ this.$backwardLineIterator = function(session) {
+ var searchSelection = this.$options.scope == Search.SELECTION;
+
+ var range = session.getSelection().getRange();
+ var start = searchSelection ? range.end : range.start;
+
+ var firstRow = searchSelection ? range.start.row : 0;
+ var firstColumn = searchSelection ? range.start.column : 0;
+ var lastRow = searchSelection ? range.end.row : session.getLength() - 1;
+
+ var wrap = this.$options.wrap;
+
+ return {
+ forEach : function(callback) {
+ var row = start.row;
+
+ var line = session.getLine(row).substring(0, start.column);
+ var startIndex = 0;
+ var stop = false;
+ var inWrap = false;
+
+ while (!callback(line, startIndex, row)) {
+
+ if (stop)
+ return;
+
+ row--;
+ startIndex = 0;
+
+ if (row < firstRow) {
+ if (wrap) {
+ row = lastRow;
+ inWrap = true;
+ } else {
+ return;
+ }
+ }
+
+ if (row == start.row)
+ stop = true;
+
+ line = session.getLine(row);
+ if (searchSelection) {
+ if (row == firstRow)
+ startIndex = firstColumn;
+ else if (row == lastRow)
+ line = line.substring(0, range.end.column);
+ }
+
+ if (inWrap && row == start.row)
+ startIndex = start.column;
+ }
+ }
+ };
+ };
+
+}).call(Search.prototype);
+
+exports.Search = Search;
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Mihai Sucan <mihai DOT sucan AT gmail DOT com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/undomanager', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+var UndoManager = function() {
+ this.reset();
+};
+
+(function() {
+
+ this.execute = function(options) {
+ var deltas = options.args[0];
+ this.$doc = options.args[1];
+ this.$undoStack.push(deltas);
+ this.$redoStack = [];
+ };
+
+ this.undo = function(dontSelect) {
+ var deltas = this.$undoStack.pop();
+ var undoSelectionRange = null;
+ if (deltas) {
+ undoSelectionRange =
+ this.$doc.undoChanges(deltas, dontSelect);
+ this.$redoStack.push(deltas);
+ }
+ return undoSelectionRange;
+ };
+
+ this.redo = function(dontSelect) {
+ var deltas = this.$redoStack.pop();
+ var redoSelectionRange = null;
+ if (deltas) {
+ redoSelectionRange =
+ this.$doc.redoChanges(deltas, dontSelect);
+ this.$undoStack.push(deltas);
+ }
+ return redoSelectionRange;
+ };
+
+ this.reset = function() {
+ this.$undoStack = [];
+ this.$redoStack = [];
+ };
+
+ this.hasUndo = function() {
+ return this.$undoStack.length > 0;
+ };
+
+ this.hasRedo = function() {
+ return this.$redoStack.length > 0;
+ };
+
+}).call(UndoManager.prototype);
+
+exports.UndoManager = UndoManager;
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian@ajax.org>
+ * Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)
+ * Julian Viereck <julian.viereck@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/virtual_renderer', ['require', 'exports', 'module' , 'pilot/oop', 'pilot/dom', 'pilot/event', 'pilot/useragent', 'ace/layer/gutter', 'ace/layer/marker', 'ace/layer/text', 'ace/layer/cursor', 'ace/scrollbar', 'ace/renderloop', 'pilot/event_emitter', 'text/ace/css/editor.css'], function(require, exports, module) {
+
+var oop = require("pilot/oop");
+var dom = require("pilot/dom");
+var event = require("pilot/event");
+var useragent = require("pilot/useragent");
+var GutterLayer = require("ace/layer/gutter").Gutter;
+var MarkerLayer = require("ace/layer/marker").Marker;
+var TextLayer = require("ace/layer/text").Text;
+var CursorLayer = require("ace/layer/cursor").Cursor;
+var ScrollBar = require("ace/scrollbar").ScrollBar;
+var RenderLoop = require("ace/renderloop").RenderLoop;
+var EventEmitter = require("pilot/event_emitter").EventEmitter;
+var editorCss = require("text/ace/css/editor.css");
+
+// import CSS once
+dom.importCssString(editorCss);
+
+var VirtualRenderer = function(container, theme) {
+ this.container = container;
+ dom.addCssClass(this.container, "ace_editor");
+
+ this.setTheme(theme);
+
+ this.$gutter = dom.createElement("div");
+ this.$gutter.className = "ace_gutter";
+ this.container.appendChild(this.$gutter);
+
+ this.scroller = dom.createElement("div");
+ this.scroller.className = "ace_scroller";
+ this.container.appendChild(this.scroller);
+
+ this.content = dom.createElement("div");
+ this.content.className = "ace_content";
+ this.scroller.appendChild(this.content);
+
+ this.$gutterLayer = new GutterLayer(this.$gutter);
+ this.$markerBack = new MarkerLayer(this.content);
+
+ var textLayer = this.$textLayer = new TextLayer(this.content);
+ this.canvas = textLayer.element;
+
+ this.$markerFront = new MarkerLayer(this.content);
+
+ this.characterWidth = textLayer.getCharacterWidth();
+ this.lineHeight = textLayer.getLineHeight();
+
+ this.$cursorLayer = new CursorLayer(this.content);
+ this.$cursorPadding = 8;
+
+ // Indicates whether the horizontal scrollbar is visible
+ this.$horizScroll = true;
+ this.$horizScrollAlwaysVisible = true;
+
+ this.scrollBar = new ScrollBar(container);
+ this.scrollBar.addEventListener("scroll", this.onScroll.bind(this));
+
+ this.scrollTop = 0;
+
+ this.cursorPos = {
+ row : 0,
+ column : 0
+ };
+
+ var _self = this;
+ this.$textLayer.addEventListener("changeCharaterSize", function() {
+ _self.characterWidth = textLayer.getCharacterWidth();
+ _self.lineHeight = textLayer.getLineHeight();
+ _self.$updatePrintMargin();
+ _self.onResize(true);
+
+ _self.$loop.schedule(_self.CHANGE_FULL);
+ });
+ event.addListener(this.$gutter, "click", this.$onGutterClick.bind(this));
+ event.addListener(this.$gutter, "dblclick", this.$onGutterClick.bind(this));
+
+ this.$size = {
+ width: 0,
+ height: 0,
+ scrollerHeight: 0,
+ scrollerWidth: 0
+ };
+
+ this.$loop = new RenderLoop(this.$renderChanges.bind(this));
+ this.$loop.schedule(this.CHANGE_FULL);
+
+ this.setPadding(4);
+ this.$updatePrintMargin();
+};
+
+(function() {
+ this.showGutter = true;
+
+ this.CHANGE_CURSOR = 1;
+ this.CHANGE_MARKER = 2;
+ this.CHANGE_GUTTER = 4;
+ this.CHANGE_SCROLL = 8;
+ this.CHANGE_LINES = 16;
+ this.CHANGE_TEXT = 32;
+ this.CHANGE_SIZE = 64;
+ this.CHANGE_MARKER_BACK = 128;
+ this.CHANGE_MARKER_FRONT = 256;
+ this.CHANGE_FULL = 512;
+
+ oop.implement(this, EventEmitter);
+
+ this.setSession = function(session) {
+ this.session = session;
+ this.$cursorLayer.setSession(session);
+ this.$markerBack.setSession(session);
+ this.$markerFront.setSession(session);
+ this.$gutterLayer.setSession(session);
+ this.$textLayer.setSession(session);
+ this.$loop.schedule(this.CHANGE_FULL);
+ };
+
+ /**
+ * Triggers partial update of the text layer
+ */
+ this.updateLines = function(firstRow, lastRow) {
+ if (lastRow === undefined)
+ lastRow = Infinity;
+
+ if (!this.$changedLines) {
+ this.$changedLines = {
+ firstRow: firstRow,
+ lastRow: lastRow
+ };
+ }
+ else {
+ if (this.$changedLines.firstRow > firstRow)
+ this.$changedLines.firstRow = firstRow;
+
+ if (this.$changedLines.lastRow < lastRow)
+ this.$changedLines.lastRow = lastRow;
+ }
+
+ this.$loop.schedule(this.CHANGE_LINES);
+ };
+
+ /**
+ * Triggers full update of the text layer
+ */
+ this.updateText = function() {
+ this.$loop.schedule(this.CHANGE_TEXT);
+ };
+
+ /**
+ * Triggers a full update of all layers
+ */
+ this.updateFull = function() {
+ this.$loop.schedule(this.CHANGE_FULL);
+ };
+
+ this.updateFontSize = function() {
+ this.$textLayer.checkForSizeChanges();
+ };
+
+ /**
+ * Triggers resize of the editor
+ */
+ this.onResize = function(force) {
+ var changes = this.CHANGE_SIZE;
+
+ var height = dom.getInnerHeight(this.container);
+ if (force || this.$size.height != height) {
+ this.$size.height = height;
+
+ this.scroller.style.height = height + "px";
+ this.scrollBar.setHeight(this.scroller.clientHeight);
+
+ if (this.session) {
+ this.scrollToY(this.getScrollTop());
+ changes = changes | this.CHANGE_FULL;
+ }
+ }
+
+ var width = dom.getInnerWidth(this.container);
+ if (force || this.$size.width != width) {
+ this.$size.width = width;
+
+ var gutterWidth = this.showGutter ? this.$gutter.offsetWidth : 0;
+ this.scroller.style.left = gutterWidth + "px";
+ this.scroller.style.width = Math.max(0, width - gutterWidth - this.scrollBar.getWidth()) + "px";
+
+ if (this.session.getUseWrapMode()) {
+ var availableWidth = this.scroller.clientWidth - this.$padding * 2;
+ var limit = Math.floor(availableWidth / this.characterWidth) - 1;
+ if (this.session.adjustWrapLimit(limit) || force) {
+ changes = changes | this.CHANGE_FULL;
+ }
+ }
+ }
+
+ this.$size.scrollerWidth = this.scroller.clientWidth;
+ this.$size.scrollerHeight = this.scroller.clientHeight;
+ this.$loop.schedule(changes);
+ };
+
+ this.$onGutterClick = function(e) {
+ var pageX = event.getDocumentX(e);
+ var pageY = event.getDocumentY(e);
+
+ this._dispatchEvent("gutter" + e.type, {
+ row: this.screenToTextCoordinates(pageX, pageY).row,
+ htmlEvent: e
+ });
+ };
+
+ this.setShowInvisibles = function(showInvisibles) {
+ if (this.$textLayer.setShowInvisibles(showInvisibles))
+ this.$loop.schedule(this.CHANGE_TEXT);
+ };
+
+ this.getShowInvisibles = function() {
+ return this.$textLayer.showInvisibles;
+ };
+
+ this.$showPrintMargin = true;
+ this.setShowPrintMargin = function(showPrintMargin) {
+ this.$showPrintMargin = showPrintMargin;
+ this.$updatePrintMargin();
+ };
+
+ this.getShowPrintMargin = function() {
+ return this.$showPrintMargin;
+ };
+
+ this.$printMarginColumn = 80;
+ this.setPrintMarginColumn = function(showPrintMargin) {
+ this.$printMarginColumn = showPrintMargin;
+ this.$updatePrintMargin();
+ };
+
+ this.getPrintMarginColumn = function() {
+ return this.$printMarginColumn;
+ };
+
+ this.getShowGutter = function(){
+ return this.showGutter;
+ };
+
+ this.setShowGutter = function(show){
+ if(this.showGutter === show)
+ return;
+ this.$gutter.style.display = show ? "block" : "none";
+ this.showGutter = show;
+ this.onResize(true);
+ };
+
+ this.$updatePrintMargin = function() {
+ var containerEl;
+
+ if (!this.$showPrintMargin && !this.$printMarginEl)
+ return;
+
+ if (!this.$printMarginEl) {
+ containerEl = dom.createElement("div");
+ containerEl.className = "ace_print_margin_layer";
+ this.$printMarginEl = dom.createElement("div");
+ this.$printMarginEl.className = "ace_print_margin";
+ containerEl.appendChild(this.$printMarginEl);
+ this.content.insertBefore(containerEl, this.$textLayer.element);
+ }
+
+ var style = this.$printMarginEl.style;
+ style.left = ((this.characterWidth * this.$printMarginColumn) + this.$padding * 2) + "px";
+ style.visibility = this.$showPrintMargin ? "visible" : "hidden";
+ };
+
+ this.getContainerElement = function() {
+ return this.container;
+ };
+
+ this.getMouseEventTarget = function() {
+ return this.content;
+ };
+
+ this.getTextAreaContainer = function() {
+ return this.container;
+ };
+
+ this.moveTextAreaToCursor = function(textarea) {
+ // in IE the native cursor always shines through
+ if (useragent.isIE)
+ return;
+
+ var pos = this.$cursorLayer.getPixelPosition();
+ if (!pos)
+ return;
+
+ var bounds = this.content.getBoundingClientRect();
+ var offset = (this.layerConfig && this.layerConfig.offset) || 0;
+
+ textarea.style.left = (bounds.left + pos.left + this.$padding) + "px";
+ textarea.style.top = (bounds.top + pos.top - this.scrollTop + offset) + "px";
+ };
+
+ this.getFirstVisibleRow = function() {
+ return (this.layerConfig || {}).firstRow || 0;
+ };
+
+ this.getFirstFullyVisibleRow = function(){
+ if (!this.layerConfig)
+ return 0;
+
+ return this.layerConfig.firstRow + (this.layerConfig.offset === 0 ? 0 : 1);
+ };
+
+ this.getLastFullyVisibleRow = function() {
+ if (!this.layerConfig)
+ return 0;
+
+ var flint = Math.floor((this.layerConfig.height + this.layerConfig.offset) / this.layerConfig.lineHeight);
+ return this.layerConfig.firstRow - 1 + flint;
+ };
+
+ this.getLastVisibleRow = function() {
+ return (this.layerConfig || {}).lastRow || 0;
+ };
+
+ this.$padding = null;
+ this.setPadding = function(padding) {
+ this.$padding = padding;
+ this.content.style.padding = "0 " + padding + "px";
+ this.$loop.schedule(this.CHANGE_FULL);
+ this.$updatePrintMargin();
+ };
+
+ this.getHScrollBarAlwaysVisible = function() {
+ return this.$horizScrollAlwaysVisible;
+ };
+
+ this.setHScrollBarAlwaysVisible = function(alwaysVisible) {
+ if (this.$horizScrollAlwaysVisible != alwaysVisible) {
+ this.$horizScrollAlwaysVisible = alwaysVisible;
+ if (!this.$horizScrollAlwaysVisible || !this.$horizScroll)
+ this.$loop.schedule(this.CHANGE_SCROLL);
+ }
+ };
+
+ this.onScroll = function(e) {
+ this.scrollToY(e.data);
+ };
+
+ this.$updateScrollBar = function() {
+ this.scrollBar.setInnerHeight(this.layerConfig.maxHeight);
+ this.scrollBar.setScrollTop(this.scrollTop);
+ };
+
+ this.$renderChanges = function(changes) {
+ if (!changes || !this.session)
+ return;
+
+ // text, scrolling and resize changes can cause the view port size to change
+ if (!this.layerConfig ||
+ changes & this.CHANGE_FULL ||
+ changes & this.CHANGE_SIZE ||
+ changes & this.CHANGE_TEXT ||
+ changes & this.CHANGE_LINES ||
+ changes & this.CHANGE_SCROLL
+ )
+ this.$computeLayerConfig();
+
+ // full
+ if (changes & this.CHANGE_FULL) {
+ this.$textLayer.update(this.layerConfig);
+ if (this.showGutter)
+ this.$gutterLayer.update(this.layerConfig);
+ this.$markerBack.update(this.layerConfig);
+ this.$markerFront.update(this.layerConfig);
+ this.$cursorLayer.update(this.layerConfig);
+ this.$updateScrollBar();
+ return;
+ }
+
+ // scrolling
+ if (changes & this.CHANGE_SCROLL) {
+ if (changes & this.CHANGE_TEXT || changes & this.CHANGE_LINES)
+ this.$textLayer.update(this.layerConfig);
+ else
+ this.$textLayer.scrollLines(this.layerConfig);
+
+ if (this.showGutter)
+ this.$gutterLayer.update(this.layerConfig);
+ this.$markerBack.update(this.layerConfig);
+ this.$markerFront.update(this.layerConfig);
+ this.$cursorLayer.update(this.layerConfig);
+ this.$updateScrollBar();
+ return;
+ }
+
+ if (changes & this.CHANGE_TEXT) {
+ this.$textLayer.update(this.layerConfig);
+ if (this.showGutter)
+ this.$gutterLayer.update(this.layerConfig);
+ }
+ else if (changes & this.CHANGE_LINES) {
+ this.$updateLines();
+ this.$updateScrollBar();
+ if (this.showGutter)
+ this.$gutterLayer.update(this.layerConfig);
+ } else if (changes & this.CHANGE_GUTTER) {
+ if (this.showGutter)
+ this.$gutterLayer.update(this.layerConfig);
+ }
+
+ if (changes & this.CHANGE_CURSOR)
+ this.$cursorLayer.update(this.layerConfig);
+
+ if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_FRONT)) {
+ this.$markerFront.update(this.layerConfig);
+ }
+
+ if (changes & (this.CHANGE_MARKER | this.CHANGE_MARKER_BACK)) {
+ this.$markerBack.update(this.layerConfig);
+ }
+
+ if (changes & this.CHANGE_SIZE)
+ this.$updateScrollBar();
+ };
+
+ this.$computeLayerConfig = function() {
+ var session = this.session;
+
+ var offset = this.scrollTop % this.lineHeight;
+ var minHeight = this.$size.scrollerHeight + this.lineHeight;
+
+ var longestLine = this.$getLongestLine();
+ var widthChanged = !this.layerConfig ? true : (this.layerConfig.width != longestLine);
+
+ var horizScroll = this.$horizScrollAlwaysVisible || this.$size.scrollerWidth - longestLine < 0;
+ var horizScrollChanged = this.$horizScroll !== horizScroll;
+ this.$horizScroll = horizScroll;
+ if (horizScrollChanged)
+ this.scroller.style.overflowX = horizScroll ? "scroll" : "hidden";
+
+ var maxHeight = this.session.getScreenLength() * this.lineHeight;
+ this.scrollTop = Math.max(0, Math.min(this.scrollTop, maxHeight - this.$size.scrollerHeight));
+
+ var lineCount = Math.ceil(minHeight / this.lineHeight) - 1;
+ var firstRow = Math.max(0, Math.round((this.scrollTop - offset) / this.lineHeight));
+ var lastRow = firstRow + lineCount;
+
+ // Map lines on the screen to lines in the document.
+ var firstRowScreen, firstRowHeight;
+ var lineHeight = { lineHeight: this.lineHeight };
+ firstRow = session.screenToDocumentRow(firstRow, 0);
+
+ // Check if firstRow is inside of a foldLine. If true, then use the first
+ // row of the foldLine.
+ var foldLine = session.getFoldLine(firstRow);
+ if (foldLine) {
+ firstRow = foldLine.start.row;
+ }
+
+ firstRowScreen = session.documentToScreenRow(firstRow, 0);
+ firstRowHeight = session.getRowHeight(lineHeight, firstRow);
+
+ lastRow = Math.min(session.screenToDocumentRow(lastRow, 0), session.getLength() - 1);
+ minHeight = this.$size.scrollerHeight + session.getRowHeight(lineHeight, lastRow)+
+ firstRowHeight;
+
+ offset = this.scrollTop - firstRowScreen * this.lineHeight;
+
+ this.layerConfig = {
+ width : longestLine,
+ padding : this.$padding,
+ firstRow : firstRow,
+ firstRowScreen: firstRowScreen,
+ lastRow : lastRow,
+ lineHeight : this.lineHeight,
+ characterWidth : this.characterWidth,
+ minHeight : minHeight,
+ maxHeight : maxHeight,
+ offset : offset,
+ height : this.$size.scrollerHeight
+ };
+
+ // For debugging.
+ // console.log(JSON.stringify(this.layerConfig));
+
+ this.$gutterLayer.element.style.marginTop = (-offset) + "px";
+ this.content.style.marginTop = (-offset) + "px";
+ this.content.style.width = longestLine + "px";
+ this.content.style.height = minHeight + "px";
+
+ // scroller.scrollWidth was smaller than scrollLeft we needed
+ if (this.$desiredScrollLeft) {
+ this.scrollToX(this.$desiredScrollLeft);
+ this.$desiredScrollLeft = 0;
+ }
+
+ // Horizontal scrollbar visibility may have changed, which changes
+ // the client height of the scroller
+ if (horizScrollChanged)
+ this.onResize(true);
+ };
+
+ this.$updateLines = function() {
+ var firstRow = this.$changedLines.firstRow;
+ var lastRow = this.$changedLines.lastRow;
+ this.$changedLines = null;
+
+ var layerConfig = this.layerConfig;
+
+ // if the update changes the width of the document do a full redraw
+ if (layerConfig.width != this.$getLongestLine())
+ return this.$textLayer.update(layerConfig);
+
+ if (firstRow > layerConfig.lastRow + 1) { return; }
+ if (lastRow < layerConfig.firstRow) { return; }
+
+ // if the last row is unknown -> redraw everything
+ if (lastRow === Infinity) {
+ if (this.showGutter)
+ this.$gutterLayer.update(layerConfig);
+ this.$textLayer.update(layerConfig);
+ return;
+ }
+
+ // else update only the changed rows
+ this.$textLayer.updateLines(layerConfig, firstRow, lastRow);
+ };
+
+ this.$getLongestLine = function() {
+ var charCount = this.session.getScreenWidth() + 1;
+ if (this.$textLayer.showInvisibles)
+ charCount += 1;
+
+ return Math.max(this.$size.scrollerWidth - this.$padding * 2, Math.round(charCount * this.characterWidth));
+ };
+
+ this.updateFrontMarkers = function() {
+ this.$markerFront.setMarkers(this.session.getMarkers(true));
+ this.$loop.schedule(this.CHANGE_MARKER_FRONT);
+ };
+
+ this.updateBackMarkers = function() {
+ this.$markerBack.setMarkers(this.session.getMarkers());
+ this.$loop.schedule(this.CHANGE_MARKER_BACK);
+ };
+
+ this.addGutterDecoration = function(row, className){
+ this.$gutterLayer.addGutterDecoration(row, className);
+ this.$loop.schedule(this.CHANGE_GUTTER);
+ };
+
+ this.removeGutterDecoration = function(row, className){
+ this.$gutterLayer.removeGutterDecoration(row, className);
+ this.$loop.schedule(this.CHANGE_GUTTER);
+ };
+
+ this.setBreakpoints = function(rows) {
+ this.$gutterLayer.setBreakpoints(rows);
+ this.$loop.schedule(this.CHANGE_GUTTER);
+ };
+
+ this.setAnnotations = function(annotations) {
+ this.$gutterLayer.setAnnotations(annotations);
+ this.$loop.schedule(this.CHANGE_GUTTER);
+ };
+
+ this.updateCursor = function() {
+ this.$loop.schedule(this.CHANGE_CURSOR);
+ };
+
+ this.hideCursor = function() {
+ this.$cursorLayer.hideCursor();
+ };
+
+ this.showCursor = function() {
+ this.$cursorLayer.showCursor();
+ };
+
+ this.scrollCursorIntoView = function() {
+ // the editor is not visible
+ if (this.$size.scrollerHeight === 0)
+ return;
+
+ var pos = this.$cursorLayer.getPixelPosition();
+
+ var left = pos.left + this.$padding;
+ var top = pos.top;
+
+ if (this.scrollTop > top) {
+ this.scrollToY(top);
+ }
+
+ if (this.scrollTop + this.$size.scrollerHeight < top + this.lineHeight) {
+ this.scrollToY(top + this.lineHeight - this.$size.scrollerHeight);
+ }
+
+ var scrollLeft = this.scroller.scrollLeft;
+
+ if (scrollLeft > left) {
+ this.scrollToX(left);
+ }
+
+ if (scrollLeft + this.$size.scrollerWidth < left + this.characterWidth) {
+ if (left > this.layerConfig.width)
+ this.$desiredScrollLeft = left + 2 * this.characterWidth;
+ else
+ this.scrollToX(Math.round(left + this.characterWidth - this.$size.scrollerWidth));
+ }
+ };
+
+ this.getScrollTop = function() {
+ return this.scrollTop;
+ };
+
+ this.getScrollLeft = function() {
+ return this.scroller.scrollLeft;
+ };
+
+ this.getScrollTopRow = function() {
+ return this.scrollTop / this.lineHeight;
+ };
+
+ this.getScrollBottomRow = function() {
+ return Math.max(0, Math.floor((this.scrollTop + this.$size.scrollerHeight) / this.lineHeight) - 1);
+ };
+
+ this.scrollToRow = function(row) {
+ this.scrollToY(row * this.lineHeight);
+ };
+
+ this.scrollToLine = function(line, center) {
+ var lineHeight = { lineHeight: this.lineHeight };
+ var offset = 0;
+ for (var l = 1; l < line; l++) {
+ offset += this.session.getRowHeight(lineHeight, l-1);
+ }
+
+ if (center) {
+ offset -= this.$size.scrollerHeight / 2;
+ }
+ this.scrollToY(offset);
+ };
+
+ this.scrollToY = function(scrollTop) {
+ // after calling scrollBar.setScrollTop
+ // scrollbar sends us event with same scrollTop. ignore it
+ scrollTop = Math.max(0, scrollTop);
+ if (this.scrollTop !== scrollTop) {
+ this.$loop.schedule(this.CHANGE_SCROLL);
+ this.scrollTop = scrollTop;
+ }
+ };
+
+ this.scrollToX = function(scrollLeft) {
+ if (scrollLeft <= this.$padding)
+ scrollLeft = 0;
+
+ this.scroller.scrollLeft = scrollLeft;
+ };
+
+ this.scrollBy = function(deltaX, deltaY) {
+ deltaY && this.scrollToY(this.scrollTop + deltaY);
+ deltaX && this.scrollToX(this.scroller.scrollLeft + deltaX);
+ };
+
+ this.screenToTextCoordinates = function(pageX, pageY) {
+ var canvasPos = this.scroller.getBoundingClientRect();
+
+ var col = Math.round((pageX + this.scroller.scrollLeft - canvasPos.left - this.$padding - dom.getPageScrollLeft())
+ / this.characterWidth);
+ var row = Math.floor((pageY + this.scrollTop - canvasPos.top - dom.getPageScrollTop())
+ / this.lineHeight);
+
+ return this.session.screenToDocumentPosition(row, Math.max(col, 0));
+ };
+
+ this.textToScreenCoordinates = function(row, column) {
+ var canvasPos = this.scroller.getBoundingClientRect();
+ var pos = this.session.documentToScreenPosition(row, column);
+
+ var x = this.$padding + Math.round(pos.column * this.characterWidth);
+ var y = pos.row * this.lineHeight;
+
+ return {
+ pageX: canvasPos.left + x - this.getScrollLeft(),
+ pageY: canvasPos.top + y - this.getScrollTop()
+ };
+ };
+
+ this.visualizeFocus = function() {
+ dom.addCssClass(this.container, "ace_focus");
+ };
+
+ this.visualizeBlur = function() {
+ dom.removeCssClass(this.container, "ace_focus");
+ };
+
+ this.showComposition = function(position) {
+ if (!this.$composition) {
+ this.$composition = dom.createElement("div");
+ this.$composition.className = "ace_composition";
+ this.content.appendChild(this.$composition);
+ }
+
+ this.$composition.innerHTML = "&#160;";
+
+ var pos = this.$cursorLayer.getPixelPosition();
+ var style = this.$composition.style;
+ style.top = pos.top + "px";
+ style.left = (pos.left + this.$padding) + "px";
+ style.height = this.lineHeight + "px";
+
+ this.hideCursor();
+ };
+
+ this.setCompositionText = function(text) {
+ dom.setInnerText(this.$composition, text);
+ };
+
+ this.hideComposition = function() {
+ this.showCursor();
+
+ if (!this.$composition)
+ return;
+
+ var style = this.$composition.style;
+ style.top = "-10000px";
+ style.left = "-10000px";
+ };
+
+ this.setTheme = function(theme) {
+ var _self = this;
+
+ this.$themeValue = theme;
+ if (!theme || typeof theme == "string") {
+ theme = theme || "ace/theme/textmate";
+ require([theme], function(theme) {
+ afterLoad(theme);
+ });
+ } else {
+ afterLoad(theme);
+ }
+
+ function afterLoad(theme) {
+ if (_self.$theme)
+ dom.removeCssClass(_self.container, _self.$theme);
+
+ _self.$theme = theme ? theme.cssClass : null;
+
+ if (_self.$theme)
+ dom.addCssClass(_self.container, _self.$theme);
+
+ // force re-measure of the gutter width
+ if (_self.$size) {
+ _self.$size.width = 0;
+ _self.onResize();
+ }
+ }
+ };
+
+ this.getTheme = function() {
+ return this.$themeValue;
+ };
+
+ // Methods allows to add / remove CSS classnames to the editor element.
+ // This feature can be used by plug-ins to provide a visual indication of
+ // a certain mode that editor is in.
+
+ this.setStyle = function setStyle(style) {
+ dom.addCssClass(this.container, style);
+ };
+
+ this.unsetStyle = function unsetStyle(style) {
+ dom.removeCssClass(this.container, style);
+ };
+
+ this.destroy = function() {
+ this.$textLayer.destroy();
+ this.$cursorLayer.destroy();
+ };
+
+}).call(VirtualRenderer.prototype);
+
+exports.VirtualRenderer = VirtualRenderer;
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Julian Viereck <julian DOT viereck AT gmail DOT com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/layer/gutter', ['require', 'exports', 'module' , 'pilot/dom'], function(require, exports, module) {
+
+var dom = require("pilot/dom");
+
+var Gutter = function(parentEl) {
+ this.element = dom.createElement("div");
+ this.element.className = "ace_layer ace_gutter-layer";
+ parentEl.appendChild(this.element);
+
+ this.$breakpoints = [];
+ this.$annotations = [];
+ this.$decorations = [];
+};
+
+(function() {
+
+ this.setSession = function(session) {
+ this.session = session;
+ };
+
+ this.addGutterDecoration = function(row, className){
+ if (!this.$decorations[row])
+ this.$decorations[row] = "";
+ this.$decorations[row] += " ace_" + className;
+ }
+
+ this.removeGutterDecoration = function(row, className){
+ this.$decorations[row] = this.$decorations[row].replace(" ace_" + className, "");
+ };
+
+ this.setBreakpoints = function(rows) {
+ this.$breakpoints = rows.concat();
+ };
+
+ this.setAnnotations = function(annotations) {
+ // iterate over sparse array
+ this.$annotations = [];
+ for (var row in annotations) if (annotations.hasOwnProperty(row)) {
+ var rowAnnotations = annotations[row];
+ if (!rowAnnotations)
+ continue;
+
+ var rowInfo = this.$annotations[row] = {
+ text: []
+ };
+ for (var i=0; i<rowAnnotations.length; i++) {
+ var annotation = rowAnnotations[i];
+ rowInfo.text.push(annotation.text.replace(/"/g, "&quot;").replace(/'/g, "&#8217;").replace(/</, "&lt;"));
+ var type = annotation.type;
+ if (type == "error")
+ rowInfo.className = "ace_error";
+ else if (type == "warning" && rowInfo.className != "ace_error")
+ rowInfo.className = "ace_warning";
+ else if (type == "info" && (!rowInfo.className))
+ rowInfo.className = "ace_info";
+ }
+ }
+ };
+
+ this.update = function(config) {
+ this.$config = config;
+
+ var emptyAnno = {className: "", text: []};
+ var html = [];
+ var i = config.firstRow;
+ var lastRow = config.lastRow;
+ var fold = this.session.getNextFold(i);
+ var foldStart = fold ? fold.start.row : Infinity;
+
+ while (true) {
+ if(i > foldStart) {
+ i = fold.end.row + 1;
+ fold = this.session.getNextFold(i);
+ foldStart = fold ?fold.start.row :Infinity;
+ }
+ if(i > lastRow)
+ break;
+
+ var annotation = this.$annotations[i] || emptyAnno;
+ html.push("<div class='ace_gutter-cell",
+ this.$decorations[i] || "",
+ this.$breakpoints[i] ? " ace_breakpoint " : " ",
+ annotation.className,
+ "' title='", annotation.text.join("\n"),
+ "' style='height:", config.lineHeight, "px;'>", (i+1));
+
+ var wrappedRowLength = this.session.getRowLength(i) - 1;
+ while (wrappedRowLength--) {
+ html.push("</div><div class='ace_gutter-cell' style='height:", config.lineHeight, "px'>&brvbar;</div>");
+ }
+
+ html.push("</div>");
+
+ i++;
+ }
+ this.element = dom.setInnerHtml(this.element, html.join(""));
+ this.element.style.height = config.minHeight + "px";
+ };
+
+}).call(Gutter.prototype);
+
+exports.Gutter = Gutter;
+
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Julian Viereck <julian.viereck@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/layer/marker', ['require', 'exports', 'module' , 'ace/range', 'pilot/dom'], function(require, exports, module) {
+
+var Range = require("ace/range").Range;
+var dom = require("pilot/dom");
+
+var Marker = function(parentEl) {
+ this.element = dom.createElement("div");
+ this.element.className = "ace_layer ace_marker-layer";
+ parentEl.appendChild(this.element);
+};
+
+(function() {
+
+ this.setSession = function(session) {
+ this.session = session;
+ };
+
+ this.setMarkers = function(markers) {
+ this.markers = markers;
+ };
+
+ this.update = function(config) {
+ var config = config || this.config;
+ if (!config)
+ return;
+
+ this.config = config;
+
+ var html = [];
+ for ( var key in this.markers) {
+ var marker = this.markers[key];
+
+ var range = marker.range.clipRows(config.firstRow, config.lastRow);
+ if (range.isEmpty()) continue;
+
+ range = range.toScreenRange(this.session);
+
+ if (marker.renderer) {
+ var top = this.$getTop(range.start.row, config);
+ var left = Math.round(range.start.column * config.characterWidth);
+ marker.renderer(html, range, left, top, config);
+ }
+ else if (range.isMultiLine()) {
+ if (marker.type == "text") {
+ this.drawTextMarker(html, range, marker.clazz, config);
+ } else {
+ this.drawMultiLineMarker(html, range, marker.clazz, config);
+ }
+ }
+ else {
+ this.drawSingleLineMarker(html, range, marker.clazz, config);
+ }
+ }
+ this.element = dom.setInnerHtml(this.element, html.join(""));
+ };
+
+ this.$getTop = function(row, layerConfig) {
+ return (row - layerConfig.firstRowScreen) * layerConfig.lineHeight;
+ };
+
+ this.drawTextMarker = function(stringBuilder, range, clazz, layerConfig) {
+ // selection start
+ var row = range.start.row;
+
+ var lineRange = new Range(row, range.start.column,
+ row, this.session.getScreenLastRowColumn(row));
+ this.drawSingleLineMarker(stringBuilder, lineRange, clazz, layerConfig, 1);
+
+ // selection end
+ var row = range.end.row;
+ var lineRange = new Range(row, 0, row, range.end.column);
+ this.drawSingleLineMarker(stringBuilder, lineRange, clazz, layerConfig);
+
+ for (var row = range.start.row + 1; row < range.end.row; row++) {
+ lineRange.start.row = row;
+ lineRange.end.row = row;
+ lineRange.end.column = this.session.getScreenLastRowColumn(row);
+ this.drawSingleLineMarker(stringBuilder, lineRange, clazz, layerConfig, 1);
+ }
+ };
+
+ this.drawMultiLineMarker = function(stringBuilder, range, clazz, layerConfig) {
+ // from selection start to the end of the line
+ var height = layerConfig.lineHeight;
+ var width = Math.round(layerConfig.width - (range.start.column * layerConfig.characterWidth));
+ var top = this.$getTop(range.start.row, layerConfig);
+ var left = Math.round(range.start.column * layerConfig.characterWidth);
+
+ stringBuilder.push(
+ "<div class='", clazz, "' style='",
+ "height:", height, "px;",
+ "width:", width, "px;",
+ "top:", top, "px;",
+ "left:", left, "px;'></div>"
+ );
+
+ // from start of the last line to the selection end
+ var top = this.$getTop(range.end.row, layerConfig);
+ var width = Math.round(range.end.column * layerConfig.characterWidth);
+
+ stringBuilder.push(
+ "<div class='", clazz, "' style='",
+ "height:", height, "px;",
+ "top:", top, "px;",
+ "width:", width, "px;'></div>"
+ );
+
+ // all the complete lines
+ var height = (range.end.row - range.start.row - 1) * layerConfig.lineHeight;
+ if (height < 0)
+ return;
+ var top = this.$getTop(range.start.row + 1, layerConfig);
+
+ stringBuilder.push(
+ "<div class='", clazz, "' style='",
+ "height:", height, "px;",
+ "width:", layerConfig.width, "px;",
+ "top:", top, "px;'></div>"
+ );
+ };
+
+ this.drawSingleLineMarker = function(stringBuilder, range, clazz, layerConfig, extraLength) {
+ var height = layerConfig.lineHeight;
+ var width = Math.round((range.end.column + (extraLength || 0) - range.start.column) * layerConfig.characterWidth);
+ var top = this.$getTop(range.start.row, layerConfig);
+ var left = Math.round(range.start.column * layerConfig.characterWidth);
+
+ stringBuilder.push(
+ "<div class='", clazz, "' style='",
+ "height:", height, "px;",
+ "width:", width, "px;",
+ "top:", top, "px;",
+ "left:", left,"px;'></div>"
+ );
+ };
+
+}).call(Marker.prototype);
+
+exports.Marker = Marker;
+
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Julian Viereck <julian DOT viereck AT gmail DOT com>
+ * Mihai Sucan <mihai.sucan@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/layer/text', ['require', 'exports', 'module' , 'pilot/oop', 'pilot/dom', 'pilot/lang', 'pilot/useragent', 'pilot/event_emitter'], function(require, exports, module) {
+
+var oop = require("pilot/oop");
+var dom = require("pilot/dom");
+var lang = require("pilot/lang");
+var useragent = require("pilot/useragent");
+var EventEmitter = require("pilot/event_emitter").EventEmitter;
+
+var Text = function(parentEl) {
+ this.element = dom.createElement("div");
+ this.element.className = "ace_layer ace_text-layer";
+ parentEl.appendChild(this.element);
+
+ this.$characterSize = this.$measureSizes() || {width: 0, height: 0};
+ this.$pollSizeChanges();
+};
+
+(function() {
+
+ oop.implement(this, EventEmitter);
+
+ this.EOF_CHAR = "&para;";
+ this.EOL_CHAR = "&not;";
+ this.TAB_CHAR = "&rarr;";
+ this.SPACE_CHAR = "&middot;";
+
+ this.getLineHeight = function() {
+ return this.$characterSize.height || 1;
+ };
+
+ this.getCharacterWidth = function() {
+ return this.$characterSize.width || 1;
+ };
+
+ this.checkForSizeChanges = function() {
+ var size = this.$measureSizes();
+ if (size && (this.$characterSize.width !== size.width || this.$characterSize.height !== size.height)) {
+ this.$characterSize = size;
+ this._dispatchEvent("changeCharaterSize", {data: size});
+ }
+ };
+
+ this.$pollSizeChanges = function() {
+ var self = this;
+ this.$pollSizeChangesTimer = setInterval(function() {
+ self.checkForSizeChanges();
+ }, 500);
+ };
+
+ this.$fontStyles = {
+ fontFamily : 1,
+ fontSize : 1,
+ fontWeight : 1,
+ fontStyle : 1,
+ lineHeight : 1
+ };
+
+ this.$measureSizes = function() {
+ var n = 1000;
+ if (!this.$measureNode) {
+ var measureNode = this.$measureNode = dom.createElement("div");
+ var style = measureNode.style;
+
+ style.width = style.height = "auto";
+ style.left = style.top = (-n * 40) + "px";
+
+ style.visibility = "hidden";
+ style.position = "absolute";
+ style.overflow = "visible";
+ style.whiteSpace = "nowrap";
+
+ // in FF 3.6 monospace fonts can have a fixed sub pixel width.
+ // that's why we have to measure many characters
+ // Note: characterWidth can be a float!
+ measureNode.innerHTML = lang.stringRepeat("Xy", n);
+
+ if (document.body) {
+ document.body.appendChild(measureNode);
+ } else {
+ var container = this.element.parentNode;
+ while (!dom.hasCssClass(container, "ace_editor"))
+ container = container.parentNode;
+ container.appendChild(measureNode);
+ }
+
+ }
+
+ var style = this.$measureNode.style;
+ var computedStyle = dom.computedStyle(this.element);
+ for (var prop in this.$fontStyles)
+ style[prop] = computedStyle[prop];
+
+ var size = {
+ height: this.$measureNode.offsetHeight,
+ width: this.$measureNode.offsetWidth / (n * 2)
+ };
+
+ // Size and width can be null if the editor is not visible or
+ // detached from the document
+ if (size.width == 0 && size.height == 0)
+ return null;
+
+ return size;
+ };
+
+ this.setSession = function(session) {
+ this.session = session;
+ };
+
+ this.showInvisibles = false;
+ this.setShowInvisibles = function(showInvisibles) {
+ if (this.showInvisibles == showInvisibles)
+ return false;
+
+ this.showInvisibles = showInvisibles;
+ return true;
+ };
+
+ this.$tabStrings = [];
+ this.$computeTabString = function() {
+ var tabSize = this.session.getTabSize();
+ var tabStr = this.$tabStrings = [0];
+ for (var i = 1; i < tabSize + 1; i++) {
+ if (this.showInvisibles) {
+ tabStr.push("<span class='ace_invisible'>"
+ + this.TAB_CHAR
+ + new Array(i).join("&#160;")
+ + "</span>");
+ } else {
+ tabStr.push(new Array(i+1).join("&#160;"));
+ }
+ }
+
+ };
+
+ this.updateLines = function(config, firstRow, lastRow) {
+ this.$computeTabString();
+ // Due to wrap line changes there can be new lines if e.g.
+ // the line to updated wrapped in the meantime.
+ if (this.config.lastRow != config.lastRow ||
+ this.config.firstRow != config.firstRow) {
+ this.scrollLines(config);
+ }
+ this.config = config;
+
+ var first = Math.max(firstRow, config.firstRow);
+ var last = Math.min(lastRow, config.lastRow);
+
+ var lineElements = this.element.childNodes,
+ lineElementsIdx = 0;
+
+ for (var row = config.firstRow; row < first; row++) {
+ var foldLine = this.session.getFoldLine(row);
+ if (foldLine) {
+ if (foldLine.containsRow(first)) {
+ break;
+ } else {
+ row = foldLine.end.row;
+ }
+ }
+ lineElementsIdx ++;
+ }
+
+ for (var i=first; i<=last; i++) {
+ var lineElement = lineElements[lineElementsIdx++];
+ if (!lineElement)
+ continue;
+
+ var html = [];
+ var tokens = this.session.getTokens(i, i);
+ this.$renderLine(html, i, tokens[0].tokens);
+ lineElement = dom.setInnerHtml(lineElement, html.join(""));
+
+ i = this.session.getRowFoldEnd(i);
+ }
+ };
+
+ this.scrollLines = function(config) {
+ this.$computeTabString();
+ var oldConfig = this.config;
+ this.config = config;
+
+ if (!oldConfig || oldConfig.lastRow < config.firstRow)
+ return this.update(config);
+
+ if (config.lastRow < oldConfig.firstRow)
+ return this.update(config);
+
+ var el = this.element;
+ if (oldConfig.firstRow < config.firstRow)
+ for (var row=this.session.getFoldedRowCount(oldConfig.firstRow, config.firstRow - 1); row>0; row--)
+ el.removeChild(el.firstChild);
+
+ if (oldConfig.lastRow > config.lastRow)
+ for (var row=this.session.getFoldedRowCount(config.lastRow + 1, oldConfig.lastRow); row>0; row--)
+ el.removeChild(el.lastChild);
+
+ if (config.firstRow < oldConfig.firstRow) {
+ var fragment = this.$renderLinesFragment(config, config.firstRow, oldConfig.firstRow - 1);
+ if (el.firstChild)
+ el.insertBefore(fragment, el.firstChild);
+ else
+ el.appendChild(fragment);
+ }
+
+ if (config.lastRow > oldConfig.lastRow) {
+ var fragment = this.$renderLinesFragment(config, oldConfig.lastRow + 1, config.lastRow);
+ el.appendChild(fragment);
+ }
+ };
+
+ this.$renderLinesFragment = function(config, firstRow, lastRow) {
+ var fragment = document.createDocumentFragment(),
+ row = firstRow,
+ fold = this.session.getNextFold(row),
+ foldStart = fold ?fold.start.row :Infinity;
+
+ while (true) {
+ if(row > foldStart) {
+ row = fold.end.row+1;
+ fold = this.session.getNextFold(row);
+ foldStart = fold ?fold.start.row :Infinity;
+ }
+ if(row > lastRow)
+ break;
+
+ var lineEl = dom.createElement("div");
+
+ lineEl.className = "ace_line";
+
+ var html = [];
+ // Get the tokens per line as there might be some lines in between
+ // beeing folded.
+ // OPTIMIZE: If there is a long block of unfolded lines, just make
+ // this call once for that big block of unfolded lines.
+ var tokens = this.session.getTokens(row, row);
+ if (tokens.length == 1)
+ this.$renderLine(html, row, tokens[0].tokens);
+
+ // don't use setInnerHtml since we are working with an empty DIV
+ lineEl.innerHTML = html.join("");
+ fragment.appendChild(lineEl);
+
+ row++;
+ }
+ return fragment;
+ };
+
+ this.update = function(config) {
+ this.$computeTabString();
+ this.config = config;
+
+ var html = [];
+ var firstRow = config.firstRow, lastRow = config.lastRow;
+
+ var row = firstRow,
+ fold = this.session.getNextFold(row),
+ foldStart = fold ?fold.start.row :Infinity;
+
+ while (true) {
+ if(row > foldStart) {
+ row = fold.end.row+1;
+ fold = this.session.getNextFold(row);
+ foldStart = fold ?fold.start.row :Infinity;
+ }
+ if(row > lastRow)
+ break;
+
+ html.push("<div class='ace_line'>");
+ // Get the tokens per line as there might be some lines in between
+ // beeing folded.
+ // OPTIMIZE: If there is a long block of unfolded lines, just make
+ // this call once for that big block of unfolded lines.
+ var tokens = this.session.getTokens(row, row);
+ if (tokens.length == 1)
+ this.$renderLine(html, row, tokens[0].tokens);
+ html.push("</div>");
+
+ row++;
+ }
+ this.element = dom.setInnerHtml(this.element, html.join(""));
+ };
+
+ this.$textToken = {
+ "text": true,
+ "rparen": true,
+ "lparen": true
+ };
+
+ this.$renderToken = function(stringBuilder, screenColumn, token, value) {
+ var self = this;
+ var replaceReg = /\t|&|<|( +)|([\v\f \u00a0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000])|[\u1100-\u115F]|[\u11A3-\u11A7]|[\u11FA-\u11FF]|[\u2329-\u232A]|[\u2E80-\u2E99]|[\u2E9B-\u2EF3]|[\u2F00-\u2FD5]|[\u2FF0-\u2FFB]|[\u3000-\u303E]|[\u3041-\u3096]|[\u3099-\u30FF]|[\u3105-\u312D]|[\u3131-\u318E]|[\u3190-\u31BA]|[\u31C0-\u31E3]|[\u31F0-\u321E]|[\u3220-\u3247]|[\u3250-\u32FE]|[\u3300-\u4DBF]|[\u4E00-\uA48C]|[\uA490-\uA4C6]|[\uA960-\uA97C]|[\uAC00-\uD7A3]|[\uD7B0-\uD7C6]|[\uD7CB-\uD7FB]|[\uF900-\uFAFF]|[\uFE10-\uFE19]|[\uFE30-\uFE52]|[\uFE54-\uFE66]|[\uFE68-\uFE6B]|[\uFF01-\uFF60]|[\uFFE0-\uFFE6]/g;
+ var replaceFunc = function(c, a, b, tabIdx, idx4) {
+ if (c.charCodeAt(0) == 32) {
+ return new Array(c.length+1).join("&#160;");
+ } else if (c == "\t") {
+ var tabSize = self.session.getScreenTabSize(screenColumn + tabIdx);
+ screenColumn += tabSize - 1;
+ return self.$tabStrings[tabSize];
+ } else if (c == "&") {
+ if (useragent.isOldGecko)
+ return "&";
+ else
+ return "&amp;";
+ } else if (c == "<") {
+ return "&lt;";
+ } else if (c.match(/[\v\f \u00a0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000]/)) {
+ if (self.showInvisibles) {
+ var space = new Array(c.length+1).join(self.SPACE_CHAR);
+ return "<span class='ace_invisible'>" + space + "</span>";
+ } else {
+ return "&#160;";
+ }
+ } else {
+ screenColumn += 1;
+ return "<span class='ace_cjk' style='width:" +
+ (self.config.characterWidth * 2) +
+ "px'>" + c + "</span>";
+ }
+ };
+
+ var output = value.replace(replaceReg, replaceFunc);
+
+ if (!this.$textToken[token.type]) {
+ var classes = "ace_" + token.type.replace(/\./g, " ace_");
+ stringBuilder.push("<span class='", classes, "'>", output, "</span>");
+ }
+ else {
+ stringBuilder.push(output);
+ }
+ return value.length;
+ };
+
+ this.$renderLineCore = function(stringBuilder, lastRow, tokens, splits) {
+ var chars = 0,
+ split = 0,
+ splitChars,
+ characterWidth = this.config.characterWidth,
+ screenColumn = 0,
+ self = this;
+
+ function addToken(token, value) {
+ screenColumn += self.$renderToken(
+ stringBuilder, screenColumn, token, value);
+ }
+
+ if (!splits || splits.length == 0) {
+ splitChars = Number.MAX_VALUE;
+ } else {
+ splitChars = splits[0];
+ }
+
+ stringBuilder.push("<div style='height:",
+ this.config.lineHeight, "px",
+ "'>");
+ for (var i = 0; i < tokens.length; i++) {
+ var token = tokens[i];
+ var value = token.value;
+
+ if (chars + value.length < splitChars) {
+ addToken(token, value);
+ chars += value.length;
+ } else {
+ while (chars + value.length >= splitChars) {
+ addToken(token, value.substring(0, splitChars - chars));
+ value = value.substring(splitChars - chars);
+ chars = splitChars;
+ stringBuilder.push("</div>",
+ "<div style='height:",
+ this.config.lineHeight, "px",
+ "'>");
+
+ split ++;
+ screenColumn = 0;
+ splitChars = splits[split] || Number.MAX_VALUE;
+ }
+ if (value.length != 0) {
+ chars += value.length;
+ addToken(token, value);
+ }
+ }
+ }
+
+ if (this.showInvisibles) {
+ if (lastRow !== this.session.getLength() - 1) {
+ stringBuilder.push("<span class='ace_invisible'>" + this.EOL_CHAR + "</span>");
+ } else {
+ stringBuilder.push("<span class='ace_invisible'>" + this.EOF_CHAR + "</span>");
+ }
+ }
+ stringBuilder.push("</div>");
+ };
+
+ this.$renderLine = function(stringBuilder, row, tokens) {
+ // Check if the line to render is folded or not. If not, things are
+ // simple, otherwise, we need to fake some things...
+ if (!this.session.isRowFolded(row)) {
+ var splits = this.session.getRowSplitData(row);
+ this.$renderLineCore(stringBuilder, row, tokens, splits);
+ } else {
+ this.$renderFoldLine(stringBuilder, row, tokens);
+ }
+ };
+
+ this.$renderFoldLine = function(stringBuilder, row, tokens) {
+ var session = this.session,
+ foldLine = session.getFoldLine(row),
+ renderTokens = [];
+
+ function addTokens(tokens, from, to) {
+ var idx = 0, col = 0;
+ while ((col + tokens[idx].value.length) < from) {
+ col += tokens[idx].value.length;
+ idx++;
+
+ if (idx == tokens.length) {
+ return;
+ }
+ }
+ if (col != from) {
+ var value = tokens[idx].value.substring(from - col);
+ // Check if the token value is longer then the from...to spacing.
+ if (value.length > (to - from)) {
+ value = value.substring(0, to - from);
+ }
+
+ renderTokens.push({
+ type: tokens[idx].type,
+ value: value
+ });
+
+ col = from + value.length;
+ idx += 1;
+ }
+
+ while (col < to) {
+ var value = tokens[idx].value;
+ if (value.length + col > to) {
+ value = value.substring(0, to - col);
+ }
+ renderTokens.push({
+ type: tokens[idx].type,
+ value: value
+ });
+ col += value.length;
+ idx += 1;
+ }
+ }
+
+ foldLine.walk(function(placeholder, row, column, lastColumn, isNewRow) {
+ if (placeholder) {
+ renderTokens.push({
+ type: "fold",
+ value: placeholder
+ });
+ } else {
+ if (isNewRow) {
+ tokens = this.session.getTokens(row, row)[0].tokens;
+ }
+ if (tokens.length != 0) {
+ addTokens(tokens, lastColumn, column);
+ }
+ }
+ }.bind(this), foldLine.end.row, this.session.getLine(foldLine.end.row).length);
+
+ // TODO: Build a fake splits array!
+ var splits = this.session.$useWrapMode?this.session.$wrapData[row]:null;
+ this.$renderLineCore(stringBuilder, row, renderTokens, splits);
+ };
+
+ this.destroy = function() {
+ clearInterval(this.$pollSizeChangesTimer);
+ };
+
+}).call(Text.prototype);
+
+exports.Text = Text;
+
+});
+/* vim:ts=4:sts=4:sw=4:
+ * ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ * Julian Viereck <julian.viereck@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/layer/cursor', ['require', 'exports', 'module' , 'pilot/dom'], function(require, exports, module) {
+
+var dom = require("pilot/dom");
+
+var Cursor = function(parentEl) {
+ this.element = dom.createElement("div");
+ this.element.className = "ace_layer ace_cursor-layer";
+ parentEl.appendChild(this.element);
+
+ this.cursor = dom.createElement("div");
+ this.cursor.className = "ace_cursor ace_hidden";
+ this.element.appendChild(this.cursor);
+
+ this.isVisible = false;
+};
+
+(function() {
+
+ this.setSession = function(session) {
+ this.session = session;
+ };
+
+ this.hideCursor = function() {
+ this.isVisible = false;
+ dom.addCssClass(this.cursor, "ace_hidden");
+ clearInterval(this.blinkId);
+ };
+
+ this.showCursor = function() {
+ this.isVisible = true;
+ dom.removeCssClass(this.cursor, "ace_hidden");
+ this.cursor.style.visibility = "visible";
+ this.restartTimer();
+ };
+
+ this.restartTimer = function() {
+ clearInterval(this.blinkId);
+ if (!this.isVisible) {
+ return;
+ }
+
+ var cursor = this.cursor;
+ this.blinkId = setInterval(function() {
+ cursor.style.visibility = "hidden";
+ setTimeout(function() {
+ cursor.style.visibility = "visible";
+ }, 400);
+ }, 1000);
+ };
+
+ this.getPixelPosition = function(onScreen) {
+ if (!this.config || !this.session) {
+ return {
+ left : 0,
+ top : 0
+ };
+ }
+
+ var position = this.session.selection.getCursor();
+ var pos = this.session.documentToScreenPosition(position);
+ var cursorLeft = Math.round(pos.column * this.config.characterWidth);
+ var cursorTop = (pos.row - (onScreen ? this.config.firstRowScreen : 0)) *
+ this.config.lineHeight;
+
+ return {
+ left : cursorLeft,
+ top : cursorTop
+ };
+ };
+
+ this.update = function(config) {
+ this.config = config;
+
+ this.pixelPos = this.getPixelPosition(true);
+
+ this.cursor.style.left = this.pixelPos.left + "px";
+ this.cursor.style.top = this.pixelPos.top + "px";
+ this.cursor.style.width = config.characterWidth + "px";
+ this.cursor.style.height = config.lineHeight + "px";
+
+ var overwrite = this.session.getOverwrite()
+ if (overwrite != this.overwrite) {
+ this.overwrite = overwrite;
+ if (overwrite)
+ dom.addCssClass(this.cursor, "ace_overwrite");
+ else
+ dom.removeCssClass(this.cursor, "ace_overwrite");
+ }
+
+ this.restartTimer();
+ };
+
+ this.destroy = function() {
+ clearInterval(this.blinkId);
+ }
+
+}).call(Cursor.prototype);
+
+exports.Cursor = Cursor;
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/scrollbar', ['require', 'exports', 'module' , 'pilot/oop', 'pilot/dom', 'pilot/event', 'pilot/event_emitter'], function(require, exports, module) {
+
+var oop = require("pilot/oop");
+var dom = require("pilot/dom");
+var event = require("pilot/event");
+var EventEmitter = require("pilot/event_emitter").EventEmitter;
+
+var ScrollBar = function(parent) {
+ this.element = dom.createElement("div");
+ this.element.className = "ace_sb";
+
+ this.inner = dom.createElement("div");
+ this.element.appendChild(this.inner);
+
+ parent.appendChild(this.element);
+
+ this.width = dom.scrollbarWidth();
+ this.element.style.width = this.width + "px";
+
+ event.addListener(this.element, "scroll", this.onScroll.bind(this));
+};
+
+(function() {
+ oop.implement(this, EventEmitter);
+
+ this.onScroll = function() {
+ this._dispatchEvent("scroll", {data: this.element.scrollTop});
+ };
+
+ this.getWidth = function() {
+ return this.width;
+ };
+
+ this.setHeight = function(height) {
+ this.element.style.height = height + "px";
+ };
+
+ this.setInnerHeight = function(height) {
+ this.inner.style.height = height + "px";
+ };
+
+ this.setScrollTop = function(scrollTop) {
+ this.element.scrollTop = scrollTop;
+ };
+
+}).call(ScrollBar.prototype);
+
+exports.ScrollBar = ScrollBar;
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/renderloop', ['require', 'exports', 'module' , 'pilot/event'], function(require, exports, module) {
+
+var event = require("pilot/event");
+
+var RenderLoop = function(onRender) {
+ this.onRender = onRender;
+ this.pending = false;
+ this.changes = 0;
+};
+
+(function() {
+
+ this.schedule = function(change) {
+ //this.onRender(change);
+ //return;
+ this.changes = this.changes | change;
+ if (!this.pending) {
+ this.pending = true;
+ var _self = this;
+ this.setTimeoutZero(function() {
+ _self.pending = false;
+ var changes = _self.changes;
+ _self.changes = 0;
+ _self.onRender(changes);
+ })
+ }
+ };
+
+ this.setTimeoutZero = window.requestAnimationFrame ||
+ window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.oRequestAnimationFrame ||
+ window.msRequestAnimationFrame;
+
+ if (this.setTimeoutZero) {
+
+ this.setTimeoutZero = this.setTimeoutZero.bind(window)
+ } else if (window.postMessage) {
+
+ this.messageName = "zero-timeout-message";
+
+ this.setTimeoutZero = function(callback) {
+ if (!this.attached) {
+ var _self = this;
+ event.addListener(window, "message", function(e) {
+ if (_self.callback && e.data == _self.messageName) {
+ event.stopPropagation(e);
+ _self.callback();
+ }
+ });
+ this.attached = true;
+ }
+ this.callback = callback;
+ window.postMessage(this.messageName, "*");
+ }
+
+ } else {
+
+ this.setTimeoutZero = function(callback) {
+ setTimeout(callback, 0);
+ }
+ }
+
+}).call(RenderLoop.prototype);
+
+exports.RenderLoop = RenderLoop;
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Ajax.org Code Editor (ACE).
+ *
+ * The Initial Developer of the Original Code is
+ * Ajax.org B.V.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Fabian Jakobs <fabian AT ajax DOT org>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('ace/theme/textmate', ['require', 'exports', 'module' , 'pilot/dom'], function(require, exports, module) {
+
+ var dom = require("pilot/dom");
+
+ var cssText = ".ace-tm .ace_editor {\
+ border: 2px solid rgb(159, 159, 159);\
+}\
+\
+.ace-tm .ace_editor.ace_focus {\
+ border: 2px solid #327fbd;\
+}\
+\
+.ace-tm .ace_gutter {\
+ width: 50px;\
+ background: #e8e8e8;\
+ color: #333;\
+ overflow : hidden;\
+}\
+\
+.ace-tm .ace_gutter-layer {\
+ width: 100%;\
+ text-align: right;\
+}\
+\
+.ace-tm .ace_gutter-layer .ace_gutter-cell {\
+ padding-right: 6px;\
+}\
+\
+.ace-tm .ace_print_margin {\
+ width: 1px;\
+ background: #e8e8e8;\
+}\
+\
+.ace-tm .ace_text-layer {\
+ cursor: text;\
+}\
+\
+.ace-tm .ace_cursor {\
+ border-left: 2px solid black;\
+}\
+\
+.ace-tm .ace_cursor.ace_overwrite {\
+ border-left: 0px;\
+ border-bottom: 1px solid black;\
+}\
+ \
+.ace-tm .ace_line .ace_invisible {\
+ color: rgb(191, 191, 191);\
+}\
+\
+.ace-tm .ace_line .ace_keyword {\
+ color: blue;\
+}\
+\
+.ace-tm .ace_line .ace_constant.ace_buildin {\
+ color: rgb(88, 72, 246);\
+}\
+\
+.ace-tm .ace_line .ace_constant.ace_language {\
+ color: rgb(88, 92, 246);\
+}\
+\
+.ace-tm .ace_line .ace_constant.ace_library {\
+ color: rgb(6, 150, 14);\
+}\
+\
+.ace-tm .ace_line .ace_invalid {\
+ background-color: rgb(153, 0, 0);\
+ color: white;\
+}\
+\
+.ace-tm .ace_line .ace_fold {\
+ background-color: #E4E4E4;\
+ border-radius: 3px;\
+}\
+\
+.ace-tm .ace_line .ace_support.ace_function {\
+ color: rgb(60, 76, 114);\
+}\
+\
+.ace-tm .ace_line .ace_support.ace_constant {\
+ color: rgb(6, 150, 14);\
+}\
+\
+.ace-tm .ace_line .ace_support.ace_type,\
+.ace-tm .ace_line .ace_support.ace_class {\
+ color: rgb(109, 121, 222);\
+}\
+\
+.ace-tm .ace_line .ace_keyword.ace_operator {\
+ color: rgb(104, 118, 135);\
+}\
+\
+.ace-tm .ace_line .ace_string {\
+ color: rgb(3, 106, 7);\
+}\
+\
+.ace-tm .ace_line .ace_comment {\
+ color: rgb(76, 136, 107);\
+}\
+\
+.ace-tm .ace_line .ace_comment.ace_doc {\
+ color: rgb(0, 102, 255);\
+}\
+\
+.ace-tm .ace_line .ace_comment.ace_doc.ace_tag {\
+ color: rgb(128, 159, 191);\
+}\
+\
+.ace-tm .ace_line .ace_constant.ace_numeric {\
+ color: rgb(0, 0, 205);\
+}\
+\
+.ace-tm .ace_line .ace_variable {\
+ color: rgb(49, 132, 149);\
+}\
+\
+.ace-tm .ace_line .ace_xml_pe {\
+ color: rgb(104, 104, 91);\
+}\
+\
+.ace-tm .ace_marker-layer .ace_selection {\
+ background: rgb(181, 213, 255);\
+}\
+\
+.ace-tm .ace_marker-layer .ace_step {\
+ background: rgb(252, 255, 0);\
+}\
+\
+.ace-tm .ace_marker-layer .ace_stack {\
+ background: rgb(164, 229, 101);\
+}\
+\
+.ace-tm .ace_marker-layer .ace_bracket {\
+ margin: -1px 0 0 -1px;\
+ border: 1px solid rgb(192, 192, 192);\
+}\
+\
+.ace-tm .ace_marker-layer .ace_active_line {\
+ background: rgb(232, 242, 254);\
+}\
+\
+.ace-tm .ace_marker-layer .ace_selected_word {\
+ background: rgb(250, 250, 255);\
+ border: 1px solid rgb(200, 200, 250);\
+}\
+\
+.ace-tm .ace_string.ace_regex {\
+ color: rgb(255, 0, 0)\
+}";
+
+ // import CSS once
+ dom.importCssString(cssText);
+
+ exports.cssClass = "ace-tm";
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is DomTemplate.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com) (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/environment', ['require', 'exports', 'module' , 'pilot/settings'], function(require, exports, module) {
+
+
+var settings = require("pilot/settings").settings;
+
+/**
+ * Create an environment object
+ */
+function create() {
+ return {
+ settings: settings
+ };
+};
+
+exports.create = create;
+
+
+});
+define("text/ace/css/editor.css", [], ".ace_editor {" +
+ " position: absolute;" +
+ " overflow: hidden;" +
+ "" +
+ " font-family: \"Menlo\", \"Monaco\", \"Courier New\", monospace;" +
+ " font-size: 12px;" +
+ "}" +
+ "" +
+ ".ace_scroller {" +
+ " position: absolute;" +
+ " overflow-x: scroll;" +
+ " overflow-y: hidden;" +
+ "}" +
+ "" +
+ ".ace_content {" +
+ " position: absolute;" +
+ " box-sizing: border-box;" +
+ " -moz-box-sizing: border-box;" +
+ " -webkit-box-sizing: border-box;" +
+ "}" +
+ "" +
+ ".ace_composition {" +
+ " position: absolute;" +
+ " background: #555;" +
+ " color: #DDD;" +
+ " z-index: 4;" +
+ "}" +
+ "" +
+ ".ace_gutter {" +
+ " position: absolute;" +
+ " overflow-x: hidden;" +
+ " overflow-y: hidden;" +
+ " height: 100%;" +
+ "}" +
+ "" +
+ ".ace_gutter-cell.ace_error {" +
+ " background-image: url(\"data:image/gif,GIF89a%10%00%10%00%D5%00%00%F5or%F5%87%88%F5nr%F4ns%EBmq%F5z%7F%DDJT%DEKS%DFOW%F1Yc%F2ah%CE(7%CE)8%D18E%DD%40M%F2KZ%EBU%60%F4%60m%DCir%C8%16(%C8%19*%CE%255%F1%3FR%F1%3FS%E6%AB%B5%CA%5DI%CEn%5E%F7%A2%9A%C9G%3E%E0a%5B%F7%89%85%F5yy%F6%82%80%ED%82%80%FF%BF%BF%E3%C4%C4%FF%FF%FF%FF%FF%FF%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00!%F9%04%01%00%00%25%00%2C%00%00%00%00%10%00%10%00%00%06p%C0%92pH%2C%1A%8F%C8%D2H%93%E1d4%23%E4%88%D3%09mB%1DN%B48%F5%90%40%60%92G%5B%94%20%3E%22%D2%87%24%FA%20%24%C5%06A%00%20%B1%07%02B%A38%89X.v%17%82%11%13q%10%0Fi%24%0F%8B%10%7BD%12%0Ei%09%92%09%0EpD%18%15%24%0A%9Ci%05%0C%18F%18%0B%07%04%01%04%06%A0H%18%12%0D%14%0D%12%A1I%B3%B4%B5IA%00%3B\");" +
+ " background-repeat: no-repeat;" +
+ " background-position: 4px center;" +
+ "}" +
+ "" +
+ ".ace_gutter-cell.ace_warning {" +
+ " background-image: url(\"data:image/gif,GIF89a%10%00%10%00%D5%00%00%FF%DBr%FF%DE%81%FF%E2%8D%FF%E2%8F%FF%E4%96%FF%E3%97%FF%E5%9D%FF%E6%9E%FF%EE%C1%FF%C8Z%FF%CDk%FF%D0s%FF%D4%81%FF%D5%82%FF%D5%83%FF%DC%97%FF%DE%9D%FF%E7%B8%FF%CCl%7BQ%13%80U%15%82W%16%81U%16%89%5B%18%87%5B%18%8C%5E%1A%94d%1D%C5%83-%C9%87%2F%C6%84.%C6%85.%CD%8B2%C9%871%CB%8A3%CD%8B5%DC%98%3F%DF%9BB%E0%9CC%E1%A5U%CB%871%CF%8B5%D1%8D6%DB%97%40%DF%9AB%DD%99B%E3%B0p%E7%CC%AE%FF%FF%FF%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00!%F9%04%01%00%00%2F%00%2C%00%00%00%00%10%00%10%00%00%06a%C0%97pH%2C%1A%8FH%A1%ABTr%25%87%2B%04%82%F4%7C%B9X%91%08%CB%99%1C!%26%13%84*iJ9(%15G%CA%84%14%01%1A%97%0C%03%80%3A%9A%3E%81%84%3E%11%08%B1%8B%20%02%12%0F%18%1A%0F%0A%03'F%1C%04%0B%10%16%18%10%0B%05%1CF%1D-%06%07%9A%9A-%1EG%1B%A0%A1%A0U%A4%A5%A6BA%00%3B\");" +
+ " background-repeat: no-repeat;" +
+ " background-position: 4px center;" +
+ "}" +
+ "" +
+ ".ace_editor .ace_sb {" +
+ " position: absolute;" +
+ " overflow-x: hidden;" +
+ " overflow-y: scroll;" +
+ " right: 0;" +
+ "}" +
+ "" +
+ ".ace_editor .ace_sb div {" +
+ " position: absolute;" +
+ " width: 1px;" +
+ " left: 0;" +
+ "}" +
+ "" +
+ ".ace_editor .ace_print_margin_layer {" +
+ " z-index: 0;" +
+ " position: absolute;" +
+ " overflow: hidden;" +
+ " margin: 0;" +
+ " left: 0;" +
+ " height: 100%;" +
+ " width: 100%;" +
+ "}" +
+ "" +
+ ".ace_editor .ace_print_margin {" +
+ " position: absolute;" +
+ " height: 100%;" +
+ "}" +
+ "" +
+ ".ace_editor textarea {" +
+ " position: fixed;" +
+ " z-index: -1;" +
+ " width: 10px;" +
+ " height: 30px;" +
+ " opacity: 0;" +
+ " background: transparent;" +
+ " appearance: none;" +
+ " border: none;" +
+ " resize: none;" +
+ " outline: none;" +
+ " overflow: hidden;" +
+ "}" +
+ "" +
+ ".ace_layer {" +
+ " z-index: 1;" +
+ " position: absolute;" +
+ " overflow: hidden;" +
+ " white-space: nowrap;" +
+ " height: 100%;" +
+ " width: 100%;" +
+ "}" +
+ "" +
+ ".ace_text-layer {" +
+ " font-family: Monaco, \"Courier New\", monospace;" +
+ " color: black;" +
+ "}" +
+ "" +
+ ".ace_cjk {" +
+ " display: inline-block;" +
+ " text-align: center;" +
+ "}" +
+ "" +
+ ".ace_cursor-layer {" +
+ " z-index: 4;" +
+ " cursor: text;" +
+ " pointer-events: none;" +
+ "}" +
+ "" +
+ ".ace_cursor {" +
+ " z-index: 4;" +
+ " position: absolute;" +
+ "}" +
+ "" +
+ ".ace_cursor.ace_hidden {" +
+ " opacity: 0.2;" +
+ "}" +
+ "" +
+ ".ace_line {" +
+ " white-space: nowrap;" +
+ "}" +
+ "" +
+ ".ace_marker-layer {" +
+ " cursor: text;" +
+ " pointer-events: none;" +
+ "}" +
+ "" +
+ ".ace_marker-layer .ace_step {" +
+ " position: absolute;" +
+ " z-index: 3;" +
+ "}" +
+ "" +
+ ".ace_marker-layer .ace_selection {" +
+ " position: absolute;" +
+ " z-index: 4;" +
+ "}" +
+ "" +
+ ".ace_marker-layer .ace_bracket {" +
+ " position: absolute;" +
+ " z-index: 5;" +
+ "}" +
+ "" +
+ ".ace_marker-layer .ace_active_line {" +
+ " position: absolute;" +
+ " z-index: 2;" +
+ "}" +
+ "" +
+ ".ace_marker-layer .ace_selected_word {" +
+ " position: absolute;" +
+ " z-index: 6;" +
+ " box-sizing: border-box;" +
+ " -moz-box-sizing: border-box;" +
+ " -webkit-box-sizing: border-box;" +
+ "}" +
+ "" +
+ ".ace_line .ace_fold {" +
+ " cursor: pointer;" +
+ "}" +
+ "" +
+ ".ace_dragging .ace_marker-layer, .ace_dragging .ace_text-layer {" +
+ " cursor: move;" +
+ "}" +
+ "");
+
+define("text/styles.css", [], "html {" +
+ " height: 100%;" +
+ " width: 100%;" +
+ " overflow: hidden;" +
+ "}" +
+ "" +
+ "body {" +
+ " overflow: hidden;" +
+ " margin: 0;" +
+ " padding: 0;" +
+ " height: 100%;" +
+ " width: 100%;" +
+ " font-family: Arial, Helvetica, sans-serif, Tahoma, Verdana, sans-serif;" +
+ " font-size: 12px;" +
+ " background: rgb(14, 98, 165);" +
+ " color: white;" +
+ "}" +
+ "" +
+ "#logo {" +
+ " padding: 15px;" +
+ " margin-left: 65px;" +
+ "}" +
+ "" +
+ "#editor {" +
+ " position: absolute;" +
+ " top: 0px;" +
+ " left: 280px;" +
+ " bottom: 0px;" +
+ " right: 0px;" +
+ " background: white;" +
+ "}" +
+ "" +
+ "#controls {" +
+ " padding: 5px;" +
+ "}" +
+ "" +
+ "#controls td {" +
+ " text-align: right;" +
+ "}" +
+ "" +
+ "#controls td + td {" +
+ " text-align: left;" +
+ "}" +
+ "" +
+ "#cockpitInput {" +
+ " position: absolute;" +
+ " left: 280px;" +
+ " right: 0px;" +
+ " bottom: 0;" +
+ "" +
+ " border: none; outline: none;" +
+ " font-family: consolas, courier, monospace;" +
+ " font-size: 120%;" +
+ "}" +
+ "" +
+ "#cockpitOutput {" +
+ " padding: 10px;" +
+ " margin: 0 15px;" +
+ " border: 1px solid #AAA;" +
+ " -moz-border-radius-topleft: 10px;" +
+ " -moz-border-radius-topright: 10px;" +
+ " border-top-left-radius: 4px; border-top-right-radius: 4px;" +
+ " background: #DDD; color: #000;" +
+ "}");
+
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Kevin Dangoor (kdangoor@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+require(["ace/ace"], function(ace) {
+ window.ace = ace;
+}); \ No newline at end of file
diff --git a/www/js/ace/ace.js b/www/js/ace/ace.js
new file mode 100644
index 0000000..7ac56c0
--- /dev/null
+++ b/www/js/ace/ace.js
@@ -0,0 +1 @@
+(function(){var a=function(){return this}();if(a.require&&a.define)require.packaged=!0;else{var b=function(a,c,d){typeof a!="string"?b.original?b.original.apply(window,arguments):(console.error("dropping module because define wasn't a string."),console.trace()):(arguments.length==2&&(d=c),define.modules||(define.modules={}),define.modules[a]=d)};a.define&&(b.original=a.define),a.define=b;var c=function(a,b){if(Object.prototype.toString.call(a)==="[object Array]"){var e=[];for(var f=0,g=a.length;f<g;++f){var h=d(a[f]);if(!h&&c.original)return c.original.apply(window,arguments);e.push(h)}b&&b.apply(null,e)}else{if(typeof a=="string"){var i=d(a);if(!i&&c.original)return c.original.apply(window,arguments);b&&b();return i}if(c.original)return c.original.apply(window,arguments)}};a.require&&(c.original=a.require),a.require=c,require.packaged=!0;var d=function(a){var b=define.modules[a];if(b==null){console.error("Missing module: "+a);return null}if(typeof b=="function"){var c={};b(require,c,{id:a,uri:""}),define.modules[a]=c;return c}return b}}})(),define("pilot/fixoldbrowsers",["require","exports","module"],function(a,b,c){if(!Function.prototype.bind){var d=Array.prototype.slice;Function.prototype.bind=function(b){var c=this;if(typeof c.apply!="function"||typeof c.call!="function")return new TypeError;var e=d.call(arguments),f=function a(){if(this instanceof a){var b=Object.create(c.prototype);c.apply(b,e.concat(d.call(arguments)));return b}return c.call.apply(c,e.concat(d.call(arguments)))};f.length=typeof c=="function"?Math.max(c.length-e.length,0):0;return f}}var e=Function.prototype.call,f=Array.prototype,g=Object.prototype,h=e.bind(g.hasOwnProperty),i,j,k,l,m;if(m=h(g,"__defineGetter__"))i=e.bind(g.__defineGetter__),j=e.bind(g.__defineSetter__),k=e.bind(g.__lookupGetter__),l=e.bind(g.__lookupSetter__);Array.isArray||(Array.isArray=function(b){return Object.prototype.toString.call(b)==="[object Array]"}),Array.prototype.forEach||(Array.prototype.forEach=function(b,c){var d=+this.length;for(var e=0;e<d;e++)e in this&&b.call(c,this[e],e,this)}),Array.prototype.map||(Array.prototype.map=function(b){var c=+this.length;if(typeof b!="function")throw new TypeError;var d=Array(c),e=arguments[1];for(var f=0;f<c;f++)f in this&&(d[f]=b.call(e,this[f],f,this));return d}),Array.prototype.filter||(Array.prototype.filter=function(b){var c=[],d=arguments[1];for(var e=0;e<this.length;e++)b.call(d,this[e])&&c.push(this[e]);return c}),Array.prototype.every||(Array.prototype.every=function(b){var c=arguments[1];for(var d=0;d<this.length;d++)if(!b.call(c,this[d]))return!1;return!0}),Array.prototype.some||(Array.prototype.some=function(b){var c=arguments[1];for(var d=0;d<this.length;d++)if(b.call(c,this[d]))return!0;return!1}),Array.prototype.reduce||(Array.prototype.reduce=function(b){var c=+this.length;if(typeof b!="function")throw new TypeError;if(c===0&&arguments.length===1)throw new TypeError;var d=0;if(arguments.length>=2)var e=arguments[1];else do{if(d in this){e=this[d++];break}if(++d>=c)throw new TypeError}while(!0);for(;d<c;d++)d in this&&(e=b.call(null,e,this[d],d,this));return e}),Array.prototype.reduceRight||(Array.prototype.reduceRight=function(b){var c=+this.length;if(typeof b!="function")throw new TypeError;if(c===0&&arguments.length===1)throw new TypeError;var d=c-1;if(arguments.length>=2)var e=arguments[1];else do{if(d in this){e=this[d--];break}if(--d<0)throw new TypeError}while(!0);for(;d>=0;d--)d in this&&(e=b.call(null,e,this[d],d,this));return e}),Array.prototype.indexOf||(Array.prototype.indexOf=function(b){var c=this.length;if(!c)return-1;var d=arguments[1]||0;if(d>=c)return-1;d<0&&(d+=c);for(;d<c;d++){if(!h(this,d))continue;if(b===this[d])return d}return-1}),Array.prototype.lastIndexOf||(Array.prototype.lastIndexOf=function(b){var c=this.length;if(!c)return-1;var d=arguments[1]||c;d<0&&(d+=c),d=Math.min(d,c-1);for(;d>=0;d--){if(!h(this,d))continue;if(b===this[d])return d}return-1}),Object.getPrototypeOf||(Object.getPrototypeOf=function(b){return b.__proto__||b.constructor.prototype});if(!Object.getOwnPropertyDescriptor){var n="Object.getOwnPropertyDescriptor called on a non-object: ";Object.getOwnPropertyDescriptor=function(b,c){if(typeof b!="object"&&typeof b!="function"||b===null)throw new TypeError(n+b);if(!h(b,c))return undefined;var d,e,f;d={enumerable:!0,configurable:!0};if(m){var i=b.__proto__;b.__proto__=g;var e=k(b,c),f=l(b,c);b.__proto__=i;if(e||f){e&&(descriptor.get=e),f&&(descriptor.set=f);return descriptor}}descriptor.value=b[c];return descriptor}}Object.getOwnPropertyNames||(Object.getOwnPropertyNames=function(b){return Object.keys(b)}),Object.create||(Object.create=function(b,c){var d;if(b===null)d={"__proto__":null};else{if(typeof b!="object")throw new TypeError("typeof prototype["+typeof b+"] != 'object'");var e=function(){};e.prototype=b,d=new e,d.__proto__=b}typeof c!="undefined"&&Object.defineProperties(d,c);return d});if(!Object.defineProperty){var o="Property description must be an object: ",p="Object.defineProperty called on non-object: ",q="getters & setters can not be defined on this javascript engine";Object.defineProperty=function(b,c,d){if(typeof b!="object"&&typeof b!="function")throw new TypeError(p+b);if(typeof b!="object"||b===null)throw new TypeError(o+d);if(h(d,"value"))if(m&&(k(b,c)||l(b,c))){var e=b.__proto__;b.__proto__=g,delete b[c],b[c]=d.value,b.prototype}else b[c]=d.value;else{if(!m)throw new TypeError(q);h(d,"get")&&i(b,c,d.get),h(d,"set")&&j(b,c,d.set)}return b}}Object.defineProperties||(Object.defineProperties=function(b,c){for(var d in c)h(c,d)&&Object.defineProperty(b,d,c[d]);return b}),Object.seal||(Object.seal=function(b){return b}),Object.freeze||(Object.freeze=function(b){return b});try{Object.freeze(function(){})}catch(r){Object.freeze=function(b){return function(c){return typeof c=="function"?c:b(c)}}(Object.freeze)}Object.preventExtensions||(Object.preventExtensions=function(b){return b}),Object.isSealed||(Object.isSealed=function(b){return!1}),Object.isFrozen||(Object.isFrozen=function(b){return!1}),Object.isExtensible||(Object.isExtensible=function(b){return!0});if(!Object.keys){var s=!0,t=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],u=t.length;for(var v in{toString:null})s=!1;Object.keys=function a(b){if(typeof b!="object"&&typeof b!="function"||b===null)throw new TypeError("Object.keys called on a non-object");var a=[];for(var c in b)h(b,c)&&a.push(c);if(s)for(var d=0,e=u;d<e;d++){var f=t[d];h(b,f)&&a.push(f)}return a}}Date.prototype.toISOString||(Date.prototype.toISOString=function(){return this.getUTCFullYear()+"-"+(this.getUTCMonth()+1)+"-"+this.getUTCDate()+"T"+this.getUTCHours()+":"+this.getUTCMinutes()+":"+this.getUTCSeconds()+"Z"}),Date.now||(Date.now=function(){return(new Date).getTime()}),Date.prototype.toJSON||(Date.prototype.toJSON=function(b){if(typeof this.toISOString!="function")throw new TypeError;return this.toISOString()}),isNaN(Date.parse("T00:00"))&&(Date=function(a){var b=function(c,d,e,f,g,h,i){var j=arguments.length;if(this instanceof a){var k=j===1&&String(c)===c?new a(b.parse(c)):j>=7?new a(c,d,e,f,g,h,i):j>=6?new a(c,d,e,f,g,h):j>=5?new a(c,d,e,f,g):j>=4?new a(c,d,e,f):j>=3?new a(c,d,e):j>=2?new a(c,d):j>=1?new a(c):new a;k.constructor=b;return k}return a.apply(this,arguments)},c=new RegExp("^(?:((?:[+-]\\d\\d)?\\d\\d\\d\\d)(?:-(\\d\\d)(?:-(\\d\\d))?)?)?(?:T(\\d\\d):(\\d\\d)(?::(\\d\\d)(?:\\.(\\d\\d\\d))?)?)?(?:Z|([+-])(\\d\\d):(\\d\\d))?$");for(var d in a)b[d]=a[d];b.now=a.now,b.UTC=a.UTC,b.prototype=a.prototype,b.prototype.constructor=b,b.parse=function(d){var e=c.exec(d);if(e){e.shift();var f=e[0]===undefined;for(var g=0;g<10;g++){if(g===7)continue;e[g]=+(e[g]||(g<3?1:0)),g===1&&e[g]--}if(f)return((e[3]*60+e[4])*60+e[5])*1e3+e[6];var h=(e[8]*60+e[9])*60*1e3;e[6]==="-"&&(h=-h);return a.UTC.apply(this,e.slice(0,7))+h}return a.parse.apply(this,arguments)};return b}(Date));if(!String.prototype.trim){var w=/^\s\s*/,x=/\s\s*$/;String.prototype.trim=function(){return String(this).replace(w,"").replace(x,"")}}}),define("ace/ace",["require","exports","module","pilot/index","pilot/fixoldbrowsers","pilot/plugin_manager","pilot/dom","pilot/event","ace/editor","ace/edit_session","ace/undomanager","ace/virtual_renderer","ace/theme/textmate","pilot/environment"],function(a,b,c){a("pilot/index"),a("pilot/fixoldbrowsers");var d=a("pilot/plugin_manager").catalog;d.registerPlugins(["pilot/index"]);var e=a("pilot/dom"),f=a("pilot/event"),g=a("ace/editor").Editor,h=a("ace/edit_session").EditSession,i=a("ace/undomanager").UndoManager,j=a("ace/virtual_renderer").VirtualRenderer;b.edit=function(b){typeof b=="string"&&(b=document.getElementById(b));var c=new h(e.getInnerText(b));c.setUndoManager(new i),b.innerHTML="";var k=new g(new j(b,a("ace/theme/textmate")));k.setSession(c);var l=a("pilot/environment").create();d.startupPlugins({env:l}).then(function(){l.document=c,l.editor=k,k.resize(),f.addListener(window,"resize",function(){k.resize()}),b.env=l}),k.env=l;return k}}),define("pilot/index",["require","exports","module","pilot/fixoldbrowsers","pilot/types/basic","pilot/types/command","pilot/types/settings","pilot/commands/settings","pilot/commands/basic","pilot/settings/canon","pilot/canon"],function(a,b,c){b.startup=function(b,c){a("pilot/fixoldbrowsers"),a("pilot/types/basic").startup(b,c),a("pilot/types/command").startup(b,c),a("pilot/types/settings").startup(b,c),a("pilot/commands/settings").startup(b,c),a("pilot/commands/basic").startup(b,c),a("pilot/settings/canon").startup(b,c),a("pilot/canon").startup(b,c)},b.shutdown=function(b,c){a("pilot/types/basic").shutdown(b,c),a("pilot/types/command").shutdown(b,c),a("pilot/types/settings").shutdown(b,c),a("pilot/commands/settings").shutdown(b,c),a("pilot/commands/basic").shutdown(b,c),a("pilot/settings/canon").shutdown(b,c),a("pilot/canon").shutdown(b,c)}}),define("pilot/types/basic",["require","exports","module","pilot/types"],function(a,b,c){function m(a){if(a instanceof e)this.subtype=a;else{if(typeof a!="string")throw new Error("Can' handle array subtype");this.subtype=d.getType(a);if(this.subtype==null)throw new Error("Unknown array subtype: "+a)}}function l(a){if(typeof a.defer!="function")throw new Error("Instances of DeferredType need typeSpec.defer to be a function that returns a type");Object.keys(a).forEach(function(b){this[b]=a[b]},this)}function j(a){if(!Array.isArray(a.data)&&typeof a.data!="function")throw new Error("instances of SelectionType need typeSpec.data to be an array or function that returns an array:"+JSON.stringify(a));Object.keys(a).forEach(function(b){this[b]=a[b]},this)}var d=a("pilot/types"),e=d.Type,f=d.Conversion,g=d.Status,h=new e;h.stringify=function(a){return a},h.parse=function(a){if(typeof a!="string")throw new Error("non-string passed to text.parse()");return new f(a)},h.name="text";var i=new e;i.stringify=function(a){if(!a)return null;return""+a},i.parse=function(a){if(typeof a!="string")throw new Error("non-string passed to number.parse()");if(a.replace(/\s/g,"").length===0)return new f(null,g.INCOMPLETE,"");var b=new f(parseInt(a,10));isNaN(b.value)&&(b.status=g.INVALID,b.message="Can't convert \""+a+'" to a number.');return b},i.decrement=function(a){return a-1},i.increment=function(a){return a+1},i.name="number",j.prototype=new e,j.prototype.stringify=function(a){return a},j.prototype.parse=function(a){if(typeof a!="string")throw new Error("non-string passed to parse()");if(!this.data)throw new Error("Missing data on selection type extension.");var b=typeof this.data=="function"?this.data():this.data,c=!1,d,e=[];b.forEach(function(b){a==b?(d=this.fromString(b),c=!0):b.indexOf(a)===0&&e.push(this.fromString(b))},this);if(c)return new f(d);this.noMatch&&this.noMatch();if(e.length>0){var h="Possibilities"+(a.length===0?"":" for '"+a+"'");return new f(null,g.INCOMPLETE,h,e)}var h="Can't use '"+a+"'.";return new f(null,g.INVALID,h,e)},j.prototype.fromString=function(a){return a},j.prototype.decrement=function(a){var b=typeof this.data=="function"?this.data():this.data,c;if(a==null)c=b.length-1;else{var d=this.stringify(a),c=b.indexOf(d);c=c===0?b.length-1:c-1}return this.fromString(b[c])},j.prototype.increment=function(a){var b=typeof this.data=="function"?this.data():this.data,c;if(a==null)c=0;else{var d=this.stringify(a),c=b.indexOf(d);c=c===b.length-1?0:c+1}return this.fromString(b[c])},j.prototype.name="selection",b.SelectionType=j;var k=new j({name:"bool",data:["true","false"],stringify:function(a){return""+a},fromString:function(a){return a==="true"?!0:!1}});l.prototype=new e,l.prototype.stringify=function(a){return this.defer().stringify(a)},l.prototype.parse=function(a){return this.defer().parse(a)},l.prototype.decrement=function(a){var b=this.defer();return b.decrement?b.decrement(a):undefined},l.prototype.increment=function(a){var b=this.defer();return b.increment?b.increment(a):undefined},l.prototype.name="deferred",b.DeferredType=l,m.prototype=new e,m.prototype.stringify=function(a){return a.join(" ")},m.prototype.parse=function(a){return this.defer().parse(a)},m.prototype.name="array";var n=!1;b.startup=function(){n||(n=!0,d.registerType(h),d.registerType(i),d.registerType(k),d.registerType(j),d.registerType(l),d.registerType(m))},b.shutdown=function(){n=!1,d.unregisterType(h),d.unregisterType(i),d.unregisterType(k),d.unregisterType(j),d.unregisterType(l),d.unregisterType(m)}}),define("pilot/types",["require","exports","module"],function(a,b,c){function h(a,b){if(a.substr(-2)==="[]"){var c=a.slice(0,-2);return new g.array(c)}var d=g[a];typeof d=="function"&&(d=new d(b));return d}function f(){}function e(a,b,c,e){this.value=a,this.status=b||d.VALID,this.message=c,this.predictions=e||[]}var d={VALID:{toString:function(){return"VALID"},valueOf:function(){return 0}},INCOMPLETE:{toString:function(){return"INCOMPLETE"},valueOf:function(){return 1}},INVALID:{toString:function(){return"INVALID"},valueOf:function(){return 2}},combine:function(a){var b=d.VALID;for(var c=0;c<a.length;c++)a[c].valueOf()>b.valueOf()&&(b=a[c]);return b}};b.Status=d,b.Conversion=e,f.prototype={stringify:function(a){throw new Error("not implemented")},parse:function(a){throw new Error("not implemented")},name:undefined,increment:function(a){return undefined},decrement:function(a){return undefined},getDefault:function(){return this.parse("")}},b.Type=f;var g={};b.registerType=function(a){if(typeof a=="object"){if(!(a instanceof f))throw new Error("Can't registerType using: "+a);if(!a.name)throw new Error("All registered types must have a name");g[a.name]=a}else{if(typeof a!="function")throw new Error("Unknown type: "+a);if(!a.prototype.name)throw new Error("All registered types must have a name");g[a.prototype.name]=a}},b.registerTypes=function(c){Object.keys(c).forEach(function(a){var d=c[a];d.name=a,b.registerType(d)})},b.deregisterType=function(a){delete g[a.name]},b.getType=function(a){if(typeof a=="string")return h(a);if(typeof a=="object"){if(!a.name)throw new Error("Missing 'name' member to typeSpec");return h(a.name,a)}throw new Error("Can't extract type from "+a)}}),define("pilot/types/command",["require","exports","module","pilot/canon","pilot/types/basic","pilot/types"],function(a,b,c){var d=a("pilot/canon"),e=a("pilot/types/basic").SelectionType,f=a("pilot/types"),g=new e({name:"command",data:function(){return d.getCommandNames()},stringify:function(a){return a.name},fromString:function(a){return d.getCommand(a)}});b.startup=function(){f.registerType(g)},b.shutdown=function(){f.unregisterType(g)}}),define("pilot/canon",["require","exports","module","pilot/console","pilot/stacktrace","pilot/oop","pilot/useragent","pilot/keys","pilot/event_emitter","pilot/typecheck","pilot/catalog","pilot/types","pilot/lang"],function(a,b,c){function J(a){a=a||{},this.command=a.command,this.args=a.args,this.typed=a.typed,this._begunOutput=!1,this.start=new Date,this.end=null,this.completed=!1,this.error=!1}function G(a,b,c,e,f){function h(){a.exec(b,g.args,g),!g.isAsync&&!g.isDone&&g.done()}typeof a=="string"&&(a=q[a]);if(!a)return!1;var g=new J({sender:c,command:a,args:e||{},typed:f});if(g.getStatus()==l.INVALID){d.error("Canon.exec: Invalid parameter(s) passed to "+a.name);return!1}if(g.getStatus()==l.INCOMPLETE){var i,j=b[c];if(!j||!j.getArgsProvider||!(i=j.getArgsProvider()))i=F;i(g,function(){g.getStatus()==l.VALID&&h()});return!0}h();return!0}function F(a,b){var c=a.args,d=a.command.params;for(var e=0;e<d.length;e++){var f=d[e];if(a.getParamStatus(f)!=l.VALID||f.defaultValue===null){var g=f.description;f.defaultValue===null&&(g+=" (optional)");var h=prompt(g,f.defaultValue||"");if(!h){b();return}c[f.name]=h}}b()}function E(){return z}function D(a){return q[a]}function C(a){var b=typeof a=="string"?a:a.name;delete q[b],n.arrayRemove(z,b)}function B(a,b){var c=b.type;b.type=m.getType(c);if(b.type==null)throw new Error("In "+a+"/"+b.name+": can't find type for: "+JSON.stringify(c))}function A(a){if(!a.name)throw new Error("All registered commands must have a name");a.params==null&&(a.params=[]);if(!Array.isArray(a.params))throw new Error("command.params must be an array in "+a.name);a.params.forEach(function(b){if(!b.name)throw new Error("In "+a.name+": all params must have a name");B(a.name,b)},this),q[a.name]=a,a.bindKey&&w(a),z.push(a.name),z.sort()}function y(a,b,c,d){var e=x(a,b,c,d);return e?G(e,a,b,{}):!1}function x(a,b,c,d){j.isNumber(d)&&(d=h.keyCodeToString(d));var e=(s[c]||{})[d]||[];for(var f=0;f<e.length;f++)if(e[f].sender(a,b,c,d))return e[f].command;var g=r[b];return g&&g[c]&&g[c][d]}function w(a){var b=a.bindKey,c=b[v],d=r,e=s;if(!b.sender)throw new Error("All key bindings must have a sender");if(!b.mac&&b.mac!==null)throw new Error("All key bindings must have a mac key binding");if(!b.win&&b.win!==null)throw new Error("All key bindings must have a windows key binding");if(!!b[v])if(typeof b.sender=="string"){var f=t(b.sender,"\\|",null,!0);f.forEach(function(b){d[b]||(d[b]={}),c.split("|").forEach(function(c){u(c,a,d[b])})})}else{if(!j.isFunction(b.sender))throw new Error("Key binding must have a sender that is a string or function");var g={command:a,sender:b.sender};keyData=u(c),e[keyData.hashId]||(e[keyData.hashId]={}),e[keyData.hashId][keyData.key]?e[keyData.hashId][keyData.key].push(g):e[keyData.hashId][keyData.key]=[g]}}function u(a,b,c){var d,e=0,f=t(a,"\\-",null,!0),g=0,i=f.length;for(;g<i;++g)h.KEY_MODS[f[g]]?e=e|h.KEY_MODS[f[g]]:d=f[g]||"-";if(c==null)return{key:d,hashId:e};(c[e]||(c[e]={}))[d]=b}function t(a,b,c,d){return(d&&a.toLowerCase()||a).replace(/(?:^\s+|\n|\s+$)/g,"").split(new RegExp("[\\s ]*"+b+"[\\s ]*","g"),c||999)}var d=a("pilot/console"),e=a("pilot/stacktrace").Trace,f=a("pilot/oop"),g=a("pilot/useragent"),h=a("pilot/keys"),i=a("pilot/event_emitter").EventEmitter,j=a("pilot/typecheck"),k=a("pilot/catalog"),l=a("pilot/types").Status,m=a("pilot/types"),n=a("pilot/lang"),o={name:"command",description:"A command is a bit of functionality with optional typed arguments which can do something small like moving the cursor around the screen, or large like cloning a project from VCS.",indexOn:"name"};b.startup=function(a,b){k.addExtensionSpec(o)},b.shutdown=function(a,b){k.removeExtensionSpec(o)};var p={name:"thing",description:"thing is an example command",params:[{name:"param1",description:"an example parameter",type:"text",defaultValue:null}],exec:function(a,b,c){thing()}},q={},r={},s={},v=g.isMac?"mac":"win",z=[];b.removeCommand=C,b.addCommand=A,b.getCommand=D,b.getCommandNames=E,b.findKeyCommand=x,b.exec=G,b.execKeyCommand=y,b.upgradeType=B,f.implement(b,i);var H=[],I=100;f.implement(J.prototype,i),J.prototype.getParamStatus=function(a){var b=this.args||{};if(a.name in b){if(b[a.name]==null)return a.defaultValue===null?l.VALID:l.INCOMPLETE;var c,d=b[a.name].toString();try{c=a.type.parse(d)}catch(e){return l.INVALID}if(c.status!=l.VALID)return c.status}else if(a.defaultValue===undefined)return l.INCOMPLETE;return l.VALID},J.prototype.getParamNameStatus=function(a){var b=this.command.params||[];for(var c=0;c<b.length;c++)if(b[c].name==a)return this.getParamStatus(b[c]);throw"Parameter '"+a+"' not defined on command '"+this.command.name+"'"},J.prototype.getStatus=function(){var a=this.args||{},b=this.command.params;if(!b||b.length==0)return l.VALID;var c=[];for(var d=0;d<b.length;d++)c.push(this.getParamStatus(b[d]));return l.combine(c)},J.prototype._beginOutput=function(){this._begunOutput=!0,this.outputs=[],H.push(this);while(H.length>I)H.shiftObject();b._dispatchEvent("output",{requests:H,request:this})},J.prototype.doneWithError=function(a){this.error=!0,this.done(a)},J.prototype.async=function(){this.isAsync=!0,this._begunOutput||this._beginOutput()},J.prototype.output=function(a){this._begunOutput||this._beginOutput(),typeof a!="string"&&!(a instanceof Node)&&(a=a.toString()),this.outputs.push(a),this.isDone=!0,this._dispatchEvent("output",{});return this},J.prototype.done=function(a){this.completed=!0,this.end=new Date,this.duration=this.end.getTime()-this.start.getTime(),a&&this.output(a),this.isDone||(this.isDone=!0,this._dispatchEvent("output",{}))},b.Request=J}),define("pilot/console",["require","exports","module"],function(a,b,c){var d=function(){},e=["assert","count","debug","dir","dirxml","error","group","groupEnd","info","log","profile","profileEnd","time","timeEnd","trace","warn"];typeof window=="undefined"?e.forEach(function(a){b[a]=function(){var b=Array.prototype.slice.call(arguments),c={op:"log",method:a,args:b};postMessage(JSON.stringify(c))}}):e.forEach(function(a){window.console&&window.console[a]?b[a]=Function.prototype.bind.call(window.console[a],window.console):b[a]=d})}),define("pilot/stacktrace",["require","exports","module","pilot/useragent","pilot/console"],function(a,b,c){function i(){}function g(a){for(var b=0;b<a.length;++b){var c=a[b];typeof c=="object"?a[b]="#object":typeof c=="function"?a[b]="#function":typeof c=="string"&&(a[b]='"'+c+'"')}return a.join(",")}var d=a("pilot/useragent"),e=a("pilot/console"),f=function(){return d.isGecko?"firefox":d.isOpera?"opera":"other"}(),h={chrome:function(a){var b=a.stack;if(!b){e.log(a);return[]}return b.replace(/^.*?\n/,"").replace(/^.*?\n/,"").replace(/^.*?\n/,"").replace(/^[^\(]+?[\n$]/gm,"").replace(/^\s+at\s+/gm,"").replace(/^Object.<anonymous>\s*\(/gm,"{anonymous}()@").split("\n")},firefox:function(a){var b=a.stack;if(!b){e.log(a);return[]}b=b.replace(/(?:\n@:0)?\s+$/m,""),b=b.replace(/^\(/gm,"{anonymous}(");return b.split("\n")},opera:function(a){var b=a.message.split("\n"),c="{anonymous}",d=/Line\s+(\d+).*?script\s+(http\S+)(?:.*?in\s+function\s+(\S+))?/i,e,f,g;for(e=4,f=0,g=b.length;e<g;e+=2)d.test(b[e])&&(b[f++]=(RegExp.$3?RegExp.$3+"()@"+RegExp.$2+RegExp.$1:c+"()@"+RegExp.$2+":"+RegExp.$1)+" -- "+b[e+1].replace(/^\s+/,""));b.splice(f,b.length-f);return b},other:function(a){var b="{anonymous}",c=/function\s*([\w\-$]+)?\s*\(/i,d=[],e=0,f,h,i=10;while(a&&d.length<i){f=c.test(a.toString())?RegExp.$1||b:b,h=Array.prototype.slice.call(a.arguments),d[e++]=f+"("+g(h)+")";if(a===a.caller&&window.opera)break;a=a.caller}return d}};i.prototype={sourceCache:{},ajax:function(a){var b=this.createXMLHTTPObject();if(!!b){b.open("GET",a,!1),b.setRequestHeader("User-Agent","XMLHTTP/1.0"),b.send("");return b.responseText}},createXMLHTTPObject:function(){var a,b=[function(){return new XMLHttpRequest},function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new ActiveXObject("Msxml3.XMLHTTP")},function(){return new ActiveXObject("Microsoft.XMLHTTP")}];for(var c=0;c<b.length;c++)try{a=b[c](),this.createXMLHTTPObject=b[c];return a}catch(d){}},getSource:function(a){a in this.sourceCache||(this.sourceCache[a]=this.ajax(a).split("\n"));return this.sourceCache[a]},guessFunctions:function(a){for(var b=0;b<a.length;++b){var c=/{anonymous}\(.*\)@(\w+:\/\/([-\w\.]+)+(:\d+)?[^:]+):(\d+):?(\d+)?/,d=a[b],e=c.exec(d);if(e){var f=e[1],g=e[4];if(f&&g){var h=this.guessFunctionName(f,g);a[b]=d.replace("{anonymous}",h)}}}return a},guessFunctionName:function(a,b){try{return this.guessFunctionNameFromLines(b,this.getSource(a))}catch(c){return"getSource failed with url: "+a+", exception: "+c.toString()}},guessFunctionNameFromLines:function(a,b){var c=/function ([^(]*)\(([^)]*)\)/,d=/['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(function|eval|new Function)/,e="",f=10;for(var g=0;g<f;++g){e=b[a-g]+e;if(e!==undefined){var h=d.exec(e);if(h)return h[1];h=c.exec(e);if(h&&h[1])return h[1]}}return"(?)"}};var j=new i,k=[/http:\/\/localhost:4020\/sproutcore.js:/];b.ignoreFramesMatching=function(a){k.push(a)},b.Trace=function(b,c){this._ex=b,this._stack=h[f](b),c&&(this._stack=j.guessFunctions(this._stack))},b.Trace.prototype.log=function(a){a<=0&&(a=999999999);var b=0;for(var c=0;c<this._stack.length&&b<a;c++){var d=this._stack[c],f=!0;k.forEach(function(a){a.test(d)&&(f=!1)}),f&&(e.debug(d),b++)}}}),define("pilot/useragent",["require","exports","module"],function(a,b,c){var d=(navigator.platform.match(/mac|win|linux/i)||["other"])[0].toLowerCase(),e=navigator.userAgent,f=navigator.appVersion;b.isWin=d=="win",b.isMac=d=="mac",b.isLinux=d=="linux",b.isIE=!1,b.isGecko=b.isMozilla=window.controllers&&window.navigator.product==="Gecko",b.isOldGecko=b.isGecko&&/rv\:1/.test(navigator.userAgent),b.isOpera=window.opera&&Object.prototype.toString.call(window.opera)=="[object Opera]",b.isWebKit=parseFloat(e.split("WebKit/")[1])||undefined,b.isAIR=e.indexOf("AdobeAIR")>=0,b.isIPad=e.indexOf("iPad")>=0,b.OS={LINUX:"LINUX",MAC:"MAC",WINDOWS:"WINDOWS"},b.getOS=function(){return b.isMac?b.OS.MAC:b.isLinux?b.OS.LINUX:b.OS.WINDOWS}}),define("pilot/oop",["require","exports","module"],function(a,b,c){b.inherits=function(){var a=function(){};return function(b,c){a.prototype=c.prototype,b.super_=c.prototype,b.prototype=new a,b.prototype.constructor=b}}(),b.mixin=function(a,b){for(var c in b)a[c]=b[c]},b.implement=function(a,c){b.mixin(a,c)}}),define("pilot/keys",["require","exports","module","pilot/oop"],function(a,b,c){var d=a("pilot/oop"),e=function(){var a={MODIFIER_KEYS:{16:"Shift",17:"Ctrl",18:"Alt",224:"Meta"},KEY_MODS:{ctrl:1,alt:2,option:2,shift:4,meta:8,command:8},FUNCTION_KEYS:{8:"Backspace",9:"Tab",13:"Return",19:"Pause",27:"Esc",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"Left",38:"Up",39:"Right",40:"Down",44:"Print",45:"Insert",46:"Delete",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"Numlock",145:"Scrolllock"},PRINTABLE_KEYS:{32:" ",48:"0",49:"1",50:"2",51:"3",52:"4",53:"5",54:"6",55:"7",56:"8",57:"9",59:";",61:"=",65:"a",66:"b",67:"c",68:"d",69:"e",70:"f",71:"g",72:"h",73:"i",74:"j",75:"k",76:"l",77:"m",78:"n",79:"o",80:"p",81:"q",82:"r",83:"s",84:"t",85:"u",86:"v",87:"w",88:"x",89:"y",90:"z",107:"+",109:"-",110:".",188:",",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:'"'}};for(i in a.FUNCTION_KEYS){var b=a.FUNCTION_KEYS[i].toUpperCase();a[b]=parseInt(i,10)}d.mixin(a,a.MODIFIER_KEYS),d.mixin(a,a.PRINTABLE_KEYS),d.mixin(a,a.FUNCTION_KEYS);return a}();d.mixin(b,e),b.keyCodeToString=function(a){return(e[a]||String.fromCharCode(a)).toLowerCase()}}),define("pilot/event_emitter",["require","exports","module"],function(a,b,c){var d={};d._emit=d._dispatchEvent=function(a,b){this._eventRegistry=this._eventRegistry||{};var c=this._eventRegistry[a];if(!!c&&!!c.length){var b=b||{};b.type=a;for(var d=0;d<c.length;d++)c[d](b)}},d.on=d.addEventListener=function(a,b){this._eventRegistry=this._eventRegistry||{};var c=this._eventRegistry[a];if(!c)var c=this._eventRegistry[a]=[];c.indexOf(b)==-1&&c.push(b)},d.removeListener=d.removeEventListener=function(a,b){this._eventRegistry=this._eventRegistry||{};var c=this._eventRegistry[a];if(!!c){var d=c.indexOf(b);d!==-1&&c.splice(d,1)}},d.removeAllListeners=function(a){this._eventRegistry&&(this._eventRegistry[a]=[])},b.EventEmitter=d}),define("pilot/typecheck",["require","exports","module"],function(a,b,c){var d=Object.prototype.toString;b.isString=function(a){return a&&d.call(a)==="[object String]"},b.isBoolean=function(a){return a&&d.call(a)==="[object Boolean]"},b.isNumber=function(a){return a&&d.call(a)==="[object Number]"&&isFinite(a)},b.isObject=function(a){return a!==undefined&&(a===null||typeof a=="object"||Array.isArray(a)||b.isFunction(a))},b.isFunction=function(a){return a&&d.call(a)==="[object Function]"}}),define("pilot/catalog",["require","exports","module"],function(a,b,c){var d={};b.addExtensionSpec=function(a){d[a.name]=a},b.removeExtensionSpec=function(a){typeof a=="string"?delete d[a]:delete d[a.name]},b.getExtensionSpec=function(a){return d[a]},b.getExtensionSpecs=function(){return Object.keys(d)}}),define("pilot/lang",["require","exports","module"],function(a,b,c){b.stringReverse=function(a){return a.split("").reverse().join("")},b.stringRepeat=function(a,b){return Array(b+1).join(a)};var d=/^\s\s*/,e=/\s\s*$/;b.stringTrimLeft=function(a){return a.replace(d,"")},b.stringTrimRight=function(a){return a.replace(e,"")},b.copyObject=function(a){var b={};for(var c in a)b[c]=a[c];return b},b.copyArray=function(a){var b=[];for(i=0,l=a.length;i<l;i++)a[i]&&typeof a[i]=="object"?b[i]=this.copyObject(a[i]):b[i]=a[i];return b},b.deepCopy=function(a){if(typeof a!="object")return a;var b=a.constructor();for(var c in a)typeof a[c]=="object"?b[c]=this.deepCopy(a[c]):b[c]=a[c];return b},b.arrayToMap=function(a){var b={};for(var c=0;c<a.length;c++)b[a[c]]=1;return b},b.arrayRemove=function(a,b){for(var c=0;c<=a.length;c++)b===a[c]&&a.splice(c,1)},b.escapeRegExp=function(a){return a.replace(/([.*+?^${}()|[\]\/\\])/g,"\\$1")},b.deferredCall=function(a){var b=null,c=function(){b=null,a()},d=function(a){b||(b=setTimeout(c,a||0));return d};d.schedule=d,d.call=function(){this.cancel(),a();return d},d.cancel=function(){clearTimeout(b),b=null;return d};return d}}),define("pilot/types/settings",["require","exports","module","pilot/types/basic","pilot/types","pilot/settings"],function(a,b,c){var d=a("pilot/types/basic").SelectionType,e=a("pilot/types/basic").DeferredType,f=a("pilot/types"),g=a("pilot/settings").settings,h,i=new d({name:"setting",data:function(){return k.settings.getSettingNames()},stringify:function(a){h=a;return a.name},fromString:function(a){h=g.getSetting(a);return h},noMatch:function(){h=null}}),j=new e({name:"settingValue",defer:function(){return h?h.type:f.getType("text")},getDefault:function(){var a=this.parse("");if(h){var b=h.get();if(a.predictions.length===0)a.predictions.push(b);else{var c=!1;for(;;){var d=a.predictions.indexOf(b);if(d===-1)break;a.predictions.splice(d,1),c=!0}c&&a.predictions.push(b)}}return a}}),k;b.startup=function(a,b){k=a.env,f.registerType(i),f.registerType(j)},b.shutdown=function(a,b){f.unregisterType(i),f.unregisterType(j)}}),define("pilot/settings",["require","exports","module","pilot/console","pilot/oop","pilot/types","pilot/event_emitter","pilot/catalog"],function(a,b,c){function l(){}function k(a){this._deactivated={},this._settings={},this._settingNames=[],a&&this.setPersister(a)}function j(a,b){this._settings=b,Object.keys(a).forEach(function(b){this[b]=a[b]},this),this.type=f.getType(this.type);if(this.type==null)throw new Error("In "+this.name+": can't find type for: "+JSON.stringify(a.type));if(!this.name)throw new Error("Setting.name == undefined. Ignoring.",this);if(!this.defaultValue===undefined)throw new Error("Setting.defaultValue == undefined",this);this.onChange&&this.on("change",this.onChange.bind(this)),this.set(this.defaultValue)}var d=a("pilot/console"),e=a("pilot/oop"),f=a("pilot/types"),g=a("pilot/event_emitter").EventEmitter,h=a("pilot/catalog"),i={name:"setting",description:"A setting is something that the application offers as a way to customize how it works",register:"env.settings.addSetting",indexOn:"name"};b.startup=function(a,b){h.addExtensionSpec(i)},b.shutdown=function(a,b){h.removeExtensionSpec(i)},j.prototype={get:function(){return this.value},set:function(a){this.value!==a&&(this.value=a,this._settings.persister&&this._settings.persister.persistValue(this._settings,this.name,a),this._dispatchEvent("change",{setting:this,value:a}))},resetValue:function(){this.set(this.defaultValue)}},e.implement(j.prototype,g),k.prototype={addSetting:function(a){var b=new j(a,this);this._settings[b.name]=b,this._settingNames.push(b.name),this._settingNames.sort()},addSettings:function(b){Object.keys(b).forEach(function(a){var c=b[a];"name"in c||(c.name=a),this.addSetting(c)},this)},removeSetting:function(a){var b=typeof a=="string"?a:a.name;a=this._settings[b],delete this._settings[b],util.arrayRemove(this._settingNames,b),settings.removeAllListeners("change")},removeSettings:function(b){Object.keys(b).forEach(function(a){var c=b[a];"name"in c||(c.name=a),this.removeSettings(c)},this)},getSettingNames:function(){return this._settingNames},getSetting:function(a){return this._settings[a]},setPersister:function(a){this._persister=a,a&&a.loadInitialValues(this)},resetAll:function(){this.getSettingNames().forEach(function(a){this.resetValue(a)},this)},_list:function(){var a=[];this.getSettingNames().forEach(function(b){a.push({key:b,value:this.getSetting(b).get()})},this);return a},_loadDefaultValues:function(){this._loadFromObject(this._getDefaultValues())},_loadFromObject:function(a){for(var b in a)if(a.hasOwnProperty(b)){var c=this._settings[b];if(c){var d=c.type.parse(a[b]);this.set(b,d)}else this.set(b,a[b])}},_saveToObject:function(){return this.getSettingNames().map(function(a){return this._settings[a].type.stringify(this.get(a))}.bind(this))},_getDefaultValues:function(){return this.getSettingNames().map(function(a){return this._settings[a].spec.defaultValue}.bind(this))}},b.settings=new k,l.prototype={loadInitialValues:function(a){a._loadDefaultValues();var b=cookie.get("settings");a._loadFromObject(JSON.parse(b))},persistValue:function(a,b,c){try{var e=JSON.stringify(a._saveToObject());cookie.set("settings",e)}catch(f){d.error("Unable to JSONify the settings! "+f);return}}},b.CookiePersister=l}),define("pilot/commands/settings",["require","exports","module","pilot/canon"],function(a,b,c){var d={name:"set",params:[{name:"setting",type:"setting",description:"The name of the setting to display or alter",defaultValue:null},{name:"value",type:"settingValue",description:"The new value for the chosen setting",defaultValue:null}],description:"define and show settings",exec:function(a,b,c){var d;if(!b.setting){var e=a.settings.getSettingNames();d="",e.sort(function(a,b){return a.localeCompare(b)}),e.forEach(function(b){var c=a.settings.getSetting(b),e="https://wiki.mozilla.org/Labs/Skywriter/Settings#"+c.name;d+='<a class="setting" href="'+e+'" title="View external documentation on setting: '+c.name+'" target="_blank">'+c.name+"</a> = "+c.value+"<br/>"})}else b.value===undefined?d="<strong>"+setting.name+"</strong> = "+setting.get():(b.setting.set(b.value),d="Setting: <strong>"+b.setting.name+"</strong> = "+b.setting.get());c.done(d)}},e={name:"unset",params:[{name:"setting",type:"setting",description:"The name of the setting to return to defaults"}],description:"unset a setting entirely",exec:function(a,b,c){var d=a.settings.get(b.setting);d?(d.reset(),c.done("Reset "+d.name+" to default: "+a.settings.get(b.setting))):c.doneWithError("No setting with the name <strong>"+b.setting+"</strong>.")}},f=a("pilot/canon");b.startup=function(a,b){f.addCommand(d),f.addCommand(e)},b.shutdown=function(a,b){f.removeCommand(d),f.removeCommand(e)}}),define("pilot/commands/basic",["require","exports","module","pilot/typecheck","pilot/canon"],function(require,exports,module){var checks=require("pilot/typecheck"),canon=require("pilot/canon"),helpMessages={plainPrefix:'<h2>Welcome to Skywriter - Code in the Cloud</h2><ul><li><a href="http://labs.mozilla.com/projects/skywriter" target="_blank">Home Page</a></li><li><a href="https://wiki.mozilla.org/Labs/Skywriter" target="_blank">Wiki</a></li><li><a href="https://wiki.mozilla.org/Labs/Skywriter/UserGuide" target="_blank">User Guide</a></li><li><a href="https://wiki.mozilla.org/Labs/Skywriter/Tips" target="_blank">Tips and Tricks</a></li><li><a href="https://wiki.mozilla.org/Labs/Skywriter/FAQ" target="_blank">FAQ</a></li><li><a href="https://wiki.mozilla.org/Labs/Skywriter/DeveloperGuide" target="_blank">Developers Guide</a></li></ul>',plainSuffix:'For more information, see the <a href="https://wiki.mozilla.org/Labs/Skywriter">Skywriter Wiki</a>.'},helpCommandSpec={name:"help",params:[{name:"search",type:"text",description:"Search string to narrow the output.",defaultValue:null}],description:"Get help on the available commands.",exec:function(a,b,c){var d=[],e=canon.getCommand(b.search);if(e&&e.exec)d.push(e.description?e.description:"No description for "+b.search);else{var f=!1;!b.search&&helpMessages.plainPrefix&&d.push(helpMessages.plainPrefix),e?(d.push("<h2>Sub-Commands of "+e.name+"</h2>"),d.push("<p>"+e.description+"</p>")):b.search?(b.search=="hidden"&&(b.search="",f=!0),d.push("<h2>Commands starting with '"+b.search+"':</h2>")):d.push("<h2>Available Commands:</h2>");var g=canon.getCommandNames();g.sort(),d.push("<table>");for(var h=0;h<g.length;h++){e=canon.getCommand(g[h]);if(!f&&e.hidden)continue;if(e.description===undefined)continue;if(b.search&&e.name.indexOf(b.search)!==0)continue;if(!b.search&&e.name.indexOf(" ")!=-1)continue;if(e&&e.name==b.search)continue;d.push("<tr>"),d.push('<th class="right">'+e.name+"</th>"),d.push("<td>"+e.description+"</td>"),d.push("</tr>")}d.push("</table>"),!b.search&&helpMessages.plainSuffix&&d.push(helpMessages.plainSuffix)}c.done(d.join(""))}},evalCommandSpec={name:"eval",params:[{name:"javascript",type:"text",description:"The JavaScript to evaluate"}],description:"evals given js code and show the result",hidden:!0,exec:function(env,args,request){var result,javascript=args.javascript;try{result=eval(javascript)}catch(e){result="<b>Error: "+e.message+"</b>"}var msg="",type="",x;if(checks.isFunction(result))msg=(result+"").replace(/\n/g,"<br>").replace(/ /g,"&#160"),type="function";else if(checks.isObject(result)){Array.isArray(result)?type="array":type="object";var items=[],value;for(x in result)result.hasOwnProperty(x)&&(checks.isFunction(result[x])?value="[function]":checks.isObject(result[x])?value="[object]":value=result[x],items.push({name:x,value:value}));items.sort(function(a,b){return a.name.toLowerCase()<b.name.toLowerCase()?-1:1});for(x=0;x<items.length;x++)msg+="<b>"+items[x].name+"</b>: "+items[x].value+"<br>"}else msg=result,type=typeof result;request.done("Result for eval <b>'"+javascript+"'</b>"+" (type: "+type+"): <br><br>"+msg)}},versionCommandSpec={name:"version",description:"show the Skywriter version",hidden:!0,exec:function(a,b,c){var d="Skywriter "+skywriter.versionNumber+" ("+skywriter.versionCodename+")";c.done(d)}},skywriterCommandSpec={name:"skywriter",hidden:!0,exec:function(a,b,c){var d=Math.floor(Math.random()*messages.length);c.done("Skywriter "+messages[d])}},messages=["really wants you to trick it out in some way.","is your Web editor.","would love to be like Emacs on the Web.","is written on the Web platform, so you can tweak it."],canon=require("pilot/canon");exports.startup=function(a,b){canon.addCommand(helpCommandSpec),canon.addCommand(evalCommandSpec),canon.addCommand(skywriterCommandSpec)},exports.shutdown=function(a,b){canon.removeCommand(helpCommandSpec),canon.removeCommand(evalCommandSpec),canon.removeCommand(skywriterCommandSpec)}}),define("pilot/settings/canon",["require","exports","module"],function(a,b,c){var d={name:"historyLength",description:"How many typed commands do we recall for reference?",type:"number",defaultValue:50};b.startup=function(a,b){a.env.settings.addSetting(d)},b.shutdown=function(a,b){a.env.settings.removeSetting(d)}}),define("pilot/plugin_manager",["require","exports","module","pilot/promise"],function(a,b,c){var d=a("pilot/promise").Promise;b.REASONS={APP_STARTUP:1,APP_SHUTDOWN:2,PLUGIN_ENABLE:3,PLUGIN_DISABLE:4,PLUGIN_INSTALL:5,PLUGIN_UNINSTALL:6,PLUGIN_UPGRADE:7,PLUGIN_DOWNGRADE:8},b.Plugin=function(a){this.name=a,this.status=this.INSTALLED},b.Plugin.prototype={NEW:0,INSTALLED:1,REGISTERED:2,STARTED:3,UNREGISTERED:4,SHUTDOWN:5,install:function(b,c){var e=new d;if(this.status>this.NEW){e.resolve(this);return e}a([this.name],function(a){a.install&&a.install(b,c),this.status=this.INSTALLED,e.resolve(this)}.bind(this));return e},register:function(b,c){var e=new d;if(this.status!=this.INSTALLED){e.resolve(this);return e}a([this.name],function(a){a.register&&a.register(b,c),this.status=this.REGISTERED,e.resolve(this)}.bind(this));return e},startup:function(c,e){e=e||b.REASONS.APP_STARTUP;var f=new d;if(this.status!=this.REGISTERED){f.resolve(this);return f}a([this.name],function(a){a.startup&&a.startup(c,e),this.status=this.STARTED,f.resolve(this)}.bind(this));return f},shutdown:function(b,c){this.status==this.STARTED&&(pluginModule=a(this.name),pluginModule.shutdown&&pluginModule.shutdown(b,c))}},b.PluginCatalog=function(){this.plugins={}},b.PluginCatalog.prototype={registerPlugins:function(a,c,e){var f=[];a.forEach(function(a){var d=this.plugins[a];d===undefined&&(d=new b.Plugin(a),this.plugins[a]=d,f.push(d.register(c,e)))}.bind(this));return d.group(f)},startupPlugins:function(a,b){var c=[];for(var e in this.plugins){var f=this.plugins[e];c.push(f.startup(a,b))}return d.group(c)}},b.catalog=new b.PluginCatalog}),define("pilot/promise",["require","exports","module","pilot/console","pilot/stacktrace"],function(a,b,c){var d=a("pilot/console"),e=a("pilot/stacktrace").Trace,f=-1,g=0,h=1,i=0,j=!1,k=[],l=[];Promise=function(){this._status=g,this._value=undefined,this._onSuccessHandlers=[],this._onErrorHandlers=[],this._id=i++,k[this._id]=this},Promise.prototype.isPromise=!0,Promise.prototype.isComplete=function(){return this._status!=g},Promise.prototype.isResolved=function(){return this._status==h},Promise.prototype.isRejected=function(){return this._status==f},Promise.prototype.then=function(a,b){typeof a=="function"&&(this._status===h?a.call(null,this._value):this._status===g&&this._onSuccessHandlers.push(a)),typeof b=="function"&&(this._status===f?b.call(null,this._value):this._status===g&&this._onErrorHandlers.push(b));return this},Promise.prototype.chainPromise=function(a){var b=new Promise;b._chainedFrom=this,this.then(function(c){try{b.resolve(a(c))}catch(d){b.reject(d)}},function(a){b.reject(a)});return b},Promise.prototype.resolve=function(a){return this._complete(this._onSuccessHandlers,h,a,"resolve")},Promise.prototype.reject=function(a){return this._complete(this._onErrorHandlers,f,a,"reject")},Promise.prototype._complete=function(a,b,c,f){if(this._status!=g){d.group("Promise already closed"),d.error("Attempted "+f+"() with ",c),d.error("Previous status = ",this._status,", previous value = ",this._value),d.trace(),this._completeTrace&&(d.error("Trace of previous completion:"),this._completeTrace.log(5)),d.groupEnd();return this}j&&(this._completeTrace=new e(new Error)),this._status=b,this._value=c,a.forEach(function(a){a.call(null,this._value)},this),this._onSuccessHandlers.length=0,this._onErrorHandlers.length=0,delete k[this._id],l.push(this);while(l.length>20)l.shift();return this},Promise.group=function(a){a instanceof Array||(a=Array.prototype.slice.call(arguments));if(a.length===0)return(new Promise).resolve([]);var b=new Promise,c=[],d=0,e=function(e){return function(g){c[e]=g,d++,b._status!==f&&d===a.length&&b.resolve(c)}};a.forEach(function(a,c){var d=e(c),f=b.reject.bind(b);a.then(d,f)});return b},b.Promise=Promise,b._outstanding=k,b._recent=l}),define("pilot/dom",["require","exports","module"],function(a,b,c){var d="http://www.w3.org/1999/xhtml";b.createElement=function(a,b){return document.createElementNS?document.createElementNS(b||d,a):document.createElement(a)},b.setText=function(a,b){a.innerText!==undefined&&(a.innerText=b),a.textContent!==undefined&&(a.textContent=b)},document.documentElement.classList?(b.hasCssClass=function(a,b){return a.classList.contains(b)},b.addCssClass=function(a,b){a.classList.add(b)},b.removeCssClass=function(a,b){a.classList.remove(b)},b.toggleCssClass=function(a,b){return a.classList.toggle(b)}):(b.hasCssClass=function(a,b){var c=a.className.split(/\s+/g);return c.indexOf(b)!==-1},b.addCssClass=function(a,c){b.hasCssClass(a,c)||(a.className+=" "+c)},b.removeCssClass=function(a,b){var c=a.className.split(/\s+/g);for(;;){var d=c.indexOf(b);if(d==-1)break;c.splice(d,1)}a.className=c.join(" ")},b.toggleCssClass=function(a,b){var c=a.className.split(/\s+/g),d=!0;for(;;){var e=c.indexOf(b);if(e==-1)break;d=!1,c.splice(e,1)}d&&c.push(b),a.className=c.join(" ");return d}),b.setCssClass=function(a,c,d){d?b.addCssClass(a,c):b.removeCssClass(a,c)},b.importCssString=function(a,b){b=b||document;if(b.createStyleSheet){var c=b.createStyleSheet();c.cssText=a}else{var e=b.createElementNS?b.createElementNS(d,"style"):b.createElement("style");e.appendChild(b.createTextNode(a));var f=b.getElementsByTagName("head")[0]||b.documentElement;f.appendChild(e)}},b.getInnerWidth=function(a){return parseInt(b.computedStyle(a,"paddingLeft"))+parseInt(b.computedStyle(a,"paddingRight"))+a.clientWidth},b.getInnerHeight=function(a){return parseInt(b.computedStyle(a,"paddingTop"))+parseInt(b.computedStyle(a,"paddingBottom"))+a.clientHeight},window.pageYOffset!==undefined?(b.getPageScrollTop=function(){return window.pageYOffset},b.getPageScrollLeft=function(){return window.pageXOffset}):(b.getPageScrollTop=function(){return document.body.scrollTop},b.getPageScrollLeft=function(){return document.body.scrollLeft}),window.getComputedStyle?b.computedStyle=function(a,b){if(b)return(window.getComputedStyle(a,"")||{})[b]||"";return window.getComputedStyle(a,"")||{}}:b.computedStyle=function(a,b){if(b)return a.currentStyle[b];return a.currentStyle},b.scrollbarWidth=function(){var a=b.createElement("p");a.style.width="100%",a.style.height="200px";var c=b.createElement("div"),d=c.style;d.position="absolute",d.left="-10000px",d.overflow="hidden",d.width="200px",d.height="150px",c.appendChild(a);var e=document.body||document.documentElement;e.appendChild(c);var f=a.offsetWidth;d.overflow="scroll";var g=a.offsetWidth;f==g&&(g=c.clientWidth),e.removeChild(c);return f-g},b.setInnerHtml=function(a,b){var c=a.cloneNode(!1);c.innerHTML=b,a.parentNode.replaceChild(c,a);return c},b.setInnerText=function(a,b){document.body&&"textContent"in document.body?a.textContent=b:a.innerText=b},b.getInnerText=function(a){return document.body&&"textContent"in document.body?a.textContent:a.innerText||a.textContent||""},b.getParentWindow=function(a){return a.defaultView||a.parentWindow},b.getSelectionStart=function(a){var b;try{b=a.selectionStart||0}catch(c){b=0}return b},b.setSelectionStart=function(a,b){return a.selectionStart=b},b.getSelectionEnd=function(a){var b;try{b=a.selectionEnd||0}catch(c){b=0}return b},b.setSelectionEnd=function(a,b){return a.selectionEnd=b}}),define("pilot/event",["require","exports","module","pilot/keys","pilot/useragent","pilot/dom"],function(a,b,c){function g(a,b,c){var f=0;e.isOpera&&e.isMac?f=0|(b.metaKey?1:0)|(b.altKey?2:0)|(b.shiftKey?4:0)|(b.ctrlKey?8:0):f=0|(b.ctrlKey?1:0)|(b.altKey?2:0)|(b.shiftKey?4:0)|(b.metaKey?8:0);if(c in d.MODIFIER_KEYS){switch(d.MODIFIER_KEYS[c]){case"Alt":f=2;break;case"Shift":f=4;break;case"Ctrl":f=1;break;default:f=8}c=0}f&8&&(c==91||c==93)&&(c=0);if(f==0&&!(c in d.FUNCTION_KEYS))return!1;return a(b,f,c)}var d=a("pilot/keys"),e=a("pilot/useragent"),f=a("pilot/dom");b.addListener=function(a,b,c){if(a.addEventListener)return a.addEventListener(b,c,!1);if(a.attachEvent){var d=function(){c(window.event)};c._wrapper=d,a.attachEvent("on"+b,d)}},b.removeListener=function(a,b,c){if(a.removeEventListener)return a.removeEventListener(b,c,!1);a.detachEvent&&a.detachEvent("on"+b,c._wrapper||c)},b.stopEvent=function(a){b.stopPropagation(a),b.preventDefault(a);return!1},b.stopPropagation=function(a){a.stopPropagation?a.stopPropagation():a.cancelBubble=!0},b.preventDefault=function(a){a.preventDefault?a.preventDefault():a.returnValue=!1},b.getDocumentX=function(a){return a.clientX?a.clientX+f.getPageScrollLeft():a.pageX},b.getDocumentY=function(a){return a.clientY?a.clientY+f.getPageScrollTop():a.pageY},b.getButton=function(a){if(a.type=="dblclick")return 0;if(a.type=="contextmenu")return 2;return a.preventDefault?a.button:{1:0,2:2,4:1}[a.button]},document.documentElement.setCapture?b.capture=function(a,c,d){function f(e){c&&c(e),d&&d(),b.removeListener(a,"mousemove",c),b.removeListener(a,"mouseup",f),b.removeListener(a,"losecapture",f),a.releaseCapture()}function e(a){c(a);return b.stopPropagation(a)}b.addListener(a,"mousemove",c),b.addListener(a,"mouseup",f),b.addListener(a,"losecapture",f),a.setCapture()}:b.capture=function(a,b,c){function e(a){b&&b(a),c&&c(),document.removeEventListener("mousemove",d,!0),document.removeEventListener("mouseup",e,!0),a.stopPropagation()}function d(a){b(a),a.stopPropagation()}document.addEventListener("mousemove",d,!0),document.addEventListener("mouseup",e,!0)},b.addMouseWheelListener=function(a,c){var d=function(a){a.wheelDelta!==undefined?a.wheelDeltaX!==undefined?(a.wheelX=-a.wheelDeltaX/8,a.wheelY=-a.wheelDeltaY/8):(a.wheelX=0,a.wheelY=-a.wheelDelta/8):a.axis&&a.axis==a.HORIZONTAL_AXIS?(a.wheelX=(a.detail||0)*5,a.wheelY=0):(a.wheelX=0,a.wheelY=(a.detail||0)*5),c(a)};b.addListener(a,"DOMMouseScroll",d),b.addListener(a,"mousewheel",d)},b.addMultiMouseDownListener=function(a,c,d,f,g){var h=0,i,j,k=function(a){h+=1,h==1&&(i=a.clientX,j=a.clientY,setTimeout(function(){h=0},f||600));var e=b.getButton(a)==c;if(!e||Math.abs(a.clientX-i)>5||Math.abs(a.clientY-j)>5)h=0;h==d&&(h=0,g(a));if(e)return b.preventDefault(a)};b.addListener(a,"mousedown",k),e.isIE&&b.addListener(a,"dblclick",k)},b.addCommandKeyListener=function(a,c){var d=b.addListener;if(e.isOldGecko){var f=null;d(a,"keydown",function(a){f=a.keyCode}),d(a,"keypress",function(a){return g(c,a,f)})}else{var h=null;d(a,"keydown",function(a){h=a.keyIdentifier||a.keyCode;return g(c,a,a.keyCode)}),e.isMac&&e.isOpera&&d(a,"keypress",function(a){var b=a.keyIdentifier||a.keyCode;if(h!==b)return g(c,a,a.keyCode);h=null})}}}),define("ace/editor",["require","exports","module","pilot/fixoldbrowsers","pilot/oop","pilot/event","pilot/lang","pilot/useragent","ace/keyboard/textinput","ace/mouse_handler","ace/keyboard/keybinding","ace/edit_session","ace/search","ace/range","pilot/event_emitter"],function(a,b,c){a("pilot/fixoldbrowsers");var d=a("pilot/oop"),e=a("pilot/event"),f=a("pilot/lang"),g=a("pilot/useragent"),h=a("ace/keyboard/textinput").TextInput,i=a("ace/mouse_handler").MouseHandler,j=a("ace/keyboard/keybinding").KeyBinding,k=a("ace/edit_session").EditSession,l=a("ace/search").Search,m=a("ace/range").Range,n=a("pilot/event_emitter").EventEmitter,o=function(a,b){var c=a.getContainerElement();this.container=c,this.renderer=a,this.textInput=new h(a.getTextAreaContainer(),this),this.keyBinding=new j(this),g.isIPad||(this.$mouseHandler=new i(this)),this.$blockScrolling=0,this.$search=(new l).set({wrap:!0}),this.setSession(b||new k(""))};(function(){d.implement(this,n),this.$forwardEvents={gutterclick:1,gutterdblclick:1},this.$originalAddEventListener=this.addEventListener,this.$originalRemoveEventListener=this.removeEventListener,this.addEventListener=function(a,b){return this.$forwardEvents[a]?this.renderer.addEventListener(a,b):this.$originalAddEventListener(a,b)},this.removeEventListener=function(a,b){return this.$forwardEvents[a]?this.renderer.removeEventListener(a,b):this.$originalRemoveEventListener(a,b)},this.setKeyboardHandler=function(a){this.keyBinding.setKeyboardHandler(a)},this.getKeyboardHandler=function(){return this.keyBinding.getKeyboardHandler()},this.setSession=function(a){if(this.session!=a){if(this.session){var b=this.session;this.session.removeEventListener("change",this.$onDocumentChange),this.session.removeEventListener("changeMode",this.$onChangeMode),this.session.removeEventListener("tokenizerUpdate",this.$onTokenizerUpdate),this.session.removeEventListener("changeTabSize",this.$onChangeTabSize),this.session.removeEventListener("changeWrapLimit",this.$onChangeWrapLimit),this.session.removeEventListener("changeWrapMode",this.$onChangeWrapMode),this.session.removeEventListener("onChangeFold",this.$onChangeFold),this.session.removeEventListener("changeFrontMarker",this.$onChangeFrontMarker),this.session.removeEventListener("changeBackMarker",this.$onChangeBackMarker),this.session.removeEventListener("changeBreakpoint",this.$onChangeBreakpoint),this.session.removeEventListener("changeAnnotation",this.$onChangeAnnotation),this.session.removeEventListener("changeOverwrite",this.$onCursorChange);var c=this.session.getSelection();c.removeEventListener("changeCursor",this.$onCursorChange),c.removeEventListener("changeSelection",this.$onSelectionChange),this.session.setScrollTopRow(this.renderer.getScrollTopRow())}this.session=a,this.$onDocumentChange=this.onDocumentChange.bind(this),a.addEventListener("change",this.$onDocumentChange),this.renderer.setSession(a),this.$onChangeMode=this.onChangeMode.bind(this),a.addEventListener("changeMode",this.$onChangeMode),this.$onTokenizerUpdate=this.onTokenizerUpdate.bind(this),a.addEventListener("tokenizerUpdate",this.$onTokenizerUpdate),this.$onChangeTabSize=this.renderer.updateText.bind(this.renderer),a.addEventListener("changeTabSize",this.$onChangeTabSize),this.$onChangeWrapLimit=this.onChangeWrapLimit.bind(this),a.addEventListener("changeWrapLimit",this.$onChangeWrapLimit),this.$onChangeWrapMode=this.onChangeWrapMode.bind(this),a.addEventListener("changeWrapMode",this.$onChangeWrapMode),this.$onChangeFold=this.onChangeFold.bind(this),a.addEventListener("changeFold",this.$onChangeFold),this.$onChangeFrontMarker=this.onChangeFrontMarker.bind(this),this.session.addEventListener("changeFrontMarker",this.$onChangeFrontMarker),this.$onChangeBackMarker=this.onChangeBackMarker.bind(this),this.session.addEventListener("changeBackMarker",this.$onChangeBackMarker),this.$onChangeBreakpoint=this.onChangeBreakpoint.bind(this),this.session.addEventListener("changeBreakpoint",this.$onChangeBreakpoint),this.$onChangeAnnotation=this.onChangeAnnotation.bind(this),this.session.addEventListener("changeAnnotation",this.$onChangeAnnotation),this.$onCursorChange=this.onCursorChange.bind(this),this.session.addEventListener("changeOverwrite",this.$onCursorChange),this.selection=a.getSelection(),this.selection.addEventListener("changeCursor",this.$onCursorChange),this.$onSelectionChange=this.onSelectionChange.bind(this),this.selection.addEventListener("changeSelection",this.$onSelectionChange),this.onChangeMode(),this.onCursorChange(),this.onSelectionChange(),this.onChangeFrontMarker(),this.onChangeBackMarker(),this.onChangeBreakpoint(),this.onChangeAnnotation(),this.renderer.scrollToRow(a.getScrollTopRow()),this.renderer.updateFull(),this._dispatchEvent("changeSession",{session:a,oldSession:b})}},this.getSession=function(){return this.session},this.getSelection=function(){return this.selection},this.resize=function(){this.renderer.onResize()},this.setTheme=function(a){this.renderer.setTheme(a)},this.getTheme=function(){return this.renderer.getTheme()},this.setStyle=function(a){this.renderer.setStyle(a)},this.unsetStyle=function(a){this.renderer.unsetStyle(a)},this.$highlightBrackets=function(){this.session.$bracketHighlight&&(this.session.removeMarker(this.session.$bracketHighlight),this.session.$bracketHighlight=null);if(!this.$highlightPending){var a=this;this.$highlightPending=!0,setTimeout(function(){a.$highlightPending=!1;var b=a.session.findMatchingBracket(a.getCursorPosition());if(b){var c=new m(b.row,b.column,b.row,b.column+1);a.session.$bracketHighlight=a.session.addMarker(c,"ace_bracket")}},10)}},this.focus=function(){var a=this;g.isIE||setTimeout(function(){a.textInput.focus()}),this.textInput.focus()},this.blur=function(){this.textInput.blur()},this.onFocus=function(){this.renderer.showCursor(),this.renderer.visualizeFocus(),this._dispatchEvent("focus")},this.onBlur=function(){this.renderer.hideCursor(),this.renderer.visualizeBlur(),this._dispatchEvent("blur")},this.onDocumentChange=function(a){var b=a.data,c=b.range;if(c.start.row==c.end.row&&b.action!="insertLines"&&b.action!="removeLines")var d=c.end.row;else d=Infinity;this.renderer.updateLines(c.start.row,d),this.renderer.updateCursor()},this.onTokenizerUpdate=function(a){var b=a.data;this.renderer.updateLines(b.first,b.last)},this.onCursorChange=function(a){this.renderer.updateCursor(),this.$blockScrolling||this.renderer.scrollCursorIntoView(),this.renderer.moveTextAreaToCursor(this.textInput.getElement()),this.$highlightBrackets(),this.$updateHighlightActiveLine()},this.$updateHighlightActiveLine=function(){var a=this.getSession();a.$highlightLineMarker&&a.removeMarker(a.$highlightLineMarker),a.$highlightLineMarker=null;if(this.getHighlightActiveLine()&&(this.getSelectionStyle()!="line"||!this.selection.isMultiLine())){var b=this.getCursorPosition(),c=this.session.getFoldLine(b.row),d;c?d=new m(c.start.row,0,c.end.row+1,0):d=new m(b.row,0,b.row+1,0),a.$highlightLineMarker=a.addMarker(d,"ace_active_line","line")}},this.onSelectionChange=function(a){var b=this.getSession();b.$selectionMarker&&b.removeMarker(b.$selectionMarker),b.$selectionMarker=null;if(!this.selection.isEmpty()){var c=this.selection.getRange(),d=this.getSelectionStyle();b.$selectionMarker=b.addMarker(c,"ace_selection",d)}else this.$updateHighlightActiveLine();this.$highlightSelectedWord&&this.session.getMode().highlightSelection(this)},this.onChangeFrontMarker=function(){this.renderer.updateFrontMarkers()},this.onChangeBackMarker=function(){this.renderer.updateBackMarkers()},this.onChangeBreakpoint=function(){this.renderer.setBreakpoints(this.session.getBreakpoints())},this.onChangeAnnotation=function(){this.renderer.setAnnotations(this.session.getAnnotations())},this.onChangeMode=function(){this.renderer.updateText()},this.onChangeWrapLimit=function(){this.renderer.updateFull()},this.onChangeWrapMode=function(){this.renderer.onResize(!0)},this.onChangeFold=function(){this.$updateHighlightActiveLine(),this.renderer.updateFull()},this.getCopyText=function(){return this.selection.isEmpty()?"":this.session.getTextRange(this.getSelectionRange())},this.onCut=function(){this.$readOnly||this.selection.isEmpty()||(this.session.remove(this.getSelectionRange()),this.clearSelection())},this.insert=function(a){if(!this.$readOnly){var b=this.session,c=b.getMode(),d=this.getCursorPosition();if(this.getBehavioursEnabled()){var e=c.transformAction(b.getState(d.row),"insertion",this,b,a);e&&(a=e.text)}a=a.replace("\t",this.session.getTabString());if(!this.selection.isEmpty()){var d=this.session.remove(this.getSelectionRange());this.clearSelection()}else if(this.session.getOverwrite()){var f=new m.fromPoints(d,d);f.end.column+=a.length,this.session.remove(f)}this.clearSelection();var g=d.column,h=b.getState(d.row),i=c.checkOutdent(h,b.getLine(d.row),a),j=b.getLine(d.row),k=c.getNextLineIndent(h,j.slice(0,d.column),b.getTabString()),l=b.insert(d,a);e&&e.selection&&(e.selection.length==2?this.selection.setSelectionRange(new m(d.row,g+e.selection[0],d.row,g+e.selection[1])):this.selection.setSelectionRange(new m(d.row+e.selection[0],e.selection[1],d.row+e.selection[2],e.selection[3])));var h=b.getState(d.row);if(b.getDocument().isNewLine(a)){this.moveCursorTo(d.row+1,0);var n=b.getTabSize(),o=Number.MAX_VALUE;for(var p=d.row+1;p<=l.row;++p){var q=0;j=b.getLine(p);for(var r=0;r<j.length;++r)if(j.charAt(r)=="\t")q+=n;else if(j.charAt(r)==" ")q+=1;else break;/[^\s]/.test(j)&&(o=Math.min(q,o))}for(var p=d.row+1;p<=l.row;++p){var s=o;j=b.getLine(p);for(var r=0;r<j.length&&s>0;++r)j.charAt(r)=="\t"?s-=n:j.charAt(r)==" "&&(s-=1);b.remove(new m(p,0,p,r))}b.indentRows(d.row+1,l.row,k)}else i&&c.autoOutdent(h,b,d.row)}},this.onTextInput=function(a){this.keyBinding.onTextInput(a)},this.onCommandKey=function(a,b,c){this.keyBinding.onCommandKey(a,b,c)},this.setOverwrite=function(a){this.session.setOverwrite(a)},this.getOverwrite=function(){return this.session.getOverwrite()},this.toggleOverwrite=function(){this.session.toggleOverwrite()},this.setScrollSpeed=function(a){this.$mouseHandler.setScrollSpeed(a)},this.getScrollSpeed=function(){return this.$mouseHandler.getScrollSpeed()},this.$selectionStyle="line",this.setSelectionStyle=function(a){this.$selectionStyle!=a&&(this.$selectionStyle=a,this.onSelectionChange(),this._dispatchEvent("changeSelectionStyle",{data:a}))},this.getSelectionStyle=function(){return this.$selectionStyle},this.$highlightActiveLine=!0,this.setHighlightActiveLine=function(a){this.$highlightActiveLine!=a&&(this.$highlightActiveLine=a,this.$updateHighlightActiveLine())},this.getHighlightActiveLine=function(){return this.$highlightActiveLine},this.$highlightSelectedWord=!0,this.setHighlightSelectedWord=function(a){this.$highlightSelectedWord!=a&&(this.$highlightSelectedWord=a,a?this.session.getMode().highlightSelection(this):this.session.getMode().clearSelectionHighlight(this))},this.getHighlightSelectedWord=function(){return this.$highlightSelectedWord},this.setShowInvisibles=function(a){this.getShowInvisibles()!=a&&this.renderer.setShowInvisibles(a)},this.getShowInvisibles=function(){return this.renderer.getShowInvisibles()},this.setShowPrintMargin=function(a){this.renderer.setShowPrintMargin(a)},this.getShowPrintMargin=function(){return this.renderer.getShowPrintMargin()},this.setPrintMarginColumn=function(a){this.renderer.setPrintMarginColumn(a)},this.getPrintMarginColumn=function(){return this.renderer.getPrintMarginColumn()},this.$readOnly=!1,this.setReadOnly=function(a){this.$readOnly=a},this.getReadOnly=function(){return this.$readOnly},this.$modeBehaviours=!1,this.setBehavioursEnabled=function(a){this.$modeBehaviours=a},this.getBehavioursEnabled=function(){return this.$modeBehaviours},this.removeRight=function(){this.$readOnly||(this.selection.isEmpty()&&this.selection.selectRight(),this.session.remove(this.getSelectionRange()),this.clearSelection())},this.removeLeft=function(){if(!this.$readOnly){this.selection.isEmpty()&&this.selection.selectLeft();var a=this.getSelectionRange();if(this.getBehavioursEnabled()){var b=this.session,c=b.getState(a.start.row),d=b.getMode().transformAction(c,"deletion",this,b,a);d!==!1&&(a=d)}this.session.remove(a),this.clearSelection()}},this.removeWordRight=function(){this.$readOnly||(this.selection.isEmpty()&&this.selection.selectWordRight(),this.session.remove(this.getSelectionRange()),this.clearSelection())},this.removeWordLeft=function(){this.$readOnly||(this.selection.isEmpty()&&this.selection.selectWordLeft(),this.session.remove(this.getSelectionRange()),this.clearSelection())},this.removeToLineStart=function(){this.$readOnly||(this.selection.isEmpty()&&this.selection.selectLineStart(),this.session.remove(this.getSelectionRange()),this.clearSelection())},this.removeToLineEnd=function(){if(!this.$readOnly){this.selection.isEmpty()&&this.selection.selectLineEnd();var a=this.getSelectionRange();a.start.column==a.end.column&&a.start.row==a.end.row&&(a.end.column=0,a.end.row++),this.session.remove(a),this.clearSelection()}},this.splitLine=function(){if(!this.$readOnly){this.selection.isEmpty()||(this.session.remove(this.getSelectionRange()),this.clearSelection());var a=this.getCursorPosition();this.insert("\n"),this.moveCursorToPosition(a)}},this.transposeLetters=function(){if(!this.$readOnly){if(!this.selection.isEmpty())return;var a=this.getCursorPosition(),b=a.column;if(b==0)return;var c=this.session.getLine(a.row);if(b<c.length)var d=c.charAt(b)+c.charAt(b-1),e=new m(a.row,b-1,a.row,b+1);else var d=c.charAt(b-1)+c.charAt(b-2),e=new m(a.row,b-2,a.row,b);this.session.replace(e,d)}},this.indent=function(){if(!this.$readOnly){var a=this.session,b=this.getSelectionRange();if(!(b.start.row<b.end.row||b.start.column<b.end.column)){var d;if(this.session.getUseSoftTabs()){var e=a.getTabSize(),g=this.getCursorPosition(),h=a.documentToScreenColumn(g.row,g.column),i=e-h%e;d=f.stringRepeat(" ",i)}else d="\t";return this.onTextInput(d)}var c=this.$getSelectedRows();a.indentRows(c.first,c.last,"\t")}},this.blockOutdent=function(){if(!this.$readOnly){var a=this.session.getSelection();this.session.outdentRows(a.getRange())}},this.toggleCommentLines=function(){if(!this.$readOnly){var a=this.session.getState(this.getCursorPosition().row),b=this.$getSelectedRows();this.session.getMode().toggleCommentLines(a,this.session,b.first,b.last)}},this.removeLines=function(){if(!this.$readOnly){var a=this.$getSelectedRows();this.session.remove(new m(a.first,0,a.last+1,0)),this.clearSelection()}},this.moveLinesDown=function(){this.$readOnly||this.$moveLines(function(a,b){return this.session.moveLinesDown(a,b)})},this.moveLinesUp=function(){this.$readOnly||this.$moveLines(function(a,b){return this.session.moveLinesUp(a,b)})},this.moveText=function(a,b){if(this.$readOnly)return null;return this.session.moveText(a,b)},this.copyLinesUp=function(){this.$readOnly||this.$moveLines(function(a,b){this.session.duplicateLines(a,b);return 0})},this.copyLinesDown=function(){this.$readOnly||this.$moveLines(function(a,b){return this.session.duplicateLines(a,b)})},this.$moveLines=function(a){var b=this.$getSelectedRows(),c=a.call(this,b.first,b.last),d=this.selection;d.setSelectionAnchor(b.last+c+1,0),d.$moveSelection(function(){d.moveCursorTo(b.first+c,0)})},this.$getSelectedRows=function(){var a=this.getSelectionRange().collapseRows();return{first:a.start.row,last:a.end.row}},this.onCompositionStart=function(a){this.renderer.showComposition(this.getCursorPosition())},this.onCompositionUpdate=function(a){this.renderer.setCompositionText(a)},this.onCompositionEnd=function(){this.renderer.hideComposition()},this.getFirstVisibleRow=function(){return this.renderer.getFirstVisibleRow()},this.getLastVisibleRow=function(){return this.renderer.getLastVisibleRow()},this.isRowVisible=function(a){return a>=this.getFirstVisibleRow()&&a<=this.getLastVisibleRow()},this.$getVisibleRowCount=function(){return this.renderer.getScrollBottomRow()-this.renderer.getScrollTopRow()+1},this.$getPageDownRow=function(){return this.renderer.getScrollBottomRow()},this.$getPageUpRow=function(){var a=this.renderer.getScrollTopRow(),b=this.renderer.getScrollBottomRow();return a-(b-a)},this.selectPageDown=function(){var a=this.$getPageDownRow()+Math.floor(this.$getVisibleRowCount()/2);this.scrollPageDown();var b=this.getSelection(),c=this.session.documentToScreenPosition(b.getSelectionLead()),d=this.session.screenToDocumentPosition(a,c.column);b.selectTo(d.row,d.column)},this.selectPageUp=function(){var a=this.renderer.getScrollTopRow()-this.renderer.getScrollBottomRow(),b=this.$getPageUpRow()+Math.round(a/2);this.scrollPageUp();var c=this.getSelection(),d=this.session.documentToScreenPosition(c.getSelectionLead()),e=this.session.screenToDocumentPosition(b,d.column);c.selectTo(e.row,e.column)},this.gotoPageDown=function(){var a=this.$getPageDownRow(),b=this.getCursorPositionScreen().column;this.scrollToRow(a),this.getSelection().moveCursorToScreen(a,b)},this.gotoPageUp=function(){var a=this.$getPageUpRow(),b=this.getCursorPositionScreen().column;this.scrollToRow(a),this.getSelection().moveCursorToScreen(a,b)},this.scrollPageDown=function(){this.scrollToRow(this.$getPageDownRow())},this.scrollPageUp=function(){this.renderer.scrollToRow(this.$getPageUpRow())},this.scrollToRow=function(a){this.renderer.scrollToRow(a)},this.scrollToLine=function(a,b){this.renderer.scrollToLine(a,b)},this.centerSelection=function(){var a=this.getSelectionRange(),b=Math.floor(a.start.row+(a.end.row-a.start.row)/2);this.renderer.scrollToLine(b,!0)},this.getCursorPosition=function(){return this.selection.getCursor()},this.getCursorPositionScreen=function(){return this.session.documentToScreenPosition(this.getCursorPosition())},this.getSelectionRange=function(){return this.selection.getRange()},this.selectAll=function(){this.$blockScrolling+=1,this.selection.selectAll(),this.$blockScrolling-=1},this.clearSelection=function(){this.selection.clearSelection()},this.moveCursorTo=function(a,b){this.selection.moveCursorTo(a,b)},this.moveCursorToPosition=function(a){this.selection.moveCursorToPosition(a)},this.gotoLine=function(a,b){this.selection.clearSelection(),this.$blockScrolling+=1,this.moveCursorTo(a-1,b||0),this.$blockScrolling-=1,this.isRowVisible(this.getCursorPosition().row)||this.scrollToLine(a,!0)},this.navigateTo=function(a,b){this.clearSelection(),this.moveCursorTo(a,b)},this.navigateUp=function(a){this.selection.clearSelection(),a=a||1,this.selection.moveCursorBy(-a,0)},this.navigateDown=function(a){this.selection.clearSelection(),a=a||1,this.selection.moveCursorBy(a,0)},this.navigateLeft=function(a){if(!this.selection.isEmpty()){var b=this.getSelectionRange().start;this.moveCursorToPosition(b)}else{a=a||1;while(a--)this.selection.moveCursorLeft()}this.clearSelection()},this.navigateRight=function(a){if(!this.selection.isEmpty()){var b=this.getSelectionRange().end;this.moveCursorToPosition(b)}else{a=a||1;while(a--)this.selection.moveCursorRight()}this.clearSelection()},this.navigateLineStart=function(){this.selection.moveCursorLineStart(),this.clearSelection()},this.navigateLineEnd=function(){this.selection.moveCursorLineEnd(),this.clearSelection()},this.navigateFileEnd=function(){this.selection.moveCursorFileEnd(),this.clearSelection()},this.navigateFileStart=function(){this.selection.moveCursorFileStart(),this.clearSelection()},this.navigateWordRight=function(){this.selection.moveCursorWordRight(),this.clearSelection()},this.navigateWordLeft=function(){this.selection.moveCursorWordLeft(),this.clearSelection()},this.replace=function(a,b){b&&this.$search.set(b);var c=this.$search.find(this.session);this.$tryReplace(c,a),c!==null&&this.selection.setSelectionRange(c)},this.replaceAll=function(a,b){b&&this.$search.set(b);var c=this.$search.findAll(this.session);if(!!c.length){var d=this.getSelectionRange();this.clearSelection(),this.selection.moveCursorTo(0,0),this.$blockScrolling+=1;for(var e=c.length-1;e>=0;--e)this.$tryReplace(c[e],a);this.selection.setSelectionRange(d),this.$blockScrolling-=1}},this.$tryReplace=function(a,b){var c=this.session.getTextRange(a),b=this.$search.replace(c,b);if(b!==null){a.end=this.session.replace(a,b);return a}return null},this.getLastSearchOptions=function(){return this.$search.getOptions()},this.find=function(a,b){this.clearSelection(),b=b||{},b.needle=a,this.$search.set(b),this.$find()},this.findNext=function(a){a=a||{},typeof a.backwards=="undefined"&&(a.backwards=!1),this.$search.set(a),this.$find()},this.findPrevious=function(a){a=a||{},typeof a.backwards=="undefined"&&(a.backwards=!0),this.$search.set(a),this.$find()},this.$find=function(a){this.selection.isEmpty()||this.$search.set({needle:this.session.getTextRange(this.getSelectionRange())}),typeof a!="undefined"&&this.$search.set({backwards:a});var b=this.$search.find(this.session);b&&(this.gotoLine(b.end.row+1,b.end.column),this.selection.setSelectionRange(b))},this.undo=function(){this.session.getUndoManager().undo()},this.redo=function(){this.session.getUndoManager().redo()},this.destroy=function(){this.renderer.destroy()}}).call(o.prototype),b.Editor=o}),define("ace/keyboard/textinput",["require","exports","module","pilot/event","pilot/useragent","pilot/dom"],function(a,b,c){var d=a("pilot/event"),e=a("pilot/useragent"),f=a("pilot/dom"),g=function(a,b){function k(a){if(!i){var d=a||c.value;d&&(d.charCodeAt(d.length-1)==g.charCodeAt(0)?(d=d.slice(0,-1),d&&b.onTextInput(d)):b.onTextInput(d))}i=!1,c.value=g,c.select()}var c=f.createElement("textarea");c.style.left="-10000px",a.appendChild(c);var g=String.fromCharCode(0);k();var h=!1,i=!1,j="",l=function(a){e.isIE&&c.value.charCodeAt(0)>128||setTimeout(function(){h||k()},0)},m=function(a){h=!0,e.isIE||(k(),c.value=""),b.onCompositionStart(),e.isGecko||setTimeout(n,0)},n=function(){!h||b.onCompositionUpdate(c.value)},o=function(a){h=!1,b.onCompositionEnd(),e.isGecko?k():setTimeout(function(){h||k()},0)},p=function(a){i=!0;var d=b.getCopyText();d?c.value=d:a.preventDefault(),c.select(),setTimeout(function(){k()},0)},q=function(a){i=!0;var d=b.getCopyText();d?(c.value=d,b.onCut()):a.preventDefault(),c.select(),setTimeout(function(){k()},0)};d.addCommandKeyListener(c,b.onCommandKey.bind(b)),d.addListener(c,"keypress",l);if(e.isIE){var r={13:1,27:1};d.addListener(c,"keyup",function(a){h&&(!c.value||r[a.keyCode])&&setTimeout(o,0);(c.value.charCodeAt(0)|0)<129||(h?n():m())})}d.addListener(c,"textInput",l),d.addListener(c,"paste",function(a){a.clipboardData&&a.clipboardData.getData?(k(a.clipboardData.getData("text/plain")),a.preventDefault()):l()}),e.isIE||d.addListener(c,"propertychange",l),e.isIE?(d.addListener(c,"beforecopy",function(a){var c=b.getCopyText();c?clipboardData.setData("Text",c):a.preventDefault()}),d.addListener(a,"keydown",function(a){if(a.ctrlKey&&a.keyCode==88){var c=b.getCopyText();c&&(clipboardData.setData("Text",c),b.onCut()),d.preventDefault(a)}})):(d.addListener(c,"copy",p),d.addListener(c,"cut",q)),d.addListener(c,"compositionstart",m),e.isGecko&&d.addListener(c,"text",n),e.isWebKit&&d.addListener(c,"keyup",n),d.addListener(c,"compositionend",o),d.addListener(c,"blur",function(){b.onBlur()}),d.addListener(c,"focus",function(){b.onFocus(),c.select()}),this.focus=function(){b.onFocus(),c.select(),c.focus()},this.blur=function(){c.blur()},this.getElement=function(){return c},this.onContextMenu=function(a,b){a&&(j||(j=c.style.cssText),c.style.cssText="position:fixed; z-index:1000;left:"+(a.x-2)+"px; top:"+(a.y-2)+"px;"),b&&(c.value="")},this.onContextMenuClose=function(){setTimeout(function(){j&&(c.style.cssText=j,j=""),k()},0)}};b.TextInput=g}),define("ace/mouse_handler",["require","exports","module","pilot/event","pilot/dom"],function(a,b,c){var d=a("pilot/event"),e=a("pilot/dom"),f=0,g=1,h=2,i=250,j=5,k=function(a){this.editor=a,d.addListener(a.container,"mousedown",function(b){a.focus();return d.preventDefault(b)}),d.addListener(a.container,"selectstart",function(a){return d.preventDefault(a)});var b=a.renderer.getMouseEventTarget();d.addListener(b,"mousedown",this.onMouseDown.bind(this)),d.addMultiMouseDownListener(b,0,2,500,this.onMouseDoubleClick.bind(this)),d.addMultiMouseDownListener(b,0,3,600,this.onMouseTripleClick.bind(this)),d.addMultiMouseDownListener(b,0,4,600,this.onMouseQuadClick.bind(this)),d.addMouseWheelListener(b,this.onMouseWheel.bind(this))};(function(){this.$scrollSpeed=1,this.setScrollSpeed=function(a){this.$scrollSpeed=a},this.getScrollSpeed=function(){return this.$scrollSpeed},this.$getEventPosition=function(a){var b=d.getDocumentX(a),c=d.getDocumentY(a),e=this.editor.renderer.screenToTextCoordinates(b,c);e.row=Math.max(0,Math.min(e.row,this.editor.session.getLength()-1));return e},this.$distance=function(a,b,c,d){return Math.sqrt(Math.pow(c-a,2)+Math.pow(d-b,2))},this.onMouseDown=function(a){function D(b){a.shiftKey?l.selection.selectToPosition(b):m.$clickSelection||(l.moveCursorToPosition(b),l.selection.clearSelection(b.row,b.column)),p=g}var b=d.getDocumentX(a),c=d.getDocumentY(a),k=this.$getEventPosition(a),l=this.editor,m=this,n=l.getSelectionRange(),o=n.isEmpty(),p=f,q=!1,r=d.getButton(a);if(r!==0)o&&l.moveCursorToPosition(k),r==2&&(l.textInput.onContextMenu({x:b,y:c},o),d.capture(l.container,function(){},l.textInput.onContextMenuClose));else{var s=l.session.getFoldAt(k.row,k.column,1);if(s){l.selection.setSelectionRange(s.range);return}q=!l.getReadOnly()&&!o&&n.contains(k.row,k.column),q||D(k);var t,u,v=l.getOverwrite(),w=(new Date).getTime(),x,y,z=function(a){t=d.getDocumentX(a),u=d.getDocumentY(a)},A=function(){clearInterval(G),p==f?D(k):p==h&&B(),m.$clickSelection=null,p=f},B=function(){e.removeCssClass(l.container,"ace_dragging"),l.session.removeMarker(dragSelectionMarker),m.$clickSelection||x||(l.moveCursorToPosition(k),l.selection.clearSelection(k.row,k.column));if(!!x){if(y.contains(x.row,x.column)){x=null;return}l.clearSelection();var a=l.moveText(y,x);if(!a){x=null;return}l.selection.setSelectionRange(a)}},C=function(){if(t!==undefined&&u!==undefined){if(p==f){var a=m.$distance(b,c,t,u),d=(new Date).getTime();if(a>j){p=g;var k=l.renderer.screenToTextCoordinates(t,u);k.row=Math.max(0,Math.min(k.row,l.session.getLength()-1)),D(k)}else if(d-w>i){p=h,y=l.getSelectionRange();var n=l.getSelectionStyle();dragSelectionMarker=l.session.addMarker(y,"ace_selection",n),l.clearSelection(),e.addCssClass(l.container,"ace_dragging")}}p==h?F():p==g&&E()}},E=function(){var a=l.renderer.screenToTextCoordinates(t,u);a.row=Math.max(0,Math.min(a.row,l.session.getLength()-1));if(m.$clickSelection)if(m.$clickSelection.contains(a.row,a.column))l.selection.setSelectionRange(m.$clickSelection);else{if(m.$clickSelection.compare(a.row,a.column)==-1)var b=m.$clickSelection.end;else var b=m.$clickSelection.start;l.selection.setSelectionAnchor(b.row,b.column),l.selection.selectToPosition(a)}else l.selection.selectToPosition(a);l.renderer.scrollCursorIntoView()},F=function(){x=l.renderer.screenToTextCoordinates(t,u),x.row=Math.max(0,Math.min(x.row,l.session.getLength()-1)),l.moveCursorToPosition(x)};d.capture(l.container,z,A);var G=setInterval(C,20);return d.preventDefault(a)}},this.onMouseDoubleClick=function(a){var b=this.editor,c=this.$getEventPosition(a),d=b.session.getFoldAt(c.row,c.column,1);d?b.session.expandFold(d):(b.moveCursorToPosition(c),b.selection.selectWord(),this.$clickSelection=b.getSelectionRange())},this.onMouseTripleClick=function(a){var b=this.$getEventPosition(a);this.editor.moveCursorToPosition(b),this.editor.selection.selectLine(),this.$clickSelection=this.editor.getSelectionRange()},this.onMouseQuadClick=function(a){this.editor.selectAll(),this.$clickSelection=this.editor.getSelectionRange()},this.onMouseWheel=function(a){var b=this.$scrollSpeed*2;this.editor.renderer.scrollBy(a.wheelX*b,a.wheelY*b);return d.preventDefault(a)}}).call(k.prototype),b.MouseHandler=k}),define("ace/keyboard/keybinding",["require","exports","module","pilot/useragent","pilot/keys","pilot/event","pilot/settings","pilot/canon","ace/commands/default_commands"],function(a,b,c){var d=a("pilot/useragent"),e=a("pilot/keys"),f=a("pilot/event"),g=a("pilot/settings").settings,h=a("pilot/canon");a("ace/commands/default_commands");var i=function(a){this.$editor=a,this.$data={},this.$keyboardHandler=null};(function(){this.setKeyboardHandler=function(a){this.$keyboardHandler!=a&&(this.$data={},this.$keyboardHandler=a)},this.getKeyboardHandler=function(){return this.$keyboardHandler},this.$callKeyboardHandler=function(a,b,c,d){var e={editor:this.$editor},g;this.$keyboardHandler&&(g=this.$keyboardHandler.handleKeyboard(this.$data,b,c,d,a));if(!g||!g.command)b!=0||d!=0?g={command:h.findKeyCommand(e,"editor",b,c)}:g={command:"inserttext",args:{text:c}};if(g){var i=h.exec(g.command,e,"editor",g.args);if(i)return f.stopEvent(a)}},this.onCommandKey=function(a,b,c){var d=e.keyCodeToString(c);this.$callKeyboardHandler(a,b,d,c)},this.onTextInput=function(a){this.$callKeyboardHandler({},0,a,0)}}).call(i.prototype),b.KeyBinding=i}),define("ace/commands/default_commands",["require","exports","module","pilot/lang","pilot/canon"],function(a,b,c){function f(a,b){return{win:a,mac:b,sender:"editor"}}var d=a("pilot/lang"),e=a("pilot/canon");e.addCommand({name:"null",exec:function(a,b,c){}}),e.addCommand({name:"selectall",bindKey:f("Ctrl-A","Command-A"),exec:function(a,b,c){a.editor.selectAll()}}),e.addCommand({name:"removeline",bindKey:f("Ctrl-D","Command-D"),exec:function(a,b,c){a.editor.removeLines()}}),e.addCommand({name:"gotoline",bindKey:f("Ctrl-L","Command-L"),exec:function(a,b,c){var d=parseInt(prompt("Enter line number:"));isNaN(d)||a.editor.gotoLine(d)}}),e.addCommand({name:"togglecomment",bindKey:f("Ctrl-7","Command-7"),exec:function(a,b,c){a.editor.toggleCommentLines()}}),e.addCommand({name:"findnext",bindKey:f("Ctrl-K","Command-G"),exec:function(a,b,c){a.editor.findNext()}}),e.addCommand({name:"findprevious",bindKey:f("Ctrl-Shift-K","Command-Shift-G"),exec:function(a,b,c){a.editor.findPrevious()}}),e.addCommand({name:"find",bindKey:f("Ctrl-F","Command-F"),exec:function(a,b,c){var d=prompt("Find:");a.editor.find(d)}}),e.addCommand({name:"replace",bindKey:f("Ctrl-R","Command-Option-F"),exec:function(a,b,c){var d=prompt("Find:");if(!!d){var e=prompt("Replacement:");if(!e)return;a.editor.replace(e,{needle:d})}}}),e.addCommand({name:"replaceall",bindKey:f("Ctrl-Shift-R","Command-Shift-Option-F"),exec:function(a,b,c){var d=prompt("Find:");if(!!d){var e=prompt("Replacement:");if(!e)return;a.editor.replaceAll(e,{needle:d})}}}),e.addCommand({name:"undo",bindKey:f("Ctrl-Z","Command-Z"),exec:function(a,b,c){a.editor.undo()}}),e.addCommand({name:"redo",bindKey:f("Ctrl-Shift-Z|Ctrl-Y","Command-Shift-Z|Command-Y"),exec:function(a,b,c){a.editor.redo()}}),e.addCommand({name:"overwrite",bindKey:f("Insert","Insert"),exec:function(a,b,c){a.editor.toggleOverwrite()}}),e.addCommand({name:"copylinesup",bindKey:f("Ctrl-Alt-Up","Command-Option-Up"),exec:function(a,b,c){a.editor.copyLinesUp()}}),e.addCommand({name:"movelinesup",bindKey:f("Alt-Up","Option-Up"),exec:function(a,b,c){a.editor.moveLinesUp()}}),e.addCommand({name:"selecttostart",bindKey:f("Ctrl-Shift-Home|Alt-Shift-Up","Command-Shift-Up"),exec:function(a,b,c){a.editor.getSelection().selectFileStart()}}),e.addCommand({name:"gotostart",bindKey:f("Ctrl-Home|Ctrl-Up","Command-Home|Command-Up"),exec:function(a,b,c){a.editor.navigateFileStart()}}),e.addCommand({name:"selectup",bindKey:f("Shift-Up","Shift-Up"),exec:function(a,b,c){a.editor.getSelection().selectUp()}}),e.addCommand({name:"golineup",bindKey:f("Up","Up|Ctrl-P"),exec:function(a,b,c){a.editor.navigateUp(b.times)}}),e.addCommand({name:"copylinesdown",bindKey:f("Ctrl-Alt-Down","Command-Option-Down"),exec:function(a,b,c){a.editor.copyLinesDown()}}),e.addCommand({name:"movelinesdown",bindKey:f("Alt-Down","Option-Down"),exec:function(a,b,c){a.editor.moveLinesDown()}}),e.addCommand({name:"selecttoend",bindKey:f("Ctrl-Shift-End|Alt-Shift-Down","Command-Shift-Down"),exec:function(a,b,c){a.editor.getSelection().selectFileEnd()}}),e.addCommand({name:"gotoend",bindKey:f("Ctrl-End|Ctrl-Down","Command-End|Command-Down"),exec:function(a,b,c){a.editor.navigateFileEnd()}}),e.addCommand({name:"selectdown",bindKey:f("Shift-Down","Shift-Down"),exec:function(a,b,c){a.editor.getSelection().selectDown()}}),e.addCommand({name:"golinedown",bindKey:f("Down","Down|Ctrl-N"),exec:function(a,b,c){a.editor.navigateDown(b.times)}}),e.addCommand({name:"selectwordleft",bindKey:f("Ctrl-Shift-Left","Option-Shift-Left"),exec:function(a,b,c){a.editor.getSelection().selectWordLeft()}}),e.addCommand({name:"gotowordleft",bindKey:f("Ctrl-Left","Option-Left"),exec:function(a,b,c){a.editor.navigateWordLeft()}}),e.addCommand({name:"selecttolinestart",bindKey:f("Alt-Shift-Left","Command-Shift-Left"),exec:function(a,b,c){a.editor.getSelection().selectLineStart()}}),e.addCommand({name:"gotolinestart",bindKey:f("Alt-Left|Home","Command-Left|Home|Ctrl-A"),exec:function(a,b,c){a.editor.navigateLineStart()}}),e.addCommand({name:"selectleft",bindKey:f("Shift-Left","Shift-Left"),exec:function(a,b,c){a.editor.getSelection().selectLeft()}}),e.addCommand({name:"gotoleft",bindKey:f("Left","Left|Ctrl-B"),exec:function(a,b,c){a.editor.navigateLeft(b.times)}}),e.addCommand({name:"selectwordright",bindKey:f("Ctrl-Shift-Right","Option-Shift-Right"),exec:function(a,b,c){a.editor.getSelection().selectWordRight()}}),e.addCommand({name:"gotowordright",bindKey:f("Ctrl-Right","Option-Right"),exec:function(a,b,c){a.editor.navigateWordRight()}}),e.addCommand({name:"selecttolineend",bindKey:f("Alt-Shift-Right","Command-Shift-Right"),exec:function(a,b,c){a.editor.getSelection().selectLineEnd()}}),e.addCommand({name:"gotolineend",bindKey:f("Alt-Right|End","Command-Right|End|Ctrl-E"),exec:function(a,b,c){a.editor.navigateLineEnd()}}),e.addCommand({name:"selectright",bindKey:f("Shift-Right","Shift-Right"),exec:function(a,b,c){a.editor.getSelection().selectRight()}}),e.addCommand({name:"gotoright",bindKey:f("Right","Right|Ctrl-F"),exec:function(a,b,c){a.editor.navigateRight(b.times)}}),e.addCommand({name:"selectpagedown",bindKey:f("Shift-PageDown","Shift-PageDown"),exec:function(a,b,c){a.editor.selectPageDown()}}),e.addCommand({name:"pagedown",bindKey:f(null,"PageDown"),exec:function(a,b,c){a.editor.scrollPageDown()}}),e.addCommand({name:"gotopagedown",bindKey:f("PageDown","Option-PageDown|Ctrl-V"),exec:function(a,b,c){a.editor.gotoPageDown()}}),e.addCommand({name:"selectpageup",bindKey:f("Shift-PageUp","Shift-PageUp"),exec:function(a,b,c){a.editor.selectPageUp()}}),e.addCommand({name:"pageup",bindKey:f(null,"PageUp"),exec:function(a,b,c){a.editor.scrollPageUp()}}),e.addCommand({name:"gotopageup",bindKey:f("PageUp","Option-PageUp"),exec:function(a,b,c){a.editor.gotoPageUp()}}),e.addCommand({name:"selectlinestart",bindKey:f("Shift-Home","Shift-Home"),exec:function(a,b,c){a.editor.getSelection().selectLineStart()}}),e.addCommand({name:"selectlineend",bindKey:f("Shift-End","Shift-End"),exec:function(a,b,c){a.editor.getSelection().selectLineEnd()}}),e.addCommand({name:"del",bindKey:f("Delete","Delete|Ctrl-D"),exec:function(a,b,c){a.editor.removeRight()}}),e.addCommand({name:"backspace",bindKey:f("Ctrl-Backspace|Command-Backspace|Option-Backspace|Shift-Backspace|Backspace","Ctrl-Backspace|Command-Backspace|Shift-Backspace|Backspace|Ctrl-H"),exec:function(a,b,c){a.editor.removeLeft()}}),e.addCommand({name:"removetolinestart",bindKey:f(null,"Option-Backspace"),exec:function(a,b,c){a.editor.removeToLineStart()}}),e.addCommand({name:"removetolineend",bindKey:f(null,"Ctrl-K"),exec:function(a,b,c){a.editor.removeToLineEnd()}}),e.addCommand({name:"removewordleft",bindKey:f(null,"Alt-Backspace|Ctrl-Alt-Backspace"),exec:function(a,b,c){a.editor.removeWordLeft()}}),e.addCommand({name:"removewordright",bindKey:f(null,"Alt-Delete"),exec:function(a,b,c){a.editor.removeWordRight()}}),e.addCommand({name:"outdent",bindKey:f("Shift-Tab","Shift-Tab"),exec:function(a,b,c){a.editor.blockOutdent()}}),e.addCommand({name:"indent",bindKey:f("Tab","Tab"),exec:function(a,b,c){a.editor.indent()}}),e.addCommand({name:"inserttext",exec:function(a,b,c){a.editor.insert(d.stringRepeat(b.text||"",b.times||1))}}),e.addCommand({name:"centerselection",bindKey:f(null,"Ctrl-L"),exec:function(a,b,c){a.editor.centerSelection()}}),e.addCommand({name:"splitline",bindKey:f(null,"Ctrl-O"),exec:function(a,b,c){a.editor.splitLine()}}),e.addCommand({name:"transposeletters",bindKey:f("Ctrl-T","Ctrl-T"),exec:function(a,b,c){a.editor.transposeLetters()}})}),define("ace/edit_session",["require","exports","module","pilot/oop","pilot/lang","pilot/event_emitter","ace/selection","ace/mode/text","ace/range","ace/document","ace/background_tokenizer","ace/edit_session/folding"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("pilot/event_emitter").EventEmitter,g=a("ace/selection").Selection,h=a("ace/mode/text").Mode,j=a("ace/range").Range,k=a("ace/document").Document,l=a("ace/background_tokenizer").BackgroundTokenizer,m=function(a,b){this.$modified=!0,this.$breakpoints=[],this.$frontMarkers={},this.$backMarkers={},this.$markerId=1,this.$rowCache=[],this.$rowCacheSize=1e3,this.$wrapData=[],this.$foldData=[],this.$foldData.toString=function(){var a="";this.forEach(function(b){a+="\n"+b.toString()});return a},this.$docChangeCounter=0,a instanceof k?this.setDocument(a):this.setDocument(new k(a)),this.selection=new g(this),b?this.setMode(b):this.setMode(new h)};(function(){function o(a){if(a<4352)return!1;return a>=4352&&a<=4447||a>=4515&&a<=4519||a>=4602&&a<=4607||a>=9001&&a<=9002||a>=11904&&a<=11929||a>=11931&&a<=12019||a>=12032&&a<=12245||a>=12272&&a<=12283||a>=12288&&a<=12350||a>=12353&&a<=12438||a>=12441&&a<=12543||a>=12549&&a<=12589||a>=12593&&a<=12686||a>=12688&&a<=12730||a>=12736&&a<=12771||a>=12784&&a<=12830||a>=12832&&a<=12871||a>=12880&&a<=13054||a>=13056&&a<=19903||a>=19968&&a<=42124||a>=42128&&a<=42182||a>=43360&&a<=43388||a>=44032&&a<=55203||a>=55216&&a<=55238||a>=55243&&a<=55291||a>=63744&&a<=64255||a>=65040&&a<=65049||a>=65072&&a<=65106||a>=65108&&a<=65126||a>=65128&&a<=65131||a>=65281&&a<=65376||a>=65504&&a<=65510}d.implement(this,f),this.setDocument=function(a){if(this.doc)throw new Error("Document is already set");this.doc=a,a.on("change",this.onChange.bind(this)),a.on("changeStart",this.onChangeStart.bind(this)),a.on("changeEnd",this.onChangeEnd.bind(this)),this.on("changeFold",this.onChangeFold.bind(this))},this.getDocument=function(){return this.doc},this.onChangeStart=function(){this.$docChangeCounter++},this.$resetRowCache=function(a){if(a==0)this.$rowCache=[];else{var b=this.$rowCache;for(var c=0;c<b.length;c++)if(b[c].docRow>=a){b.splice(c,b.length);return}}},this.onChangeEnd=function(){this.$docChangeCounter--,this.$docChangeCounter==0&&!this.$fromUndo&&this.$undoManager&&(this.$deltasFold.length&&(this.$deltas.push({group:"fold",deltas:this.$deltasFold}),this.$deltasFold=[]),this.$deltasDoc&&(this.$deltas.push({group:"doc",deltas:this.$deltasDoc}),this.$deltasDoc=[]),this.$informUndoManager.schedule())},this.onChangeFold=function(a){var b=a.data;this.$resetRowCache(b.start.row)},this.onChange=function(a){var b=a.data;this.$modified=!0,this.$resetRowCache(b.range.start.row);var c=this.$updateInternalDataOnChange(a);!this.$fromUndo&&this.$undoManager&&!b.ignore&&(this.$deltasDoc.push(b),c&&c.length!=0&&this.$deltasFold.push({action:"removeFolds",folds:c})),this.bgTokenizer.start(b.range.start.row),this._dispatchEvent("change",a)},this.setValue=function(a){this.doc.setValue(a),this.$resetRowCache(0),this.$deltas=[],this.$deltasDoc=[],this.$deltasFold=[],this.getUndoManager().reset()},this.getValue=this.toString=function(){return this.doc.getValue()},this.getSelection=function(){return this.selection},this.getState=function(a){return this.bgTokenizer.getState(a)},this.getTokens=function(a,b){return this.bgTokenizer.getTokens(a,b)},this.setUndoManager=function(a){this.$undoManager=a,this.$resetRowCache(0),this.$deltas=[],this.$deltasDoc=[],this.$deltasFold=[],this.$informUndoManager&&this.$informUndoManager.cancel();if(a){var b=this;this.$syncInformUndoManager=function(){b.$informUndoManager.cancel(),b.$deltas.length>0&&a.execute({action:"aceupdate",args:[b.$deltas,b]}),b.$deltas=[]},this.$informUndoManager=e.deferredCall(this.$syncInformUndoManager)}},this.$defaultUndoManager={undo:function(){},redo:function(){},reset:function(){}},this.getUndoManager=function(){return this.$undoManager||this.$defaultUndoManager},this.getTabString=function(){return this.getUseSoftTabs()?e.stringRepeat(" ",this.getTabSize()):"\t"},this.$useSoftTabs=!0,this.setUseSoftTabs=function(a){this.$useSoftTabs!==a&&(this.$useSoftTabs=a)},this.getUseSoftTabs=function(){return this.$useSoftTabs},this.$tabSize=4,this.setTabSize=function(a){!isNaN(a)&&this.$tabSize!==a&&(this.$modified=!0,this.$tabSize=a,this._dispatchEvent("changeTabSize"))},this.getTabSize=function(){return this.$tabSize},this.isTabStop=function(a){return this.$useSoftTabs&&a.column%this.$tabSize==0},this.$overwrite=!1,this.setOverwrite=function(a){this.$overwrite!=a&&(this.$overwrite=a,this._dispatchEvent("changeOverwrite"))},this.getOverwrite=function(){return this.$overwrite},this.toggleOverwrite=function(){this.setOverwrite(!this.$overwrite)},this.getBreakpoints=function(){return this.$breakpoints},this.setBreakpoints=function(a){this.$breakpoints=[];for(var b=0;b<a.length;b++)this.$breakpoints[a[b]]=!0;this._dispatchEvent("changeBreakpoint",{})},this.clearBreakpoints=function(){this.$breakpoints=[],this._dispatchEvent("changeBreakpoint",{})},this.setBreakpoint=function(a){this.$breakpoints[a]=!0,this._dispatchEvent("changeBreakpoint",{})},this.clearBreakpoint=function(a){delete this.$breakpoints[a],this._dispatchEvent("changeBreakpoint",{})},this.getBreakpoints=function(){return this.$breakpoints},this.addMarker=function(a,b,c,d){var e=this.$markerId++,f={range:a,type:c||"line",renderer:typeof c=="function"?c:null,clazz:b,inFront:!!d};d?(this.$frontMarkers[e]=f,this._dispatchEvent("changeFrontMarker")):(this.$backMarkers[e]=f,this._dispatchEvent("changeBackMarker"));return e},this.removeMarker=function(a){var b=this.$frontMarkers[a]||this.$backMarkers[a];if(!!b){var c=b.inFront?this.$frontMarkers:this.$backMarkers;b&&(delete c[a],this._dispatchEvent(b.inFront?"changeFrontMarker":"changeBackMarker"))}},this.getMarkers=function(a){return a?this.$frontMarkers:this.$backMarkers},this.setAnnotations=function(a){this.$annotations={};for(var b=0;b<a.length;b++){var c=a[b],d=c.row;this.$annotations[d]?this.$annotations[d].push(c):this.$annotations[d]=[c]}this._dispatchEvent("changeAnnotation",{})},this.getAnnotations=function(){return this.$annotations},this.clearAnnotations=function(){this.$annotations={},this._dispatchEvent("changeAnnotation",{})},this.$detectNewLine=function(a){var b=a.match(/^.*?(\r?\n)/m);b?this.$autoNewLine=b[1]:this.$autoNewLine="\n"},this.tokenRe=/^[\w\d]+/g,this.nonTokenRe=/^(?:[^\w\d]|[\u3040-\u309F]|[\u30A0-\u30FF]|[\u4E00-\u9FFF\uF900-\uFAFF\u3400-\u4DBF])+/g,this.getWordRange=function(a,b){var c=this.getLine(a),d=!1;b>0&&(d=!!c.charAt(b-1).match(this.tokenRe)),d||(d=!!c.charAt(b).match(this.tokenRe));var e=d?this.tokenRe:this.nonTokenRe,f=b;if(f>0){do f--;while(f>=0&&c.charAt(f).match(e));f++}var g=b;while(g<c.length&&c.charAt(g).match(e))g++;return new j(a,f,a,g)},this.setNewLineMode=function(a){this.doc.setNewLineMode(a)},this.getNewLineMode=function(){return this.doc.getNewLineMode()},this.$useWorker=!0,this.setUseWorker=function(a){this.$useWorker!=a&&(this.$useWorker=a,this.$stopWorker(),a&&this.$startWorker())},this.getUseWorker=function(){return this.$useWorker},this.onReloadTokenizer=function(a){var b=a.data;this.bgTokenizer.start(b.first),this._dispatchEvent("tokenizerUpdate",a)},this.$mode=null,this.setMode=function(a){if(this.$mode!==a){this.$mode=a,this.$stopWorker(),this.$useWorker&&this.$startWorker();var b=a.getTokenizer();if(b.addEventListener!==undefined){var c=this.onReloadTokenizer.bind(this);b.addEventListener("update",c)}if(!this.bgTokenizer){this.bgTokenizer=new l(b);var d=this;this.bgTokenizer.addEventListener("update",function(a){d._dispatchEvent("tokenizerUpdate",a)})}else this.bgTokenizer.setTokenizer(b);this.bgTokenizer.setDocument(this.getDocument()),this.bgTokenizer.start(0),this._dispatchEvent("changeMode")}},this.$stopWorker=function(){this.$worker&&this.$worker.terminate(),this.$worker=null},this.$startWorker=function(){if(typeof Worker!="undefined"&&!a.noWorker)try{this.$worker=this.$mode.createWorker(this)}catch(b){console.log("Could not load worker"),console.log(b),this.$worker=null}else this.$worker=null},this.getMode=function(){return this.$mode},this.$scrollTop=0,this.setScrollTopRow=function(a){this.$scrollTop!==a&&(this.$scrollTop=a,this._dispatchEvent("changeScrollTop"))},this.getScrollTopRow=function(){return this.$scrollTop},this.getWidth=function(){this.$computeWidth();return this.width},this.getScreenWidth=function(){this.$computeWidth();return this.screenWidth},this.$computeWidth=function(a){if(this.$modified||a){this.$modified=!1;var b=this.doc.getAllLines(),c=0,d=0;for(var e=0;e<b.length;e++){var f=this.getFoldLine(e),g,h;g=b[e];if(f){var i=f.range.end;g=this.getFoldDisplayLine(f),e=i.row}h=g.length,c=Math.max(c,h),this.$useWrapMode||(d=Math.max(d,this.$getStringScreenWidth(g)[0]))}this.width=c,this.$useWrapMode?this.screenWidth=this.$wrapLimit:this.screenWidth=d}},this.getLine=function(a){return this.doc.getLine(a)},this.getLines=function(a,b){return this.doc.getLines(a,b)},this.getLength=function(){return this.doc.getLength()},this.getTextRange=function(a){return this.doc.getTextRange(a)},this.findMatchingBracket=function(a){if(a.column==0)return null;var b=this.getLine(a.row).charAt(a.column-1);if(b=="")return null;var c=b.match(/([\(\[\{])|([\)\]\}])/);if(!c)return null;return c[1]?this.$findClosingBracket(c[1],a):this.$findOpeningBracket(c[2],a)},this.$brackets={")":"(","(":")","]":"[","[":"]","{":"}","}":"{"},this.$findOpeningBracket=function(a,b){var c=this.$brackets[a],d=b.column-2,e=b.row,f=1,g=this.getLine(e);for(;;){while(d>=0){var h=g.charAt(d);if(h==c){f-=1;if(f==0)return{row:e,column:d}}else h==a&&(f+=1);d-=1}e-=1;if(e<0)break;var g=this.getLine(e),d=g.length-1}return null},this.$findClosingBracket=function(a,b){var c=this.$brackets[a],d=b.column,e=b.row,f=1,g=this.getLine(e),h=this.getLength();for(;;){while(d<g.length){var i=g.charAt(d);if(i==c){f-=1;if(f==0)return{row:e,column:d}}else i==a&&(f+=1);d+=1}e+=1;if(e>=h)break;var g=this.getLine(e),d=0}return null},this.insert=function(a,b){return this.doc.insert(a,b)},this.remove=function(a){return this.doc.remove(a)},this.undoChanges=function(a,b){if(!!a.length){this.$fromUndo=!0;var c=null;for(var d=a.length-1;d!=-1;d--)delta=a[d],delta.group=="doc"?(this.doc.revertDeltas(delta.deltas),c=this.$getUndoSelection(delta.deltas,!0,c)):delta.deltas.forEach(function(a){this.addFolds(a.folds)},this);this.$fromUndo=!1,c&&!b&&this.selection.setSelectionRange(c);return c}},this.redoChanges=function(a,b){if(!!a.length){this.$fromUndo=!0;var c=null;for(var d=0;d<a.length;d++)delta=a[d],delta.group=="doc"&&(this.doc.applyDeltas(delta.deltas),c=this.$getUndoSelection(delta.deltas,!1,c));this.$fromUndo=!1,c&&!b&&this.selection.setSelectionRange(c);return c}},this.$getUndoSelection=function(a,b,c){function d(a){var c=a.action=="insertText"||a.action=="insertLines";return b?!c:c}var e=a[0],f,g,h=!1;d(e)?(f=e.range.clone(),h=!0):(f=j.fromPoints(e.range.start,e.range.start),h=!1);for(var i=1;i<a.length;i++)e=a[i],d(e)?(g=e.range.start,f.compare(g.row,g.column)==-1&&f.setStart(e.range.start),g=e.range.end,f.compare(g.row,g.column)==1&&f.setEnd(e.range.end),h=!0):(g=e.range.start,f.compare(g.row,g.column)==-1&&(f=j.fromPoints(e.range.start,e.range.start)),h=!1);if(c!=null){var k=c.compareRange(f);k==1?f.setStart(c.start):k==-1&&f.setEnd(c.end)}return f},this.replace=function(a,b){return this.doc.replace(a,b)},this.moveText=function(a,b){var c=this.getTextRange(a);this.remove(a);var d=b.row,e=b.column;!a.isMultiLine()&&a.start.row==d&&a.end.column<e&&(e-=c.length);if(a.isMultiLine()&&a.end.row<d){var f=this.doc.$split(c);d-=f.length-1}var g=d+a.end.row-a.start.row,h=a.isMultiLine()?a.end.column:e+a.end.column-a.start.column,i=new j(d,e,g,h);this.insert(i.start,c);return i},this.indentRows=function(a,b,c){c=c.replace(/\t/g,this.getTabString());for(var d=a;d<=b;d++)this.insert({row:d,column:0},c)},this.outdentRows=function(a){var b=a.collapseRows(),c=new j(0,0,0,0),d=this.getTabSize();for(var e=b.start.row;e<=b.end.row;++e){var f=this.getLine(e);c.start.row=e,c.end.row=e;for(var g=0;g<d;++g)if(f.charAt(g)!=" ")break;g<d&&f.charAt(g)=="\t"?(c.start.column=g,c.end.column=g+1):(c.start.column=0,c.end.column=g),this.remove(c)}},this.moveLinesUp=function(a,b){if(a<=0)return 0;var c=this.doc.removeLines(a,b);this.doc.insertLines(a-1,c);return-1},this.moveLinesDown=function(a,b){if(b>=this.doc.getLength()-1)return 0;var c=this.doc.removeLines(a,b);this.doc.insertLines(a+1,c);return 1},this.duplicateLines=function(a,b){var a=this.$clipRowToDocument(a),b=this.$clipRowToDocument(b),c=this.getLines(a,b);this.doc.insertLines(a,c);var d=b-a+1;return d},this.$clipRowToDocument=function(a){return Math.max(0,Math.min(a,this.doc.getLength()-1))},this.$wrapLimit=80,this.$useWrapMode=!1,this.$wrapLimitRange={min:null,max:null},this.setUseWrapMode=function(a){if(a!=this.$useWrapMode){this.$useWrapMode=a,this.$modified=!0,this.$resetRowCache(0);if(a){var b=this.getLength();this.$wrapData=[];for(i=0;i<b;i++)this.$wrapData.push([]);this.$updateWrapData(0,b-1)}this._dispatchEvent("changeWrapMode")}},this.getUseWrapMode=function(){return this.$useWrapMode},this.setWrapLimitRange=function(a,b){if(this.$wrapLimitRange.min!==a||this.$wrapLimitRange.max!==b)this.$wrapLimitRange.min=a,this.$wrapLimitRange.max=b,this.$modified=!0,this._dispatchEvent("changeWrapMode")},this.adjustWrapLimit=function(a){var b=this.$constrainWrapLimit(a);if(b!=this.$wrapLimit&&b>0){this.$wrapLimit=b,this.$modified=!0,this.$useWrapMode&&(this.$updateWrapData(0,this.getLength()-1),this.$resetRowCache(0),this._dispatchEvent("changeWrapLimit"));return!0}return!1},this.$constrainWrapLimit=function(a){var b=this.$wrapLimitRange.min;b&&(a=Math.max(b,a));var c=this.$wrapLimitRange.max;c&&(a=Math.min(c,a));return Math.max(1,a)},this.getWrapLimit=function(){return this.$wrapLimit},this.getWrapLimitRange=function(){return{min:this.$wrapLimitRange.min,max:this.$wrapLimitRange.max}},this.$updateInternalDataOnChange=function(a){var b=this.$useWrapMode,c,d=a.data.action,e=a.data.range.start.row,f=a.data.range.end.row,g=a.data.range.start,h=a.data.range.end,i=null;d.indexOf("Lines")!=-1?(d=="insertLines"?f=e+a.data.lines.length:f=e,c=a.data.lines.length):c=f-e;if(c!=0)if(d.indexOf("remove")!=-1){b&&this.$wrapData.splice(e,c);var j=this.$foldData;i=this.getFoldsInRange(a.data.range),this.removeFolds(i);var k=this.getFoldLine(h.row),l=0;if(k){k.addRemoveChars(h.row,h.column,g.column-h.column),k.shiftRow(-c);var m=this.getFoldLine(e);m&&m!==k&&(m.merge(k),k=m),l=j.indexOf(k)+1}for(l;l<j.length;l++){var k=j[l];k.start.row>=h.row&&k.shiftRow(-c)}f=e}else{var n;if(b){n=[e,0];for(var o=0;o<c;o++)n.push([]);this.$wrapData.splice.apply(this.$wrapData,n)}var j=this.$foldData,k=this.getFoldLine(e),l=0;if(k){var p=k.range.compareInside(g.row,g.column);p==0?(k=k.split(g.row,g.column),k.shiftRow(c),k.addRemoveChars(f,0,h.column-g.column)):p==-1&&(k.addRemoveChars(e,0,h.column-g.column),k.shiftRow(c)),l=j.indexOf(k)+1}for(l;l<j.length;l++){var k=j[l];k.start.row>=e&&k.shiftRow(c)}}else{var q;c=Math.abs(a.data.range.start.column-a.data.range.end.column),d.indexOf("remove")!=-1&&(i=this.getFoldsInRange(a.data.range),this.removeFolds(i),c=-c);var k=this.getFoldLine(e);k&&k.addRemoveChars(e,g.column,c)}b&&this.$wrapData.length!=this.doc.getLength()&&console.error("doc.getLength() and $wrapData.length have to be the same!"),b&&this.$updateWrapData(e,f);return i},this.$updateWrapData=function(a,b){var c=this.doc.getAllLines(),d=this.getTabSize(),f=this.$wrapData,i=this.$wrapLimit,j,l,m=a;b=Math.min(b,c.length-1);while(m<=b){l=this.getFoldLine(m);if(!l)j=this.$getDisplayTokens(e.stringTrimRight(c[m]));else{j=[],l.walk(function(a,b,d,e){var f;if(a){f=this.$getDisplayTokens(a,j.length),f[0]=g;for(var i=1;i<f.length;i++)f[i]=h}else f=this.$getDisplayTokens(c[b].substring(e,d),j.length);j=j.concat(f)}.bind(this),l.end.row,c[l.end.row].length+1);while(j.length!=0&&j[j.length-1]>=k)j.pop()}f[m]=this.$computeWrapSplits(j,i,d),m=this.getRowFoldEnd(m)+1}};var b=1,c=2,g=3,h=4,k=10,m=11,n=12;this.$computeWrapSplits=function(a,b,c){function j(b){var c=a.slice(f,b),e=c.length;c.join("").replace(/12/g,function(a){e-=1}).replace(/2/g,function(a){e-=1}),i+=e,d.push(i),f=b}if(a.length==0)return[];var c=this.getTabSize(),d=[],e=a.length,f=0,i=0;while(e-f>b){var l=f+b;if(a[l]>=k){while(a[l]>=k)l++;j(l);continue}if(a[l]==g||a[l]==h){for(l;l!=f-1;l--)if(a[l]==g)break;if(l>f){j(l);continue}l=f+b;for(l;l<a.length;l++)if(a[l]!=h)break;if(l==a.length)break;j(l);continue}for(l;l!=f-1;l--)if(a[l]>=g){l++;break}if(l>f){j(l);continue}l=f+b,j(f+b)}return d},this.$getDisplayTokens=function(a,d){var e=[],f;d=d||0;for(var g=0;g<a.length;g++){var h=a.charCodeAt(g);if(h==9){f=this.getScreenTabSize(e.length+d),e.push(m);for(var i=1;i<f;i++)e.push(n)}else h==32?e.push(k):o(h)?e.push(b,c):e.push(b)}return e},this.$getStringScreenWidth=function(a,b,c){if(b==0)return[0,0];b==null&&(b=c+a.length*Math.max(this.getTabSize(),2)),c=c||0;var d,e;for(e=0;e<a.length;e++){d=a.charCodeAt(e),d==9?c+=this.getScreenTabSize(c):o(d)?c+=2:c+=1;if(c>b)break}return[c,e]},this.getRowLength=function(a){return!this.$useWrapMode||!this.$wrapData[a]?1:this.$wrapData[a].length+1},this.getRowHeight=function(a,b){return this.getRowLength(b)*a.lineHeight},this.getScreenLastRowColumn=function(a){return this.screenToDocumentColumn(a,Number.MAX_VALUE/10)},this.getDocumentLastRowColumn=function(a,b){var c=this.documentToScreenRow(a,b);return this.getScreenLastRowColumn(c)},this.getDocumentLastRowColumnPosition=function(a,b){var c=this.documentToScreenRow(a,b);return this.screenToDocumentPosition(c,Number.MAX_VALUE/10)},this.getRowSplitData=function(a){return this.$useWrapMode?this.$wrapData[a]:undefined},this.getScreenTabSize=function(a){return this.$tabSize-a%this.$tabSize},this.screenToDocumentRow=function(a,b){return this.screenToDocumentPosition(a,b).row},this.screenToDocumentColumn=function(a,b){return this.screenToDocumentPosition(a,b).column},this.screenToDocumentPosition=function(a,b){var c,d=0,e=0,f,g,h=0,i=0,j=null,k=this.$rowCache,l=!k.length;for(var m=0;m<k.length;m++)k[m].screenRow<a&&(h=k[m].screenRow,d=k[m].docRow,l=m==k.length-1);var n=d,o=this.getLength()-1,p=this.getNextFold(d),q=p?p.start.row:Infinity;while(h<=a){l&&d-n>this.$rowCacheSize&&(k.push({docRow:d,screenRow:h}),n=d),i=this.getRowLength(d);if(h+i-1>=a||d>=o)break;h+=i,d++,d>q&&(d=p.end.row+1,p=this.getNextFold(d),q=p?p.start.row:Infinity)}p&&p.start.row<=d?c=this.getFoldDisplayLine(p):(c=this.getLine(d),p=null),this.$useWrapMode&&(j=this.$wrapData[d],j&&(f=j[a-h],a>h&&j.length&&(e=j[a-h-1]||j[j.length-1],c=c.substring(e)))),e+=this.$getStringScreenWidth(c,b)[1],this.$useWrapMode?e>=f&&(e=f-1):e=Math.min(e,c.length);if(p)return p.idxToPosition(e);return{row:d,column:e}},this.documentToScreenPosition=function(a,b){b==null&&(b=a.column,a=a.row);var c;if(this.$useWrapMode){c=this.$wrapData;if(a>c.length-1)return{row:this.getScreenLength(),column:c.length==0?0:c[c.length-1].length-1}}var d=0,e=0,f=null,g=null;g=this.getFoldAt(a,b,1),g&&(a=g.start.row,b=g.start.column);var h,i=0,j=this.$rowCache,k=!j.length;for(var l=0;l<j.length;l++)j[l].docRow<a&&(d=j[l].screenRow,i=j[l].docRow,k=l==j.length-1);var m=i,n=this.getNextFold(i),o=n?n.start.row:Infinity;while(i<a){if(i>=o){h=n.end.row+1;if(h>a)break;n=this.getNextFold(h),o=n?n.start.row:Infinity}else h=i+1;k&&i-m>this.$rowCacheSize&&(j.push({docRow:i,screenRow:d}),m=i),d+=this.getRowLength(i),i=h}var p="";n&&i>=o?(p=this.getFoldDisplayLine(n,a,b),f=n.start.row):(p=this.getLine(a).substring(0,b),f=a);if(this.$useWrapMode){var q=c[f],r=0;while(p.length>=q[r])d++,r++;p=p.substring(q[r-1]||0,p.length)}return{row:d,column:this.$getStringScreenWidth(p)[0]}},this.documentToScreenColumn=function(a,b){return this.documentToScreenPosition(a,b).column},this.documentToScreenRow=function(a,b){return this.documentToScreenPosition(a,b).row},this.getScreenLength=function(){var a=0,b=null,c=null;if(!this.$useWrapMode){a=this.getLength();var d=this.$foldData;for(var e=0;e<d.length;e++)c=d[e],a-=c.end.row-c.start.row}else for(var f=0;f<this.$wrapData.length;f++)(c=this.getFoldLine(f,b))?(f=c.end.row,a+=1):a+=this.$wrapData[f].length+1;return a}}).call(m.prototype),a("ace/edit_session/folding").Folding.call(m.prototype),b.EditSession=m}),define("ace/selection",["require","exports","module","pilot/oop","pilot/lang","pilot/event_emitter","ace/range"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("pilot/event_emitter").EventEmitter,g=a("ace/range").Range,h=function(a){this.session=a,this.doc=a.getDocument(),this.clearSelection(),this.selectionLead=this.doc.createAnchor(0,0),this.selectionAnchor=this.doc.createAnchor(0,0);var b=this;this.selectionLead.on("change",function(a){b._dispatchEvent("changeCursor"),b.$isEmpty||b._dispatchEvent("changeSelection"),a.old.row==a.value.row&&b.$updateDesiredColumn()}),this.selectionAnchor.on("change",function(){b.$isEmpty||b._dispatchEvent("changeSelection")})};(function(){d.implement(this,f),this.isEmpty=function(){return this.$isEmpty||this.selectionAnchor.row==this.selectionLead.row&&this.selectionAnchor.column==this.selectionLead.column},this.isMultiLine=function(){if(this.isEmpty())return!1;return this.getRange().isMultiLine()},this.getCursor=function(){return this.selectionLead.getPosition()},this.setSelectionAnchor=function(a,b){this.selectionAnchor.setPosition(a,b),this.$isEmpty&&(this.$isEmpty=!1,this._dispatchEvent("changeSelection"))},this.getSelectionAnchor=function(){return this.$isEmpty?this.getSelectionLead():this.selectionAnchor.getPosition()},this.getSelectionLead=function(){return this.selectionLead.getPosition()},this.shiftSelection=function(a){if(this.$isEmpty)this.moveCursorTo(this.selectionLead.row,this.selectionLead.column+a);else{var b=this.getSelectionAnchor(),c=this.getSelectionLead(),d=this.isBackwards();(!d||b.column!==0)&&this.setSelectionAnchor(b.row,b.column+a),(d||c.column!==0)&&this.$moveSelection(function(){this.moveCursorTo(c.row,c.column+a)})}},this.isBackwards=function(){var a=this.selectionAnchor,b=this.selectionLead;return a.row>b.row||a.row==b.row&&a.column>b.column},this.getRange=function(){var a=this.selectionAnchor,b=this.selectionLead;if(this.isEmpty())return g.fromPoints(b,b);return this.isBackwards()?g.fromPoints(b,a):g.fromPoints(a,b)},this.clearSelection=function(){this.$isEmpty||(this.$isEmpty=!0,this._dispatchEvent("changeSelection"))},this.selectAll=function(){var a=this.doc.getLength()-1;this.setSelectionAnchor(a,this.doc.getLine(a).length),this.moveCursorTo(0,0)},this.setSelectionRange=function(a,b){b?(this.setSelectionAnchor(a.end.row,a.end.column),this.selectTo(a.start.row,a.start.column)):(this.setSelectionAnchor(a.start.row,a.start.column),this.selectTo(a.end.row,a.end.column)),this.$updateDesiredColumn()},this.$updateDesiredColumn=function(){var a=this.getCursor();this.$desiredColumn=this.session.documentToScreenColumn(a.row,a.column)},this.$moveSelection=function(a){var b=this.selectionLead;this.$isEmpty&&this.setSelectionAnchor(b.row,b.column),a.call(this)},this.selectTo=function(a,b){this.$moveSelection(function(){this.moveCursorTo(a,b)})},this.selectToPosition=function(a){this.$moveSelection(function(){this.moveCursorToPosition(a)})},this.selectUp=function(){this.$moveSelection(this.moveCursorUp)},this.selectDown=function(){this.$moveSelection(this.moveCursorDown)},this.selectRight=function(){this.$moveSelection(this.moveCursorRight)},this.selectLeft=function(){this.$moveSelection(this.moveCursorLeft)},this.selectLineStart=function(){this.$moveSelection(this.moveCursorLineStart)},this.selectLineEnd=function(){this.$moveSelection(this.moveCursorLineEnd)},this.selectFileEnd=function(){this.$moveSelection(this.moveCursorFileEnd)},this.selectFileStart=function(){this.$moveSelection(this.moveCursorFileStart)},this.selectWordRight=function(){this.$moveSelection(this.moveCursorWordRight)},this.selectWordLeft=function(){this.$moveSelection(this.moveCursorWordLeft)},this.selectWord=function(){var a=this.getCursor(),b=this.session.getWordRange(a.row,a.column);this.setSelectionRange(b)},this.selectLine=function(){var a=this.selectionLead.row,b,c=this.session.getFoldLine(a);c?(a=c.start.row,b=c.end.row):b=a,this.setSelectionAnchor(a,0),this.$moveSelection(function(){this.moveCursorTo(b+1,0)})},this.moveCursorUp=function(){this.moveCursorBy(-1,0)},this.moveCursorDown=function(){this.moveCursorBy(1,0)},this.moveCursorLeft=function(){var a=this.selectionLead.getPosition(),b;if(b=this.session.getFoldAt(a.row,a.column,-1))this.moveCursorTo(b.start.row,b.start.column);else if(a.column==0)a.row>0&&this.moveCursorTo(a.row-1,this.doc.getLine(a.row-1).length);else{var c=this.session.getTabSize();this.session.isTabStop(a)&&this.doc.getLine(a.row).slice(a.column-c,a.column).split(" ").length-1==c?this.moveCursorBy(0,-c):this.moveCursorBy(0,-1)}},this.moveCursorRight=function(){var a=this.selectionLead.getPosition(),b;if(b=this.session.getFoldAt(a.row,a.column,1))this.moveCursorTo(b.end.row,b.end.column);else if(this.selectionLead.column==this.doc.getLine(this.selectionLead.row).length)this.selectionLead.row<this.doc.getLength()-1&&this.moveCursorTo(this.selectionLead.row+1,0);else{var c=this.session.getTabSize(),a=this.selectionLead;this.session.isTabStop(a)&&this.doc.getLine(a.row).slice(a.column,a.column+c).split(" ").length-1==c?this.moveCursorBy(0,c):this.moveCursorBy(0,1)}},this.moveCursorLineStart=function(){var a=this.selectionLead.row,b=this.selectionLead.column,c=this.session.documentToScreenRow(a,b),d=this.session.screenToDocumentPosition(c,0),e=this.session.getDisplayLine(a,b,d.row,d.column),f=e.match(/^\s*/);f[0].length==0||f[0].length>=b-d.column?this.moveCursorTo(d.row,d.column):this.moveCursorTo(d.row,d.column+f[0].length)},this.moveCursorLineEnd=function(){var a=this.selectionLead,b=this.session.getDocumentLastRowColumnPosition(a.row,a.column);this.moveCursorTo(b.row,b.column)},this.moveCursorFileEnd=function(){var a=this.doc.getLength()-1,b=this.doc.getLine(a).length;this.moveCursorTo(a,b)},this.moveCursorFileStart=function(){this.moveCursorTo(0,0)},this.moveCursorWordRight=function(){var a=this.selectionLead.row,b=this.selectionLead.column,c=this.doc.getLine(a),d=c.substring(b),e;this.session.nonTokenRe.lastIndex=0,this.session.tokenRe.lastIndex=0;var f;if(f=this.session.getFoldAt(a,b,1))this.moveCursorTo(f.end.row,f.end.column);else{if(b==c.length){this.moveCursorRight();return}if(e=this.session.nonTokenRe.exec(d))b+=this.session.nonTokenRe.lastIndex,this.session.nonTokenRe.lastIndex=0;else if(e=this.session.tokenRe.exec(d))b+=this.session.tokenRe.lastIndex,this.session.tokenRe.lastIndex=0;this.moveCursorTo(a,b)}},this.moveCursorWordLeft=function(){var a=this.selectionLead.row,b=this.selectionLead.column,c;if(c=this.session.getFoldAt(a,b,-1))this.moveCursorTo(c.start.row,c.start.column);else{if(b==0){this.moveCursorLeft();return}var d=this.session.getFoldStringAt(a,b,-1);d==null&&(d=this.doc.getLine(a).substring(0,b));var f=e.stringReverse(d),g;this.session.nonTokenRe.lastIndex=0,this.session.tokenRe.lastIndex=0;if(g=this.session.nonTokenRe.exec(f))b-=this.session.nonTokenRe.lastIndex,this.session.nonTokenRe.lastIndex=0;else if(g=this.session.tokenRe.exec(f))b-=this.session.tokenRe.lastIndex,this.session.tokenRe.lastIndex=0;this.moveCursorTo(a,b)}},this.moveCursorBy=function(a,b){var c=this.session.documentToScreenPosition(this.selectionLead.row,this.selectionLead.column),d=b==0&&this.$desiredColumn||c.column,e=this.session.screenToDocumentPosition(c.row+a,d);this.moveCursorTo(e.row,e.column+b,b==0)},this.moveCursorToPosition=function(a){this.moveCursorTo(a.row,a.column)},this.moveCursorTo=function(a,b,c){var d=this.session.getFoldAt(a,b,1);d&&(a=d.start.row,b=d.start.column),this.selectionLead.setPosition(a,b),c||this.$updateDesiredColumn(this.selectionLead.column)},this.moveCursorToScreen=function(a,b,c){var d=this.session.screenToDocumentPosition(a,b);a=d.row,b=d.column,this.moveCursorTo(a,b,c)}}).call(h.prototype),b.Selection=h}),define("ace/range",["require","exports","module"],function(a,b,c){var d=function(a,b,c,d){this.start={row:a,column:b},this.end={row:c,column:d}};(function(){this.toString=function(){return"Range: ["+this.start.row+"/"+this.start.column+"] -> ["+this.end.row+"/"+this.end.column+"]"},this.contains=function(a,b){return this.compare(a,b)==0},this.compareRange=function(a){var b,c=a.end,d=a.start;b=this.compare(c.row,c.column);if(b==1){b=this.compare(d.row,d.column);return b==1?2:b==0?1:0}if(b==-1)return-2;b=this.compare(d.row,d.column);return b==-1?-1:b==1?42:0},this.containsRange=function(a){var b=this.compareRange(a);return b==-1||b==0||b==1},this.isEnd=function(a,b){return this.end.row==a&&this.end.column==b},this.isStart=function(a,b){return this.start.row==a&&this.start.column==b},this.setStart=function(a,b){typeof a=="object"?(this.start.column=a.column,this.start.row=a.row):(this.start.row=a,this.start.column=b)},this.setEnd=function(a,b){typeof a=="object"?(this.end.column=a.column,this.end.row=a.row):(this.end.row=a,this.end.column=b)},this.inside=function(a,b){if(this.compare(a,b)==0)return this.isEnd(a,b)||this.isStart(a,b)?!1:!0;return!1},this.insideStart=function(a,b){if(this.compare(a,b)==0)return this.isEnd(a,b)?!1:!0;return!1},this.insideEnd=function(a,b){if(this.compare(a,b)==0)return this.isStart(a,b)?!1:!0;return!1},this.compare=function(a,b){if(!this.isMultiLine()&&a===this.start.row)return b<this.start.column?-1:b>this.end.column?1:0;if(a<this.start.row)return-1;if(a>this.end.row)return 1;if(this.start.row===a)return b>=this.start.column?0:-1;if(this.end.row===a)return b<=this.end.column?0:1;return 0},this.compareStart=function(a,b){return this.start.row==a&&this.start.column==b?-1:this.compare(a,b)},this.compareEnd=function(a,b){return this.end.row==a&&this.end.column==b?1:this.compare(a,b)},this.compareInside=function(a,b){return this.end.row==a&&this.end.column==b?1:this.start.row==a&&this.start.column==b?-1:this.compare(a,b)},this.clipRows=function(a,b){if(this.end.row>b)var c={row:b+1,column:0};if(this.start.row>b)var e={row:b+1,column:0};if(this.start.row<a)var e={row:a,column:0};if(this.end.row<a)var c={row:a,column:0};return d.fromPoints(e||this.start,c||this.end)},this.extend=function(a,b){var c=this.compare(a,b);if(c==0)return this;if(c==-1)var e={row:a,column:b};else var f={row:a,column:b};return d.fromPoints(e||this.start,f||this.end)},this.isEmpty=function(){return this.start.row==this.end.row&&this.start.column==this.end.column},this.isMultiLine=function(){return this.start.row!==this.end.row},this.clone=function(){return d.fromPoints(this.start,this.end)},this.collapseRows=function(){return this.end.column==0?new d(this.start.row,0,Math.max(this.start.row,this.end.row-1),0):new d(this.start.row,0,this.end.row,0)},this.toScreenRange=function(a){var b=a.documentToScreenPosition(this.start),c=a.documentToScreenPosition(this.end);return new d(b.row,b.column,c.row,c.column)}}).call(d.prototype),d.fromPoints=function(a,b){return new d(a.row,a.column,b.row,b.column)},b.Range=d}),define("ace/mode/text",["require","exports","module","ace/tokenizer","ace/mode/text_highlight_rules","ace/mode/behaviour"],function(a,b,c){var d=a("ace/tokenizer").Tokenizer,e=a("ace/mode/text_highlight_rules").TextHighlightRules,f=a("ace/mode/behaviour").Behaviour,g=function(){this.$tokenizer=new d((new e).getRules()),this.$behaviour=new f};(function(){this.getTokenizer=function(){return this.$tokenizer},this.toggleCommentLines=function(a,b,c,d){},this.getNextLineIndent=function(a,b,c){return""},this.checkOutdent=function(a,b,c){return!1},this.autoOutdent=function(a,b,c){},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""},this.createWorker=function(a){return null},this.highlightSelection=function(a){var b=a.session;b.$selectionOccurrences||(b.$selectionOccurrences=[]),b.$selectionOccurrences.length&&this.clearSelectionHighlight(a);var c=a.getSelectionRange();if(!c.isEmpty()&&!c.isMultiLine()){var d=c.start.column-1,e=c.end.column+1,f=b.getLine(c.start.row),g=f.length,h=f.substring(Math.max(d,0),Math.min(e,g));if(d>=0&&/^[\w\d]/.test(h)||e<=g&&/[\w\d]$/.test(h))return;h=f.substring(c.start.column,c.end.column);if(!/^[\w\d]+$/.test(h))return;var i=a.getCursorPosition(),j={wrap:!0,wholeWord:!0,caseSensitive:!0,needle:h},k=a.$search.getOptions();a.$search.set(j);var l=a.$search.findAll(b);l.forEach(function(a){if(!a.contains(i.row,i.column)){var c=b.addMarker(a,"ace_selected_word");b.$selectionOccurrences.push(c)}}),a.$search.set(k)}},this.clearSelectionHighlight=function(a){!a.session.$selectionOccurrences||(a.session.$selectionOccurrences.forEach(function(b){a.session.removeMarker(b)}),a.session.$selectionOccurrences=[])},this.createModeDelegates=function(a){if(!!this.$embeds){this.$modes={};for(var b=0;b<this.$embeds.length;b++)a[this.$embeds[b]]&&(this.$modes[this.$embeds[b]]=new a[this.$embeds[b]]);var c=["toggleCommentLines","getNextLineIndent","checkOutdent","autoOutdent","transformAction"];for(var b=0;b<c.length;b++)(function(a){var d=c[b],e=a[d];a[c[b]]=function(){return this.$delegator(d,arguments,e)}})(this)}},this.$delegator=function(a,b,c){var d=b[0];for(var e=0;e<this.$embeds.length;e++){if(!this.$modes[this.$embeds[e]])continue;var f=d.split(this.$embeds[e]);if(!f[0]&&f[1]){b[0]=f[1];var g=this.$modes[this.$embeds[e]];return g[a].apply(g,b)}}var h=c.apply(this,b);return c?h:undefined},this.transformAction=function(a,b,c,d,e){if(this.$behaviour){var f=this.$behaviour.getBehaviours();for(var g in f)if(f[g][b]){var h=f[g][b].apply(this,arguments);if(h!==!1)return h}}return!1}}).call(g.prototype),b.Mode=g}),define("ace/tokenizer",["require","exports","module"],function(a,b,c){var d=function(a){this.rules=a,this.regExps={},this.matchMappings={};for(var b in this.rules){var c=this.rules[b],d=c,e=[],f=0,g=this.matchMappings[b]={};for(var h=0;h<d.length;h++){var i=(new RegExp("(?:("+d[h].regex+")|(.))")).exec("a").length-2,j=d[h].regex.replace(/\\([0-9]+)/g,function(a,b){return"\\"+(parseInt(b,10)+f+1)});g[f]={rule:h,len:i},f+=i,e.push(j)}this.regExps[b]=new RegExp("(?:("+e.join(")|(")+")|(.))","g")}};(function(){this.getLineTokens=function(a,b){var c=b,d=this.rules[c],e=this.matchMappings[c],f=this.regExps[c];f.lastIndex=0;var g,h=[],i=0,j={type:null,value:""};while(g=f.exec(a)){var k="text",l=[g[0]];for(var m=0;m<g.length-2;m++)if(g[m+1]!==undefined){var n=d[e[m].rule];e[m].len>1&&(l=g.slice(m+2,m+1+e[m].len)),typeof n.token=="function"?k=n.token.apply(this,l):k=n.token,n.next&&n.next!==c&&(c=n.next,d=this.rules[c],e=this.matchMappings[c],i=f.lastIndex,f=this.regExps[c],f.lastIndex=i);break}typeof k=="string"&&(typeof l!="string"&&(l=[l.join("")]),k=[k]);for(var m=0;m<l.length;m++)j.type!==k[m]?(j.type&&h.push(j),j={type:k[m],value:l[m]}):j.value+=l[m];if(i==a.length)break;i=f.lastIndex}j.type&&h.push(j);return{tokens:h,state:c}}}).call(d.prototype),b.Tokenizer=d}),define("ace/mode/text_highlight_rules",["require","exports","module","pilot/lang"],function(a,b,c){var d=a("pilot/lang"),e=function(){this.$rules={start:[{token:"empty_line",regex:"^$"},{token:"text",regex:".+"}]}};(function(){this.addRules=function(a,b){for(var c in a){var d=a[c];for(var e=0;e<d.length;e++){var f=d[e];f.next?f.next=b+f.next:f.next=b+c}this.$rules[b+c]=d}},this.getRules=function(){return this.$rules},this.embedRules=function(a,b,c,e){var f=(new a).getRules();if(e)for(var g=0;g<e.length;g++)e[g]=b+e[g];else{e=[];for(var h in f)e.push(b+h)}this.addRules(f,b);for(var g=0;g<e.length;g++)Array.prototype.unshift.apply(this.$rules[e[g]],d.deepCopy(c));this.$embeds||(this.$embeds=[]),this.$embeds.push(b)},this.getEmbeds=function(){return this.$embeds}}).call(e.prototype),b.TextHighlightRules=e}),define("ace/mode/behaviour",["require","exports","module"],function(a,b,c){var d=function(){this.$behaviours={}};(function(){this.add=function(a,b,c){switch(undefined){case this.$behaviours:this.$behaviours={};case this.$behaviours[a]:this.$behaviours[a]={}}this.$behaviours[a][b]=c},this.addBehaviours=function(a){for(var b in a)for(var c in a[b])this.add(b,c,a[b][c])},this.remove=function(a){this.$behaviours&&this.$behaviours[a]&&delete this.$behaviours[a]},this.inherit=function(a,b){if(typeof a=="function")var c=(new a).getBehaviours(b);else var c=a.getBehaviours(b);this.addBehaviours(c)},this.getBehaviours=function(a){if(!a)return this.$behaviours;var b={};for(var c=0;c<a.length;c++)this.$behaviours[a[c]]&&(b[a[c]]=this.$behaviours[a[c]]);return b}}).call(d.prototype),b.Behaviour=d}),define("ace/document",["require","exports","module","pilot/oop","pilot/event_emitter","ace/range","ace/anchor"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=a("ace/range").Range,g=a("ace/anchor").Anchor,h=function(a){this.$lines=[],Array.isArray(a)?this.insertLines(0,a):a.length==0?this.$lines=[""]:this.insert({row:0,column:0},a)};(function(){d.implement(this,e),this.setValue=function(a){var b=this.getLength();this.remove(new f(0,0,b,this.getLine(b-1).length)),this.insert({row:0,column:0},a)},this.getValue=function(){return this.getAllLines().join(this.getNewLineCharacter())},this.createAnchor=function(a,b){return new g(this,a,b)},"aaa".split(/a/).length==0?this.$split=function(a){return a.replace(/\r\n|\r/g,"\n").split("\n")}:this.$split=function(a){return a.split(/\r\n|\r|\n/)},this.$detectNewLine=function(a){var b=a.match(/^.*?(\r?\n)/m);b?this.$autoNewLine=b[1]:this.$autoNewLine="\n"},this.getNewLineCharacter=function(){switch(this.$newLineMode){case"windows":return"\r\n";case"unix":return"\n";case"auto":return this.$autoNewLine}},this.$autoNewLine="\n",this.$newLineMode="auto",this.setNewLineMode=function(a){this.$newLineMode!==a&&(this.$newLineMode=a)},this.getNewLineMode=function(){return this.$newLineMode},this.isNewLine=function(a){return a=="\r\n"||a=="\r"||a=="\n"},this.getLine=function(a){return this.$lines[a]||""},this.getLines=function(a,b){return this.$lines.slice(a,b+1)},this.getAllLines=function(){return this.getLines(0,this.getLength())},this.getLength=function(){return this.$lines.length},this.getTextRange=function(a){if(a.start.row==a.end.row)return this.$lines[a.start.row].substring(a.start.column,a.end.column);var b=[];b.push(this.$lines[a.start.row].substring(a.start.column)),b.push.apply(b,this.getLines(a.start.row+1,a.end.row-1)),b.push(this.$lines[a.end.row].substring(0,a.end.column));return b.join(this.getNewLineCharacter())},this.$clipPosition=function(a){var b=this.getLength();a.row>=b&&(a.row=Math.max(0,b-1),a.column=this.getLine(b-1).length);return a},this.insert=function(a,b){if(b.length==0)return a;a=this.$clipPosition(a),this.getLength()<=1&&this.$detectNewLine(b);var c=this.$split(b),d=c.splice(0,1)[0],e=c.length==0?null:c.splice(c.length-1,1)[0];this._dispatchEvent("changeStart"),a=this.insertInLine(a,d),e!==null&&(a=this.insertNewLine(a),a=this.insertLines(a.row,c),a=this.insertInLine(a,e||"")),this._dispatchEvent("changeEnd");return a},this.insertLines=function(a,b){if(b.length==0)return{row:a,column:0};var c=[a,0];c.push.apply(c,b),this.$lines.splice.apply(this.$lines,c),this._dispatchEvent("changeStart");var d=new f(a,0,a+b.length,0),e={action:"insertLines",range:d,lines:b};this._dispatchEvent("change",{data:e}),this._dispatchEvent("changeEnd");return d.end},this.insertNewLine=function(a){a=this.$clipPosition(a);var b=this.$lines[a.row]||"";this._dispatchEvent("changeStart"),this.$lines[a.row]=b.substring(0,a.column),this.$lines.splice(a.row+1,0,b.substring(a.column,b.length));var c={row:a.row+1,column:0},d={action:"insertText",range:f.fromPoints(a,c),text:this.getNewLineCharacter()};this._dispatchEvent("change",{data:d}),this._dispatchEvent("changeEnd");return c},this.insertInLine=function(a,b){if(b.length==0)return a;var c=this.$lines[a.row]||"";this._dispatchEvent("changeStart"),this.$lines[a.row]=c.substring(0,a.column)+b+c.substring(a.column);var d={row:a.row,column:a.column+b.length},e={action:"insertText",range:f.fromPoints(a,d),text:b};this._dispatchEvent("change",{data:e}),this._dispatchEvent("changeEnd");return d},this.remove=function(a){a.start=this.$clipPosition(a.start),a.end=this.$clipPosition(a.end);if(a.isEmpty())return a.start;var b=a.start.row,c=a.end.row;this._dispatchEvent("changeStart");if(a.isMultiLine()){var d=a.start.column==0?b:b+1,e=c-1;a.end.column>0&&this.removeInLine(c,0,a.end.column),e>=d&&this.removeLines(d,e),d!=b&&(this.removeInLine(b,a.start.column,this.getLine(b).length),this.removeNewLine(a.start.row))}else this.removeInLine(b,a.start.column,a.end.column);this._dispatchEvent("changeEnd");return a.start},this.removeInLine=function(a,b,c){if(b!=c){var d=new f(a,b,a,c),e=this.getLine(a),g=e.substring(b,c),h=e.substring(0,b)+e.substring(c,e.length);this._dispatchEvent("changeStart"),this.$lines.splice(a,1,h);var i={action:"removeText",range:d,text:g};this._dispatchEvent("change",{data:i}),this._dispatchEvent("changeEnd");return d.start}},this.removeLines=function(a,b){this._dispatchEvent("changeStart");var c=new f(a,0,b+1,0),d=this.$lines.splice(a,b-a+1),e={action:"removeLines",range:c,nl:this.getNewLineCharacter(),lines:d};this._dispatchEvent("change",{data:e}),this._dispatchEvent("changeEnd");return d},this.removeNewLine=function(a){var b=this.getLine(a),c=this.getLine(a+1),d=new f(a,b.length,a+1,0),e=b+c;this._dispatchEvent("changeStart"),this.$lines.splice(a,2,e);var g={action:"removeText",range:d,text:this.getNewLineCharacter()};this._dispatchEvent("change",{data:g}),this._dispatchEvent("changeEnd")},this.replace=function(a,b){if(b.length==0&&a.isEmpty())return a.start;if(b==this.getTextRange(a))return a.end;this._dispatchEvent("changeStart"),this.remove(a);if(b)var c=this.insert(a.start,b);else c=a.start;this._dispatchEvent("changeEnd");return c},this.applyDeltas=function(a){for(var b=0;b<a.length;b++){var c=a[b],d=f.fromPoints(c.range.start,c.range.end);c.action=="insertLines"?this.insertLines(d.start.row,c.lines):c.action=="insertText"?this.insert(d.start,c.text):c.action=="removeLines"?this.removeLines(d.start.row,d.end.row-1):c.action=="removeText"&&this.remove(d)}},this.revertDeltas=function(a){for(var b=a.length-1;b>=0;b--){var c=a[b],d=f.fromPoints(c.range.start,c.range.end);c.action=="insertLines"?this.removeLines(d.start.row,d.end.row-1):c.action=="insertText"?this.remove(d):c.action=="removeLines"?this.insertLines(d.start.row,c.lines):c.action=="removeText"&&this.insert(d.start,c.text)}}}).call(h.prototype),b.Document=h}),define("ace/anchor",["require","exports","module","pilot/oop","pilot/event_emitter"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=b.Anchor=function(a,b,c){this.document=a,typeof c=="undefined"?this.setPosition(b.row,b.column):this.setPosition(b,c),this.$onChange=this.onChange.bind(this),a.on("change",this.$onChange)};(function(){d.implement(this,e),this.getPosition=function(){return this.$clipPositionToDocument(this.row,this.column)},this.getDocument=function(){return this.document},this.onChange=function(a){var b=a.data,c=b.range;if(c.start.row!=c.end.row||c.start.row==this.row){if(c.start.row>this.row)return;if(c.start.row==this.row&&c.start.column>this.column)return;var d=this.row,e=this.column;b.action==="insertText"?c.start.row===d&&c.start.column<=e?c.start.row===c.end.row?e+=c.end.column-c.start.column:(e-=c.start.column,d+=c.end.row-c.start.row):c.start.row!==c.end.row&&c.start.row<d&&(d+=c.end.row-c.start.row):b.action==="insertLines"?c.start.row<=d&&(d+=c.end.row-c.start.row):b.action=="removeText"?c.start.row==d&&c.start.column<e?c.end.column>=e?e=c.start.column:e=Math.max(0,e-(c.end.column-c.start.column)):c.start.row!==c.end.row&&c.start.row<d?(c.end.row==d&&(e=Math.max(0,e-c.end.column)+c.start.column),d-=c.end.row-c.start.row):c.end.row==d&&(d-=c.end.row-c.start.row,e=Math.max(0,e-c.end.column)+c.start.column):b.action=="removeLines"&&c.start.row<=d&&(c.end.row<=d?d-=c.end.row-c.start.row:(d=c.start.row,e=0)),this.setPosition(d,e,!0)}},this.setPosition=function(a,b,c){c?pos={row:a,column:b}:pos=this.$clipPositionToDocument(a,b);if(this.row!=pos.row||this.column!=pos.column){var d={row:this.row,column:this.column};this.row=pos.row,this.column=pos.column,this._dispatchEvent("change",{old:d,value:pos})}},this.detach=function(){this.document.removeEventListener("change",this.$onChange)},this.$clipPositionToDocument=function(a,b){var c={};a>=this.document.getLength()?(c.row=Math.max(0,this.document.getLength()-1),c.column=this.document.getLine(c.row).length):a<0?(c.row=0,c.column=0):(c.row=a,c.column=Math.min(this.document.getLine(c.row).length,Math.max(0,b))),b<0&&(c.column=0);return c}}).call(f.prototype)}),define("ace/background_tokenizer",["require","exports","module","pilot/oop","pilot/event_emitter"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=function(a,b){this.running=!1,this.lines=[],this.currentLine=0,this.tokenizer=a;var c=this;this.$worker=function(){if(!!c.running){var a=new Date,b=c.currentLine,d=c.doc,e=0,f=d.getLength();while(c.currentLine<f){c.lines[c.currentLine]=c.$tokenizeRows(c.currentLine,c.currentLine)[0],c.currentLine++,e+=1;if(e%5==0&&new Date-a>20){c.fireUpdateEvent(b,c.currentLine-1),c.running=setTimeout(c.$worker,20);return}}c.running=!1,c.fireUpdateEvent(b,f-1)}}};(function(){d.implement(this,e),this.setTokenizer=function(a){this.tokenizer=a,this.lines=[],this.start(0)},this.setDocument=function(a){this.doc=a,this.lines=[],this.stop()},this.fireUpdateEvent=function(a,b){var c={first:a,last:b};this._dispatchEvent("update",{data:c})},this.start=function(a){this.currentLine=Math.min(a||0,this.currentLine,this.doc.getLength()),this.lines.splice(this.currentLine,this.lines.length),this.stop(),this.running=setTimeout(this.$worker,700)},this.stop=function(){this.running&&clearTimeout(this.running),this.running=!1},this.getTokens=function(a,b){return this.$tokenizeRows(a,b)},this.getState=function(a){return this.$tokenizeRows(a,a)[0].state},this.$tokenizeRows=function(a,b){if(!this.doc)return[];var c=[],d="start",e=!1;a>0&&this.lines[a-1]?(d=this.lines[a-1].state,e=!0):a==0?(d="start",e=!0):this.lines.length>0&&(d=this.lines[this.lines.length-1].state);var f=this.doc.getLines(a,b);for(var g=a;g<=b;g++)if(!this.lines[g]){var h=this.tokenizer.getLineTokens(f[g-a]||"",d),d=h.state;c.push(h),e&&(this.lines[g]=h)}else{var h=this.lines[g];d=h.state,c.push(h)}return c}}).call(f.prototype),b.BackgroundTokenizer=f}),define("ace/edit_session/folding",["require","exports","module","ace/range","ace/edit_session/fold_line"],function(a,b,c){function g(){this.getFoldAt=function(a,b,c){var d=this.getFoldLine(a);if(!d)return null;var e=d.folds,f;for(var g=0;g<e.length;g++){f=e[g];if(f.range.contains(a,b)){if(c==1&&f.range.isEnd(a,b))continue;if(c==-1&&f.range.isStart(a,b))continue;return f}}},this.getFoldsInRange=function(a){a=a.clone();var b=a.start,c=a.end,d=this.$foldData,e,f,g,h=[];b.column+=1,c.column-=1;for(var i=0;i<d.length;i++){g=d[i].range.compareRange(a);if(g==2)continue;if(g==-2)break;e=d[i].folds;for(var j=0;j<e.length;j++){f=e[j],g=f.range.compareRange(a);if(g==-2)break;if(g==2)continue;if(g==42)break;h.push(f)}}return h},this.getFoldStringAt=function(a,b,c,d){var d=d||this.getFoldLine(a);if(!d)return null;var e,f,g,h;f={end:{column:0}};for(var i=0;i<d.folds.length;i++){e=d.folds[i],g=e.range.compareEnd(a,b);if(g==-1){h=this.getLine(e.start.row).substring(f.end.column,e.start.column);break}if(g==0)return null;f=e}h||(h=this.getLine(e.start.row).substring(f.end.column));return c==-1?h.substring(0,b-f.end.column):c==1?h.substring(b-f.end.column):h},this.getFoldLine=function(a,b){var c=this.$foldData,d=0;b&&(d=c.indexOf(b)),d==-1&&(d=0);for(d;d<c.length;d++){var e=c[d];if(e.start.row<=a&&e.end.row>=a)return e;if(e.end.row>a)return null}return null},this.getNextFold=function(a,b){var c=this.$foldData,d,e=0;b&&(e=c.indexOf(b)),e==-1&&(e=0);for(e;e<c.length;e++){var f=c[e];if(f.end.row>=a)return f}return null},this.getFoldedRowCount=function(a,b){var c=this.$foldData,d=b-a+1;for(var e=0;e<c.length;e++){var f=c[e],g=f.end.row,h=f.start.row;if(g>=b){h<b&&(h>=a?d-=b-h:d=0);break}g>=a&&(h>=a?d-=g-h:d-=g-a+1)}return d},this.$addFoldLine=function(a){this.$foldData.push(a),this.$foldData.sort(function(a,b){return a.start.row-b.start.row});return a},this.addFold=function(a,b,c,g,h){var i,j=this.$foldData,k=null,l,m,n,o,p=!1;a instanceof f&&(n=a,b=n.range,a=n.placeholder),b instanceof d?(i=b,b=i.start.row,c=i.start.column,g=i.end.row,h=i.end.column):i=new d(b,c,g,h);if(a.length<2)throw"Placeholder has to be at least 2 characters";if(b==g&&h-c<2)throw"The range has to be at least 2 characters width";m=this.getFoldAt(b,c,1);if(m&&m.range.isEnd(g,h)&&m.range.isStart(b,c))return m;m=this.getFoldAt(b,c,1);if(m&&!m.range.isStart(b,c))throw"A fold can't start inside of an already existing fold";m=this.getFoldAt(g,h,-1);if(m&&!m.range.isEnd(g,h))throw"A fold can't end inside of an already existing fold";if(g>=this.doc.getLength())throw"End of fold is outside of the document.";if(h>this.getLine(g).length||c>this.getLine(b).length)throw"End of fold is outside of the document.";m=n||new f(i,a),o=this.getFoldsInRange(i),o.length>0&&(this.removeFolds(o),m.subFolds=o);for(var q=0;q<j.length;q++){l=j[q];if(g==l.start.row){l.addFold(m),p=!0;break}if(b==l.end.row){l.addFold(m),p=!0;if(!m.sameRow){foldLineNext=j[q+1];if(foldLineNext&&foldLineNext.start.row==g){l.merge(foldLineNext);break}}break}if(g<=l.start.row)break}p||(l=this.$addFoldLine(new e(this.$foldData,m))),this.$useWrapMode&&this.$updateWrapData(l.start.row,l.start.row),this.$modified=!0,this._dispatchEvent("changeFold",{data:m});return m},this.addFolds=function(a){a.forEach(function(a){this.addFold(a)},this)},this.removeFold=function(a){var b=a.foldLine,c=b.start.row,d=b.end.row,e=this.$foldData,f=b.folds;if(f.length==1)e.splice(e.indexOf(b),1);else if(b.range.isEnd(a.end.row,a.end.column))f.pop(),b.end.row=f[f.length-1].end.row,b.end.column=f[f.length-1].end.column;else if(b.range.isStart(a.start.row,a.start.column))f.shift(),b.start.row=f[0].start.row,b.start.column=f[0].start.column;else if(a.sameRow)f.splice(f.indexOf(a),1);else{var g=b.split(a.start.row,a.start.column);g.folds.shift(),b.start.row=f[0].start.row,b.start.column=f[0].start.column,this.$addFoldLine(g)}this.$useWrapMode&&this.$updateWrapData(c,d),this.$modified=!0,this._dispatchEvent("changeFold",{data:a})},this.removeFolds=function(a){var b=[];for(var c=0;c<a.length;c++)b.push(a[c]);b.forEach(function(a){this.removeFold(a)},this),this.$modified=!0},this.expandFold=function(a){this.removeFold(a),a.subFolds.forEach(function(a){this.addFold(a)},this),a.subFolds=[]},this.expandFolds=function(a){a.forEach(function(a){this.expandFold(a)},this)},this.isRowFolded=function(a,b){return!!this.getFoldLine(a,b)},this.getRowFoldEnd=function(a,b){var c=this.getFoldLine(a,b);return c?c.end.row:a},this.getFoldDisplayLine=function(a,b,c,d,e){d==null&&(d=a.start.row,e=0),b==null&&(b=a.end.row,c=this.getLine(b).length);var f="",g=this.doc,h="";a.walk(function(a,b,c,f,i){if(!(b<d)){if(b==d){if(c<e)return;f=Math.max(e,f)}a?h+=a:h+=g.getLine(b).substring(f,c)}}.bind(this),b,c);return h},this.getDisplayLine=function(a,b,c,d){var e=this.getFoldLine(a);if(!e){var f;f=this.doc.getLine(a);return f.substring(d||0,b||f.length)}return this.getFoldDisplayLine(e,a,b,c,d)},this.$cloneFoldData=function(){var a=this.$foldData,b=[];b=this.$foldData.map(function(a){var c=a.folds.map(function(a){return a.clone()});return new e(b,c)});return b}}function f(a,b){this.foldLine=null,this.placeholder=b,this.range=a,this.start=a.start,this.end=a.end,this.sameRow=a.start.row==a.end.row,this.subFolds=[]}var d=a("ace/range").Range,e=a("ace/edit_session/fold_line").FoldLine;f.prototype.toString=function(){return'"'+this.placeholder+'" '+this.range.toString()},f.prototype.setFoldLine=function(a){this.foldLine=a,this.subFolds.forEach(function(b){b.setFoldLine(a)})},f.prototype.clone=function(){var a=this.range.clone(),b=new f(a,this.placeholder);this.subFolds.forEach(function(a){b.subFolds.push(a.clone())});return b},b.Folding=g}),define("ace/edit_session/fold_line",["require","exports","module","ace/range"],function(a,b,c){function e(a,b){this.foldData=a,Array.isArray(b)?this.folds=b:b=this.folds=[b];var c=b[b.length-1];this.range=new d(b[0].start.row,b[0].start.column,c.end.row,c.end.column),this.start=this.range.start,this.end=this.range.end,this.folds.forEach(function(a){a.setFoldLine(this)},this)}var d=a("ace/range").Range;(function(){this.shiftRow=function(a){this.start.row+=a,this.end.row+=a,this.folds.forEach(function(b){b.start.row+=a,b.end.row+=a})},this.addFold=function(a){if(a.sameRow){if(a.start.row<this.startRow||a.endRow>this.endRow)throw"Can't add a fold to this FoldLine as it has no connection";this.folds.push(a),this.folds.sort(function(a,b){return-a.range.compareEnd(b.start.row,b.start.column)}),this.range.compareEnd(a.start.row,a.start.column)>0?(this.end.row=a.end.row,this.end.column=a.end.column):this.range.compareStart(a.end.row,a.end.column)<0&&(this.start.row=a.start.row,this.start.column=a.start.column)}else if(a.start.row==this.end.row)this.folds.push(a),this.end.row=a.end.row,this.end.column=a.end.column;else if(a.end.row==this.start.row)this.folds.unshift(a),this.start.row=a.start.row,this.start.column=a.start.column;else throw"Trying to add fold to FoldRow that doesn't have a matching row";a.foldLine=this},this.containsRow=function(a){return a>=this.start.row&&a<=this.end.row},this.walk=function(a,b,c){var d=0,e=this.folds,f,g,h,i=!0;b==null&&(b=this.end.row,c=this.end.column);for(var j=0;j<e.length;j++){f=e[j],g=f.range.compareStart(b,c);if(g==-1){a(null,b,c,d,i);return}h=a(null,f.start.row,f.start.column,d,i),h=!h&&a(f.placeholder,f.start.row,f.start.column,d);if(h||g==0)return;i=!f.sameRow,d=f.end.column}a(null,b,c,d,i)},this.getNextFoldTo=function(a,b){var c,d;for(var e=0;e<this.folds.length;e++){c=this.folds[e],d=c.range.compareEnd(a,b);if(d==-1)return{fold:c,kind:"after"};if(d==0)return{fold:c,kind:"inside"}}return null},this.addRemoveChars=function(a,b,c){var d=this.getNextFoldTo(a,b),e,f;if(d){e=d.fold;if(d.kind=="inside"&&e.start.column!=b&&e.start.row!=a)throw"Moving characters inside of a fold should never be reached";if(e.start.row==a){f=this.folds;var g=f.indexOf(e);g==0&&(this.start.column+=c);for(g;g<f.length;g++){e=f[g],e.start.column+=c;if(!e.sameRow)return;e.end.column+=c}this.end.column+=c}}},this.split=function(a,b){var c=this.getNextFoldTo(a,b).fold,d=this.folds,f=this.foldData;if(!c)return null;var g=d.indexOf(c),h=d[g-1];this.end.row=h.end.row,this.end.column=h.end.column,d=d.splice(g,d.length-g);var i=new e(f,d);f.splice(f.indexOf(this)+1,0,i);return i},this.merge=function(a){var b=a.folds;for(var c=0;c<b.length;c++)this.addFold(b[c]);var d=this.foldData;d.splice(d.indexOf(a),1)},this.toString=function(){var a=[this.range.toString()+": ["];this.folds.forEach(function(b){a.push(" "+b.toString())}),a.push("]");return a.join("\n")},this.idxToPosition=function(a){var b=0,c;for(var d=0;d<this.folds.length;d++){var c=this.folds[d];a-=c.start.column-b;if(a<0)return{row:c.start.row,column:c.start.column+a};a-=c.placeholder.length;if(a<0)return c.start;b=c.end.column}return{row:this.end.row,column:this.end.column+a}}}).call(e.prototype),b.FoldLine=e}),define("ace/search",["require","exports","module","pilot/lang","pilot/oop","ace/range"],function(a,b,c){var d=a("pilot/lang"),e=a("pilot/oop"),f=a("ace/range").Range,g=function(){this.$options={needle:"",backwards:!1,wrap:!1,caseSensitive:!1,wholeWord:!1,scope:g.ALL,regExp:!1}};g.ALL=1,g.SELECTION=2,function(){this.set=function(a){e.mixin(this.$options,a);return this},this.getOptions=function(){return d.copyObject(this.$options)},this.find=function(a){if(!this.$options.needle)return null;if(this.$options.backwards)var b=this.$backwardMatchIterator(a);else b=this.$forwardMatchIterator(a);var c=null;b.forEach(function(a){c=a;return!0});return c},this.findAll=function(a){if(!this.$options.needle)return[];if(this.$options.backwards)var b=this.$backwardMatchIterator(a);else b=this.$forwardMatchIterator(a);var c=[];b.forEach(function(a){c.push(a)});return c},this.replace=function(a,b){var c=this.$assembleRegExp(),d=c.exec(a);return d&&d[0].length==a.length?this.$options.regExp?a.replace(c,b):b:null},this.$forwardMatchIterator=function(a){var b=this.$assembleRegExp(),c=this;return{forEach:function(d){c.$forwardLineIterator(a).forEach(function(a,e,f){e&&(a=a.substring(e));var g=[];a.replace(b,function(a){var b=arguments[arguments.length-2];g.push({str:a,offset:e+b});return a});for(var h=0;h<g.length;h++){var i=g[h],j=c.$rangeFromMatch(f,i.offset,i.str.length);if(d(j))return!0}})}}},this.$backwardMatchIterator=function(a){var b=this.$assembleRegExp(),c=this;return{forEach:function(d){c.$backwardLineIterator(a).forEach(function(a,e,f){e&&(a=a.substring(e));var g=[];a.replace(b,function(a,b){g.push({str:a,offset:e+b});return a});for(var h=g.length-1;h>=0;h--){var i=g[h],j=c.$rangeFromMatch(f,i.offset,i.str.length);if(d(j))return!0}})}}},this.$rangeFromMatch=function(a,b,c){return new f(a,b,a,b+c)},this.$assembleRegExp=function(){if(this.$options.regExp)var a=this.$options.needle;else a=d.escapeRegExp(this.$options.needle);this.$options.wholeWord&&(a="\\b"+a+"\\b");var b="g";this.$options.caseSensitive||(b+="i");var c=new RegExp(a,b);return c},this.$forwardLineIterator=function(a){function k(e){var f=a.getLine(e);b&&e==c.end.row&&(f=f.substring(0,c.end.column)),j&&e==d.row&&(f=f.substring(0,d.column));return f}var b=this.$options.scope==g.SELECTION,c=a.getSelection().getRange(),d=a.getSelection().getCursor(),e=b?c.start.row:0,f=b?c.start.column:0,h=b?c.end.row:a.getLength()-1,i=this.$options.wrap,j=!1;return{forEach:function(a){var b=d.row,c=k(b),g=d.column,l=!1;j=!1;while(!a(c,g,b)){if(l)return;b++,g=0;if(b>h)if(i)b=e,g=f,j=!0;else return;b==d.row&&(l=!0),c=k(b)}}}},this.$backwardLineIterator=function(a){var b=this.$options.scope==g.SELECTION,c=a.getSelection().getRange(),d=b?c.end:c.start,e=b?c.start.row:0,f=b?c.start.column:0,h=b?c.end.row:a.getLength()-1,i=this.$options.wrap;return{forEach:function(g){var j=d.row,k=a.getLine(j).substring(0,d.column),l=0,m=!1,n=!1;while(!g(k,l,j)){if(m)return;j--,l=0;if(j<e)if(i)j=h,n=!0;else return;j==d.row&&(m=!0),k=a.getLine(j),b&&(j==e?l=f:j==h&&(k=k.substring(0,c.end.column))),n&&j==d.row&&(l=d.column)}}}}}.call(g.prototype),b.Search=g}),define("ace/undomanager",["require","exports","module"],function(a,b,c){var d=function(){this.reset()};(function(){this.execute=function(a){var b=a.args[0];this.$doc=a.args[1],this.$undoStack.push(b),this.$redoStack=[]},this.undo=function(a){var b=this.$undoStack.pop(),c=null;b&&(c=this.$doc.undoChanges(b,a),this.$redoStack.push(b));return c},this.redo=function(a){var b=this.$redoStack.pop(),c=null;b&&(c=this.$doc.redoChanges(b,a),this.$undoStack.push(b));return c},this.reset=function(){this.$undoStack=[],this.$redoStack=[]},this.hasUndo=function(){return this.$undoStack.length>0},this.hasRedo=function(){return this.$redoStack.length>0}}).call(d.prototype),b.UndoManager=d}),define("ace/virtual_renderer",["require","exports","module","pilot/oop","pilot/dom","pilot/event","pilot/useragent","ace/layer/gutter","ace/layer/marker","ace/layer/text","ace/layer/cursor","ace/scrollbar","ace/renderloop","pilot/event_emitter","text/ace/css/editor.css"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/dom"),f=a("pilot/event"),g=a("pilot/useragent"),h=a("ace/layer/gutter").Gutter,i=a("ace/layer/marker").Marker,j=a("ace/layer/text").Text,k=a("ace/layer/cursor").Cursor,l=a("ace/scrollbar").ScrollBar,m=a("ace/renderloop").RenderLoop,n=a("pilot/event_emitter").EventEmitter,o=a("text/ace/css/editor.css");e.importCssString(o);var p=function(a,b){this.container=a,e.addCssClass(this.container,"ace_editor"),this.setTheme(b),this.$gutter=e.createElement("div"),this.$gutter.className="ace_gutter",this.container.appendChild(this.$gutter),this.scroller=e.createElement("div"),this.scroller.className="ace_scroller",this.container.appendChild(this.scroller),this.content=e.createElement("div"),this.content.className="ace_content",this.scroller.appendChild(this.content),this.$gutterLayer=new h(this.$gutter),this.$markerBack=new i(this.content);var c=this.$textLayer=new j(this.content);this.canvas=c.element,this.$markerFront=new i(this.content),this.characterWidth=c.getCharacterWidth(),this.lineHeight=c.getLineHeight(),this.$cursorLayer=new k(this.content),this.$cursorPadding=8,this.$horizScroll=!0,this.$horizScrollAlwaysVisible=!0,this.scrollBar=new l(a),this.scrollBar.addEventListener("scroll",this.onScroll.bind(this)),this.scrollTop=0,this.cursorPos={row:0,column:0};var d=this;this.$textLayer.addEventListener("changeCharaterSize",function(){d.characterWidth=c.getCharacterWidth(),d.lineHeight=c.getLineHeight(),d.$updatePrintMargin(),d.onResize(!0),d.$loop.schedule(d.CHANGE_FULL)}),f.addListener(this.$gutter,"click",this.$onGutterClick.bind(this)),f.addListener(this.$gutter,"dblclick",this.$onGutterClick.bind(this)),this.$size={width:0,height:0,scrollerHeight:0,scrollerWidth:0},this.$loop=new m(this.$renderChanges.bind(this)),this.$loop.schedule(this.CHANGE_FULL),this.setPadding(4),this.$updatePrintMargin()};(function(){this.showGutter=!0,this.CHANGE_CURSOR=1,this.CHANGE_MARKER=2,this.CHANGE_GUTTER=4,this.CHANGE_SCROLL=8,this.CHANGE_LINES=16,this.CHANGE_TEXT=32,this.CHANGE_SIZE=64,this.CHANGE_MARKER_BACK=128,this.CHANGE_MARKER_FRONT=256,this.CHANGE_FULL=512,d.implement(this,n),this.setSession=function(a){this.session=a,this.$cursorLayer.setSession(a),this.$markerBack.setSession(a),this.$markerFront.setSession(a),this.$gutterLayer.setSession(a),this.$textLayer.setSession(a),this.$loop.schedule(this.CHANGE_FULL)},this.updateLines=function(a,b){b===undefined&&(b=Infinity),this.$changedLines?(this.$changedLines.firstRow>a&&(this.$changedLines.firstRow=a),this.$changedLines.lastRow<b&&(this.$changedLines.lastRow=b)):this.$changedLines={firstRow:a,lastRow:b},this.$loop.schedule(this.CHANGE_LINES)},this.updateText=function(){this.$loop.schedule(this.CHANGE_TEXT)},this.updateFull=function(){this.$loop.schedule(this.CHANGE_FULL)},this.updateFontSize=function(){this.$textLayer.checkForSizeChanges()},this.onResize=function(a){var b=this.CHANGE_SIZE,c=e.getInnerHeight(this.container);if(a||this.$size.height!=c)this.$size.height=c,this.scroller.style.height=c+"px",this.scrollBar.setHeight(this.scroller.clientHeight),this.session&&(this.scrollToY(this.getScrollTop()),b=b|this.CHANGE_FULL);var d=e.getInnerWidth(this.container);if(a||this.$size.width!=d){this.$size.width=d;var f=this.showGutter?this.$gutter.offsetWidth:0;this.scroller.style.left=f+"px",this.scroller.style.width=Math.max(0,d-f-this.scrollBar.getWidth())+"px";if(this.session.getUseWrapMode()){var g=this.scroller.clientWidth-this.$padding*2,h=Math.floor(g/this.characterWidth)-1;if(this.session.adjustWrapLimit(h)||a)b=b|this.CHANGE_FULL}}this.$size.scrollerWidth=this.scroller.clientWidth,this.$size.scrollerHeight=this.scroller.clientHeight,this.$loop.schedule(b)},this.$onGutterClick=function(a){var b=f.getDocumentX(a),c=f.getDocumentY(a);this._dispatchEvent("gutter"+a.type,{row:this.screenToTextCoordinates(b,c).row,htmlEvent:a})},this.setShowInvisibles=function(a){this.$textLayer.setShowInvisibles(a)&&this.$loop.schedule(this.CHANGE_TEXT)},this.getShowInvisibles=function(){return this.$textLayer.showInvisibles},this.$showPrintMargin=!0,this.setShowPrintMargin=function(a){this.$showPrintMargin=a,this.$updatePrintMargin()},this.getShowPrintMargin=function(){return this.$showPrintMargin},this.$printMarginColumn=80,this.setPrintMarginColumn=function(a){this.$printMarginColumn=a,this.$updatePrintMargin()},this.getPrintMarginColumn=function(){return this.$printMarginColumn},this.getShowGutter=function(){return this.showGutter},this.setShowGutter=function(a){this.showGutter!==a&&(this.$gutter.style.display=a?"block":"none",this.showGutter=a,this.onResize(!0))},this.$updatePrintMargin=function(){var a;if(!!this.$showPrintMargin||!!this.$printMarginEl){this.$printMarginEl||(a=e.createElement("div"),a.className="ace_print_margin_layer",this.$printMarginEl=e.createElement("div"),this.$printMarginEl.className="ace_print_margin",a.appendChild(this.$printMarginEl),this.content.insertBefore(a,this.$textLayer.element));var b=this.$printMarginEl.style;b.left=this.characterWidth*this.$printMarginColumn+this.$padding*2+"px",b.visibility=this.$showPrintMargin?"visible":"hidden"}},this.getContainerElement=function(){return this.container},this.getMouseEventTarget=function(){return this.content},this.getTextAreaContainer=function(){return this.container},this.moveTextAreaToCursor=function(a){if(!g.isIE){var b=this.$cursorLayer.getPixelPosition();if(!b)return;var c=this.content.getBoundingClientRect(),d=this.layerConfig&&this.layerConfig.offset||0;a.style.left=c.left+b.left+this.$padding+"px",a.style.top=c.top+b.top-this.scrollTop+d+"px"}},this.getFirstVisibleRow=function(){return(this.layerConfig||{}).firstRow||0},this.getFirstFullyVisibleRow=function(){if(!this.layerConfig)return 0;return this.layerConfig.firstRow+(this.layerConfig.offset===0?0:1)},this.getLastFullyVisibleRow=function(){if(!this.layerConfig)return 0;var a=Math.floor((this.layerConfig.height+this.layerConfig.offset)/this.layerConfig.lineHeight);return this.layerConfig.firstRow-1+a},this.getLastVisibleRow=function(){return(this.layerConfig||{}).lastRow||0},this.$padding=null,this.setPadding=function(a){this.$padding=a,this.content.style.padding="0 "+a+"px",this.$loop.schedule(this.CHANGE_FULL),this.$updatePrintMargin()},this.getHScrollBarAlwaysVisible=function(){return this.$horizScrollAlwaysVisible},this.setHScrollBarAlwaysVisible=function(a){this.$horizScrollAlwaysVisible!=a&&(this.$horizScrollAlwaysVisible=a,(!this.$horizScrollAlwaysVisible||!this.$horizScroll)&&this.$loop.schedule(this.CHANGE_SCROLL))},this.onScroll=function(a){this.scrollToY(a.data)},this.$updateScrollBar=function(){this.scrollBar.setInnerHeight(this.layerConfig.maxHeight),this.scrollBar.setScrollTop(this.scrollTop)},this.$renderChanges=function(a){if(!!a&&!!this.session){(!this.layerConfig||a&this.CHANGE_FULL||a&this.CHANGE_SIZE||a&this.CHANGE_TEXT||a&this.CHANGE_LINES||a&this.CHANGE_SCROLL)&&this.$computeLayerConfig();if(a&this.CHANGE_FULL){this.$textLayer.update(this.layerConfig),this.showGutter&&this.$gutterLayer.update(this.layerConfig),this.$markerBack.update(this.layerConfig),this.$markerFront.update(this.layerConfig),this.$cursorLayer.update(this.layerConfig),this.$updateScrollBar();return}if(a&this.CHANGE_SCROLL){a&this.CHANGE_TEXT||a&this.CHANGE_LINES?this.$textLayer.update(this.layerConfig):this.$textLayer.scrollLines(this.layerConfig),this.showGutter&&this.$gutterLayer.update(this.layerConfig),this.$markerBack.update(this.layerConfig),this.$markerFront.update(this.layerConfig),this.$cursorLayer.update(this.layerConfig),this.$updateScrollBar();return}a&this.CHANGE_TEXT?(this.$textLayer.update(this.layerConfig),this.showGutter&&this.$gutterLayer.update(this.layerConfig)):a&this.CHANGE_LINES?(this.$updateLines(),this.$updateScrollBar(),this.showGutter&&this.$gutterLayer.update(this.layerConfig)):a&this.CHANGE_GUTTER&&this.showGutter&&this.$gutterLayer.update(this.layerConfig),a&this.CHANGE_CURSOR&&this.$cursorLayer.update(this.layerConfig),a&(this.CHANGE_MARKER|this.CHANGE_MARKER_FRONT)&&this.$markerFront.update(this.layerConfig),a&(this.CHANGE_MARKER|this.CHANGE_MARKER_BACK)&&this.$markerBack.update(this.layerConfig),a&this.CHANGE_SIZE&&this.$updateScrollBar()}},this.$computeLayerConfig=function(){var a=this.session,b=this.scrollTop%this.lineHeight,c=this.$size.scrollerHeight+this.lineHeight,d=this.$getLongestLine(),e=this.layerConfig?this.layerConfig.width!=d:!0,f=this.$horizScrollAlwaysVisible||this.$size.scrollerWidth-d<0,g=this.$horizScroll!==f;this.$horizScroll=f,g&&(this.scroller.style.overflowX=f?"scroll":"hidden");var h=this.session.getScreenLength()*this.lineHeight;this.scrollTop=Math.max(0,Math.min(this.scrollTop,h-this.$size.scrollerHeight));var i=Math.ceil(c/this.lineHeight)-1,j=Math.max(0,Math.round((this.scrollTop-b)/this.lineHeight)),k=j+i,l,m,n={lineHeight:this.lineHeight};j=a.screenToDocumentRow(j,0);var o=a.getFoldLine(j);o&&(j=o.start.row),l=a.documentToScreenRow(j,0),m=a.getRowHeight(n,j),k=Math.min(a.screenToDocumentRow(k,0),a.getLength()-1),c=this.$size.scrollerHeight+a.getRowHeight(n,k)+m,b=this.scrollTop-l*this.lineHeight,this.layerConfig={width:d,padding:this.$padding,firstRow:j,firstRowScreen:l,lastRow:k,lineHeight:this.lineHeight,characterWidth:this.characterWidth,minHeight:c,maxHeight:h,offset:b,height:this.$size.scrollerHeight},this.$gutterLayer.element.style.marginTop=-b+"px",this.content.style.marginTop=-b+"px",this.content.style.width=d+"px",this.content.style.height=c+"px",this.$desiredScrollLeft&&(this.scrollToX(this.$desiredScrollLeft),this.$desiredScrollLeft=0),g&&this.onResize(!0)},this.$updateLines=function(){var a=this.$changedLines.firstRow,b=this.$changedLines.lastRow;this.$changedLines=null;var c=this.layerConfig;if(c.width!=this.$getLongestLine())return this.$textLayer.update(c);if(!(a>c.lastRow+1)){if(b<c.firstRow)return;if(b===Infinity){this.showGutter&&this.$gutterLayer.update(c),this.$textLayer.update(c);return}this.$textLayer.updateLines(c,a,b)}},this.$getLongestLine=function(){var a=this.session.getScreenWidth()+1;this.$textLayer.showInvisibles&&(a+=1);return Math.max(this.$size.scrollerWidth-this.$padding*2,Math.round(a*this.characterWidth))},this.updateFrontMarkers=function(){this.$markerFront.setMarkers(this.session.getMarkers(!0)),this.$loop.schedule(this.CHANGE_MARKER_FRONT)},this.updateBackMarkers=function(){this.$markerBack.setMarkers(this.session.getMarkers()),this.$loop.schedule(this.CHANGE_MARKER_BACK)},this.addGutterDecoration=function(a,b){this.$gutterLayer.addGutterDecoration(a,b),this.$loop.schedule(this.CHANGE_GUTTER)},this.removeGutterDecoration=function(a,b){this.$gutterLayer.removeGutterDecoration(a,b),this.$loop.schedule(this.CHANGE_GUTTER)},this.setBreakpoints=function(a){this.$gutterLayer.setBreakpoints(a),this.$loop.schedule(this.CHANGE_GUTTER)},this.setAnnotations=function(a){this.$gutterLayer.setAnnotations(a),this.$loop.schedule(this.CHANGE_GUTTER)},this.updateCursor=function(){this.$loop.schedule(this.CHANGE_CURSOR)},this.hideCursor=function(){this.$cursorLayer.hideCursor()},this.showCursor=function(){this.$cursorLayer.showCursor()},this.scrollCursorIntoView=function(){if(this.$size.scrollerHeight!==0){var a=this.$cursorLayer.getPixelPosition(),b=a.left+this.$padding,c=a.top;this.scrollTop>c&&this.scrollToY(c),this.scrollTop+this.$size.scrollerHeight<c+this.lineHeight&&this.scrollToY(c+this.lineHeight-this.$size.scrollerHeight);var d=this.scroller.scrollLeft;d>b&&this.scrollToX(b),d+this.$size.scrollerWidth<b+this.characterWidth&&(b>this.layerConfig.width?this.$desiredScrollLeft=b+2*this.characterWidth:this.scrollToX(Math.round(b+this.characterWidth-this.$size.scrollerWidth)))}},this.getScrollTop=function(){return this.scrollTop},this.getScrollLeft=function(){return this.scroller.scrollLeft},this.getScrollTopRow=function(){return this.scrollTop/this.lineHeight},this.getScrollBottomRow=function(){return Math.max(0,Math.floor((this.scrollTop+this.$size.scrollerHeight)/this.lineHeight)-1)},this.scrollToRow=function(a){this.scrollToY(a*this.lineHeight)},this.scrollToLine=function(a,b){var c={lineHeight:this.lineHeight},d=0;for(var e=1;e<a;e++)d+=this.session.getRowHeight(c,e-1);b&&(d-=this.$size.scrollerHeight/2),this.scrollToY(d)},this.scrollToY=function(a){a=Math.max(0,a),this.scrollTop!==a&&(this.$loop.schedule(this.CHANGE_SCROLL),this.scrollTop=a)},this.scrollToX=function(a){a<=this.$padding&&(a=0),this.scroller.scrollLeft=a},this.scrollBy=function(a,b){b&&this.scrollToY(this.scrollTop+b),a&&this.scrollToX(this.scroller.scrollLeft+a)},this.screenToTextCoordinates=function(a,b){var c=this.scroller.getBoundingClientRect(),d=Math.round((a+this.scroller.scrollLeft-c.left-this.$padding-e.getPageScrollLeft())/this.characterWidth),f=Math.floor((b+this.scrollTop-c.top-e.getPageScrollTop())/this.lineHeight);return this.session.screenToDocumentPosition(f,Math.max(d,0))},this.textToScreenCoordinates=function(a,b){var c=this.scroller.getBoundingClientRect(),d=this.session.documentToScreenPosition(a,b),e=this.$padding+Math.round(d.column*this.characterWidth),f=d.row*this.lineHeight;return{pageX:c.left+e-this.getScrollLeft(),pageY:c.top+f-this.getScrollTop()}},this.visualizeFocus=function(){e.addCssClass(this.container,"ace_focus")},this.visualizeBlur=function(){e.removeCssClass(this.container,"ace_focus")},this.showComposition=function(a){this.$composition||(this.$composition=e.createElement("div"),this.$composition.className="ace_composition",this.content.appendChild(this.$composition)),this.$composition.innerHTML="&#160;";var b=this.$cursorLayer.getPixelPosition(),c=this.$composition.style;c.top=b.top+"px",c.left=b.left+this.$padding+"px",c.height=this.lineHeight+"px",this.hideCursor()},this.setCompositionText=function(a){e.setInnerText(this.$composition,a)},this.hideComposition=function(){this.showCursor();if(!!this.$composition){var a=this.$composition.style;a.top="-10000px",a.left="-10000px"}},this.setTheme=function(b){function d(a){c.$theme&&e.removeCssClass(c.container,c.$theme),c.$theme=a?a.cssClass:null,c.$theme&&e.addCssClass(c.container,c.$theme),c.$size&&(c.$size.width=0,c.onResize())}var c=this;this.$themeValue=b,!b||typeof b=="string"?(b=b||"ace/theme/textmate",a([b],function(a){d(a)})):d(b)},this.getTheme=function(){return this.$themeValue},this.setStyle=function(b){e.addCssClass(this.container,b)},this.unsetStyle=function(b){e.removeCssClass(this.container,b)},this.destroy=function(){this.$textLayer.destroy(),this.$cursorLayer.destroy()}}).call(p.prototype),b.VirtualRenderer=p}),define("ace/layer/gutter",["require","exports","module","pilot/dom"],function(a,b,c){var d=a("pilot/dom"),e=function(a){this.element=d.createElement("div"),this.element.className="ace_layer ace_gutter-layer",a.appendChild(this.element),this.$breakpoints=[],this.$annotations=[],this.$decorations=[]};(function(){this.setSession=function(a){this.session=a},this.addGutterDecoration=function(a,b){this.$decorations[a]||(this.$decorations[a]=""),this.$decorations[a]+=" ace_"+b},this.removeGutterDecoration=function(a,b){this.$decorations[a]=this.$decorations[a].replace(" ace_"+b,"")},this.setBreakpoints=function(a){this.$breakpoints=a.concat()},this.setAnnotations=function(a){this.$annotations=[];for(var b in a)if(a.hasOwnProperty(b)){var c=a[b];if(!c)continue;var d=this.$annotations[b]={text:[]};for(var e=0;e<c.length;e++){var f=c[e];d.text.push(f.text.replace(/"/g,"&quot;").replace(/'/g,"&#8217;").replace(/</,"&lt;"));var g=f.type;g=="error"?d.className="ace_error":g=="warning"&&d.className!="ace_error"?d.className="ace_warning":g=="info"&&!d.className&&(d.className="ace_info")}}},this.update=function(a){this.$config=a;var b={className:"",text:[]},c=[],e=a.firstRow,f=a.lastRow,g=this.session.getNextFold(e),h=g?g.start.row:Infinity;for(;;){e>h&&(e=g.end.row+1,g=this.session.getNextFold(e),h=g?g.start.row:Infinity);if(e>f)break;var i=this.$annotations[e]||b;c.push("<div class='ace_gutter-cell",this.$decorations[e]||"",this.$breakpoints[e]?" ace_breakpoint ":" ",i.className,"' title='",i.text.join("\n"),"' style='height:",a.lineHeight,"px;'>",e+1);var j=this.session.getRowLength(e)-1;while(j--)c.push("</div><div class='ace_gutter-cell' style='height:",a.lineHeight,"px'>&brvbar;</div>");c.push("</div>"),e++}this.element=d.setInnerHtml(this.element,c.join("")),this.element.style.height=a.minHeight+"px"}}).call(e.prototype),b.Gutter=e}),define("ace/layer/marker",["require","exports","module","ace/range","pilot/dom"],function(a,b,c){var d=a("ace/range").Range,e=a("pilot/dom"),f=function(a){this.element=e.createElement("div"),this.element.className="ace_layer ace_marker-layer",a.appendChild(this.element)};(function(){this.setSession=function(a){this.session=a},this.setMarkers=function(a){this.markers=a},this.update=function(a){var a=a||this.config;if(!!a){this.config=a;var b=[];for(var c in this.markers){var d=this.markers[c],f=d.range.clipRows(a.firstRow,a.lastRow);if(f.isEmpty())continue;f=f.toScreenRange(this.session);if(d.renderer){var g=this.$getTop(f.start.row,a),h=Math.round(f.start.column*a.characterWidth);d.renderer(b,f,h,g,a)}else f.isMultiLine()?d.type=="text"?this.drawTextMarker(b,f,d.clazz,a):this.drawMultiLineMarker(b,f,d.clazz,a):this.drawSingleLineMarker(b,f,d.clazz,a)}this.element=e.setInnerHtml(this.element,b.join(""))}},this.$getTop=function(a,b){return(a-b.firstRowScreen)*b.lineHeight},this.drawTextMarker=function(a,b,c,e){var f=b.start.row,g=new d(f,b.start.column,f,this.session.getScreenLastRowColumn(f));this.drawSingleLineMarker(a,g,c,e,1);var f=b.end.row,g=new d(f,0,f,b.end.column);this.drawSingleLineMarker(a,g,c,e);for(var f=b.start.row+1;f<b.end.row;f++)g.start.row=f,g.end.row=f,g.end.column=this.session.getScreenLastRowColumn(f),this.drawSingleLineMarker(a,g,c,e,1)},this.drawMultiLineMarker=function(a,b,c,d){var e=d.lineHeight,f=Math.round(d.width-b.start.column*d.characterWidth),g=this.$getTop(b.start.row,d),h=Math.round(b.start.column*d.characterWidth);a.push("<div class='",c,"' style='","height:",e,"px;","width:",f,"px;","top:",g,"px;","left:",h,"px;'></div>");var g=this.$getTop(b.end.row,d),f=Math.round(b.end.column*d.characterWidth);a.push("<div class='",c,"' style='","height:",e,"px;","top:",g,"px;","width:",f,"px;'></div>");var e=(b.end.row-b.start.row-1)*d.lineHeight;if(!(e<0)){var g=this.$getTop(b.start.row+1,d);a.push("<div class='",c,"' style='","height:",e,"px;","width:",d.width,"px;","top:",g,"px;'></div>")}},this.drawSingleLineMarker=function(a,b,c,d,e){var f=d.lineHeight,g=Math.round((b.end.column+(e||0)-b.start.column)*d.characterWidth),h=this.$getTop(b.start.row,d),i=Math.round(b.start.column*d.characterWidth);a.push("<div class='",c,"' style='","height:",f,"px;","width:",g,"px;","top:",h,"px;","left:",i,"px;'></div>")}}).call(f.prototype),b.Marker=f}),define("ace/layer/text",["require","exports","module","pilot/oop","pilot/dom","pilot/lang","pilot/useragent","pilot/event_emitter"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/dom"),f=a("pilot/lang"),g=a("pilot/useragent"),h=a("pilot/event_emitter").EventEmitter,i=function(a){this.element=e.createElement("div"),this.element.className="ace_layer ace_text-layer",a.appendChild(this.element),this.$characterSize=this.$measureSizes()||{width:0,height:0},this.$pollSizeChanges()};(function(){d.implement(this,h),this.EOF_CHAR="&para;",this.EOL_CHAR="&not;",this.TAB_CHAR="&rarr;",this.SPACE_CHAR="&middot;",this.getLineHeight=function(){return this.$characterSize.height||1},this.getCharacterWidth=function(){return this.$characterSize.width||1},this.checkForSizeChanges=function(){var a=this.$measureSizes();a&&(this.$characterSize.width!==a.width||this.$characterSize.height!==a.height)&&(this.$characterSize=a,this._dispatchEvent("changeCharaterSize",{data:a}))},this.$pollSizeChanges=function(){var a=this;this.$pollSizeChangesTimer=setInterval(function(){a.checkForSizeChanges()},500)},this.$fontStyles={fontFamily:1,fontSize:1,fontWeight:1,fontStyle:1,lineHeight:1},this.$measureSizes=function(){var a=1e3;if(!this.$measureNode){var b=this.$measureNode=e.createElement("div"),c=b.style;c.width=c.height="auto",c.left=c.top=-a*40+"px",c.visibility="hidden",c.position="absolute",c.overflow="visible",c.whiteSpace="nowrap",b.innerHTML=f.stringRepeat("Xy",a);if(document.body)document.body.appendChild(b);else{var d=this.element.parentNode;while(!e.hasCssClass(d,"ace_editor"))d=d.parentNode;d.appendChild(b)}}var c=this.$measureNode.style,g=e.computedStyle(this.element);for(var h in this.$fontStyles)c[h]=g[h];var i={height:this.$measureNode.offsetHeight,width:this.$measureNode.offsetWidth/(a*2)};if(i.width==0&&i.height==0)return null;return i},this.setSession=function(a){this.session=a},this.showInvisibles=!1,this.setShowInvisibles=function(a){if(this.showInvisibles==a)return!1;this.showInvisibles=a;return!0},this.$tabStrings=[],this.$computeTabString=function(){var a=this.session.getTabSize(),b=this.$tabStrings=[0];for(var c=1;c<a+1;c++)this.showInvisibles?b.push("<span class='ace_invisible'>"+this.TAB_CHAR+Array(c).join("&#160;")+"</span>"):b.push(Array(c+1).join("&#160;"))},this.updateLines=function(a,b,c){this.$computeTabString(),(this.config.lastRow!=a.lastRow||this.config.firstRow!=a.firstRow)&&this.scrollLines(a),this.config=a;var d=Math.max(b,a.firstRow),f=Math.min(c,a.lastRow),g=this.element.childNodes,h=0;for(var i=a.firstRow;i<d;i++){var j=this.session.getFoldLine(i);if(j){if(j.containsRow(d))break;i=j.end.row}h++}for(var k=d;k<=f;k++){var l=g[h++];if(!l)continue;var m=[],n=this.session.getTokens(k,k);this.$renderLine(m,k,n[0].tokens),l=e.setInnerHtml(l,m.join("")),k=this.session.getRowFoldEnd(k)}},this.scrollLines=function(a){this.$computeTabString();var b=this.config;this.config=a;if(!b||b.lastRow<a.firstRow)return this.update(a);if(a.lastRow<b.firstRow)return this.update(a);var c=this.element;if(b.firstRow<a.firstRow)for(var d=this.session.getFoldedRowCount(b.firstRow,a.firstRow-1);d>0;d--)c.removeChild(c.firstChild);if(b.lastRow>a.lastRow)for(var d=this.session.getFoldedRowCount(a.lastRow+1,b.lastRow);d>0;d--)c.removeChild(c.lastChild);if(a.firstRow<b.firstRow){var e=this.$renderLinesFragment(a,a.firstRow,b.firstRow-1);c.firstChild?c.insertBefore(e,c.firstChild):c.appendChild(e)}if(a.lastRow>b.lastRow){var e=this.$renderLinesFragment(a,b.lastRow+1,a.lastRow);c.appendChild(e)}},this.$renderLinesFragment=function(a,b,c){var d=document.createDocumentFragment(),f=b,g=this.session.getNextFold(f),h=g?g.start.row:Infinity;for(;;){f>h&&(f=g.end.row+1,g=this.session.getNextFold(f),h=g?g.start.row:Infinity);if(f>c)break;var i=e.createElement("div");i.className="ace_line";var j=[],k=this.session.getTokens(f,f);k.length==1&&this.$renderLine(j,f,k[0].tokens),i.innerHTML=j.join(""),d.appendChild(i),f++}return d},this.update=function(a){this.$computeTabString(),this.config=a;var b=[],c=a.firstRow,d=a.lastRow,f=c,g=this.session.getNextFold(f),h=g?g.start.row:Infinity;for(;;){f>h&&(f=g.end.row+1,g=this.session.getNextFold(f),h=g?g.start.row:Infinity);if(f>d)break;b.push("<div class='ace_line'>");var i=this.session.getTokens(f,f);i.length==1&&this.$renderLine(b,f,i[0].tokens),b.push("</div>"),f++}this.element=e.setInnerHtml(this.element,b.join(""))},this.$textToken={text:!0,rparen:!0,lparen:!0},this.$renderToken=function(a,b,c,d){var e=this,f=/\t|&|<|( +)|([\v\f \u00a0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000])|[\u1100-\u115F]|[\u11A3-\u11A7]|[\u11FA-\u11FF]|[\u2329-\u232A]|[\u2E80-\u2E99]|[\u2E9B-\u2EF3]|[\u2F00-\u2FD5]|[\u2FF0-\u2FFB]|[\u3000-\u303E]|[\u3041-\u3096]|[\u3099-\u30FF]|[\u3105-\u312D]|[\u3131-\u318E]|[\u3190-\u31BA]|[\u31C0-\u31E3]|[\u31F0-\u321E]|[\u3220-\u3247]|[\u3250-\u32FE]|[\u3300-\u4DBF]|[\u4E00-\uA48C]|[\uA490-\uA4C6]|[\uA960-\uA97C]|[\uAC00-\uD7A3]|[\uD7B0-\uD7C6]|[\uD7CB-\uD7FB]|[\uF900-\uFAFF]|[\uFE10-\uFE19]|[\uFE30-\uFE52]|[\uFE54-\uFE66]|[\uFE68-\uFE6B]|[\uFF01-\uFF60]|[\uFFE0-\uFFE6]/g,h=function(a,c,d,f,h){if(a.charCodeAt(0)==32)return Array(a.length+1).join("&#160;");if(a=="\t"){var i=e.session.getScreenTabSize(b+f);b+=i-1;return e.$tabStrings[i]}if(a=="&")return g.isOldGecko?"&":"&amp;";if(a=="<")return"&lt;";if(a.match(/[\v\f \u00a0\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u200b\u2028\u2029\u3000]/)){if(e.showInvisibles){var j=Array(a.length+1).join(e.SPACE_CHAR);return"<span class='ace_invisible'>"+j+"</span>"}return"&#160;"}b+=1;return"<span class='ace_cjk' style='width:"+e.config.characterWidth*2+"px'>"+a+"</span>"},i=d.replace(f,h);if(!this.$textToken[c.type]){var j="ace_"+c.type.replace(/\./g," ace_");a.push("<span class='",j,"'>",i,"</span>")}else a.push(i);return d.length},this.$renderLineCore=function(a,b,c,d){function k(b,c){i+=j.$renderToken(a,i,b,c)}var e=0,f=0,g,h=this.config.characterWidth,i=0,j=this;!d||d.length==0?g=Number.MAX_VALUE:g=d[0],a.push("<div style='height:",this.config.lineHeight,"px","'>");for(var l=0;l<c.length;l++){var m=c[l],n=m.value;if(e+n.length<g)k(m,n),e+=n.length;else{while(e+n.length>=g)k(m,n.substring(0,g-e)),n=n.substring(g-e),e=g,a.push("</div>","<div style='height:",this.config.lineHeight,"px","'>"),f++,i=0,g=d[f]||Number.MAX_VALUE;n.length!=0&&(e+=n.length,k(m,n))}}this.showInvisibles&&(b!==this.session.getLength()-1?a.push("<span class='ace_invisible'>"+this.EOL_CHAR+"</span>"):a.push("<span class='ace_invisible'>"+this.EOF_CHAR+"</span>")),a.push("</div>")},this.$renderLine=function(a,b,c){if(!this.session.isRowFolded(b)){var d=this.session.getRowSplitData(b);this.$renderLineCore(a,b,c,d)}else this.$renderFoldLine(a,b,c)},this.$renderFoldLine=function(a,b,c){function g(a,b,c){var d=0,e=0;while(e+a[d].value.length<b){e+=a[d].value.length,d++;if(d==a.length)return}if(e!=b){var g=a[d].value.substring(b-e);g.length>c-b&&(g=g.substring(0,c-b)),f.push({type:a[d].type,value:g}),e=b+g.length,d+=1}while(e<c){var g=a[d].value;g.length+e>c&&(g=g.substring(0,c-e)),f.push({type:a[d].type,value:g}),e+=g.length,d+=1}}var d=this.session,e=d.getFoldLine(b),f=[];e.walk(function(a,b,d,e,h){a?f.push({type:"fold",value:a}):(h&&(c=this.session.getTokens(b,b)[0].tokens),c.length!=0&&g(c,e,d))}.bind(this),e.end.row,this.session.getLine(e.end.row).length);var h=this.session.$useWrapMode?this.session.$wrapData[b]:null;this.$renderLineCore(a,b,f,h)},this.destroy=function(){clearInterval(this.$pollSizeChangesTimer)}}).call(i.prototype),b.Text=i}),define("ace/layer/cursor",["require","exports","module","pilot/dom"],function(a,b,c){var d=a("pilot/dom"),e=function(a){this.element=d.createElement("div"),this.element.className="ace_layer ace_cursor-layer",a.appendChild(this.element),this.cursor=d.createElement("div"),this.cursor.className="ace_cursor ace_hidden",this.element.appendChild(this.cursor),this.isVisible=!1};(function(){this.setSession=function(a){this.session=a},this.hideCursor=function(){this.isVisible=!1,d.addCssClass(this.cursor,"ace_hidden"),clearInterval(this.blinkId)},this.showCursor=function(){this.isVisible=!0,d.removeCssClass(this.cursor,"ace_hidden"),this.cursor.style.visibility="visible",this.restartTimer()},this.restartTimer=function(){clearInterval(this.blinkId);if(!!this.isVisible){var a=this.cursor;this.blinkId=setInterval(function(){a.style.visibility="hidden",setTimeout(function(){a.style.visibility="visible"},400)},1e3)}},this.getPixelPosition=function(a){if(!this.config||!this.session)return{left:0,top:0};var b=this.session.selection.getCursor(),c=this.session.documentToScreenPosition(b),d=Math.round(c.column*this.config.characterWidth),e=(c.row-(a?this.config.firstRowScreen:0))*this.config.lineHeight;return{left:d,top:e}},this.update=function(a){this.config=a,this.pixelPos=this.getPixelPosition(!0),this.cursor.style.left=this.pixelPos.left+"px",this.cursor.style.top=this.pixelPos.top+"px",this.cursor.style.width=a.characterWidth+"px",this.cursor.style.height=a.lineHeight+"px";var b=this.session.getOverwrite();b!=this.overwrite&&(this.overwrite=b,b?d.addCssClass(this.cursor,"ace_overwrite"):d.removeCssClass(this.cursor,"ace_overwrite")),this.restartTimer()},this.destroy=function(){clearInterval(this.blinkId)}}).call(e.prototype),b.Cursor=e}),define("ace/scrollbar",["require","exports","module","pilot/oop","pilot/dom","pilot/event","pilot/event_emitter"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/dom"),f=a("pilot/event"),g=a("pilot/event_emitter").EventEmitter,h=function(a){this.element=e.createElement("div"),this.element.className="ace_sb",this.inner=e.createElement("div"),this.element.appendChild(this.inner),a.appendChild(this.element),this.width=e.scrollbarWidth(),this.element.style.width=this.width+"px",f.addListener(this.element,"scroll",this.onScroll.bind(this))};(function(){d.implement(this,g),this.onScroll=function(){this._dispatchEvent("scroll",{data:this.element.scrollTop})},this.getWidth=function(){return this.width},this.setHeight=function(a){this.element.style.height=a+"px"},this.setInnerHeight=function(a){this.inner.style.height=a+"px"},this.setScrollTop=function(a){this.element.scrollTop=a}}).call(h.prototype),b.ScrollBar=h}),define("ace/renderloop",["require","exports","module","pilot/event"],function(a,b,c){var d=a("pilot/event"),e=function(a){this.onRender=a,this.pending=!1,this.changes=0};(function(){this.schedule=function(a){this.changes=this.changes|a;if(!this.pending){this.pending=!0;var b=this;this.setTimeoutZero(function(){b.pending=!1;var a=b.changes;b.changes=0,b.onRender(a)})}},this.setTimeoutZero=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame,this.setTimeoutZero?this.setTimeoutZero=this.setTimeoutZero.bind(window):window.postMessage?(this.messageName="zero-timeout-message",this.setTimeoutZero=function(a){if(!this.attached){var b=this;d.addListener(window,"message",function(a){b.callback&&a.data==b.messageName&&(d.stopPropagation(a),b.callback())}),this.attached=!0}this.callback=a,window.postMessage(this.messageName,"*")}):this.setTimeoutZero=function(a){setTimeout(a,0)}}).call(e.prototype),b.RenderLoop=e}),define("ace/theme/textmate",["require","exports","module","pilot/dom"],function(a,b,c){var d=a("pilot/dom"),e=".ace-tm .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-tm .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-tm .ace_gutter {\n width: 50px;\n background: #e8e8e8;\n color: #333;\n overflow : hidden;\n}\n\n.ace-tm .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-tm .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-tm .ace_print_margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-tm .ace_text-layer {\n cursor: text;\n}\n\n.ace-tm .ace_cursor {\n border-left: 2px solid black;\n}\n\n.ace-tm .ace_cursor.ace_overwrite {\n border-left: 0px;\n border-bottom: 1px solid black;\n}\n \n.ace-tm .ace_line .ace_invisible {\n color: rgb(191, 191, 191);\n}\n\n.ace-tm .ace_line .ace_keyword {\n color: blue;\n}\n\n.ace-tm .ace_line .ace_constant.ace_buildin {\n color: rgb(88, 72, 246);\n}\n\n.ace-tm .ace_line .ace_constant.ace_language {\n color: rgb(88, 92, 246);\n}\n\n.ace-tm .ace_line .ace_constant.ace_library {\n color: rgb(6, 150, 14);\n}\n\n.ace-tm .ace_line .ace_invalid {\n background-color: rgb(153, 0, 0);\n color: white;\n}\n\n.ace-tm .ace_line .ace_fold {\n background-color: #E4E4E4;\n border-radius: 3px;\n}\n\n.ace-tm .ace_line .ace_support.ace_function {\n color: rgb(60, 76, 114);\n}\n\n.ace-tm .ace_line .ace_support.ace_constant {\n color: rgb(6, 150, 14);\n}\n\n.ace-tm .ace_line .ace_support.ace_type,\n.ace-tm .ace_line .ace_support.ace_class {\n color: rgb(109, 121, 222);\n}\n\n.ace-tm .ace_line .ace_keyword.ace_operator {\n color: rgb(104, 118, 135);\n}\n\n.ace-tm .ace_line .ace_string {\n color: rgb(3, 106, 7);\n}\n\n.ace-tm .ace_line .ace_comment {\n color: rgb(76, 136, 107);\n}\n\n.ace-tm .ace_line .ace_comment.ace_doc {\n color: rgb(0, 102, 255);\n}\n\n.ace-tm .ace_line .ace_comment.ace_doc.ace_tag {\n color: rgb(128, 159, 191);\n}\n\n.ace-tm .ace_line .ace_constant.ace_numeric {\n color: rgb(0, 0, 205);\n}\n\n.ace-tm .ace_line .ace_variable {\n color: rgb(49, 132, 149);\n}\n\n.ace-tm .ace_line .ace_xml_pe {\n color: rgb(104, 104, 91);\n}\n\n.ace-tm .ace_marker-layer .ace_selection {\n background: rgb(181, 213, 255);\n}\n\n.ace-tm .ace_marker-layer .ace_step {\n background: rgb(252, 255, 0);\n}\n\n.ace-tm .ace_marker-layer .ace_stack {\n background: rgb(164, 229, 101);\n}\n\n.ace-tm .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid rgb(192, 192, 192);\n}\n\n.ace-tm .ace_marker-layer .ace_active_line {\n background: rgb(232, 242, 254);\n}\n\n.ace-tm .ace_marker-layer .ace_selected_word {\n background: rgb(250, 250, 255);\n border: 1px solid rgb(200, 200, 250);\n}\n\n.ace-tm .ace_string.ace_regex {\n color: rgb(255, 0, 0)\n}";d.importCssString(e),b.cssClass="ace-tm"}),define("pilot/environment",["require","exports","module","pilot/settings"],function(a,b,c){function e(){return{settings:d}}var d=a("pilot/settings").settings;b.create=e}),define("text/ace/css/editor.css",[],'.ace_editor { position: absolute; overflow: hidden; font-family: "Menlo", "Monaco", "Courier New", monospace; font-size: 12px;}.ace_scroller { position: absolute; overflow-x: scroll; overflow-y: hidden;}.ace_content { position: absolute; box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box;}.ace_composition { position: absolute; background: #555; color: #DDD; z-index: 4;}.ace_gutter { position: absolute; overflow-x: hidden; overflow-y: hidden; height: 100%;}.ace_gutter-cell.ace_error { background-image: url("data:image/gif,GIF89a%10%00%10%00%D5%00%00%F5or%F5%87%88%F5nr%F4ns%EBmq%F5z%7F%DDJT%DEKS%DFOW%F1Yc%F2ah%CE(7%CE)8%D18E%DD%40M%F2KZ%EBU%60%F4%60m%DCir%C8%16(%C8%19*%CE%255%F1%3FR%F1%3FS%E6%AB%B5%CA%5DI%CEn%5E%F7%A2%9A%C9G%3E%E0a%5B%F7%89%85%F5yy%F6%82%80%ED%82%80%FF%BF%BF%E3%C4%C4%FF%FF%FF%FF%FF%FF%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00!%F9%04%01%00%00%25%00%2C%00%00%00%00%10%00%10%00%00%06p%C0%92pH%2C%1A%8F%C8%D2H%93%E1d4%23%E4%88%D3%09mB%1DN%B48%F5%90%40%60%92G%5B%94%20%3E%22%D2%87%24%FA%20%24%C5%06A%00%20%B1%07%02B%A38%89X.v%17%82%11%13q%10%0Fi%24%0F%8B%10%7BD%12%0Ei%09%92%09%0EpD%18%15%24%0A%9Ci%05%0C%18F%18%0B%07%04%01%04%06%A0H%18%12%0D%14%0D%12%A1I%B3%B4%B5IA%00%3B"); background-repeat: no-repeat; background-position: 4px center;}.ace_gutter-cell.ace_warning { background-image: url("data:image/gif,GIF89a%10%00%10%00%D5%00%00%FF%DBr%FF%DE%81%FF%E2%8D%FF%E2%8F%FF%E4%96%FF%E3%97%FF%E5%9D%FF%E6%9E%FF%EE%C1%FF%C8Z%FF%CDk%FF%D0s%FF%D4%81%FF%D5%82%FF%D5%83%FF%DC%97%FF%DE%9D%FF%E7%B8%FF%CCl%7BQ%13%80U%15%82W%16%81U%16%89%5B%18%87%5B%18%8C%5E%1A%94d%1D%C5%83-%C9%87%2F%C6%84.%C6%85.%CD%8B2%C9%871%CB%8A3%CD%8B5%DC%98%3F%DF%9BB%E0%9CC%E1%A5U%CB%871%CF%8B5%D1%8D6%DB%97%40%DF%9AB%DD%99B%E3%B0p%E7%CC%AE%FF%FF%FF%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00!%F9%04%01%00%00%2F%00%2C%00%00%00%00%10%00%10%00%00%06a%C0%97pH%2C%1A%8FH%A1%ABTr%25%87%2B%04%82%F4%7C%B9X%91%08%CB%99%1C!%26%13%84*iJ9(%15G%CA%84%14%01%1A%97%0C%03%80%3A%9A%3E%81%84%3E%11%08%B1%8B%20%02%12%0F%18%1A%0F%0A%03\'F%1C%04%0B%10%16%18%10%0B%05%1CF%1D-%06%07%9A%9A-%1EG%1B%A0%A1%A0U%A4%A5%A6BA%00%3B"); background-repeat: no-repeat; background-position: 4px center;}.ace_editor .ace_sb { position: absolute; overflow-x: hidden; overflow-y: scroll; right: 0;}.ace_editor .ace_sb div { position: absolute; width: 1px; left: 0;}.ace_editor .ace_print_margin_layer { z-index: 0; position: absolute; overflow: hidden; margin: 0; left: 0; height: 100%; width: 100%;}.ace_editor .ace_print_margin { position: absolute; height: 100%;}.ace_editor textarea { position: fixed; z-index: -1; width: 10px; height: 30px; opacity: 0; background: transparent; appearance: none; border: none; resize: none; outline: none; overflow: hidden;}.ace_layer { z-index: 1; position: absolute; overflow: hidden; white-space: nowrap; height: 100%; width: 100%;}.ace_text-layer { font-family: Monaco, "Courier New", monospace; color: black;}.ace_cjk { display: inline-block; text-align: center;}.ace_cursor-layer { z-index: 4; cursor: text; pointer-events: none;}.ace_cursor { z-index: 4; position: absolute;}.ace_cursor.ace_hidden { opacity: 0.2;}.ace_line { white-space: nowrap;}.ace_marker-layer { cursor: text; pointer-events: none;}.ace_marker-layer .ace_step { position: absolute; z-index: 3;}.ace_marker-layer .ace_selection { position: absolute; z-index: 4;}.ace_marker-layer .ace_bracket { position: absolute; z-index: 5;}.ace_marker-layer .ace_active_line { position: absolute; z-index: 2;}.ace_marker-layer .ace_selected_word { position: absolute; z-index: 6; box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box;}.ace_line .ace_fold { cursor: pointer;}.ace_dragging .ace_marker-layer, .ace_dragging .ace_text-layer { cursor: move;}'),define("text/styles.css",[],"html { height: 100%; width: 100%; overflow: hidden;}body { overflow: hidden; margin: 0; padding: 0; height: 100%; width: 100%; font-family: Arial, Helvetica, sans-serif, Tahoma, Verdana, sans-serif; font-size: 12px; background: rgb(14, 98, 165); color: white;}#logo { padding: 15px; margin-left: 65px;}#editor { position: absolute; top: 0px; left: 280px; bottom: 0px; right: 0px; background: white;}#controls { padding: 5px;}#controls td { text-align: right;}#controls td + td { text-align: left;}#cockpitInput { position: absolute; left: 280px; right: 0px; bottom: 0; border: none; outline: none; font-family: consolas, courier, monospace; font-size: 120%;}#cockpitOutput { padding: 10px; margin: 0 15px; border: 1px solid #AAA; -moz-border-radius-topleft: 10px; -moz-border-radius-topright: 10px; border-top-left-radius: 4px; border-top-right-radius: 4px; background: #DDD; color: #000;}"),require(["ace/ace"],function(a){window.ace=a}) \ No newline at end of file
diff --git a/www/js/ace/cockpit-uncompressed.js b/www/js/ace/cockpit-uncompressed.js
new file mode 100644
index 0000000..2b75f23
--- /dev/null
+++ b/www/js/ace/cockpit-uncompressed.js
@@ -0,0 +1,2504 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Kevin Dangoor (kdangoor@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('cockpit/index', ['require', 'exports', 'module' , 'pilot/index', 'cockpit/cli', 'cockpit/ui/settings', 'cockpit/ui/cli_view', 'cockpit/commands/basic'], function(require, exports, module) {
+
+
+exports.startup = function(data, reason) {
+ require('pilot/index');
+ require('cockpit/cli').startup(data, reason);
+ // window.testCli = require('cockpit/test/testCli');
+
+ require('cockpit/ui/settings').startup(data, reason);
+ require('cockpit/ui/cli_view').startup(data, reason);
+ require('cockpit/commands/basic').startup(data, reason);
+};
+
+/*
+exports.shutdown(data, reason) {
+};
+*/
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('cockpit/cli', ['require', 'exports', 'module' , 'pilot/console', 'pilot/lang', 'pilot/oop', 'pilot/event_emitter', 'pilot/types', 'pilot/canon'], function(require, exports, module) {
+
+
+var console = require('pilot/console');
+var lang = require('pilot/lang');
+var oop = require('pilot/oop');
+var EventEmitter = require('pilot/event_emitter').EventEmitter;
+
+//var keyboard = require('keyboard/keyboard');
+var types = require('pilot/types');
+var Status = require('pilot/types').Status;
+var Conversion = require('pilot/types').Conversion;
+var canon = require('pilot/canon');
+
+/**
+ * Normally type upgrade is done when the owning command is registered, but
+ * out commandParam isn't part of a command, so it misses out.
+ */
+exports.startup = function(data, reason) {
+ canon.upgradeType('command', commandParam);
+};
+
+/**
+ * The information required to tell the user there is a problem with their
+ * input.
+ * TODO: There a several places where {start,end} crop up. Perhaps we should
+ * have a Cursor object.
+ */
+function Hint(status, message, start, end, predictions) {
+ this.status = status;
+ this.message = message;
+
+ if (typeof start === 'number') {
+ this.start = start;
+ this.end = end;
+ this.predictions = predictions;
+ }
+ else {
+ var arg = start;
+ this.start = arg.start;
+ this.end = arg.end;
+ this.predictions = arg.predictions;
+ }
+}
+Hint.prototype = {
+};
+/**
+ * Loop over the array of hints finding the one we should display.
+ * @param hints array of hints
+ */
+Hint.sort = function(hints, cursor) {
+ // Calculate 'distance from cursor'
+ if (cursor !== undefined) {
+ hints.forEach(function(hint) {
+ if (hint.start === Argument.AT_CURSOR) {
+ hint.distance = 0;
+ }
+ else if (cursor < hint.start) {
+ hint.distance = hint.start - cursor;
+ }
+ else if (cursor > hint.end) {
+ hint.distance = cursor - hint.end;
+ }
+ else {
+ hint.distance = 0;
+ }
+ }, this);
+ }
+ // Sort
+ hints.sort(function(hint1, hint2) {
+ // Compare first based on distance from cursor
+ if (cursor !== undefined) {
+ var diff = hint1.distance - hint2.distance;
+ if (diff != 0) {
+ return diff;
+ }
+ }
+ // otherwise go with hint severity
+ return hint2.status - hint1.status;
+ });
+ // tidy-up
+ if (cursor !== undefined) {
+ hints.forEach(function(hint) {
+ delete hint.distance;
+ }, this);
+ }
+ return hints;
+};
+exports.Hint = Hint;
+
+/**
+ * A Hint that arose as a result of a Conversion
+ */
+function ConversionHint(conversion, arg) {
+ this.status = conversion.status;
+ this.message = conversion.message;
+ if (arg) {
+ this.start = arg.start;
+ this.end = arg.end;
+ }
+ else {
+ this.start = 0;
+ this.end = 0;
+ }
+ this.predictions = conversion.predictions;
+};
+oop.inherits(ConversionHint, Hint);
+
+
+/**
+ * We record where in the input string an argument comes so we can report errors
+ * against those string positions.
+ * We publish a 'change' event when-ever the text changes
+ * @param emitter Arguments use something else to pass on change events.
+ * Currently this will be the creating Requisition. This prevents dependency
+ * loops and prevents us from needing to merge listener lists.
+ * @param text The string (trimmed) that contains the argument
+ * @param start The position of the text in the original input string
+ * @param end See start
+ * @param prefix Knowledge of quotation marks and whitespace used prior to the
+ * text in the input string allows us to re-generate the original input from
+ * the arguments.
+ * @param suffix Any quotation marks and whitespace used after the text.
+ * Whitespace is normally placed in the prefix to the succeeding argument, but
+ * can be used here when this is the last argument.
+ * @constructor
+ */
+function Argument(emitter, text, start, end, prefix, suffix) {
+ this.emitter = emitter;
+ this.setText(text);
+ this.start = start;
+ this.end = end;
+ this.prefix = prefix;
+ this.suffix = suffix;
+}
+Argument.prototype = {
+ /**
+ * Return the result of merging these arguments.
+ * TODO: What happens when we're merging arguments for the single string
+ * case and some of the arguments are in quotation marks?
+ */
+ merge: function(following) {
+ if (following.emitter != this.emitter) {
+ throw new Error('Can\'t merge Arguments from different EventEmitters');
+ }
+ return new Argument(
+ this.emitter,
+ this.text + this.suffix + following.prefix + following.text,
+ this.start, following.end,
+ this.prefix,
+ following.suffix);
+ },
+
+ /**
+ * See notes on events in Assignment. We might need to hook changes here
+ * into a CliRequisition so they appear of the command line.
+ */
+ setText: function(text) {
+ if (text == null) {
+ throw new Error('Illegal text for Argument: ' + text);
+ }
+ var ev = { argument: this, oldText: this.text, text: text };
+ this.text = text;
+ this.emitter._dispatchEvent('argumentChange', ev);
+ },
+
+ /**
+ * Helper when we're putting arguments back together
+ */
+ toString: function() {
+ // TODO: There is a bug here - we should re-escape escaped characters
+ // But can we do that reliably?
+ return this.prefix + this.text + this.suffix;
+ }
+};
+
+/**
+ * Merge an array of arguments into a single argument.
+ * All Arguments in the array are expected to have the same emitter
+ */
+Argument.merge = function(argArray, start, end) {
+ start = (start === undefined) ? 0 : start;
+ end = (end === undefined) ? argArray.length : end;
+
+ var joined;
+ for (var i = start; i < end; i++) {
+ var arg = argArray[i];
+ if (!joined) {
+ joined = arg;
+ }
+ else {
+ joined = joined.merge(arg);
+ }
+ }
+ return joined;
+};
+
+/**
+ * We sometimes need a way to say 'this error occurs where ever the cursor is'
+ */
+Argument.AT_CURSOR = -1;
+
+
+/**
+ * A link between a parameter and the data for that parameter.
+ * The data for the parameter is available as in the preferred type and as
+ * an Argument for the CLI.
+ * <p>We also record validity information where applicable.
+ * <p>For values, null and undefined have distinct definitions. null means
+ * that a value has been provided, undefined means that it has not.
+ * Thus, null is a valid default value, and common because it identifies an
+ * parameter that is optional. undefined means there is no value from
+ * the command line.
+ * @constructor
+ */
+function Assignment(param, requisition) {
+ this.param = param;
+ this.requisition = requisition;
+ this.setValue(param.defaultValue);
+};
+Assignment.prototype = {
+ /**
+ * The parameter that we are assigning to
+ * @readonly
+ */
+ param: undefined,
+
+ /**
+ * Report on the status of the last parse() conversion.
+ * @see types.Conversion
+ */
+ conversion: undefined,
+
+ /**
+ * The current value in a type as specified by param.type
+ */
+ value: undefined,
+
+ /**
+ * The string version of the current value
+ */
+ arg: undefined,
+
+ /**
+ * The current value (i.e. not the string representation)
+ * Use setValue() to mutate
+ */
+ value: undefined,
+ setValue: function(value) {
+ if (this.value === value) {
+ return;
+ }
+
+ if (value === undefined) {
+ this.value = this.param.defaultValue;
+ this.conversion = this.param.getDefault ?
+ this.param.getDefault() :
+ this.param.type.getDefault();
+ this.arg = undefined;
+ } else {
+ this.value = value;
+ this.conversion = undefined;
+ var text = (value == null) ? '' : this.param.type.stringify(value);
+ if (this.arg) {
+ this.arg.setText(text);
+ }
+ }
+
+ this.requisition._assignmentChanged(this);
+ },
+
+ /**
+ * The textual representation of the current value
+ * Use setValue() to mutate
+ */
+ arg: undefined,
+ setArgument: function(arg) {
+ if (this.arg === arg) {
+ return;
+ }
+ this.arg = arg;
+ this.conversion = this.param.type.parse(arg.text);
+ this.conversion.arg = arg; // TODO: make this automatic?
+ this.value = this.conversion.value;
+ this.requisition._assignmentChanged(this);
+ },
+
+ /**
+ * Create a list of the hints associated with this parameter assignment.
+ * Generally there will be only one hint generated because we're currently
+ * only displaying one hint at a time, ordering by distance from cursor
+ * and severity. Since distance from cursor will be the same for all hints
+ * from this assignment all but the most severe will ever be used. It might
+ * make sense with more experience to alter this to function to be getHint()
+ */
+ getHint: function() {
+ // Allow the parameter to provide documentation
+ if (this.param.getCustomHint && this.value && this.arg) {
+ var hint = this.param.getCustomHint(this.value, this.arg);
+ if (hint) {
+ return hint;
+ }
+ }
+
+ // If there is no argument, use the cursor position
+ var message = '<strong>' + this.param.name + '</strong>: ';
+ if (this.param.description) {
+ // TODO: This should be a short description - do we need to trim?
+ message += this.param.description.trim();
+
+ // Ensure the help text ends with '. '
+ if (message.charAt(message.length - 1) !== '.') {
+ message += '.';
+ }
+ if (message.charAt(message.length - 1) !== ' ') {
+ message += ' ';
+ }
+ }
+ var status = Status.VALID;
+ var start = this.arg ? this.arg.start : Argument.AT_CURSOR;
+ var end = this.arg ? this.arg.end : Argument.AT_CURSOR;
+ var predictions;
+
+ // Non-valid conversions will have useful information to pass on
+ if (this.conversion) {
+ status = this.conversion.status;
+ if (this.conversion.message) {
+ message += this.conversion.message;
+ }
+ predictions = this.conversion.predictions;
+ }
+
+ // Hint if the param is required, but not provided
+ var argProvided = this.arg && this.arg.text !== '';
+ var dataProvided = this.value !== undefined || argProvided;
+ if (this.param.defaultValue === undefined && !dataProvided) {
+ status = Status.INVALID;
+ message += '<strong>Required<\strong>';
+ }
+
+ return new Hint(status, message, start, end, predictions);
+ },
+
+ /**
+ * Basically <tt>setValue(conversion.predictions[0])</tt> done in a safe
+ * way.
+ */
+ complete: function() {
+ if (this.conversion && this.conversion.predictions &&
+ this.conversion.predictions.length > 0) {
+ this.setValue(this.conversion.predictions[0]);
+ }
+ },
+
+ /**
+ * If the cursor is at 'position', do we have sufficient data to start
+ * displaying the next hint. This is both complex and important.
+ * For example, if the user has just typed:<ul>
+ * <li>'set tabstop ' then they clearly want to know about the valid
+ * values for the tabstop setting, so the hint is based on the next
+ * parameter.
+ * <li>'set tabstop' (without trailing space) - they will probably still
+ * want to know about the valid values for the tabstop setting because
+ * there is no confusion about the setting in question.
+ * <li>'set tabsto' they've not finished typing a setting name so the hint
+ * should be based on the current parameter.
+ * <li>'set tabstop' (when there is an additional tabstopstyle setting) we
+ * can't make assumptions about the setting - we're not finished.
+ * </ul>
+ * <p>Note that the input for 2 and 4 is identical, only the configuration
+ * has changed, so hint display is environmental.
+ *
+ * <p>This function works out if the cursor is before the end of this
+ * assignment (assuming that we've asked the same thing of the previous
+ * assignment) and then attempts to work out if we should use the hint from
+ * the next assignment even though technically the cursor is still inside
+ * this one due to the rules above.
+ */
+ isPositionCaptured: function(position) {
+ if (!this.arg) {
+ return false;
+ }
+
+ // Note we don't check if position >= this.arg.start because that's
+ // implied by the fact that we're asking the assignments in turn, and
+ // we want to avoid thing falling between the cracks, but we do need
+ // to check that the argument does have a position
+ if (this.arg.start === -1) {
+ return false;
+ }
+
+ // We're clearly done if the position is past the end of the text
+ if (position > this.arg.end) {
+ return false;
+ }
+
+ // If we're AT the end, the position is captured if either the status
+ // is not valid or if there are other valid options including current
+ if (position === this.arg.end) {
+ return this.conversion.status !== Status.VALID ||
+ this.conversion.predictions.length !== 0;
+ }
+
+ // Otherwise we're clearly inside
+ return true;
+ },
+
+ /**
+ * Replace the current value with the lower value if such a concept
+ * exists.
+ */
+ decrement: function() {
+ var replacement = this.param.type.decrement(this.value);
+ if (replacement != null) {
+ this.setValue(replacement);
+ }
+ },
+
+ /**
+ * Replace the current value with the higher value if such a concept
+ * exists.
+ */
+ increment: function() {
+ var replacement = this.param.type.increment(this.value);
+ if (replacement != null) {
+ this.setValue(replacement);
+ }
+ },
+
+ /**
+ * Helper when we're rebuilding command lines.
+ */
+ toString: function() {
+ return this.arg ? this.arg.toString() : '';
+ }
+};
+exports.Assignment = Assignment;
+
+
+/**
+ * This is a special parameter to reflect the command itself.
+ */
+var commandParam = {
+ name: '__command',
+ type: 'command',
+ description: 'The command to execute',
+
+ /**
+ * Provide some documentation for a command.
+ */
+ getCustomHint: function(command, arg) {
+ var docs = [];
+ docs.push('<strong><tt> &gt; ');
+ docs.push(command.name);
+ if (command.params && command.params.length > 0) {
+ command.params.forEach(function(param) {
+ if (param.defaultValue === undefined) {
+ docs.push(' [' + param.name + ']');
+ }
+ else {
+ docs.push(' <em>[' + param.name + ']</em>');
+ }
+ }, this);
+ }
+ docs.push('</tt></strong><br/>');
+
+ docs.push(command.description ? command.description : '(No description)');
+ docs.push('<br/>');
+
+ if (command.params && command.params.length > 0) {
+ docs.push('<ul>');
+ command.params.forEach(function(param) {
+ docs.push('<li>');
+ docs.push('<strong><tt>' + param.name + '</tt></strong>: ');
+ docs.push(param.description ? param.description : '(No description)');
+ if (param.defaultValue === undefined) {
+ docs.push(' <em>[Required]</em>');
+ }
+ else if (param.defaultValue === null) {
+ docs.push(' <em>[Optional]</em>');
+ }
+ else {
+ docs.push(' <em>[Default: ' + param.defaultValue + ']</em>');
+ }
+ docs.push('</li>');
+ }, this);
+ docs.push('</ul>');
+ }
+
+ return new Hint(Status.VALID, docs.join(''), arg);
+ }
+};
+
+/**
+ * A Requisition collects the information needed to execute a command.
+ * There is no point in a requisition for parameter-less commands because there
+ * is no information to collect. A Requisition is a collection of assignments
+ * of values to parameters, each handled by an instance of Assignment.
+ * CliRequisition adds functions for parsing input from a command line to this
+ * class.
+ * <h2>Events<h2>
+ * We publish the following events:<ul>
+ * <li>argumentChange: The text of some argument has changed. It is likely that
+ * any UI component displaying this argument will need to be updated. (Note that
+ * this event is actually published by the Argument itself - see the docs for
+ * Argument for more details)
+ * The event object looks like: { argument: A, oldText: B, text: B }
+ * <li>commandChange: The command has changed. It is likely that a UI
+ * structure will need updating to match the parameters of the new command.
+ * The event object looks like { command: A }
+ * @constructor
+ */
+function Requisition(env) {
+ this.env = env;
+ this.commandAssignment = new Assignment(commandParam, this);
+}
+
+Requisition.prototype = {
+ /**
+ * The command that we are about to execute.
+ * @see setCommandConversion()
+ * @readonly
+ */
+ commandAssignment: undefined,
+
+ /**
+ * The count of assignments. Excludes the commandAssignment
+ * @readonly
+ */
+ assignmentCount: undefined,
+
+ /**
+ * The object that stores of Assignment objects that we are filling out.
+ * The Assignment objects are stored under their param.name for named
+ * lookup. Note: We make use of the property of Javascript objects that
+ * they are not just hashmaps, but linked-list hashmaps which iterate in
+ * insertion order.
+ * Excludes the commandAssignment.
+ */
+ _assignments: undefined,
+
+ /**
+ * The store of hints generated by the assignments. We are trying to prevent
+ * the UI from needing to access this in broad form, but instead use
+ * methods that query part of this structure.
+ */
+ _hints: undefined,
+
+ /**
+ * When the command changes, we need to keep a bunch of stuff in sync
+ */
+ _assignmentChanged: function(assignment) {
+ // This is all about re-creating Assignments
+ if (assignment.param.name !== '__command') {
+ return;
+ }
+
+ this._assignments = {};
+
+ if (assignment.value) {
+ assignment.value.params.forEach(function(param) {
+ this._assignments[param.name] = new Assignment(param, this);
+ }, this);
+ }
+
+ this.assignmentCount = Object.keys(this._assignments).length;
+ this._dispatchEvent('commandChange', { command: assignment.value });
+ },
+
+ /**
+ * Assignments have an order, so we need to store them in an array.
+ * But we also need named access ...
+ */
+ getAssignment: function(nameOrNumber) {
+ var name = (typeof nameOrNumber === 'string') ?
+ nameOrNumber :
+ Object.keys(this._assignments)[nameOrNumber];
+ return this._assignments[name];
+ },
+
+ /**
+ * Where parameter name == assignment names - they are the same.
+ */
+ getParameterNames: function() {
+ return Object.keys(this._assignments);
+ },
+
+ /**
+ * A *shallow* clone of the assignments.
+ * This is useful for systems that wish to go over all the assignments
+ * finding values one way or another and wish to trim an array as they go.
+ */
+ cloneAssignments: function() {
+ return Object.keys(this._assignments).map(function(name) {
+ return this._assignments[name];
+ }, this);
+ },
+
+ /**
+ * Collect the statuses from the Assignments.
+ * The hints returned are sorted by severity
+ */
+ _updateHints: function() {
+ // TODO: work out when to clear this out for the plain Requisition case
+ // this._hints = [];
+ this.getAssignments(true).forEach(function(assignment) {
+ this._hints.push(assignment.getHint());
+ }, this);
+ Hint.sort(this._hints);
+
+ // We would like to put some initial help here, but for anyone but
+ // a complete novice a 'type help' message is very annoying, so we
+ // need to find a way to only display this message once, or for
+ // until the user click a 'close' button or similar
+ // TODO: Add special case for '' input
+ },
+
+ /**
+ * Returns the most severe status
+ */
+ getWorstHint: function() {
+ return this._hints[0];
+ },
+
+ /**
+ * Extract the names and values of all the assignments, and return as
+ * an object.
+ */
+ getArgsObject: function() {
+ var args = {};
+ this.getAssignments().forEach(function(assignment) {
+ args[assignment.param.name] = assignment.value;
+ }, this);
+ return args;
+ },
+
+ /**
+ * Access the arguments as an array.
+ * @param includeCommand By default only the parameter arguments are
+ * returned unless (includeCommand === true), in which case the list is
+ * prepended with commandAssignment.arg
+ */
+ getAssignments: function(includeCommand) {
+ var args = [];
+ if (includeCommand === true) {
+ args.push(this.commandAssignment);
+ }
+ Object.keys(this._assignments).forEach(function(name) {
+ args.push(this.getAssignment(name));
+ }, this);
+ return args;
+ },
+
+ /**
+ * Reset all the assignments to their default values
+ */
+ setDefaultValues: function() {
+ this.getAssignments().forEach(function(assignment) {
+ assignment.setValue(undefined);
+ }, this);
+ },
+
+ /**
+ * Helper to call canon.exec
+ */
+ exec: function() {
+ canon.exec(this.commandAssignment.value,
+ this.env,
+ "cli",
+ this.getArgsObject(),
+ this.toCanonicalString());
+ },
+
+ /**
+ * Extract a canonical version of the input
+ */
+ toCanonicalString: function() {
+ var line = [];
+ line.push(this.commandAssignment.value.name);
+ Object.keys(this._assignments).forEach(function(name) {
+ var assignment = this._assignments[name];
+ var type = assignment.param.type;
+ // TODO: This will cause problems if there is a non-default value
+ // after a default value. Also we need to decide when to use
+ // named parameters in place of positional params. Both can wait.
+ if (assignment.value !== assignment.param.defaultValue) {
+ line.push(' ');
+ line.push(type.stringify(assignment.value));
+ }
+ }, this);
+ return line.join('');
+ }
+};
+oop.implement(Requisition.prototype, EventEmitter);
+exports.Requisition = Requisition;
+
+
+/**
+ * An object used during command line parsing to hold the various intermediate
+ * data steps.
+ * <p>The 'output' of the update is held in 2 objects: input.hints which is an
+ * array of hints to display to the user. In the future this will become a
+ * single value.
+ * <p>The other output value is input.requisition which gives access to an
+ * args object for use in executing the final command.
+ *
+ * <p>The majority of the functions in this class are called in sequence by the
+ * constructor. Their task is to add to <tt>hints</tt> fill out the requisition.
+ * <p>The general sequence is:<ul>
+ * <li>_tokenize(): convert _typed into _parts
+ * <li>_split(): convert _parts into _command and _unparsedArgs
+ * <li>_assign(): convert _unparsedArgs into requisition
+ * </ul>
+ *
+ * @param typed {string} The instruction as typed by the user so far
+ * @param options {object} A list of optional named parameters. Can be any of:
+ * <b>flags</b>: Flags for us to check against the predicates specified with the
+ * commands. Defaulted to <tt>keyboard.buildFlags({ });</tt>
+ * if not specified.
+ * @constructor
+ */
+function CliRequisition(env, options) {
+ Requisition.call(this, env);
+
+ if (options && options.flags) {
+ /**
+ * TODO: We were using a default of keyboard.buildFlags({ });
+ * This allowed us to have commands that only existed in certain contexts
+ * - i.e. Javascript specific commands.
+ */
+ this.flags = options.flags;
+ }
+}
+oop.inherits(CliRequisition, Requisition);
+(function() {
+ /**
+ * Called by the UI when ever the user interacts with a command line input
+ * @param input A structure that details the state of the input field.
+ * It should look something like: { typed:a, cursor: { start:b, end:c } }
+ * Where a is the contents of the input field, and b and c are the start
+ * and end of the cursor/selection respectively.
+ */
+ CliRequisition.prototype.update = function(input) {
+ this.input = input;
+ this._hints = [];
+
+ var args = this._tokenize(input.typed);
+ this._split(args);
+
+ if (this.commandAssignment.value) {
+ this._assign(args);
+ }
+
+ this._updateHints();
+ };
+
+ /**
+ * Return an array of Status scores so we can create a marked up
+ * version of the command line input.
+ */
+ CliRequisition.prototype.getInputStatusMarkup = function() {
+ // 'scores' is an array which tells us what chars are errors
+ // Initialize with everything VALID
+ var scores = this.toString().split('').map(function(ch) {
+ return Status.VALID;
+ });
+ // For all chars in all hints, check and upgrade the score
+ this._hints.forEach(function(hint) {
+ for (var i = hint.start; i <= hint.end; i++) {
+ if (hint.status > scores[i]) {
+ scores[i] = hint.status;
+ }
+ }
+ }, this);
+ return scores;
+ };
+
+ /**
+ * Reconstitute the input from the args
+ */
+ CliRequisition.prototype.toString = function() {
+ return this.getAssignments(true).map(function(assignment) {
+ return assignment.toString();
+ }, this).join('');
+ };
+
+ var superUpdateHints = CliRequisition.prototype._updateHints;
+ /**
+ * Marks up hints in a number of ways:
+ * - Makes INCOMPLETE hints that are not near the cursor INVALID since
+ * they can't be completed by typing
+ * - Finds the most severe hint, and annotates the array with it
+ * - Finds the hint to display, and also annotates the array with it
+ * TODO: I'm wondering if array annotation is evil and we should replace
+ * this with an object. Need to find out more.
+ */
+ CliRequisition.prototype._updateHints = function() {
+ superUpdateHints.call(this);
+
+ // Not knowing about cursor positioning, the requisition and assignments
+ // can't know this, but anything they mark as INCOMPLETE is actually
+ // INVALID unless the cursor is actually inside that argument.
+ var c = this.input.cursor;
+ this._hints.forEach(function(hint) {
+ var startInHint = c.start >= hint.start && c.start <= hint.end;
+ var endInHint = c.end >= hint.start && c.end <= hint.end;
+ var inHint = startInHint || endInHint;
+ if (!inHint && hint.status === Status.INCOMPLETE) {
+ hint.status = Status.INVALID;
+ }
+ }, this);
+
+ Hint.sort(this._hints);
+ };
+
+ /**
+ * Accessor for the hints array.
+ * While we could just use the hints property, using getHints() is
+ * preferred for symmetry with Requisition where it needs a function due to
+ * lack of an atomic update system.
+ */
+ CliRequisition.prototype.getHints = function() {
+ return this._hints;
+ };
+
+ /**
+ * Look through the arguments attached to our assignments for the assignment
+ * at the given position.
+ */
+ CliRequisition.prototype.getAssignmentAt = function(position) {
+ var assignments = this.getAssignments(true);
+ for (var i = 0; i < assignments.length; i++) {
+ var assignment = assignments[i];
+ if (!assignment.arg) {
+ // There is no argument in this assignment, we've fallen off
+ // the end of the obvious answers - it must be this one.
+ return assignment;
+ }
+ if (assignment.isPositionCaptured(position)) {
+ return assignment;
+ }
+ }
+
+ return assignment;
+ };
+
+ /**
+ * Split up the input taking into account ' and "
+ */
+ CliRequisition.prototype._tokenize = function(typed) {
+ // For blank input, place a dummy empty argument into the list
+ if (typed == null || typed.length === 0) {
+ return [ new Argument(this, '', 0, 0, '', '') ];
+ }
+
+ var OUTSIDE = 1; // The last character was whitespace
+ var IN_SIMPLE = 2; // The last character was part of a parameter
+ var IN_SINGLE_Q = 3; // We're inside a single quote: '
+ var IN_DOUBLE_Q = 4; // We're inside double quotes: "
+
+ var mode = OUTSIDE;
+
+ // First we un-escape. This list was taken from:
+ // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Core_Language_Features#Unicode
+ // We are generally converting to their real values except for \', \"
+ // and '\ ' which we are converting to unicode private characters so we
+ // can distinguish them from ', " and ' ', which have special meaning.
+ // They need swapping back post-split - see unescape2()
+ typed = typed
+ .replace(/\\\\/g, '\\')
+ .replace(/\\b/g, '\b')
+ .replace(/\\f/g, '\f')
+ .replace(/\\n/g, '\n')
+ .replace(/\\r/g, '\r')
+ .replace(/\\t/g, '\t')
+ .replace(/\\v/g, '\v')
+ .replace(/\\n/g, '\n')
+ .replace(/\\r/g, '\r')
+ .replace(/\\ /g, '\uF000')
+ .replace(/\\'/g, '\uF001')
+ .replace(/\\"/g, '\uF002');
+
+ function unescape2(str) {
+ return str
+ .replace(/\uF000/g, ' ')
+ .replace(/\uF001/g, '\'')
+ .replace(/\uF002/g, '"');
+ }
+
+ var i = 0;
+ var start = 0; // Where did this section start?
+ var prefix = '';
+ var args = [];
+
+ while (true) {
+ if (i >= typed.length) {
+ // There is nothing else to read - tidy up
+ if (mode !== OUTSIDE) {
+ var str = unescape2(typed.substring(start, i));
+ args.push(new Argument(this, str, start, i, prefix, ''));
+ }
+ else {
+ if (i !== start) {
+ // There's a bunch of whitespace at the end of the
+ // command add it to the last argument's suffix,
+ // creating an empty argument if needed.
+ var extra = typed.substring(start, i);
+ var lastArg = args[args.length - 1];
+ if (!lastArg) {
+ lastArg = new Argument(this, '', i, i, extra, '');
+ args.push(lastArg);
+ }
+ else {
+ lastArg.suffix += extra;
+ }
+ }
+ }
+ break;
+ }
+
+ var c = typed[i];
+ switch (mode) {
+ case OUTSIDE:
+ if (c === '\'') {
+ prefix = typed.substring(start, i + 1);
+ mode = IN_SINGLE_Q;
+ start = i + 1;
+ }
+ else if (c === '"') {
+ prefix = typed.substring(start, i + 1);
+ mode = IN_DOUBLE_Q;
+ start = i + 1;
+ }
+ else if (/ /.test(c)) {
+ // Still whitespace, do nothing
+ }
+ else {
+ prefix = typed.substring(start, i);
+ mode = IN_SIMPLE;
+ start = i;
+ }
+ break;
+
+ case IN_SIMPLE:
+ // There is an edge case of xx'xx which we are assuming to
+ // be a single parameter (and same with ")
+ if (c === ' ') {
+ var str = unescape2(typed.substring(start, i));
+ args.push(new Argument(this, str,
+ start, i, prefix, ''));
+ mode = OUTSIDE;
+ start = i;
+ prefix = '';
+ }
+ break;
+
+ case IN_SINGLE_Q:
+ if (c === '\'') {
+ var str = unescape2(typed.substring(start, i));
+ args.push(new Argument(this, str,
+ start - 1, i + 1, prefix, c));
+ mode = OUTSIDE;
+ start = i + 1;
+ prefix = '';
+ }
+ break;
+
+ case IN_DOUBLE_Q:
+ if (c === '"') {
+ var str = unescape2(typed.substring(start, i));
+ args.push(new Argument(this, str,
+ start - 1, i + 1, prefix, c));
+ mode = OUTSIDE;
+ start = i + 1;
+ prefix = '';
+ }
+ break;
+ }
+
+ i++;
+ }
+
+ return args;
+ };
+
+ /**
+ * Looks in the canon for a command extension that matches what has been
+ * typed at the command line.
+ */
+ CliRequisition.prototype._split = function(args) {
+ var argsUsed = 1;
+ var arg;
+
+ while (argsUsed <= args.length) {
+ var arg = Argument.merge(args, 0, argsUsed);
+ this.commandAssignment.setArgument(arg);
+
+ if (!this.commandAssignment.value) {
+ // Not found. break with value == null
+ break;
+ }
+
+ /*
+ // Previously we needed a way to hide commands depending context.
+ // We have not resurrected that feature yet.
+ if (!keyboard.flagsMatch(command.predicates, this.flags)) {
+ // If the predicates say 'no match' then go LA LA LA
+ command = null;
+ break;
+ }
+ */
+
+ if (this.commandAssignment.value.exec) {
+ // Valid command, break with command valid
+ for (var i = 0; i < argsUsed; i++) {
+ args.shift();
+ }
+ break;
+ }
+
+ argsUsed++;
+ }
+ };
+
+ /**
+ * Work out which arguments are applicable to which parameters.
+ * <p>This takes #_command.params and #_unparsedArgs and creates a map of
+ * param names to 'assignment' objects, which have the following properties:
+ * <ul>
+ * <li>param - The matching parameter.
+ * <li>index - Zero based index into where the match came from on the input
+ * <li>value - The matching input
+ * </ul>
+ */
+ CliRequisition.prototype._assign = function(args) {
+ if (args.length === 0) {
+ this.setDefaultValues();
+ return;
+ }
+
+ // Create an error if the command does not take parameters, but we have
+ // been given them ...
+ if (this.assignmentCount === 0) {
+ // TODO: previously we were doing some extra work to avoid this if
+ // we determined that we had args that were all whitespace, but
+ // probably given our tighter tokenize() this won't be an issue?
+ this._hints.push(new Hint(Status.INVALID,
+ this.commandAssignment.value.name +
+ ' does not take any parameters',
+ Argument.merge(args)));
+ return;
+ }
+
+ // Special case: if there is only 1 parameter, and that's of type
+ // text we put all the params into the first param
+ if (this.assignmentCount === 1) {
+ var assignment = this.getAssignment(0);
+ if (assignment.param.type.name === 'text') {
+ assignment.setArgument(Argument.merge(args));
+ return;
+ }
+ }
+
+ var assignments = this.cloneAssignments();
+ var names = this.getParameterNames();
+
+ // Extract all the named parameters
+ var used = [];
+ assignments.forEach(function(assignment) {
+ var namedArgText = '--' + assignment.name;
+
+ var i = 0;
+ while (true) {
+ var arg = args[i];
+ if (namedArgText !== arg.text) {
+ i++;
+ if (i >= args.length) {
+ break;
+ }
+ continue;
+ }
+
+ // boolean parameters don't have values, default to false
+ if (assignment.param.type.name === 'boolean') {
+ assignment.setValue(true);
+ }
+ else {
+ if (i + 1 < args.length) {
+ // Missing value portion of this named param
+ this._hints.push(new Hint(Status.INCOMPLETE,
+ 'Missing value for: ' + namedArgText,
+ args[i]));
+ }
+ else {
+ args.splice(i + 1, 1);
+ assignment.setArgument(args[i + 1]);
+ }
+ }
+
+ lang.arrayRemove(names, assignment.name);
+ args.splice(i, 1);
+ // We don't need to i++ if we splice
+ }
+ }, this);
+
+ // What's left are positional parameters assign in order
+ names.forEach(function(name) {
+ var assignment = this.getAssignment(name);
+ if (args.length === 0) {
+ // No more values
+ assignment.setValue(undefined); // i.e. default
+ }
+ else {
+ var arg = args[0];
+ args.splice(0, 1);
+ assignment.setArgument(arg);
+ }
+ }, this);
+
+ if (args.length > 0) {
+ var remaining = Argument.merge(args);
+ this._hints.push(new Hint(Status.INVALID,
+ 'Input \'' + remaining.text + '\' makes no sense.',
+ remaining));
+ }
+ };
+
+})();
+exports.CliRequisition = CliRequisition;
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('cockpit/ui/settings', ['require', 'exports', 'module' , 'pilot/types', 'pilot/types/basic'], function(require, exports, module) {
+
+
+var types = require("pilot/types");
+var SelectionType = require('pilot/types/basic').SelectionType;
+
+var direction = new SelectionType({
+ name: 'direction',
+ data: [ 'above', 'below' ]
+});
+
+var hintDirectionSetting = {
+ name: "hintDirection",
+ description: "Are hints shown above or below the command line?",
+ type: "direction",
+ defaultValue: "above"
+};
+
+var outputDirectionSetting = {
+ name: "outputDirection",
+ description: "Is the output window shown above or below the command line?",
+ type: "direction",
+ defaultValue: "above"
+};
+
+var outputHeightSetting = {
+ name: "outputHeight",
+ description: "What height should the output panel be?",
+ type: "number",
+ defaultValue: 300
+};
+
+exports.startup = function(data, reason) {
+ types.registerType(direction);
+ data.env.settings.addSetting(hintDirectionSetting);
+ data.env.settings.addSetting(outputDirectionSetting);
+ data.env.settings.addSetting(outputHeightSetting);
+};
+
+exports.shutdown = function(data, reason) {
+ types.unregisterType(direction);
+ data.env.settings.removeSetting(hintDirectionSetting);
+ data.env.settings.removeSetting(outputDirectionSetting);
+ data.env.settings.removeSetting(outputHeightSetting);
+};
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('cockpit/ui/cli_view', ['require', 'exports', 'module' , 'text!cockpit/ui/cli_view.css', 'pilot/event', 'pilot/dom', 'pilot/keys', 'pilot/canon', 'pilot/types', 'cockpit/cli', 'cockpit/ui/request_view'], function(require, exports, module) {
+
+
+var editorCss = require("text!cockpit/ui/cli_view.css");
+var event = require("pilot/event");
+var dom = require("pilot/dom");
+
+dom.importCssString(editorCss);
+
+var event = require("pilot/event");
+var keys = require("pilot/keys");
+var canon = require("pilot/canon");
+var Status = require('pilot/types').Status;
+
+var CliRequisition = require('cockpit/cli').CliRequisition;
+var Hint = require('cockpit/cli').Hint;
+var RequestView = require('cockpit/ui/request_view').RequestView;
+
+var NO_HINT = new Hint(Status.VALID, '', 0, 0);
+
+/**
+ * On startup we need to:
+ * 1. Add 3 sets of elements to the DOM for:
+ * - command line output
+ * - input hints
+ * - completion
+ * 2. Attach a set of events so the command line works
+ */
+exports.startup = function(data, reason) {
+ var cli = new CliRequisition(data.env);
+ var cliView = new CliView(cli, data.env);
+ data.env.cli = cli;
+};
+
+/**
+ * A class to handle the simplest UI implementation
+ */
+function CliView(cli, env) {
+ cli.cliView = this;
+ this.cli = cli;
+ this.doc = document;
+ this.win = dom.getParentWindow(this.doc);
+ this.env = env;
+
+ // TODO: we should have a better way to specify command lines???
+ this.element = this.doc.getElementById('cockpitInput');
+ if (!this.element) {
+ // console.log('No element with an id of cockpit. Bailing on cli');
+ return;
+ }
+
+ this.settings = env.settings;
+ this.hintDirection = this.settings.getSetting('hintDirection');
+ this.outputDirection = this.settings.getSetting('outputDirection');
+ this.outputHeight = this.settings.getSetting('outputHeight');
+
+ // If the requisition tells us something has changed, we use this to know
+ // if we should ignore it
+ this.isUpdating = false;
+
+ this.createElements();
+ this.update();
+}
+CliView.prototype = {
+ /**
+ * Create divs for completion, hints and output
+ */
+ createElements: function() {
+ var input = this.element;
+
+ this.element.spellcheck = false;
+
+ this.output = this.doc.getElementById('cockpitOutput');
+ this.popupOutput = (this.output == null);
+ if (!this.output) {
+ this.output = this.doc.createElement('div');
+ this.output.id = 'cockpitOutput';
+ this.output.className = 'cptOutput';
+ input.parentNode.insertBefore(this.output, input.nextSibling);
+
+ var setMaxOutputHeight = function() {
+ this.output.style.maxHeight = this.outputHeight.get() + 'px';
+ }.bind(this);
+ this.outputHeight.addEventListener('change', setMaxOutputHeight);
+ setMaxOutputHeight();
+ }
+
+ this.completer = this.doc.createElement('div');
+ this.completer.className = 'cptCompletion VALID';
+
+ this.completer.style.color = dom.computedStyle(input, "color");
+ this.completer.style.fontSize = dom.computedStyle(input, "fontSize");
+ this.completer.style.fontFamily = dom.computedStyle(input, "fontFamily");
+ this.completer.style.fontWeight = dom.computedStyle(input, "fontWeight");
+ this.completer.style.fontStyle = dom.computedStyle(input, "fontStyle");
+ input.parentNode.insertBefore(this.completer, input.nextSibling);
+
+ // Transfer background styling to the completer.
+ this.completer.style.backgroundColor = input.style.backgroundColor;
+ input.style.backgroundColor = 'transparent';
+
+ this.hinter = this.doc.createElement('div');
+ this.hinter.className = 'cptHints';
+ input.parentNode.insertBefore(this.hinter, input.nextSibling);
+
+ var resizer = this.resizer.bind(this);
+ event.addListener(this.win, 'resize', resizer);
+ this.hintDirection.addEventListener('change', resizer);
+ this.outputDirection.addEventListener('change', resizer);
+ resizer();
+
+ canon.addEventListener('output', function(ev) {
+ new RequestView(ev.request, this);
+ }.bind(this));
+ event.addCommandKeyListener(input, this.onCommandKey.bind(this));
+ event.addListener(input, 'keyup', this.onKeyUp.bind(this));
+
+ // cursor position affects hint severity. TODO: shortcuts for speed
+ event.addListener(input, 'mouseup', function(ev) {
+ this.isUpdating = true;
+ this.update();
+ this.isUpdating = false;
+ }.bind(this));
+
+ this.cli.addEventListener('argumentChange', this.onArgChange.bind(this));
+
+ event.addListener(input, "focus", function() {
+ dom.addCssClass(this.output, "cptFocusPopup");
+ dom.addCssClass(this.hinter, "cptFocusPopup");
+ }.bind(this));
+
+ function hideOutput() {
+ dom.removeCssClass(this.output, "cptFocusPopup");
+ dom.removeCssClass(this.hinter, "cptFocusPopup");
+ };
+ event.addListener(input, "blur", hideOutput.bind(this));
+ hideOutput.call(this);
+ },
+
+ /**
+ * We need to see the output of the latest command entered
+ */
+ scrollOutputToBottom: function() {
+ // Certain browsers have a bug such that scrollHeight is too small
+ // when content does not fill the client area of the element
+ var scrollHeight = Math.max(this.output.scrollHeight, this.output.clientHeight);
+ this.output.scrollTop = scrollHeight - this.output.clientHeight;
+ },
+
+ /**
+ * To be called on window resize or any time we want to align the elements
+ * with the input box.
+ */
+ resizer: function() {
+ var rect = this.element.getClientRects()[0];
+
+ this.completer.style.top = rect.top + 'px';
+ var height = rect.bottom - rect.top;
+ this.completer.style.height = height + 'px';
+ this.completer.style.lineHeight = height + 'px';
+ this.completer.style.left = rect.left + 'px';
+ var width = rect.right - rect.left;
+ this.completer.style.width = width + 'px';
+
+ if (this.hintDirection.get() === 'below') {
+ this.hinter.style.top = rect.bottom + 'px';
+ this.hinter.style.bottom = 'auto';
+ }
+ else {
+ this.hinter.style.top = 'auto';
+ this.hinter.style.bottom = (this.doc.documentElement.clientHeight - rect.top) + 'px';
+ }
+ this.hinter.style.left = (rect.left + 30) + 'px';
+ this.hinter.style.maxWidth = (width - 110) + 'px';
+
+ if (this.popupOutput) {
+ if (this.outputDirection.get() === 'below') {
+ this.output.style.top = rect.bottom + 'px';
+ this.output.style.bottom = 'auto';
+ }
+ else {
+ this.output.style.top = 'auto';
+ this.output.style.bottom = (this.doc.documentElement.clientHeight - rect.top) + 'px';
+ }
+ this.output.style.left = rect.left + 'px';
+ this.output.style.width = (width - 80) + 'px';
+ }
+ },
+
+ /**
+ * Ensure that TAB isn't handled by the browser
+ */
+onCommandKey: function(ev, hashId, keyCode) {
+ var stopEvent;
+ if (keyCode === keys.TAB ||
+ keyCode === keys.UP ||
+ keyCode === keys.DOWN) {
+ stopEvent = true;
+ } else if (hashId != 0 || keyCode != 0) {
+ stopEvent = canon.execKeyCommand(this.env, 'cli', hashId, keyCode);
+ }
+ stopEvent && event.stopEvent(ev);
+ },
+
+ /**
+ * The main keyboard processing loop
+ */
+ onKeyUp: function(ev) {
+ var handled;
+ /*
+ var handled = keyboardManager.processKeyEvent(ev, this, {
+ isCommandLine: true, isKeyUp: true
+ });
+ */
+
+ // RETURN does a special exec/highlight thing
+ if (ev.keyCode === keys.RETURN) {
+ var worst = this.cli.getWorstHint();
+ // Deny RETURN unless the command might work
+ if (worst.status === Status.VALID) {
+ this.cli.exec();
+ this.element.value = '';
+ }
+ else {
+ // If we've denied RETURN because the command was not VALID,
+ // select the part of the command line that is causing problems
+ // TODO: if there are 2 errors are we picking the right one?
+ dom.setSelectionStart(this.element, worst.start);
+ dom.setSelectionEnd(this.element, worst.end);
+ }
+ }
+
+ this.update();
+
+ // Special actions which delegate to the assignment
+ var current = this.cli.getAssignmentAt(dom.getSelectionStart(this.element));
+ if (current) {
+ // TAB does a special complete thing
+ if (ev.keyCode === keys.TAB) {
+ current.complete();
+ this.update();
+ }
+
+ // UP/DOWN look for some history
+ if (ev.keyCode === keys.UP) {
+ current.increment();
+ this.update();
+ }
+ if (ev.keyCode === keys.DOWN) {
+ current.decrement();
+ this.update();
+ }
+ }
+
+ return handled;
+ },
+
+ /**
+ * Actually parse the input and make sure we're all up to date
+ */
+ update: function() {
+ this.isUpdating = true;
+ var input = {
+ typed: this.element.value,
+ cursor: {
+ start: dom.getSelectionStart(this.element),
+ end: dom.getSelectionEnd(this.element.selectionEnd)
+ }
+ };
+ this.cli.update(input);
+
+ var display = this.cli.getAssignmentAt(input.cursor.start).getHint();
+
+ // 1. Update the completer with prompt/error marker/TAB info
+ dom.removeCssClass(this.completer, Status.VALID.toString());
+ dom.removeCssClass(this.completer, Status.INCOMPLETE.toString());
+ dom.removeCssClass(this.completer, Status.INVALID.toString());
+
+ var completion = '<span class="cptPrompt">&gt;</span> ';
+ if (this.element.value.length > 0) {
+ var scores = this.cli.getInputStatusMarkup();
+ completion += this.markupStatusScore(scores);
+ }
+
+ // Display the "-> prediction" at the end of the completer
+ if (this.element.value.length > 0 &&
+ display.predictions && display.predictions.length > 0) {
+ var tab = display.predictions[0];
+ completion += ' &nbsp;&#x21E5; ' + (tab.name ? tab.name : tab);
+ }
+ this.completer.innerHTML = completion;
+ dom.addCssClass(this.completer, this.cli.getWorstHint().status.toString());
+
+ // 2. Update the hint element
+ var hint = '';
+ if (this.element.value.length !== 0) {
+ hint += display.message;
+ if (display.predictions && display.predictions.length > 0) {
+ hint += ': [ ';
+ display.predictions.forEach(function(prediction) {
+ hint += (prediction.name ? prediction.name : prediction);
+ hint += ' | ';
+ }, this);
+ hint = hint.replace(/\| $/, ']');
+ }
+ }
+
+ this.hinter.innerHTML = hint;
+ if (hint.length === 0) {
+ dom.addCssClass(this.hinter, 'cptNoPopup');
+ }
+ else {
+ dom.removeCssClass(this.hinter, 'cptNoPopup');
+ }
+
+ this.isUpdating = false;
+ },
+
+ /**
+ * Markup an array of Status values with spans
+ */
+ markupStatusScore: function(scores) {
+ var completion = '';
+ // Create mark-up
+ var i = 0;
+ var lastStatus = -1;
+ while (true) {
+ if (lastStatus !== scores[i]) {
+ completion += '<span class=' + scores[i].toString() + '>';
+ lastStatus = scores[i];
+ }
+ completion += this.element.value[i];
+ i++;
+ if (i === this.element.value.length) {
+ completion += '</span>';
+ break;
+ }
+ if (lastStatus !== scores[i]) {
+ completion += '</span>';
+ }
+ }
+
+ return completion;
+ },
+
+ /**
+ * Update the input element to reflect the changed argument
+ */
+ onArgChange: function(ev) {
+ if (this.isUpdating) {
+ return;
+ }
+
+ var prefix = this.element.value.substring(0, ev.argument.start);
+ var suffix = this.element.value.substring(ev.argument.end);
+ var insert = typeof ev.text === 'string' ? ev.text : ev.text.name;
+ this.element.value = prefix + insert + suffix;
+ // Fix the cursor.
+ var insertEnd = (prefix + insert).length;
+ this.element.selectionStart = insertEnd;
+ this.element.selectionEnd = insertEnd;
+ }
+};
+exports.CliView = CliView;
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('cockpit/ui/request_view', ['require', 'exports', 'module' , 'pilot/dom', 'pilot/event', 'text!cockpit/ui/request_view.html', 'pilot/domtemplate', 'text!cockpit/ui/request_view.css'], function(require, exports, module) {
+
+var dom = require("pilot/dom");
+var event = require("pilot/event");
+var requestViewHtml = require("text!cockpit/ui/request_view.html");
+var Templater = require("pilot/domtemplate").Templater;
+
+var requestViewCss = require("text!cockpit/ui/request_view.css");
+dom.importCssString(requestViewCss);
+
+/**
+ * Pull the HTML into the DOM, but don't add it to the document
+ */
+var templates = document.createElement('div');
+templates.innerHTML = requestViewHtml;
+var row = templates.querySelector('.cptRow');
+
+/**
+ * Work out the path for images.
+ * TODO: This should probably live in some utility area somewhere
+ */
+function imageUrl(path) {
+ var dataUrl;
+ try {
+ dataUrl = require('text!cockpit/ui/' + path);
+ } catch (e) { }
+ if (dataUrl) {
+ return dataUrl;
+ }
+
+ var filename = module.id.split('/').pop() + '.js';
+ var imagePath;
+
+ if (module.uri.substr(-filename.length) !== filename) {
+ console.error('Can\'t work out path from module.uri/module.id');
+ return path;
+ }
+
+ if (module.uri) {
+ var end = module.uri.length - filename.length - 1;
+ return module.uri.substr(0, end) + "/" + path;
+ }
+
+ return filename + path;
+}
+
+
+/**
+ * Adds a row to the CLI output display
+ */
+function RequestView(request, cliView) {
+ this.request = request;
+ this.cliView = cliView;
+ this.imageUrl = imageUrl;
+
+ // Elements attached to this by the templater. For info only
+ this.rowin = null;
+ this.rowout = null;
+ this.output = null;
+ this.hide = null;
+ this.show = null;
+ this.duration = null;
+ this.throb = null;
+
+ new Templater().processNode(row.cloneNode(true), this);
+
+ this.cliView.output.appendChild(this.rowin);
+ this.cliView.output.appendChild(this.rowout);
+
+ this.request.addEventListener('output', this.onRequestChange.bind(this));
+};
+
+RequestView.prototype = {
+ /**
+ * A single click on an invocation line in the console copies the command to
+ * the command line
+ */
+ copyToInput: function() {
+ this.cliView.element.value = this.request.typed;
+ },
+
+ /**
+ * A double click on an invocation line in the console executes the command
+ */
+ executeRequest: function(ev) {
+ this.cliView.cli.update({
+ typed: this.request.typed,
+ cursor: { start:0, end:0 }
+ });
+ this.cliView.cli.exec();
+ },
+
+ hideOutput: function(ev) {
+ this.output.style.display = 'none';
+ dom.addCssClass(this.hide, 'cmd_hidden');
+ dom.removeCssClass(this.show, 'cmd_hidden');
+
+ event.stopPropagation(ev);
+ },
+
+ showOutput: function(ev) {
+ this.output.style.display = 'block';
+ dom.removeCssClass(this.hide, 'cmd_hidden');
+ dom.addCssClass(this.show, 'cmd_hidden');
+
+ event.stopPropagation(ev);
+ },
+
+ remove: function(ev) {
+ this.cliView.output.removeChild(this.rowin);
+ this.cliView.output.removeChild(this.rowout);
+ event.stopPropagation(ev);
+ },
+
+ onRequestChange: function(ev) {
+ this.duration.innerHTML = this.request.duration ?
+ 'completed in ' + (this.request.duration / 1000) + ' sec ' :
+ '';
+
+ this.output.innerHTML = '';
+ this.request.outputs.forEach(function(output) {
+ var node;
+ if (typeof output == 'string') {
+ node = document.createElement('p');
+ node.innerHTML = output;
+ } else {
+ node = output;
+ }
+ this.output.appendChild(node);
+ }, this);
+ this.cliView.scrollOutputToBottom();
+
+ dom.setCssClass(this.output, 'cmd_error', this.request.error);
+
+ this.throb.style.display = this.request.completed ? 'none' : 'block';
+ }
+};
+exports.RequestView = RequestView;
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is DomTemplate.
+ *
+ * The Initial Developer of the Original Code is Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Joe Walker (jwalker@mozilla.com) (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('pilot/domtemplate', ['require', 'exports', 'module' ], function(require, exports, module) {
+
+
+// WARNING: do not 'use_strict' without reading the notes in envEval;
+
+/**
+ * A templater that allows one to quickly template DOM nodes.
+ */
+function Templater() {
+ this.scope = [];
+};
+
+/**
+ * Recursive function to walk the tree processing the attributes as it goes.
+ * @param node the node to process. If you pass a string in instead of a DOM
+ * element, it is assumed to be an id for use with document.getElementById()
+ * @param data the data to use for node processing.
+ */
+Templater.prototype.processNode = function(node, data) {
+ if (typeof node === 'string') {
+ node = document.getElementById(node);
+ }
+ if (data === null || data === undefined) {
+ data = {};
+ }
+ this.scope.push(node.nodeName + (node.id ? '#' + node.id : ''));
+ try {
+ // Process attributes
+ if (node.attributes && node.attributes.length) {
+ // We need to handle 'foreach' and 'if' first because they might stop
+ // some types of processing from happening, and foreach must come first
+ // because it defines new data on which 'if' might depend.
+ if (node.hasAttribute('foreach')) {
+ this.processForEach(node, data);
+ return;
+ }
+ if (node.hasAttribute('if')) {
+ if (!this.processIf(node, data)) {
+ return;
+ }
+ }
+ // Only make the node available once we know it's not going away
+ data.__element = node;
+ // It's good to clean up the attributes when we've processed them,
+ // but if we do it straight away, we mess up the array index
+ var attrs = Array.prototype.slice.call(node.attributes);
+ for (var i = 0; i < attrs.length; i++) {
+ var value = attrs[i].value;
+ var name = attrs[i].name;
+ this.scope.push(name);
+ try {
+ if (name === 'save') {
+ // Save attributes are a setter using the node
+ value = this.stripBraces(value);
+ this.property(value, data, node);
+ node.removeAttribute('save');
+ } else if (name.substring(0, 2) === 'on') {
+ // Event registration relies on property doing a bind
+ value = this.stripBraces(value);
+ var func = this.property(value, data);
+ if (typeof func !== 'function') {
+ this.handleError('Expected ' + value +
+ ' to resolve to a function, but got ' + typeof func);
+ }
+ node.removeAttribute(name);
+ var capture = node.hasAttribute('capture' + name.substring(2));
+ node.addEventListener(name.substring(2), func, capture);
+ if (capture) {
+ node.removeAttribute('capture' + name.substring(2));
+ }
+ } else {
+ // Replace references in all other attributes
+ var self = this;
+ var newValue = value.replace(/\$\{[^}]*\}/g, function(path) {
+ return self.envEval(path.slice(2, -1), data, value);
+ });
+ // Remove '_' prefix of attribute names so the DOM won't try
+ // to use them before we've processed the template
+ if (name.charAt(0) === '_') {
+ node.removeAttribute(name);
+ node.setAttribute(name.substring(1), newValue);
+ } else if (value !== newValue) {
+ attrs[i].value = newValue;
+ }
+ }
+ } finally {
+ this.scope.pop();
+ }
+ }
+ }
+
+ // Loop through our children calling processNode. First clone them, so the
+ // set of nodes that we visit will be unaffected by additions or removals.
+ var childNodes = Array.prototype.slice.call(node.childNodes);
+ for (var j = 0; j < childNodes.length; j++) {
+ this.processNode(childNodes[j], data);
+ }
+
+ if (node.nodeType === Node.TEXT_NODE) {
+ this.processTextNode(node, data);
+ }
+ } finally {
+ this.scope.pop();
+ }
+};
+
+/**
+ * Handle <x if="${...}">
+ * @param node An element with an 'if' attribute
+ * @param data The data to use with envEval
+ * @returns true if processing should continue, false otherwise
+ */
+Templater.prototype.processIf = function(node, data) {
+ this.scope.push('if');
+ try {
+ var originalValue = node.getAttribute('if');
+ var value = this.stripBraces(originalValue);
+ var recurse = true;
+ try {
+ var reply = this.envEval(value, data, originalValue);
+ recurse = !!reply;
+ } catch (ex) {
+ this.handleError('Error with \'' + value + '\'', ex);
+ recurse = false;
+ }
+ if (!recurse) {
+ node.parentNode.removeChild(node);
+ }
+ node.removeAttribute('if');
+ return recurse;
+ } finally {
+ this.scope.pop();
+ }
+};
+
+/**
+ * Handle <x foreach="param in ${array}"> and the special case of
+ * <loop foreach="param in ${array}">
+ * @param node An element with a 'foreach' attribute
+ * @param data The data to use with envEval
+ */
+Templater.prototype.processForEach = function(node, data) {
+ this.scope.push('foreach');
+ try {
+ var originalValue = node.getAttribute('foreach');
+ var value = originalValue;
+
+ var paramName = 'param';
+ if (value.charAt(0) === '$') {
+ // No custom loop variable name. Use the default: 'param'
+ value = this.stripBraces(value);
+ } else {
+ // Extract the loop variable name from 'NAME in ${ARRAY}'
+ var nameArr = value.split(' in ');
+ paramName = nameArr[0].trim();
+ value = this.stripBraces(nameArr[1].trim());
+ }
+ node.removeAttribute('foreach');
+ try {
+ var self = this;
+ // Process a single iteration of a loop
+ var processSingle = function(member, clone, ref) {
+ ref.parentNode.insertBefore(clone, ref);
+ data[paramName] = member;
+ self.processNode(clone, data);
+ delete data[paramName];
+ };
+
+ // processSingle is no good for <loop> nodes where we want to work on
+ // the childNodes rather than the node itself
+ var processAll = function(scope, member) {
+ self.scope.push(scope);
+ try {
+ if (node.nodeName === 'LOOP') {
+ for (var i = 0; i < node.childNodes.length; i++) {
+ var clone = node.childNodes[i].cloneNode(true);
+ processSingle(member, clone, node);
+ }
+ } else {
+ var clone = node.cloneNode(true);
+ clone.removeAttribute('foreach');
+ processSingle(member, clone, node);
+ }
+ } finally {
+ self.scope.pop();
+ }
+ };
+
+ var reply = this.envEval(value, data, originalValue);
+ if (Array.isArray(reply)) {
+ reply.forEach(function(data, i) {
+ processAll('' + i, data);
+ }, this);
+ } else {
+ for (var param in reply) {
+ if (reply.hasOwnProperty(param)) {
+ processAll(param, param);
+ }
+ }
+ }
+ node.parentNode.removeChild(node);
+ } catch (ex) {
+ this.handleError('Error with \'' + value + '\'', ex);
+ }
+ } finally {
+ this.scope.pop();
+ }
+};
+
+/**
+ * Take a text node and replace it with another text node with the ${...}
+ * sections parsed out. We replace the node by altering node.parentNode but
+ * we could probably use a DOM Text API to achieve the same thing.
+ * @param node The Text node to work on
+ * @param data The data to use in calls to envEval
+ */
+Templater.prototype.processTextNode = function(node, data) {
+ // Replace references in other attributes
+ var value = node.data;
+ // We can't use the string.replace() with function trick (see generic
+ // attribute processing in processNode()) because we need to support
+ // functions that return DOM nodes, so we can't have the conversion to a
+ // string.
+ // Instead we process the string as an array of parts. In order to split
+ // the string up, we first replace '${' with '\uF001$' and '}' with '\uF002'
+ // We can then split using \uF001 or \uF002 to get an array of strings
+ // where scripts are prefixed with $.
+ // \uF001 and \uF002 are just unicode chars reserved for private use.
+ value = value.replace(/\$\{([^}]*)\}/g, '\uF001$$$1\uF002');
+ var parts = value.split(/\uF001|\uF002/);
+ if (parts.length > 1) {
+ parts.forEach(function(part) {
+ if (part === null || part === undefined || part === '') {
+ return;
+ }
+ if (part.charAt(0) === '$') {
+ part = this.envEval(part.slice(1), data, node.data);
+ }
+ // It looks like this was done a few lines above but see envEval
+ if (part === null) {
+ part = "null";
+ }
+ if (part === undefined) {
+ part = "undefined";
+ }
+ // if (isDOMElement(part)) { ... }
+ if (typeof part.cloneNode !== 'function') {
+ part = node.ownerDocument.createTextNode(part.toString());
+ }
+ node.parentNode.insertBefore(part, node);
+ }, this);
+ node.parentNode.removeChild(node);
+ }
+};
+
+/**
+ * Warn of string does not begin '${' and end '}'
+ * @param str the string to check.
+ * @return The string stripped of ${ and }, or untouched if it does not match
+ */
+Templater.prototype.stripBraces = function(str) {
+ if (!str.match(/\$\{.*\}/g)) {
+ this.handleError('Expected ' + str + ' to match ${...}');
+ return str;
+ }
+ return str.slice(2, -1);
+};
+
+/**
+ * Combined getter and setter that works with a path through some data set.
+ * For example:
+ * <ul>
+ * <li>property('a.b', { a: { b: 99 }}); // returns 99
+ * <li>property('a', { a: { b: 99 }}); // returns { b: 99 }
+ * <li>property('a', { a: { b: 99 }}, 42); // returns 99 and alters the
+ * input data to be { a: { b: 42 }}
+ * </ul>
+ * @param path An array of strings indicating the path through the data, or
+ * a string to be cut into an array using <tt>split('.')</tt>
+ * @param data An object to look in for the <tt>path</tt> argument
+ * @param newValue (optional) If defined, this value will replace the
+ * original value for the data at the path specified.
+ * @return The value pointed to by <tt>path</tt> before any
+ * <tt>newValue</tt> is applied.
+ */
+Templater.prototype.property = function(path, data, newValue) {
+ this.scope.push(path);
+ try {
+ if (typeof path === 'string') {
+ path = path.split('.');
+ }
+ var value = data[path[0]];
+ if (path.length === 1) {
+ if (newValue !== undefined) {
+ data[path[0]] = newValue;
+ }
+ if (typeof value === 'function') {
+ return function() {
+ return value.apply(data, arguments);
+ };
+ }
+ return value;
+ }
+ if (!value) {
+ this.handleError('Can\'t find path=' + path);
+ return null;
+ }
+ return this.property(path.slice(1), value, newValue);
+ } finally {
+ this.scope.pop();
+ }
+};
+
+/**
+ * Like eval, but that creates a context of the variables in <tt>env</tt> in
+ * which the script is evaluated.
+ * WARNING: This script uses 'with' which is generally regarded to be evil.
+ * The alternative is to create a Function at runtime that takes X parameters
+ * according to the X keys in the env object, and then call that function using
+ * the values in the env object. This is likely to be slow, but workable.
+ * @param script The string to be evaluated.
+ * @param env The environment in which to eval the script.
+ * @param context Optional debugging string in case of failure
+ * @return The return value of the script, or the error message if the script
+ * execution failed.
+ */
+Templater.prototype.envEval = function(script, env, context) {
+ with (env) {
+ try {
+ this.scope.push(context);
+ return eval(script);
+ } catch (ex) {
+ this.handleError('Template error evaluating \'' + script + '\'', ex);
+ return script;
+ } finally {
+ this.scope.pop();
+ }
+ }
+};
+
+/**
+ * A generic way of reporting errors, for easy overloading in different
+ * environments.
+ * @param message the error message to report.
+ * @param ex optional associated exception.
+ */
+Templater.prototype.handleError = function(message, ex) {
+ this.logError(message);
+ this.logError('In: ' + this.scope.join(' > '));
+ if (ex) {
+ this.logError(ex);
+ }
+};
+
+
+/**
+ * A generic way of reporting errors, for easy overloading in different
+ * environments.
+ * @param message the error message to report.
+ */
+Templater.prototype.logError = function(message) {
+ window.console && window.console.log && console.log(message);
+};
+
+exports.Templater = Templater;
+
+
+});
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Skywriter.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Skywriter Team (skywriter@mozilla.com)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+define('cockpit/commands/basic', ['require', 'exports', 'module' , 'pilot/canon'], function(require, exports, module) {
+
+
+var canon = require('pilot/canon');
+
+/**
+ * '!' command
+ */
+var bangCommandSpec = {
+ name: 'sh',
+ description: 'Execute a system command (requires server support)',
+ params: [
+ {
+ name: 'command',
+ type: 'text',
+ description: 'The string to send to the os shell.'
+ }
+ ],
+ exec: function(env, args, request) {
+ var req = new XMLHttpRequest();
+ req.open('GET', '/exec?args=' + args.command, true);
+ req.onreadystatechange = function(ev) {
+ if (req.readyState == 4) {
+ if (req.status == 200) {
+ request.done('<pre>' + req.responseText + '</pre>');
+ }
+ }
+ };
+ req.send(null);
+ }
+};
+
+var canon = require('pilot/canon');
+
+exports.startup = function(data, reason) {
+ canon.addCommand(bangCommandSpec);
+};
+
+exports.shutdown = function(data, reason) {
+ canon.removeCommand(bangCommandSpec);
+};
+
+
+});
+define("text!cockpit/ui/cli_view.css", [], "" +
+ "#cockpitInput { padding-left: 16px; }" +
+ "" +
+ ".cptOutput { overflow: auto; position: absolute; z-index: 999; display: none; }" +
+ "" +
+ ".cptCompletion { padding: 0; position: absolute; z-index: -1000; }" +
+ ".cptCompletion.VALID { background: #FFF; }" +
+ ".cptCompletion.INCOMPLETE { background: #DDD; }" +
+ ".cptCompletion.INVALID { background: #DDD; }" +
+ ".cptCompletion span { color: #FFF; }" +
+ ".cptCompletion span.INCOMPLETE { color: #DDD; border-bottom: 2px dotted #F80; }" +
+ ".cptCompletion span.INVALID { color: #DDD; border-bottom: 2px dotted #F00; }" +
+ "span.cptPrompt { color: #66F; font-weight: bold; }" +
+ "" +
+ "" +
+ ".cptHints {" +
+ " color: #000;" +
+ " position: absolute;" +
+ " border: 1px solid rgba(230, 230, 230, 0.8);" +
+ " background: rgba(250, 250, 250, 0.8);" +
+ " -moz-border-radius-topleft: 10px;" +
+ " -moz-border-radius-topright: 10px;" +
+ " border-top-left-radius: 10px; border-top-right-radius: 10px;" +
+ " z-index: 1000;" +
+ " padding: 8px;" +
+ " display: none;" +
+ "}" +
+ "" +
+ ".cptFocusPopup { display: block; }" +
+ ".cptFocusPopup.cptNoPopup { display: none; }" +
+ "" +
+ ".cptHints ul { margin: 0; padding: 0 15px; }" +
+ "" +
+ ".cptGt { font-weight: bold; font-size: 120%; }" +
+ "");
+
+define("text!cockpit/ui/request_view.css", [], "" +
+ ".cptRowIn {" +
+ " display: box; display: -moz-box; display: -webkit-box;" +
+ " box-orient: horizontal; -moz-box-orient: horizontal; -webkit-box-orient: horizontal;" +
+ " box-align: center; -moz-box-align: center; -webkit-box-align: center;" +
+ " color: #333;" +
+ " background-color: #EEE;" +
+ " width: 100%;" +
+ " font-family: consolas, courier, monospace;" +
+ "}" +
+ ".cptRowIn > * { padding-left: 2px; padding-right: 2px; }" +
+ ".cptRowIn > img { cursor: pointer; }" +
+ ".cptHover { display: none; }" +
+ ".cptRowIn:hover > .cptHover { display: block; }" +
+ ".cptRowIn:hover > .cptHover.cptHidden { display: none; }" +
+ ".cptOutTyped {" +
+ " box-flex: 1; -moz-box-flex: 1; -webkit-box-flex: 1;" +
+ " font-weight: bold; color: #000; font-size: 120%;" +
+ "}" +
+ ".cptRowOutput { padding-left: 10px; line-height: 1.2em; }" +
+ ".cptRowOutput strong," +
+ ".cptRowOutput b," +
+ ".cptRowOutput th," +
+ ".cptRowOutput h1," +
+ ".cptRowOutput h2," +
+ ".cptRowOutput h3 { color: #000; }" +
+ ".cptRowOutput a { font-weight: bold; color: #666; text-decoration: none; }" +
+ ".cptRowOutput a: hover { text-decoration: underline; cursor: pointer; }" +
+ ".cptRowOutput input[type=password]," +
+ ".cptRowOutput input[type=text]," +
+ ".cptRowOutput textarea {" +
+ " color: #000; font-size: 120%;" +
+ " background: transparent; padding: 3px;" +
+ " border-radius: 5px; -moz-border-radius: 5px; -webkit-border-radius: 5px;" +
+ "}" +
+ ".cptRowOutput table," +
+ ".cptRowOutput td," +
+ ".cptRowOutput th { border: 0; padding: 0 2px; }" +
+ ".cptRowOutput .right { text-align: right; }" +
+ "");
+
+define("text!cockpit/ui/request_view.html", [], "" +
+ "<div class=cptRow>" +
+ " <!-- The div for the input (i.e. what was typed) -->" +
+ " <div class=\"cptRowIn\" save=\"${rowin}\"" +
+ " onclick=\"${copyToInput}\"" +
+ " ondblclick=\"${executeRequest}\">" +
+ "" +
+ " <!-- What the user actually typed -->" +
+ " <div class=\"cptGt\">&gt; </div>" +
+ " <div class=\"cptOutTyped\">${request.typed}</div>" +
+ "" +
+ " <!-- The extra details that appear on hover -->" +
+ " <div class=cptHover save=\"${duration}\"></div>" +
+ " <img class=cptHover onclick=\"${hideOutput}\" save=\"${hide}\"" +
+ " alt=\"Hide command output\" _src=\"${imageUrl('images/minus.png')}\"/>" +
+ " <img class=\"cptHover cptHidden\" onclick=\"${showOutput}\" save=\"${show}\"" +
+ " alt=\"Show command output\" _src=\"${imageUrl('images/plus.png')}\"/>" +
+ " <img class=cptHover onclick=\"${remove}\"" +
+ " alt=\"Remove this command from the history\"" +
+ " _src=\"${imageUrl('images/closer.png')}\"/>" +
+ "" +
+ " </div>" +
+ "" +
+ " <!-- The div for the command output -->" +
+ " <div class=\"cptRowOut\" save=\"${rowout}\">" +
+ " <div class=\"cptRowOutput\" save=\"${output}\"></div>" +
+ " <img _src=\"${imageUrl('images/throbber.gif')}\" save=\"${throb}\"/>" +
+ " </div>" +
+ "</div>" +
+ "");
+
+define("text!cockpit/ui/images/pinaction.png", [], "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC7mlDQ1BJQ0MgUHJvZmlsZQAAeAGFVM9rE0EU/jZuqdAiCFprDrJ4kCJJWatoRdQ2/RFiawzbH7ZFkGQzSdZuNuvuJrWliOTi0SreRe2hB/+AHnrwZC9KhVpFKN6rKGKhFy3xzW5MtqXqwM5+8943731vdt8ADXLSNPWABOQNx1KiEWlsfEJq/IgAjqIJQTQlVdvsTiQGQYNz+Xvn2HoPgVtWw3v7d7J3rZrStpoHhP1A4Eea2Sqw7xdxClkSAog836Epx3QI3+PY8uyPOU55eMG1Dys9xFkifEA1Lc5/TbhTzSXTQINIOJT1cVI+nNeLlNcdB2luZsbIEL1PkKa7zO6rYqGcTvYOkL2d9H5Os94+wiHCCxmtP0a4jZ71jNU/4mHhpObEhj0cGDX0+GAVtxqp+DXCFF8QTSeiVHHZLg3xmK79VvJKgnCQOMpkYYBzWkhP10xu+LqHBX0m1xOv4ndWUeF5jxNn3tTd70XaAq8wDh0MGgyaDUhQEEUEYZiwUECGPBoxNLJyPyOrBhuTezJ1JGq7dGJEsUF7Ntw9t1Gk3Tz+KCJxlEO1CJL8Qf4qr8lP5Xn5y1yw2Fb3lK2bmrry4DvF5Zm5Gh7X08jjc01efJXUdpNXR5aseXq8muwaP+xXlzHmgjWPxHOw+/EtX5XMlymMFMXjVfPqS4R1WjE3359sfzs94i7PLrXWc62JizdWm5dn/WpI++6qvJPmVflPXvXx/GfNxGPiKTEmdornIYmXxS7xkthLqwviYG3HCJ2VhinSbZH6JNVgYJq89S9dP1t4vUZ/DPVRlBnM0lSJ93/CKmQ0nbkOb/qP28f8F+T3iuefKAIvbODImbptU3HvEKFlpW5zrgIXv9F98LZua6N+OPwEWDyrFq1SNZ8gvAEcdod6HugpmNOWls05Uocsn5O66cpiUsxQ20NSUtcl12VLFrOZVWLpdtiZ0x1uHKE5QvfEp0plk/qv8RGw/bBS+fmsUtl+ThrWgZf6b8C8/UXAeIuJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAClklEQVQ4EX1TXUhUQRQ+Z3Zmd+9uN1q2P3UpZaEwcikKekkqLKggKHJ96MHe9DmLkCDa9U198Id8kErICmIlRAN96UdE6QdBW/tBA5Uic7E0zN297L17p5mb1zYjD3eYc+d83zlnON8g5xzWNUSEdUBkHTJasRWySPP7fw3hfwkk2GoNsc0vOaJRHo1GV/GiMctkTIJRFlpZli8opK+htmf83gXeG63oteOtra0u25e7TYJIJELb26vYCACTgUe1lXV86BTn745l+MsyHqs53S/Aq4VEUa9Y6ko14eYY4u3AyM3HYwdKU35DZyblGR2+qq6W0X2Nnh07xynnVYpHORx/E1/GvvqaAZUayjMjdM2f/Lgr5E+fV93zR4u3zKCLughsZqKwAzAxaz6dPY6JgjLUF+eSP5OpjmAw2E8DvldHSvJMKPg08aRor1tc4BuALu6mOwGWdQC3mKIqRsC8mKd8wYfD78/earzSYzdMDW9QgKb0Is8CBY1mQXOiaXAHEpMDE5XTJqIq4EiyxUqKlpfkF0pyV1OTAoFAhmTmyCCoDsZNZvIkUjELQpipo0sQqYZAswZHwsEEE10M0pq2SSZY9HqNcDicJcNTpBvQJz40UbSOTh1B8bDpuY0w9Hb3kkn9lPAlBLfhfD39XTtX/blFJqiqrjbkTi63Hbofj2uL4GMsmzFgbDJ/vmMgv/lB4syJ0oXO7d3j++vio6GFsYmD6cHJreWc3/jRVVHhsOYvM8iZ36mtjPDBk/xDZE8CoHlbrlAssbTxDdDJvdb536L7I6S7Vy++6Gi4Xi9BsUthJRaLOYSPz4XALKI4j4iObd/e5UtDKUjZzYyYRyGAJv01Zj8kC5cbs5WY83hQnv0DzCXl+r8APElkq0RU6oMAAAAASUVORK5CYII=");
+
+define("text!cockpit/ui/images/throbber.gif", [], "data:image/gif;base64,R0lGODlh3AATAPQAAP///wAAAL6+vqamppycnLi4uLKyssjIyNjY2MTExNTU1Nzc3ODg4OTk5LCwsLy8vOjo6Ozs7MrKyvLy8vT09M7Ozvb29sbGxtDQ0O7u7tbW1sLCwqqqqvj4+KCgoJaWliH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAA3AATAAAF/yAgjmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgECAaEpHLJbDqf0Kh0Sq1ar9isdjoQtAQFg8PwKIMHnLF63N2438f0mv1I2O8buXjvaOPtaHx7fn96goR4hmuId4qDdX95c4+RG4GCBoyAjpmQhZN0YGYFXitdZBIVGAoKoq4CG6Qaswi1CBtkcG6ytrYJubq8vbfAcMK9v7q7D8O1ycrHvsW6zcTKsczNz8HZw9vG3cjTsMIYqQgDLAQGCQoLDA0QCwUHqfYSFw/xEPz88/X38Onr14+Bp4ADCco7eC8hQYMAEe57yNCew4IVBU7EGNDiRn8Z831cGLHhSIgdE/9chIeBgDoB7gjaWUWTlYAFE3LqzDCTlc9WOHfm7PkTqNCh54rePDqB6M+lR536hCpUqs2gVZM+xbrTqtGoWqdy1emValeXKwgcWABB5y1acFNZmEvXwoJ2cGfJrTv3bl69Ffj2xZt3L1+/fw3XRVw4sGDGcR0fJhxZsF3KtBTThZxZ8mLMgC3fRatCLYMIFCzwLEprg84OsDus/tvqdezZf13Hvr2B9Szdu2X3pg18N+68xXn7rh1c+PLksI/Dhe6cuO3ow3NfV92bdArTqC2Ebc3A8vjf5QWf15Bg7Nz17c2fj69+fnq+8N2Lty+fuP78/eV2X13neIcCeBRwxorbZrAxAJoCDHbgoG8RTshahQ9iSKEEzUmYIYfNWViUhheCGJyIP5E4oom7WWjgCeBBAJNv1DVV01MZdJhhjdkplWNzO/5oXI846njjVEIqR2OS2B1pE5PVscajkxhMycqLJgxQCwT40PjfAV4GqNSXYdZXJn5gSkmmmmJu1aZYb14V51do+pTOCmA00AqVB4hG5IJ9PvYnhIFOxmdqhpaI6GeHCtpooisuutmg+Eg62KOMKuqoTaXgicQWoIYq6qiklmoqFV0UoeqqrLbq6quwxirrrLTWauutJ4QAACH5BAkKAAAALAAAAADcABMAAAX/ICCOZGmeaKqubOu+cCzPdG3feK7vfO//wKBwSAQIBoSkcslsOp/QqHRKrVqv2Kx2OhC0BAXHx/EoCzboAcdhcLDdgwJ6nua03YZ8PMFPoBMca215eg98G36IgYNvDgOGh4lqjHd7fXOTjYV9nItvhJaIfYF4jXuIf4CCbHmOBZySdoOtj5eja59wBmYFXitdHhwSFRgKxhobBgUPAmdoyxoI0tPJaM5+u9PaCQZzZ9gP2tPcdM7L4tLVznPn6OQb18nh6NV0fu3i5OvP8/nd1qjwaasHcIPAcf/gBSyAAMMwBANYEAhWYQGDBhAyLihwYJiEjx8fYMxIcsGDAxVA/yYIOZIkBAaGPIK8INJlRpgrPeasaRPmx5QgJfB0abLjz50tSeIM+pFmUo0nQQIV+vRlTJUSnNq0KlXCSq09ozIFexEBAYkeNiwgOaEtn2LFpGEQsKCtXbcSjOmVlqDuhAx3+eg1Jo3u37sZBA9GoMAw4MB5FyMwfLht4sh7G/utPGHlYAV8Nz9OnOBz4c2VFWem/Pivar0aKCP2LFn2XwhnVxBwsPbuBAQbEGiIFg1BggoWkidva5z4cL7IlStfkED48OIYoiufYIH68+cKPkqfnsB58ePjmZd3Dj199/XE20tv6/27XO3S6z9nPCz9BP3FISDefL/Bt192/uWmAv8BFzAQAQUWWFaaBgqA11hbHWTIXWIVXifNhRlq6FqF1sm1QQYhdiAhbNEYc2KKK1pXnAIvhrjhBh0KxxiINlqQAY4UXjdcjSJyeAx2G2BYJJD7NZQkjCPKuCORKnbAIXsuKhlhBxEomAIBBzgIYXIfHfmhAAyMR2ZkHk62gJoWlNlhi33ZJZ2cQiKTJoG05Wjcm3xith9dcOK5X51tLRenoHTuud2iMnaolp3KGXrdBo7eKYF5p/mXgJcogClmcgzAR5gCKymXYqlCgmacdhp2UCqL96mq4nuDBTmgBasaCFp4sHaQHHUsGvNRiiGyep1exyIra2mS7dprrtA5++z/Z8ZKYGuGsy6GqgTIDvupRGE+6CO0x3xI5Y2mOTkBjD4ySeGU79o44mcaSEClhglgsKyJ9S5ZTGY0Bnzrj+3SiKK9Rh5zjAALCywZBk/ayCWO3hYM5Y8Dn6qxxRFsgAGoJwwgDQRtYXAAragyQOmaLKNZKGaEuUlpyiub+ad/KtPqpntypvvnzR30DBtjMhNodK6Eqrl0zU0/GjTUgG43wdN6Ra2pAhGtAAZGE5Ta8TH6wknd2IytNKaiZ+Or79oR/tcvthIcAPe7DGAs9Edwk6r3qWoTaNzY2fb9HuHh2S343Hs1VIHhYtOt+Hh551rh24vP5YvXSGzh+eeghy76GuikU9FFEainrvrqrLfu+uuwxy777LTXfkIIACH5BAkKAAAALAAAAADcABMAAAX/ICCOZGmeaKqubOu+cCzPdG3feK7vfO//wKBwSAQIBoSkcslsOp/QqHRKrVqv2Kx2OhC0BAWHB2l4CDZo9IDjcBja7UEhTV+3DXi3PJFA8xMcbHiDBgMPG31pgHBvg4Z9iYiBjYx7kWocb26OD398mI2EhoiegJlud4UFiZ5sm6Kdn2mBr5t7pJ9rlG0cHg5gXitdaxwFGArIGgoaGwYCZ3QFDwjU1AoIzdCQzdPV1c0bZ9vS3tUJBmjQaGXl1OB0feze1+faiBvk8wjnimn55e/o4OtWjp+4NPIKogsXjaA3g/fiGZBQAcEAFgQGOChgYEEDCCBBLihwQILJkxIe/3wMKfJBSQkJYJpUyRIkgwcVUJq8QLPmTYoyY6ZcyfJmTp08iYZc8MBkhZgxk9aEcPOlzp5FmwI9KdWn1qASurJkClRoWKwhq6IUqpJBAwQEMBYroAHkhLt3+RyzhgCDgAV48Wbgg+waAnoLMgTOm6DwQ8CLBzdGdvjw38V5JTg2lzhyTMeUEwBWHPgzZc4TSOM1bZia6LuqJxCmnOxv7NSsl1mGHHiw5tOuIWeAEHcFATwJME/ApgFBc3MVLEgPvE+Ddb4JokufPmFBAuvPXWu3MIF89wTOmxvOvp179evQtwf2nr6aApPyzVd3jn089e/8xdfeXe/xdZ9/d1ngHf98lbHH3V0LMrgPgsWpcFwBEFBgHmyNXWeYAgLc1UF5sG2wTHjIhNjBiIKZCN81GGyQwYq9uajeMiBOQGOLJ1KjTI40kmfBYNfc2NcGIpI4pI0vyrhjiT1WFqOOLEIZnjVOVpmajYfBiCSNLGbA5YdOkjdihSkQwIEEEWg4nQUmvYhYe+bFKaFodN5lp3rKvJYfnBKAJ+gGDMi3mmbwWYfng7IheuWihu5p32XcSWdSj+stkF95dp64jJ+RBipocHkCCp6PCiRQ6INookCAAwy0yd2CtNET3Yo7RvihBjFZAOaKDHT43DL4BQnsZMo8xx6uI1oQrHXXhHZrB28G62n/YSYxi+uzP2IrgbbHbiaer7hCiOxDFWhrbmGnLVuus5NFexhFuHLX6gkEECorlLpZo0CWJG4pLjIACykmBsp0eSSVeC15TDJeUhlkowlL+SWLNJpW2WEF87urXzNWSZ6JOEb7b8g1brZMjCg3ezBtWKKc4MvyEtwybPeaMAA1ECRoAQYHYLpbeYYCLfQ+mtL5c9CnfQpYpUtHOSejEgT9ogZ/GSqd0f2m+LR5WzOtHqlQX1pYwpC+WbXKqSYtpJ5Mt4a01lGzS3akF60AxkcTaLgAyRBPWCoDgHfJqwRuBuzdw/1ml3iCwTIeLUWJN0v4McMe7uasCTxseNWPSxc5RbvIgD7geZLbGrqCG3jepUmbbze63Y6fvjiOylbwOITPfIHEFsAHL/zwxBdvPBVdFKH88sw37/zz0Ecv/fTUV2/99SeEAAAh+QQJCgAAACwAAAAA3AATAAAF/yAgjmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgECAaEpHLJbDqf0Kh0Sq1ar9isdjoQtAQFh2cw8BQEm3T6yHEYHHD4oKCuD9qGvNsxT6QTgAkcHHmFeX11fm17hXwPG35qgnhxbwMPkXaLhgZ9gWp3bpyegX4DcG+inY+Qn6eclpiZkHh6epetgLSUcBxlD2csXXdvBQrHGgoaGhsGaIkFDwjTCArTzX+QadHU3c1ofpHc3dcGG89/4+TYktvS1NYI7OHu3fEJ5tpqBu/k+HX7+nXDB06SuoHm0KXhR65cQT8P3FRAMIAFgVMPwDCAwLHjggIHJIgceeFBg44eC/+ITCCBZYKSJ1FCWPBgpE2YMmc+qNCypwScMmnaXAkUJYOaFVyKLOqx5tCXJnMelcBzJNSYKIX2ZPkzqsyjPLku9Zr1QciVErYxaICAgEUOBRJIgzChbt0MLOPFwyBggV27eCUcmxZvg9+/dfPGo5bg8N/Ag61ZM4w4seDF1fpWhizZmoa+GSortgcaMWd/fkP/HY0MgWbTipVV++wY8GhvqSG4XUEgoYTKE+Qh0OCvggULiBckWEZ4Ggbjx5HXVc58IPQJ0idQJ66XanTpFraTe348+XLizRNcz658eHMN3rNPT+C+G/nodqk3t6a+fN3j+u0Xn3nVTQPfdRPspkL/b+dEIN8EeMm2GAYbTNABdrbJ1hyFFv5lQYTodSZABhc+loCEyhxTYYkZopdMMiNeiBxyIFajV4wYHpfBBspUl8yKHu6ooV5APsZjQxyyeNeJ3N1IYod38cgdPBUid6GCKfRWgAYU4IccSyHew8B3doGJHmMLkGkZcynKk2Z50Ym0zJzLbDCmfBbI6eIyCdyJmJmoqZmnBAXy9+Z/yOlZDZpwYihnj7IZpuYEevrYJ5mJEuqiof4l+NYDEXQpXQcMnNjZNDx1oGqJ4S2nF3EsqWrhqqVWl6JIslpAK5MaIqDeqjJq56qN1aTaQaPbHTPYr8Be6Gsyyh6Da7OkmmqP/7GyztdrNVQBm5+pgw3X7aoYKhfZosb6hyUKBHCgQKij1rghkOAJuZg1SeYIIY+nIpDvf/sqm4yNG5CY64f87qdAwSXKGqFkhPH1ZHb2EgYtw3bpKGVkPz5pJAav+gukjB1UHE/HLNJobWcSX8jiuicMMBFd2OmKwQFs2tjXpDfnPE1j30V3c7iRHlrzBD2HONzODyZtsQJMI4r0AUNaE3XNHQw95c9GC001MpIxDacFQ+ulTNTZlU3O1eWVHa6vb/pnQUUrgHHSBKIuwG+bCPyEqbAg25gMVV1iOB/IGh5YOKLKIQ6xBAcUHmzjIcIqgajZ+Ro42DcvXl7j0U4WOUd+2IGu7DWjI1pt4DYq8BPm0entuGSQY/4tBi9Ss0HqfwngBQtHbCH88MQXb/zxyFfRRRHMN+/889BHL/301Fdv/fXYZ39CCAAh+QQJCgAAACwAAAAA3AATAAAF/yAgjmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgECAaEpHLJbDqf0Kh0Sq1ar9isdjoQtAQFh2fAKXsKm7R6Q+Y43vABep0mGwwOPH7w2CT+gHZ3d3lyagl+CQNvg4yGh36LcHoGfHR/ZYOElQ9/a4ocmoRygIiRk5p8pYmZjXePaYBujHoOqp5qZHBlHAUFXitddg8PBg8KGsgayxvGkAkFDwgICtPTzX2mftHW3QnOpojG3dbYkNjk1waxsdDS1N7ga9zw1t/aifTk35fu6Qj3numL14fOuHTNECHqU4DDgQEsCCwidiHBAwYQMmpcUOCAhI8gJVzUuLGThAQnP/9abEAyI4MCIVOKZNnyJUqUJxNcGNlywYOQgHZirGkSJ8gHNEky+AkS58qWEJYC/bMzacmbQHkqNdlUJ1KoSz2i9COhmQYCEXtVrCBgwYS3cCf8qTcNQ9u4cFFOq2bPLV65Cf7dxZthbjW+CgbjnWtNgWPFcAsHdoxgWWK/iyV045sAc2S96SDn1exYw17REwpLQEYt2eW/qtPZRQAB7QoC61RW+GsBwYZ/CXb/XRCYLsAKFizEtUAc+G7lcZsjroscOvTmsoUvx15PwccJ0N8yL17N9PG/E7jv9S4hOV7pdIPDdZ+ePDzv2qMXn2b5+wTbKuAWnF3oZbABZY0lVmD/ApQd9thybxno2GGuCVDggaUpoyBsB1bGGgIYbJCBcuFJiOAyGohIInQSmmdeiBnMF2GHfNUlIoc1rncjYRjW6NgGf3VQGILWwNjBfxEZcAFbC7gHXQcfUYOYdwzQNxo5yUhQZXhvRYlMeVSuSOJHKJa5AQMQThBlZWZ6Bp4Fa1qzTAJbijcBlJrtxeaZ4lnnpZwpukWieGQmYx5ATXIplwTL8DdNZ07CtWYybNIJF4Ap4NZHe0920AEDk035kafieQrqXofK5ympn5JHKYjPrfoWcR8WWQGp4Ul32KPVgXdnqxM6OKqspjIYrGPDrlrsZtRIcOuR86nHFwbPvmes/6PH4frrqbvySh+mKGhaAARPzjjdhCramdoGGOhp44i+zogBkSDuWC5KlE4r4pHJkarXrj++Raq5iLmWLlxHBteavjG+6amJrUkJJI4Ro5sBv9AaOK+jAau77sbH7nspCwNIYIACffL7J4JtWQnen421nNzMcB6AqpRa9klonmBSiR4GNi+cJZpvwgX0ejj71W9yR+eIgaVvQgf0l/A8nWjUFhwtZYWC4hVnkZ3p/PJqNQ5NnwUQrQCGBBBMQIGTtL7abK+5JjAv1fi9bS0GLlJHgdjEgYzzARTwC1fgEWdJuKKBZzj331Y23qB3i9v5aY/rSUC4w7PaLeWXmr9NszMFoN79eeiM232o33EJAIzaSGwh++y012777bhT0UURvPfu++/ABy/88MQXb/zxyCd/QggAIfkECQoAAAAsAAAAANwAEwAABf8gII5kaZ5oqq5s675wLM90bd94ru987//AoHBIBAgGhKRyyWw6n9CodEqtWq/YrHY6ELQEBY5nwCk7xIWNer0hO95wziC9Ttg5b4ND/+Y87IBqZAaEe29zGwmJigmDfHoGiImTjXiQhJEPdYyWhXwDmpuVmHwOoHZqjI6kZ3+MqhyemJKAdo6Ge3OKbEd4ZRwFBV4rc4MPrgYPChrMzAgbyZSJBcoI1tfQoYsJydfe2amT3d7W0OGp1OTl0YtqyQrq0Lt11PDk3KGoG+nxBpvTD9QhwCctm0BzbOyMIwdOUwEDEgawIOCB2oMLgB4wgMCx44IHBySIHClBY0ePfyT/JCB5weRJCAwejFw58kGDlzBTqqTZcuPLmCIBiWx58+VHmiRLFj0JVCVLl0xl7qSZwCbOo0lFWv0pdefQrVFDJtr5gMBEYBgxqBWwYILbtxPsqMPAFu7blfa81bUbN4HAvXAzyLWnoDBguHIRFF6m4LBbwQngMYPXuC3fldbyPrMcGLM3w5wRS1iWWUNlvnElKDZtz/EEwaqvYahQoexEfyILi4RrYYKFZwJ3810QWZ2ECrx9Ew+O3K6F5Yq9zXbb+y30a7olJJ+wnLC16W97Py+uwdtx1NcLWzs/3G9e07stVPc9kHJ0BcLtQp+c3ewKAgYkUAFpCaAmmHqKLSYA/18WHEiZPRhsQF1nlLFWmIR8ZbDBYs0YZuCGpGXWmG92aWiPMwhEOOEEHXRwIALlwXjhio+BeE15IzpnInaLbZBBhhti9x2GbnVQo2Y9ZuCfCgBeMCB+DJDIolt4iVhOaNSJdCOBUfIlkmkyMpPAAvKJ59aXzTQzJo0WoJnmQF36Jp6W1qC4gWW9GZladCiyJd+KnsHImgRRVjfnaDEKuiZvbcYWo5htzefbl5LFWNeSKQAo1QXasdhiiwwUl2B21H3aQaghXnPcp1NagCqYslXAqnV+zYWcpNwVp9l5eepJnHqL4SdBi56CGlmw2Zn6aaiZjZqfb8Y2m+Cz1O0n3f+tnvrGbF6kToApCgAWoNWPeh754JA0vmajiAr4iOuOW7abQXVGNriBWoRdOK8FxNqLwX3oluubhv8yluRbegqGb536ykesuoXhyJqPQJIGbLvQhkcwjKs1zBvBwSZIsbcsDCCBAAf4ya+UEhyQoIiEJtfoZ7oxUOafE2BwgMWMqUydfC1LVtiArk0QtGkWEopzlqM9aJrKHfw5c6wKjFkmXDrbhwFockodtMGFLWpXy9JdiXN1ZDNszV4WSLQCGBKoQYHUyonqrHa4ErewAgMmcAAF7f2baIoVzC2p3gUvJtLcvIWqloy6/R04mIpLwDhciI8qLOB5yud44pHPLbA83hFDWPjNbuk9KnySN57Av+TMBvgEAgzzNhJb5K777rz37vvvVHRRxPDEF2/88cgnr/zyzDfv/PPQnxACACH5BAkKAAAALAAAAADcABMAAAX/ICCOZGmeaKqubOu+cCzPdG3feK7vfO//wKBwSAQIBoSkcslsOp/QqHRKrVqv2Kx2OhC0BIUCwcMpO84OT2HDbm8GHLQjnn6wE3g83SA3DB55G3llfHxnfnZ4gglvew6Gf4ySgmYGlpCJknochWiId3kJcZZyDn93i6KPl4eniopwq6SIoZKxhpenbhtHZRxhXisDopwPgHkGDxrLGgjLG8mC0gkFDwjX2AgJ0bXJ2djbgNJsAtbfCNB2oOnn6MmKbeXt226K1fMGi6j359D69ua+QZskjd+3cOvY9XNgp4ABCQNYEDBl7EIeCQkeMIDAseOCBwckiBSZ4ILGjh4B/40kaXIjSggMHmBcifHky5gYE6zM2OAlzGM6Z5rs+fIjTZ0tfcYMSlLCUJ8fL47kCVXmTjwPiKJkUCDnyqc3CxzQmYeAxAEGLGJYiwCDgAUT4sqdgOebArdw507IUNfuW71xdZ7DC5iuhGsKErf9CxhPYgUaEhPWyzfBMgUIJDPW6zhb5M1y+R5GjFkBaLmCM0dOfHqvztXYJnMejaFCBQlmVxAYsEGkYnQV4lqYMNyCtnYSggNekAC58uJxmTufW5w55mwKkg+nLp105uTC53a/nhg88fMTmDfDVl65Xum/IZt/3/zaag3a5W63nll1dvfiWbaaZLmpQIABCVQA2f9lAhTG112PQWYadXE9+FtmEwKWwQYQJrZagxomsOCAGVImInsSbpCBhhwug6KKcXXQQYUcYuDMggrASFmNzjjzzIrh7cUhhhHqONeGpSEW2QYxHsmjhxpgUGAKB16g4IIbMNCkXMlhaJ8GWVJo2I3NyKclYF1GxgyYDEAnXHJrMpNAm/rFBSczPiYAlwXF8ZnmesvoOdyMbx7m4o0S5LWdn4bex2Z4xYmEzaEb5EUcnxbA+WWglqIn6aHPTInCgVbdlZyMqMrIQHMRSiaBBakS1903p04w434n0loBoQFOt1yu2YAnY68RXiNsqh2s2qqxuyKb7Imtmgcrqsp6h8D/fMSpapldx55nwayK/SfqCQd2hcFdAgDp5GMvqhvakF4mZuS710WGIYy30khekRkMu92GNu6bo7r/ttjqwLaua5+HOdrKq5Cl3dcwi+xKiLBwwwom4b0E6xvuYyqOa8IAEghwQAV45VvovpkxBl2mo0W7AKbCZXoAhgMmWnOkEqx2JX5nUufbgJHpXCfMOGu2QAd8eitpW1eaNrNeMGN27mNz0swziYnpSbXN19gYtstzfXrdYjNHtAIYGFVwwAEvR1dfxdjKxVzAP0twAAW/ir2w3nzTd3W4yQWO3t0DfleB4XYnEHCEhffdKgaA29p0eo4fHLng9qoG+OVyXz0gMeWGY7qq3xhiRIEAwayNxBawxy777LTXbjsVXRSh++689+7778AHL/zwxBdv/PEnhAAAIfkECQoAAAAsAAAAANwAEwAABf8gII5kaZ5oqq5s675wLM90bd94ru987//AoHBIBAgGhKRyyWw6n9CodEqtWq/YrHY6ELQEhYLD4BlwHGg0ubBpuzdm9Dk9eCTu+MTZkDb4PXYbeIIcHHxqf4F3gnqGY2kOdQmCjHCGfpCSjHhmh2N+knmEkJmKg3uHfgaaeY2qn6t2i4t7sKAPbwIJD2VhXisDCQZgDrKDBQ8aGgjKyhvDlJMJyAjV1gjCunkP1NfVwpRtk93e2ZVt5NfCk27jD97f0LPP7/Dr4pTp1veLgvrx7AL+Q/BM25uBegoYkDCABYFhEobhkUBRwoMGEDJqXPDgQMUEFC9c1LjxQUUJICX/iMRIEgIDkycrjmzJMSXFlDNJvkwJsmdOjQwKfDz5M+PLoSGLQqgZU6XSoB/voHxawGbFlS2XGktAwKEADB0xiEWAodqGBRPSqp1wx5qCamDRrp2Qoa3bagLkzrULF4GCvHPTglRAmKxZvWsHayBcliDitHUlvGWM97FgCdYWVw4c2e/kw4HZJlCwmDBhwHPrjraGYTHqtaoxVKggoesKAgd2SX5rbUMFCxOAC8cGDwHFwBYWJCgu4XfwtcqZV0grPHj0u2SnqwU+IXph3rK5b1fOu7Bx5+K7L6/2/Xhg8uyXnQ8dvfRiDe7TwyfNuzlybKYpgIFtKhAgwEKkKcOf/wChZbBBgMucRh1so5XH3wbI1WXafRJy9iCErmX4IWHNaIAhZ6uxBxeGHXQA24P3yYfBBhmgSBozESpwongWOBhggn/N1aKG8a1YY2oVAklgCgQUUwGJ8iXAgItrWUARbwpqIOWEal0ZoYJbzmWlZCWSlsAC6VkwZonNbMAAl5cpg+NiZwpnJ0Xylegmlc+tWY1mjnGnZnB4QukMA9UJRxGOf5r4ppqDjjmnfKilh2ejGiyJAgF1XNmYbC2GmhZ5AcJVgajcXecNqM9Rx8B6bingnlotviqdkB3YCg+rtOaapFsUhSrsq6axJ6sEwoZK7I/HWpCsr57FBxJ1w8LqV/81zbkoXK3LfVeNpic0KRQG4NHoIW/XEmZuaiN6tti62/moWbk18uhjqerWS6GFpe2YVotskVssWfBOAHACrZHoWcGQwQhlvmsdXBZ/F9YLMF2jzUuYBP4a7CLCnoEHrgkDSCDAARUILAGaVVqAwQHR8pZXomm9/ONhgjrbgc2lyYxmpIRK9uSNjrXs8gEbTrYyl2ryTJmsLCdKkWzFQl1lWlOXGmifal6p9VnbQfpyY2SZyXKVV7JmZkMrgIFSyrIeUJ2r7YKnXdivUg1kAgdQ8B7IzJjGsd9zKSdwyBL03WpwDGxwuOASEP5vriO2F3nLjQdIrpaRDxqcBdgIHGA74pKrZXiR2ZWuZt49m+o3pKMC3p4Av7SNxBa456777rz37jsVXRQh/PDEF2/88cgnr/zyzDfv/PMnhAAAIfkECQoAAAAsAAAAANwAEwAABf8gII5kaZ5oqq5s675wLM90bd94ru987//AoHBIBAgGhKRyyWw6n9CodEqtWq/YrHY6ELQEhYLDUPAMHGi0weEpbN7wI8cxTzsGj4R+n+DUxwaBeBt7hH1/gYIPhox+Y3Z3iwmGk36BkIN8egOIl3h8hBuOkAaZhQlna4BrpnyWa4mleZOFjrGKcXoFA2ReKwMJBgISDw6abwUPGggazc0bBqG0G8kI1tcIwZp51djW2nC03d7BjG8J49jl4cgP3t/RetLp1+vT6O7v5fKhAvnk0UKFogeP3zmCCIoZkDCABQFhChQYuKBHgkUJkxpA2MhxQYEDFhNcvPBAI8eNCx7/gMQYckPJkxsZPLhIM8FLmDJrYiRp8mTKkCwT8IQJwSPQkENhpgQpEunNkzlpWkwKdSbGihKocowqVSvKWQkIOBSgQOYFDBgQpI0oYMGEt3AzTLKm4BqGtnDjirxW95vbvG/nWlub8G9euRsiqqWLF/AEkRoiprX2wLDeDQgkW9PQGLDgyNc665WguK8C0XAnRY6oGPUEuRLsgk5g+a3cCxUqSBC7gsCBBXcVq6swwULx4hayvctGPK8FCwsSLE9A3Hje6NOrHzeOnW695sffRi/9HfDz7sIVSNB+XXrmugo0rHcM3X388o6jr44ceb51uNjF1xcC8zk3wXiS8aYC/wESaLABBs7ch0ECjr2WAGvLsLZBeHqVFl9kGxooV0T81TVhBo6NiOEyJ4p4IYnNRBQiYCN6x4wCG3ZAY2If8jXjYRcyk2FmG/5nXAY8wqhWAii+1YGOSGLoY4VRfqiAgikwmIeS1gjAgHkWYLQZf9m49V9gDWYWY5nmTYCRM2TS5pxxb8IZGV5nhplmhJyZadxzbrpnZ2d/6rnZgHIid5xIMDaDgJfbLdrgMkKW+Rygz1kEZz1mehabkBpgiQIByVikwGTqVfDkk2/Vxxqiqur4X3fksHccre8xlxerDLiHjQIVUAgXr77yFeyuOvYqXGbMrbrqBMqaFpFFzhL7qv9i1FX7ZLR0LUNdcc4e6Cus263KbV+inkAAHhJg0BeITR6WmHcaxhvXg/AJiKO9R77ILF1FwmVdAu6WBu+ZFua72mkZWMfqBElKu0G8rFZ5n4ATp5jkmvsOq+Nj7u63ZMMPv4bveyYy6fDH+C6brgnACHBABQUrkGirz2FwAHnM4Mmhzq9yijOrOi/MKabH6VwBiYwZdukEQAvILKTWXVq0ZvH5/CfUM7M29Zetthp1eht0eqkFYw8IKXKA6mzXfTeH7fZg9zW0AhgY0TwthUa6Ch9dBeIsbsFrYkRBfgTfiG0FhwMWnbsoq3cABUYOnu/ejU/A6uNeT8u4wMb1WnBCyJJTLjjnr8o3OeJrUcpc5oCiPqAEkz8tXuLkPeDL3Uhs4fvvwAcv/PDEU9FFEcgnr/zyzDfv/PPQRy/99NRXf0IIACH5BAkKAAAALAAAAADcABMAAAX/ICCOZGmeaKqubOu+cCzPdG3feK7vfO//wKBwSAQIBoSkcslsOp/QqHRKrVqv2Kx2OhC0BIWCw/AoDziOtCHt8BQ28PjmzK57Hom8fo42+P8DeAkbeYQcfX9+gYOFg4d1bIGEjQmPbICClI9/YwaLjHAJdJeKmZOViGtpn3qOqZineoeJgG8CeWUbBV4rAwkGAhIVGL97hGACGsrKCAgbBoTRhLvN1c3PepnU1s2/oZO6AtzdBoPf4eMI3tIJyOnF0YwFD+nY8e3z7+Xfefnj9uz8cVsXCh89axgk7BrAggAwBQsYIChwQILFixIeNIDAseOCBwcSXMy2sSPHjxJE/6a0eEGjSY4MQGK86PIlypUJEmYsaTKmyJ8JW/Ls6HMkzaEn8YwMWtPkx4pGd76E4DMPRqFTY860OGhogwYagBFoKEABA46DEGBAoEBB0AUT4sqdIFKBNbcC4M6dkEEk22oYFOTdG9fvWrtsBxM23MytYL17666t9phwXwlum2lIDHmuSA2IGyuOLOHv38qLMbdFjHruZbWgRXeOe1nC2BUEDiyAMMHZuwoTLAQX3nvDOAUW5Vogru434d4JnAsnPmFB9NBshQXfa9104+Rxl8e13rZxN+CEydtVsFkd+vDjE7C/q52wOvb4s7+faz025frbxefWbSoQIAEDEUCwgf9j7bUlwHN9ZVaegxDK1xYzFMJH24L5saXABhlYxiEzHoKoIV8LYqAMaw9aZqFmJUK4YHuNfRjiXhmk+NcyJgaIolvM8BhiBx3IleN8lH1IWAcRgkZgCgYiaBGJojGgHHFTgtagAFYSZhF7/qnTpY+faVlNAnqJN0EHWa6ozAZjBtgmmBokwMB01LW5jAZwbqfmlNips4B4eOqJgDJ2+imXRZpthuigeC6XZTWIxilXmRo8iYKBCwiWmWkJVEAkfB0w8KI1IvlIpKnOkVpqdB5+h96o8d3lFnijrgprjbfGRSt0lH0nAZG5vsprWxYRW6Suq4UWqrLEsspWg8Io6yv/q6EhK0Fw0GLbjKYn5CZYBYht1laPrnEY67kyrhYbuyceiR28Pso7bYwiXjihjWsWuWF5p/H765HmNoiur3RJsGKNG/jq748XMrwmjhwCfO6QD9v7LQsDxPTAMKsFpthyJCdkmgYiw0VdXF/Om9dyv7YMWGXTLYpZg5wNR11C78oW3p8HSGgul4qyrJppgllJHJZHn0Y0yUwDXCXUNquFZNLKyYXBAVZvxtAKYIQEsmPgDacr0tltO1y/DMwYpkgUpJfTasLGzd3cdCN3gN3UWRcY3epIEPevfq+3njBxq/kqBoGBduvea8f393zICS63ivRBTqgFpgaWZEIUULdcK+frIfAAL2AjscXqrLfu+uuwx05FF0XUbvvtuOeu++689+7778AHL/wJIQAAOwAAAAAAAAAAAA==");
+
+define("text!cockpit/ui/images/closer.png", [], "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAj9JREFUeNp0ks+LUlEUx7/vV1o8Z8wUx3IEHcQmiBiQlomjRNCiZpEuEqF/oEUwq/6EhvoHggmRcJUQBM1CRJAW0aLIaGQimZJxJsWxyV/P9/R1zzWlFl04vPvOPZ9z7rnnK5imidmKRCIq+zxgdoPZ1T/ut8xeM3tcKpW6s1hhBkaj0Qj7bDebTX+324WmadxvsVigqipcLleN/d4rFoulORiLxTZY8ItOp8MBCpYkiYPj8Xjus9vtlORWoVB4KcTjcQc732dLpSRXvCZaAws6Q4WDdqsO52kNH+oCRFGEz+f7ydwBKRgMPmTXi49GI1x2D/DsznesB06ws2eDbI7w9HYN6bVjvGss4KAjwDAMq81mM2SW5Wa/3weBbz42UL9uYnVpiO2Nr9ANHSGXib2Wgm9tCYIggGKJEVkvlwgi5/FQRmTLxO6hgJVzI1x0T/fJrBtHJxPeL6tI/fsZLA6ot8lkQi8HRVbw94gkWYI5MaHrOjcCGSNRxZosy9y5cErDzn0Dqx7gcwO8WtBp4PndI35GMYqiUMUvBL5yOBz8yRfFNpbPmqgcCFh/IuHa1nR/YXGM8+oUpFhihEQiwcdRLpfVRqOBtWXWq34Gra6AXq8Hp2piZcmKT4cKnE4nwuHwdByVSmWQz+d32WCTlHG/qaHHREN9kgi0sYQfv0R4PB4EAgESQDKXy72fSy6VSnHJVatVf71eR7vd5n66mtfrRSgU4pLLZrOlf7RKK51Ok8g3/yPyR5lMZi7y3wIMAME4EigHWgKnAAAAAElFTkSuQmCC");
+
+define("text!cockpit/ui/images/pinin.png", [], "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC7mlDQ1BJQ0MgUHJvZmlsZQAAeAGFVM9rE0EU/jZuqdAiCFprDrJ4kCJJWatoRdQ2/RFiawzbH7ZFkGQzSdZuNuvuJrWliOTi0SreRe2hB/+AHnrwZC9KhVpFKN6rKGKhFy3xzW5MtqXqwM5+8943731vdt8ADXLSNPWABOQNx1KiEWlsfEJq/IgAjqIJQTQlVdvsTiQGQYNz+Xvn2HoPgVtWw3v7d7J3rZrStpoHhP1A4Eea2Sqw7xdxClkSAog836Epx3QI3+PY8uyPOU55eMG1Dys9xFkifEA1Lc5/TbhTzSXTQINIOJT1cVI+nNeLlNcdB2luZsbIEL1PkKa7zO6rYqGcTvYOkL2d9H5Os94+wiHCCxmtP0a4jZ71jNU/4mHhpObEhj0cGDX0+GAVtxqp+DXCFF8QTSeiVHHZLg3xmK79VvJKgnCQOMpkYYBzWkhP10xu+LqHBX0m1xOv4ndWUeF5jxNn3tTd70XaAq8wDh0MGgyaDUhQEEUEYZiwUECGPBoxNLJyPyOrBhuTezJ1JGq7dGJEsUF7Ntw9t1Gk3Tz+KCJxlEO1CJL8Qf4qr8lP5Xn5y1yw2Fb3lK2bmrry4DvF5Zm5Gh7X08jjc01efJXUdpNXR5aseXq8muwaP+xXlzHmgjWPxHOw+/EtX5XMlymMFMXjVfPqS4R1WjE3359sfzs94i7PLrXWc62JizdWm5dn/WpI++6qvJPmVflPXvXx/GfNxGPiKTEmdornIYmXxS7xkthLqwviYG3HCJ2VhinSbZH6JNVgYJq89S9dP1t4vUZ/DPVRlBnM0lSJ93/CKmQ0nbkOb/qP28f8F+T3iuefKAIvbODImbptU3HvEKFlpW5zrgIXv9F98LZua6N+OPwEWDyrFq1SNZ8gvAEcdod6HugpmNOWls05Uocsn5O66cpiUsxQ20NSUtcl12VLFrOZVWLpdtiZ0x1uHKE5QvfEp0plk/qv8RGw/bBS+fmsUtl+ThrWgZf6b8C8/UXAeIuJAAAACXBIWXMAAAsTAAALEwEAmpwYAAABZ0lEQVQ4Ea2TPUsDQRCGZ89Eo4FACkULEQs1CH4Uamfjn7GxEYJFIFXgChFsbPwzNnZioREkaiHBQtEiEEiMRm/dZ8OEGAxR4sBxx877Pju7M2estTJIxLrNuVwuMxQEx0ZkzcFHyRtjXt02559RtB2GYanTYzoryOfz+6l4Nbszf2niwffKmpGRo9sVW22mDgqFwp5C2gDMm+P32a3JB1N+n5JifUGeP9JeNxGryPLYjcwMP8rJ07Q9fZltQzyAstOJ2vVu5sKc1ZZkRBrOcKeb+HexPidvkpCN5JUcllZtpZFc5DgBWc5M2eysZuMuofMBSA4NWjx4PUCsXefMlI0QY3ewRg4NWi4ZTQsgrjYXema+e4VqtEMK6KXvu+4B9Bklt90vVKMeD2BI6DOt4rZ/Gk7WyKFBi4fNPIAJY0joM61SCCZ9tI1o0OIB8D+DBIkYaJRbCBH9mZgNt+bb++ufSSF/eX8BYcDeAzuQJVUAAAAASUVORK5CYII=");
+
+define("text!cockpit/ui/images/plus.png", [], "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAAZiS0dEANIA0gDS7KbF4AAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9kFGw4yFTwuJTkAAAH7SURBVCjPdZKxa1NRFMZ/956XZMgFyyMlCZRA4hBx6lBcQ00GoYi4tEstFPwLAs7iLDi7FWuHThaUggihBDI5OWRoQAmBQFISQgvvpbwX3rsOaR4K+o2H8zvfOZxPWWtZqVarGaAJPAEe3ZW/A1+Bd+1221v1qhW4vb1dA44mk0nZ8zyCIAAgk8lgjGF9fb0PHF5cXLQTsF6vP/c879P19TVBEJDJZBARAKIoSmpra2sYY561Wq3PqtFouMBgMBgYay3ZbJZ/yfd9tNaUSqUboOKISPPq6sqsVvZ9H4AvL34B8PTj/QSO45jpdHovn883Ha31znw+JwzDpCEMQx4UloM8zyOdTif3zudztNY7jog8DMMQpRRxHPPt5TCBAEZvxlyOFTsfykRRBICIlB2t9a21Nh3HMXEc8+d7VhJHWCwWyzcohdZaHBHpO46z6fs+IsLj94XECaD4unCHL8FsNouI/HRE5Nx13c3ZbIbWOnG5HKtl+53TSq7rIiLnand31wUGnU7HjEYjlFLJZN/3yRnL1FMYY8jlcmxtbd0AFel2u7dnZ2eXxpi9xWJBEASkUimstYgIQSSkUimKxSKVSgVjzN7p6emPJHL7+/s14KjX65WHwyGz2SxZbWNjg2q12gcOT05O2n9lFeDg4MAAr/4T8rfHx8dJyH8DvvbYGzKvWukAAAAASUVORK5CYII=");
+
+define("text!cockpit/ui/images/minus.png", [], "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAAZiS0dEANIA0gDS7KbF4AAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9kFGw4xMrIJw5EAAAHcSURBVCjPhZIxSxtxGMZ/976XhJA/RA5EAyJcFksnp64hjUPBoXRyCYLQTyD0UxScu0nFwalCQSgFCVk7dXAwUAiBDA2RO4W7yN1x9+9gcyhU+pteHt4H3pfncay1LOl0OgY4BN4Ar/7KP4BvwNFwOIyWu87S2O12O8DxfD73oygiSRIAarUaxhhWV1fHwMFgMBiWxl6v9y6Koi+3t7ckSUKtVkNVAcjzvNRWVlYwxry9vLz86uzs7HjAZDKZGGstjUaDfxHHMSLC5ubmHdB2VfVwNpuZ5clxHPMcRVFwc3PTXFtbO3RFZHexWJCmabnweAaoVqvlv4vFAhHZdVX1ZZqmOI5DURR8fz/lxbp9Yrz+7bD72SfPcwBU1XdF5N5aWy2KgqIoeBzPEnWVLMseYnAcRERdVR27rrsdxzGqyutP6898+GBsNBqo6i9XVS88z9sOggAR4X94noeqXoiIHPm+H9XrdYIgIAxDwjAkTVPCMESzBy3LMprNJr7v34nIkV5dXd2fn59fG2P2siwjSRIqlQrWWlSVJFcqlQqtVot2u40xZu/s7OxnWbl+v98BjkejkT+dTgmCoDxtY2ODra2tMXBweno6fNJVgP39fQN8eKbkH09OTsqS/wHFRdHPfTSfjwAAAABJRU5ErkJggg==");
+
+define("text!cockpit/ui/images/dot_clear.gif", [], "data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAEBMgA7");
+
+define("text!cockpit/ui/images/pins.png", [], "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAQCAYAAABQrvyxAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGYklEQVRIDbVWe0yURxCf/R735o6DO0FBe0RFsaL4iLXGIKa2SY3P6JGa2GpjlJjUV9NosbU++tYUbEnaQIrVaKJBG7WiNFQFUWO1UUEsVg2CAgoeHHLewcH32O58cBdQsX9Y5+7LfrszOzO/2ZnZj1BKgTBiIwVGVvKd49OVVYunDlXn6wdBKh+ogXrv+DOz1melIb+3LM5fNv2XPYE5EHY+L3PJljN5zavHpJjsQNsA/JJEgyC2+WTjy3b0GfoJW8O4aoHtDwiHQrj5lw1LLyyb1bp5zAjJTus9klrVpdD6TqH2ngVO+0dsRJnp06cLIYU4fx7NnRI3bu7UIYOeJ/McnuY88q3k62gc0S4Dgf5qhICQtIXS2lqD7BhSduPk3YfyzXaANhBBJDxYdUqCywB2qS4RdyUuSkTF/VJxcbH5j8N7/75RuFrN3Zh8OS8zqf5m4UpPeenOyP42dbtBeuvVnCdkK1e4PfPouX03mo9se+c33M8wqDk5Ofqed8REUTicQhbySUxp9u3KlMSHTtrFU6Kyn03lz15PPpW25vsZeYSIKyiVURcqeZJOH9lTNZLfnxRjU/uwrjbEUBWsapcSO2Hq4k0VfZg9EzxdDNCEjDxgNqRDme9umz/btwlsHRIEePHgAf73RdnHZ6LTuIUBN7OBQ+c1Fdnp6cZ1BQUdeRuWZi97o3ktDQQkVeFFzqJARd1A5a0Vr7ta6Kp6TZjtZ+NTIOoKF6qDrL7e0QQIUCiqMMKk8Z1Q/SCSKvzocf2B6NEN0SQn/kTO6fKJ0zqjZUlQBSpJ0GjR77w0aoc1Pr6S5/kVJrNpakV5hR+LWKN4t7sLX+p0rx2vqSta64olIulUKUgCSXLWE1R4KPPSj+5vhm2hdDOG+CkQBmhhyyKq6SaFYWTn5bB3QJRNz54AuXKn8TJjhu0Wbv+wNEKQjVhnmKopjo4FxXmetCRnC4F7BhCiCUepqAepRh0TM/gjjzOOSK2NgWZPc05qampRWJHb7dbOffep2ednzLzgczlbrQA6gHYF9BYDh9GY+FjddMweHMscmMuep07gXlMQoqw9ALoYu5MJsak9QmJA2IvAgVmoCRciooyPujJtNCv1uHt3TmK9gegFKrG9kh6oXwZiIEAtBIjORGKNTWR/WeW8XVkbjuJepLAyloM8LmTN//njKZPbraATZaLjCHEww9Ei4FFiPg6Ja5gT6gxYgLgnRDHRQwJXbz2GOw0d4A3K4GXlUtMahJjYVxiYbrwOmxIS10bFnIBOSi6Tl9Jgs0zbOEX18wyEwgLPMrxD1Y4aCK8kmTpgYcpAF27Mzs42Hjx4kA8BICUlJfKArR7LcEvTB1xEC9AoEw9OPagWkVU/D1oesmK6U911zEczMVe01oZjiMggg6ux2Qk379qh4rYKet4GjrhhwEteBgBrH8BssoXEtbHzPpSBRRSpqlNpgAiUoxzHKxLRszoVuggIisxaDQWZqkQvQjAoax3NbDbLLGuUEABNGedXqSyLRupXgDT5JfAGZNLio9B0X8Uiwk4w77MDc1D4yejjWtykPS3DX01UDCY/GPQcVDe0QYT0CIxGFvUorfvBxZsRfVrUuWruMBAb/lXCUofoFNZfzGJtowXOX0vwUSFK4BgyMKm6P6s9wQUZld+jrYyMDC0iIQDaJdG4IyZQfL3RfbFcCBIlRgc+u3CjaTApuZ9KsANgG8PNzHlWWD3tCxd6kafNNiFp5HAalAkkJ0SCV2H3CgOD9Nc/FqrXuyb0Eocvfhq171p5eyuJ1omKJEP5rQGe/FOOnXtq335z8YmvYo9cHb2t8spIb3lVSseZW46FlGY/Sk9P50P2w20UlWJUkUHIushfc5PXGAzCo0PlD2pnpCYfCXga3lu+fPlevEhWrVrFyrN/Orfv87FOW9tlqb2Kc9pV8DzioMk3UNUbXM+8B/ATBr8C8CKdvGXWGD/9sqm3dkxtzA4McMjHMB8D2ftheYXo+qzt3pXvz8/PP/vk+v8537V+yYW87Zu+RZ1ZbrexoKAA/SBpaWn4+aL5w5zGk+/jW59JiMkESW5urpiVlWXENRb1H/Yf2I9txIxz5IdkX3TsraukpsbQjz6090yb4XsAvQoRE0YvJdamtIIbOnRoUVlZ2ftsLVQzIdEXHntsaZdimssVfCpFui109+BnWPsXaWLI/zactygAAAAASUVORK5CYII=");
+
+define("text!cockpit/ui/images/pinout.png", [], "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC7mlDQ1BJQ0MgUHJvZmlsZQAAeAGFVM9rE0EU/jZuqdAiCFprDrJ4kCJJWatoRdQ2/RFiawzbH7ZFkGQzSdZuNuvuJrWliOTi0SreRe2hB/+AHnrwZC9KhVpFKN6rKGKhFy3xzW5MtqXqwM5+8943731vdt8ADXLSNPWABOQNx1KiEWlsfEJq/IgAjqIJQTQlVdvsTiQGQYNz+Xvn2HoPgVtWw3v7d7J3rZrStpoHhP1A4Eea2Sqw7xdxClkSAog836Epx3QI3+PY8uyPOU55eMG1Dys9xFkifEA1Lc5/TbhTzSXTQINIOJT1cVI+nNeLlNcdB2luZsbIEL1PkKa7zO6rYqGcTvYOkL2d9H5Os94+wiHCCxmtP0a4jZ71jNU/4mHhpObEhj0cGDX0+GAVtxqp+DXCFF8QTSeiVHHZLg3xmK79VvJKgnCQOMpkYYBzWkhP10xu+LqHBX0m1xOv4ndWUeF5jxNn3tTd70XaAq8wDh0MGgyaDUhQEEUEYZiwUECGPBoxNLJyPyOrBhuTezJ1JGq7dGJEsUF7Ntw9t1Gk3Tz+KCJxlEO1CJL8Qf4qr8lP5Xn5y1yw2Fb3lK2bmrry4DvF5Zm5Gh7X08jjc01efJXUdpNXR5aseXq8muwaP+xXlzHmgjWPxHOw+/EtX5XMlymMFMXjVfPqS4R1WjE3359sfzs94i7PLrXWc62JizdWm5dn/WpI++6qvJPmVflPXvXx/GfNxGPiKTEmdornIYmXxS7xkthLqwviYG3HCJ2VhinSbZH6JNVgYJq89S9dP1t4vUZ/DPVRlBnM0lSJ93/CKmQ0nbkOb/qP28f8F+T3iuefKAIvbODImbptU3HvEKFlpW5zrgIXv9F98LZua6N+OPwEWDyrFq1SNZ8gvAEcdod6HugpmNOWls05Uocsn5O66cpiUsxQ20NSUtcl12VLFrOZVWLpdtiZ0x1uHKE5QvfEp0plk/qv8RGw/bBS+fmsUtl+ThrWgZf6b8C8/UXAeIuJAAAACXBIWXMAAAsTAAALEwEAmpwYAAACyUlEQVQ4EW1TXUgUURQ+Z3ZmnVV3QV2xJbVSEIowQbAfLQx8McLoYX2qjB58MRSkP3vZppceYhGxgrZaIughlYpE7CHFWiiKyj9II0qxWmwlNh1Xtp2f27mz7GDlZX7uuXO+73zfuXeQMQYIgAyALppgyBtse32stsw86txkHhATn+FbfPfzxnPB+vR3RMJYuTwW6bbB4a6WS5O3Yu2VlXIesDiAamiQNKVlVXfx5I0GJ7DY7p0/+erU4dgeMJIA31WNxZmAgibOreXDqF55sY4SFUURqbi+nkjgwTyAbHhLX8yOLsSM2QRA3JRAAgd4RGPbVhkKEp8qeJ7PFyW3fw++YHtC7CkaD0amqyqihSwlMQQ0wa07IjPVI/vbexreIUrVaQV2D4RMQ/o7m12Mdfx4H3PfB9FNzTR1U2cO0Bi45aV6xNvFBNaoIAfbSiwLlqi9/hR/R3Nrhua+Oqi9TEKiB02C7YXz+Pba4MTDrpbLiMAxNgmXb+HpwVkZdoIrkn9isW7nRw/TZYaagZArAWyhfqsSDL/c9aTx7JUjGZCtYExRqCzAwGblwr6aFQ84nTo6qZ7XCeCVQNckE/KSWolvoQnxeoFFgIh8G/nA+kBAxxuQO5m9eFrwLIGJHgcyM63VFMhRSgNVyJr7og8y1vbTQpH8DIEVgxuYuexw0QECIalq5FYgEmpkgoFYltU/lnrqDz5osirSFpF7lrHAFKSWHYfEs+mY/82UnAStyMlW8sUPsVIciTZgz3jV1ebg0CEOpgPF22s1z1YQYKSXPJ1hbAhR8T26WdLhkuVfAzPR+YO1Ox5n58SmCcF6e3uzAoHA77RkevJdWH/3+f2O9TGf3w3fWQ2Hw5F/13mcsWAT+vv6DK4kFApJ/d3d1k+kJtbCrmxXHS3n8ER6b3CQbAqaEHVra6sGxcXW4SovLx+empxapS//FfwD9kpMJjMMBBAAAAAASUVORK5CYII=");
+
diff --git a/www/js/ace/cockpit.js b/www/js/ace/cockpit.js
new file mode 100644
index 0000000..ecdc74c
--- /dev/null
+++ b/www/js/ace/cockpit.js
@@ -0,0 +1 @@
+define("cockpit/index",["require","exports","module","pilot/index","cockpit/cli","cockpit/ui/settings","cockpit/ui/cli_view","cockpit/commands/basic"],function(a,b,c){b.startup=function(b,c){a("pilot/index"),a("cockpit/cli").startup(b,c),a("cockpit/ui/settings").startup(b,c),a("cockpit/ui/cli_view").startup(b,c),a("cockpit/commands/basic").startup(b,c)}}),define("cockpit/cli",["require","exports","module","pilot/console","pilot/lang","pilot/oop","pilot/event_emitter","pilot/types","pilot/canon"],function(a,b,c){function r(a,b){q.call(this,a),b&&b.flags&&(this.flags=b.flags)}function q(a){this.env=a,this.commandAssignment=new o(p,this)}function o(a,b){this.param=a,this.requisition=b,this.setValue(a.defaultValue)}function n(a,b,c,d,e,f){this.emitter=a,this.setText(b),this.start=c,this.end=d,this.prefix=e,this.suffix=f}function m(a,b){this.status=a.status,this.message=a.message,b?(this.start=b.start,this.end=b.end):(this.start=0,this.end=0),this.predictions=a.predictions}function l(a,b,c,d,e){this.status=a,this.message=b;if(typeof c=="number")this.start=c,this.end=d,this.predictions=e;else{var f=c;this.start=f.start,this.end=f.end,this.predictions=f.predictions}}var d=a("pilot/console"),e=a("pilot/lang"),f=a("pilot/oop"),g=a("pilot/event_emitter").EventEmitter,h=a("pilot/types"),i=a("pilot/types").Status,j=a("pilot/types").Conversion,k=a("pilot/canon");b.startup=function(a,b){k.upgradeType("command",p)},l.prototype={},l.sort=function(a,b){b!==undefined&&a.forEach(function(a){a.start===n.AT_CURSOR?a.distance=0:b<a.start?a.distance=a.start-b:b>a.end?a.distance=b-a.end:a.distance=0},this),a.sort(function(a,c){if(b!==undefined){var d=a.distance-c.distance;if(d!=0)return d}return c.status-a.status}),b!==undefined&&a.forEach(function(a){delete a.distance},this);return a},b.Hint=l,f.inherits(m,l),n.prototype={merge:function(a){if(a.emitter!=this.emitter)throw new Error("Can't merge Arguments from different EventEmitters");return new n(this.emitter,this.text+this.suffix+a.prefix+a.text,this.start,a.end,this.prefix,a.suffix)},setText:function(a){if(a==null)throw new Error("Illegal text for Argument: "+a);var b={argument:this,oldText:this.text,text:a};this.text=a,this.emitter._dispatchEvent("argumentChange",b)},toString:function(){return this.prefix+this.text+this.suffix}},n.merge=function(a,b,c){b=b===undefined?0:b,c=c===undefined?a.length:c;var d;for(var e=b;e<c;e++){var f=a[e];d?d=d.merge(f):d=f}return d},n.AT_CURSOR=-1,o.prototype={param:undefined,conversion:undefined,value:undefined,arg:undefined,value:undefined,setValue:function(a){if(this.value!==a){if(a===undefined)this.value=this.param.defaultValue,this.conversion=this.param.getDefault?this.param.getDefault():this.param.type.getDefault(),this.arg=undefined;else{this.value=a,this.conversion=undefined;var b=a==null?"":this.param.type.stringify(a);this.arg&&this.arg.setText(b)}this.requisition._assignmentChanged(this)}},arg:undefined,setArgument:function(a){this.arg!==a&&(this.arg=a,this.conversion=this.param.type.parse(a.text),this.conversion.arg=a,this.value=this.conversion.value,this.requisition._assignmentChanged(this))},getHint:function(){if(this.param.getCustomHint&&this.value&&this.arg){var a=this.param.getCustomHint(this.value,this.arg);if(a)return a}var b="<strong>"+this.param.name+"</strong>: ";this.param.description&&(b+=this.param.description.trim(),b.charAt(b.length-1)!=="."&&(b+="."),b.charAt(b.length-1)!==" "&&(b+=" "));var c=i.VALID,d=this.arg?this.arg.start:n.AT_CURSOR,e=this.arg?this.arg.end:n.AT_CURSOR,f;this.conversion&&(c=this.conversion.status,this.conversion.message&&(b+=this.conversion.message),f=this.conversion.predictions);var g=this.arg&&this.arg.text!=="",h=this.value!==undefined||g;this.param.defaultValue===undefined&&!h&&(c=i.INVALID,b+="<strong>Required<strong>");return new l(c,b,d,e,f)},complete:function(){this.conversion&&this.conversion.predictions&&this.conversion.predictions.length>0&&this.setValue(this.conversion.predictions[0])},isPositionCaptured:function(a){if(!this.arg)return!1;if(this.arg.start===-1)return!1;if(a>this.arg.end)return!1;if(a===this.arg.end)return this.conversion.status!==i.VALID||this.conversion.predictions.length!==0;return!0},decrement:function(){var a=this.param.type.decrement(this.value);a!=null&&this.setValue(a)},increment:function(){var a=this.param.type.increment(this.value);a!=null&&this.setValue(a)},toString:function(){return this.arg?this.arg.toString():""}},b.Assignment=o;var p={name:"__command",type:"command",description:"The command to execute",getCustomHint:function(a,b){var c=[];c.push("<strong><tt> &gt; "),c.push(a.name),a.params&&a.params.length>0&&a.params.forEach(function(a){a.defaultValue===undefined?c.push(" ["+a.name+"]"):c.push(" <em>["+a.name+"]</em>")},this),c.push("</tt></strong><br/>"),c.push(a.description?a.description:"(No description)"),c.push("<br/>"),a.params&&a.params.length>0&&(c.push("<ul>"),a.params.forEach(function(a){c.push("<li>"),c.push("<strong><tt>"+a.name+"</tt></strong>: "),c.push(a.description?a.description:"(No description)"),a.defaultValue===undefined?c.push(" <em>[Required]</em>"):a.defaultValue===null?c.push(" <em>[Optional]</em>"):c.push(" <em>[Default: "+a.defaultValue+"]</em>"),c.push("</li>")},this),c.push("</ul>"));return new l(i.VALID,c.join(""),b)}};q.prototype={commandAssignment:undefined,assignmentCount:undefined,_assignments:undefined,_hints:undefined,_assignmentChanged:function(a){a.param.name==="__command"&&(this._assignments={},a.value&&a.value.params.forEach(function(a){this._assignments[a.name]=new o(a,this)},this),this.assignmentCount=Object.keys(this._assignments).length,this._dispatchEvent("commandChange",{command:a.value}))},getAssignment:function(a){var b=typeof a=="string"?a:Object.keys(this._assignments)[a];return this._assignments[b]},getParameterNames:function(){return Object.keys(this._assignments)},cloneAssignments:function(){return Object.keys(this._assignments).map(function(a){return this._assignments[a]},this)},_updateHints:function(){this.getAssignments(!0).forEach(function(a){this._hints.push(a.getHint())},this),l.sort(this._hints)},getWorstHint:function(){return this._hints[0]},getArgsObject:function(){var a={};this.getAssignments().forEach(function(b){a[b.param.name]=b.value},this);return a},getAssignments:function(a){var b=[];a===!0&&b.push(this.commandAssignment),Object.keys(this._assignments).forEach(function(a){b.push(this.getAssignment(a))},this);return b},setDefaultValues:function(){this.getAssignments().forEach(function(a){a.setValue(undefined)},this)},exec:function(){k.exec(this.commandAssignment.value,this.env,"cli",this.getArgsObject(),this.toCanonicalString())},toCanonicalString:function(){var a=[];a.push(this.commandAssignment.value.name),Object.keys(this._assignments).forEach(function(b){var c=this._assignments[b],d=c.param.type;c.value!==c.param.defaultValue&&(a.push(" "),a.push(d.stringify(c.value)))},this);return a.join("")}},f.implement(q.prototype,g),b.Requisition=q,f.inherits(r,q),function(){r.prototype.update=function(a){this.input=a,this._hints=[];var b=this._tokenize(a.typed);this._split(b),this.commandAssignment.value&&this._assign(b),this._updateHints()},r.prototype.getInputStatusMarkup=function(){var a=this.toString().split("").map(function(a){return i.VALID});this._hints.forEach(function(b){for(var c=b.start;c<=b.end;c++)b.status>a[c]&&(a[c]=b.status)},this);return a},r.prototype.toString=function(){return this.getAssignments(!0).map(function(a){return a.toString()},this).join("")};var a=r.prototype._updateHints;r.prototype._updateHints=function(){a.call(this);var b=this.input.cursor;this._hints.forEach(function(a){var c=b.start>=a.start&&b.start<=a.end,d=b.end>=a.start&&b.end<=a.end,e=c||d;!e&&a.status===i.INCOMPLETE&&(a.status=i.INVALID)},this),l.sort(this._hints)},r.prototype.getHints=function(){return this._hints},r.prototype.getAssignmentAt=function(a){var b=this.getAssignments(!0);for(var c=0;c<b.length;c++){var d=b[c];if(!d.arg)return d;if(d.isPositionCaptured(a))return d}return d},r.prototype._tokenize=function(a){function g(a){return a.replace(/\uF000/g," ").replace(/\uF001/g,"'").replace(/\uF002/g,'"')}if(a==null||a.length===0)return[new n(this,"",0,0,"","")];var b=1,c=2,d=3,e=4,f=b;a=a.replace(/\\\\/g,"\\").replace(/\\b/g,"\b").replace(/\\f/g,"\f").replace(/\\n/g,"\n").replace(/\\r/g,"\r").replace(/\\t/g,"\t").replace(/\\v/g," ").replace(/\\n/g,"\n").replace(/\\r/g,"\r").replace(/\\ /g,"").replace(/\\'/g,"").replace(/\\"/g,"");var h=0,i=0,j="",k=[];for(;;){if(h>=a.length){if(f!==b){var l=g(a.substring(i,h));k.push(new n(this,l,i,h,j,""))}else if(h!==i){var m=a.substring(i,h),o=k[k.length-1];o?o.suffix+=m:(o=new n(this,"",h,h,m,""),k.push(o))}break}var p=a[h];switch(f){case b:p==="'"?(j=a.substring(i,h+1),f=d,i=h+1):p==='"'?(j=a.substring(i,h+1),f=e,i=h+1):/ /.test(p)||(j=a.substring(i,h),f=c,i=h);break;case c:if(p===" "){var l=g(a.substring(i,h));k.push(new n(this,l,i,h,j,"")),f=b,i=h,j=""}break;case d:if(p==="'"){var l=g(a.substring(i,h));k.push(new n(this,l,i-1,h+1,j,p)),f=b,i=h+1,j=""}break;case e:if(p==='"'){var l=g(a.substring(i,h));k.push(new n(this,l,i-1,h+1,j,p)),f=b,i=h+1,j=""}}h++}return k},r.prototype._split=function(a){var b=1,c;while(b<=a.length){var c=n.merge(a,0,b);this.commandAssignment.setArgument(c);if(!this.commandAssignment.value)break;if(this.commandAssignment.value.exec){for(var d=0;d<b;d++)a.shift();break}b++}},r.prototype._assign=function(a){if(a.length===0)this.setDefaultValues();else{if(this.assignmentCount===0){this._hints.push(new l(i.INVALID,this.commandAssignment.value.name+" does not take any parameters",n.merge(a)));return}if(this.assignmentCount===1){var b=this.getAssignment(0);if(b.param.type.name==="text"){b.setArgument(n.merge(a));return}}var c=this.cloneAssignments(),d=this.getParameterNames(),f=[];c.forEach(function(b){var c="--"+b.name,f=0;for(;;){var g=a[f];if(c!==g.text){f++;if(f>=a.length)break;continue}b.param.type.name==="boolean"?b.setValue(!0):f+1<a.length?this._hints.push(new l(i.INCOMPLETE,"Missing value for: "+c,a[f])):(a.splice(f+1,1),b.setArgument(a[f+1])),e.arrayRemove(d,b.name),a.splice(f,1)}},this),d.forEach(function(b){var c=this.getAssignment(b);if(a.length===0)c.setValue(undefined);else{var d=a[0];a.splice(0,1),c.setArgument(d)}},this);if(a.length>0){var g=n.merge(a);this._hints.push(new l(i.INVALID,"Input '"+g.text+"' makes no sense.",g))}}}}(),b.CliRequisition=r}),define("cockpit/ui/settings",["require","exports","module","pilot/types","pilot/types/basic"],function(a,b,c){var d=a("pilot/types"),e=a("pilot/types/basic").SelectionType,f=new e({name:"direction",data:["above","below"]}),g={name:"hintDirection",description:"Are hints shown above or below the command line?",type:"direction",defaultValue:"above"},h={name:"outputDirection",description:"Is the output window shown above or below the command line?",type:"direction",defaultValue:"above"},i={name:"outputHeight",description:"What height should the output panel be?",type:"number",defaultValue:300};b.startup=function(a,b){d.registerType(f),a.env.settings.addSetting(g),a.env.settings.addSetting(h),a.env.settings.addSetting(i)},b.shutdown=function(a,b){d.unregisterType(f),a.env.settings.removeSetting(g),a.env.settings.removeSetting(h),a.env.settings.removeSetting(i)}}),define("cockpit/ui/cli_view",["require","exports","module","text!cockpit/ui/cli_view.css","pilot/event","pilot/dom","pilot/keys","pilot/canon","pilot/types","cockpit/cli","cockpit/ui/request_view"],function(a,b,c){function n(a,b){a.cliView=this,this.cli=a,this.doc=document,this.win=f.getParentWindow(this.doc),this.env=b,this.element=this.doc.getElementById("cockpitInput");!this.element||(this.settings=b.settings,this.hintDirection=this.settings.getSetting("hintDirection"),this.outputDirection=this.settings.getSetting("outputDirection"),this.outputHeight=this.settings.getSetting("outputHeight"),this.isUpdating=!1,this.createElements(),this.update())}var d=a("text!cockpit/ui/cli_view.css"),e=a("pilot/event"),f=a("pilot/dom");f.importCssString(d);var e=a("pilot/event"),g=a("pilot/keys"),h=a("pilot/canon"),i=a("pilot/types").Status,j=a("cockpit/cli").CliRequisition,k=a("cockpit/cli").Hint,l=a("cockpit/ui/request_view").RequestView,m=new k(i.VALID,"",0,0);b.startup=function(a,b){var c=new j(a.env),d=new n(c,a.env);a.env.cli=c},n.prototype={createElements:function(){function d(){f.removeCssClass(this.output,"cptFocusPopup"),f.removeCssClass(this.hinter,"cptFocusPopup")}var a=this.element;this.element.spellcheck=!1,this.output=this.doc.getElementById("cockpitOutput"),this.popupOutput=this.output==null;if(!this.output){this.output=this.doc.createElement("div"),this.output.id="cockpitOutput",this.output.className="cptOutput",a.parentNode.insertBefore(this.output,a.nextSibling);var b=function(){this.output.style.maxHeight=this.outputHeight.get()+"px"}.bind(this);this.outputHeight.addEventListener("change",b),b()}this.completer=this.doc.createElement("div"),this.completer.className="cptCompletion VALID",this.completer.style.color=f.computedStyle(a,"color"),this.completer.style.fontSize=f.computedStyle(a,"fontSize"),this.completer.style.fontFamily=f.computedStyle(a,"fontFamily"),this.completer.style.fontWeight=f.computedStyle(a,"fontWeight"),this.completer.style.fontStyle=f.computedStyle(a,"fontStyle"),a.parentNode.insertBefore(this.completer,a.nextSibling),this.completer.style.backgroundColor=a.style.backgroundColor,a.style.backgroundColor="transparent",this.hinter=this.doc.createElement("div"),this.hinter.className="cptHints",a.parentNode.insertBefore(this.hinter,a.nextSibling);var c=this.resizer.bind(this);e.addListener(this.win,"resize",c),this.hintDirection.addEventListener("change",c),this.outputDirection.addEventListener("change",c),c(),h.addEventListener("output",function(a){new l(a.request,this)}.bind(this)),e.addCommandKeyListener(a,this.onCommandKey.bind(this)),e.addListener(a,"keyup",this.onKeyUp.bind(this)),e.addListener(a,"mouseup",function(a){this.isUpdating=!0,this.update(),this.isUpdating=!1}.bind(this)),this.cli.addEventListener("argumentChange",this.onArgChange.bind(this)),e.addListener(a,"focus",function(){f.addCssClass(this.output,"cptFocusPopup"),f.addCssClass(this.hinter,"cptFocusPopup")}.bind(this)),e.addListener(a,"blur",d.bind(this)),d.call(this)},scrollOutputToBottom:function(){var a=Math.max(this.output.scrollHeight,this.output.clientHeight);this.output.scrollTop=a-this.output.clientHeight},resizer:function(){var a=this.element.getClientRects()[0];this.completer.style.top=a.top+"px";var b=a.bottom-a.top;this.completer.style.height=b+"px",this.completer.style.lineHeight=b+"px",this.completer.style.left=a.left+"px";var c=a.right-a.left;this.completer.style.width=c+"px",this.hintDirection.get()==="below"?(this.hinter.style.top=a.bottom+"px",this.hinter.style.bottom="auto"):(this.hinter.style.top="auto",this.hinter.style.bottom=this.doc.documentElement.clientHeight-a.top+"px"),this.hinter.style.left=a.left+30+"px",this.hinter.style.maxWidth=c-110+"px",this.popupOutput&&(this.outputDirection.get()==="below"?(this.output.style.top=a.bottom+"px",this.output.style.bottom="auto"):(this.output.style.top="auto",this.output.style.bottom=this.doc.documentElement.clientHeight-a.top+"px"),this.output.style.left=a.left+"px",this.output.style.width=c-80+"px")},onCommandKey:function(a,b,c){var d;if(c===g.TAB||c===g.UP||c===g.DOWN)d=!0;else if(b!=0||c!=0)d=h.execKeyCommand(this.env,"cli",b,c);d&&e.stopEvent(a)},onKeyUp:function(a){var b;if(a.keyCode===g.RETURN){var c=this.cli.getWorstHint();c.status===i.VALID?(this.cli.exec(),this.element.value=""):(f.setSelectionStart(this.element,c.start),f.setSelectionEnd(this.element,c.end))}this.update();var d=this.cli.getAssignmentAt(f.getSelectionStart(this.element));d&&(a.keyCode===g.TAB&&(d.complete(),this.update()),a.keyCode===g.UP&&(d.increment(),this.update()),a.keyCode===g.DOWN&&(d.decrement(),this.update()));return b},update:function(){this.isUpdating=!0;var a={typed:this.element.value,cursor:{start:f.getSelectionStart(this.element),end:f.getSelectionEnd(this.element.selectionEnd)}};this.cli.update(a);var b=this.cli.getAssignmentAt(a.cursor.start).getHint();f.removeCssClass(this.completer,i.VALID.toString()),f.removeCssClass(this.completer,i.INCOMPLETE.toString()),f.removeCssClass(this.completer,i.INVALID.toString());var c='<span class="cptPrompt">&gt;</span> ';if(this.element.value.length>0){var d=this.cli.getInputStatusMarkup();c+=this.markupStatusScore(d)}if(this.element.value.length>0&&b.predictions&&b.predictions.length>0){var e=b.predictions[0];c+=" &nbsp;&#x21E5; "+(e.name?e.name:e)}this.completer.innerHTML=c,f.addCssClass(this.completer,this.cli.getWorstHint().status.toString());var g="";this.element.value.length!==0&&(g+=b.message,b.predictions&&b.predictions.length>0&&(g+=": [ ",b.predictions.forEach(function(a){g+=a.name?a.name:a,g+=" | "},this),g=g.replace(/\| $/,"]"))),this.hinter.innerHTML=g,g.length===0?f.addCssClass(this.hinter,"cptNoPopup"):f.removeCssClass(this.hinter,"cptNoPopup"),this.isUpdating=!1},markupStatusScore:function(a){var b="",c=0,d=-1;for(;;){d!==a[c]&&(b+="<span class="+a[c].toString()+">",d=a[c]),b+=this.element.value[c],c++;if(c===this.element.value.length){b+="</span>";break}d!==a[c]&&(b+="</span>")}return b},onArgChange:function(a){if(!this.isUpdating){var b=this.element.value.substring(0,a.argument.start),c=this.element.value.substring(a.argument.end),d=typeof a.text=="string"?a.text:a.text.name;this.element.value=b+d+c;var e=(b+d).length;this.element.selectionStart=e,this.element.selectionEnd=e}}},b.CliView=n}),define("cockpit/ui/request_view",["require","exports","module","pilot/dom","pilot/event","text!cockpit/ui/request_view.html","pilot/domtemplate","text!cockpit/ui/request_view.css"],function(a,b,c){function l(a,b){this.request=a,this.cliView=b,this.imageUrl=k,this.rowin=null,this.rowout=null,this.output=null,this.hide=null,this.show=null,this.duration=null,this.throb=null,(new g).processNode(j.cloneNode(!0),this),this.cliView.output.appendChild(this.rowin),this.cliView.output.appendChild(this.rowout),this.request.addEventListener("output",this.onRequestChange.bind(this))}function k(b){var d;try{d=a("text!cockpit/ui/"+b)}catch(e){}if(d)return d;var f=c.id.split("/").pop()+".js",g;if(c.uri.substr(-f.length)!==f){console.error("Can't work out path from module.uri/module.id");return b}if(c.uri){var h=c.uri.length-f.length-1;return c.uri.substr(0,h)+"/"+b}return f+b}var d=a("pilot/dom"),e=a("pilot/event"),f=a("text!cockpit/ui/request_view.html"),g=a("pilot/domtemplate").Templater,h=a("text!cockpit/ui/request_view.css");d.importCssString(h);var i=document.createElement("div");i.innerHTML=f;var j=i.querySelector(".cptRow");l.prototype={copyToInput:function(){this.cliView.element.value=this.request.typed},executeRequest:function(a){this.cliView.cli.update({typed:this.request.typed,cursor:{start:0,end:0}}),this.cliView.cli.exec()},hideOutput:function(a){this.output.style.display="none",d.addCssClass(this.hide,"cmd_hidden"),d.removeCssClass(this.show,"cmd_hidden"),e.stopPropagation(a)},showOutput:function(a){this.output.style.display="block",d.removeCssClass(this.hide,"cmd_hidden"),d.addCssClass(this.show,"cmd_hidden"),e.stopPropagation(a)},remove:function(a){this.cliView.output.removeChild(this.rowin),this.cliView.output.removeChild(this.rowout),e.stopPropagation(a)},onRequestChange:function(a){this.duration.innerHTML=this.request.duration?"completed in "+this.request.duration/1e3+" sec ":"",this.output.innerHTML="",this.request.outputs.forEach(function(a){var b;typeof a=="string"?(b=document.createElement("p"),b.innerHTML=a):b=a,this.output.appendChild(b)},this),this.cliView.scrollOutputToBottom(),d.setCssClass(this.output,"cmd_error",this.request.error),this.throb.style.display=this.request.completed?"none":"block"}},b.RequestView=l}),define("pilot/domtemplate",["require","exports","module"],function(require,exports,module){function Templater(){this.scope=[]}Templater.prototype.processNode=function(a,b){typeof a=="string"&&(a=document.getElementById(a));if(b===null||b===undefined)b={};this.scope.push(a.nodeName+(a.id?"#"+a.id:""));try{if(a.attributes&&a.attributes.length){if(a.hasAttribute("foreach")){this.processForEach(a,b);return}if(a.hasAttribute("if")&&!this.processIf(a,b))return;b.__element=a;var c=Array.prototype.slice.call(a.attributes);for(var d=0;d<c.length;d++){var e=c[d].value,f=c[d].name;this.scope.push(f);try{if(f==="save")e=this.stripBraces(e),this.property(e,b,a),a.removeAttribute("save");else if(f.substring(0,2)==="on"){e=this.stripBraces(e);var g=this.property(e,b);typeof g!="function"&&this.handleError("Expected "+e+" to resolve to a function, but got "+typeof g),a.removeAttribute(f);var h=a.hasAttribute("capture"+f.substring(2));a.addEventListener(f.substring(2),g,h),h&&a.removeAttribute("capture"+f.substring(2))}else{var i=this,j=e.replace(/\$\{[^}]*\}/g,function(a){return i.envEval(a.slice(2,-1),b,e)});f.charAt(0)==="_"?(a.removeAttribute(f),a.setAttribute(f.substring(1),j)):e!==j&&(c[d].value=j)}}finally{this.scope.pop()}}}var k=Array.prototype.slice.call(a.childNodes);for(var l=0;l<k.length;l++)this.processNode(k[l],b);a.nodeType===Node.TEXT_NODE&&this.processTextNode(a,b)}finally{this.scope.pop()}},Templater.prototype.processIf=function(a,b){this.scope.push("if");try{var c=a.getAttribute("if"),d=this.stripBraces(c),e=!0;try{var f=this.envEval(d,b,c);e=!!f}catch(g){this.handleError("Error with '"+d+"'",g),e=!1}e||a.parentNode.removeChild(a),a.removeAttribute("if");return e}finally{this.scope.pop()}},Templater.prototype.processForEach=function(a,b){this.scope.push("foreach");try{var c=a.getAttribute("foreach"),d=c,e="param";if(d.charAt(0)==="$")d=this.stripBraces(d);else{var f=d.split(" in ");e=f[0].trim(),d=this.stripBraces(f[1].trim())}a.removeAttribute("foreach");try{var g=this,h=function(a,c,d){d.parentNode.insertBefore(c,d),b[e]=a,g.processNode(c,b),delete b[e]},i=function(b,c){g.scope.push(b);try{if(a.nodeName==="LOOP")for(var d=0;d<a.childNodes.length;d++){var e=a.childNodes[d].cloneNode(!0);h(c,e,a)}else{var e=a.cloneNode(!0);e.removeAttribute("foreach"),h(c,e,a)}}finally{g.scope.pop()}},j=this.envEval(d,b,c);if(Array.isArray(j))j.forEach(function(a,b){i(""+b,a)},this);else for(var k in j)j.hasOwnProperty(k)&&i(k,k);a.parentNode.removeChild(a)}catch(l){this.handleError("Error with '"+d+"'",l)}}finally{this.scope.pop()}},Templater.prototype.processTextNode=function(a,b){var c=a.data;c=c.replace(/\$\{([^}]*)\}/g,"$$$1");var d=c.split(/\uF001|\uF002/);d.length>1&&(d.forEach(function(c){c!==null&&c!==undefined&&c!==""&&(c.charAt(0)==="$"&&(c=this.envEval(c.slice(1),b,a.data)),c===null&&(c="null"),c===undefined&&(c="undefined"),typeof c.cloneNode!="function"&&(c=a.ownerDocument.createTextNode(c.toString())),a.parentNode.insertBefore(c,a))},this),a.parentNode.removeChild(a))},Templater.prototype.stripBraces=function(a){if(!a.match(/\$\{.*\}/g)){this.handleError("Expected "+a+" to match ${...}");return a}return a.slice(2,-1)},Templater.prototype.property=function(a,b,c){this.scope.push(a);try{typeof a=="string"&&(a=a.split("."));var d=b[a[0]];if(a.length===1){c!==undefined&&(b[a[0]]=c);if(typeof d=="function")return function(){return d.apply(b,arguments)};return d}if(!d){this.handleError("Can't find path="+a);return null}return this.property(a.slice(1),d,c)}finally{this.scope.pop()}},Templater.prototype.envEval=function(script,env,context){with(env)try{this.scope.push(context);return eval(script)}catch(ex){this.handleError("Template error evaluating '"+script+"'",ex);return script}finally{this.scope.pop()}},Templater.prototype.handleError=function(a,b){this.logError(a),this.logError("In: "+this.scope.join(" > ")),b&&this.logError(b)},Templater.prototype.logError=function(a){window.console&&window.console.log&&console.log(a)},exports.Templater=Templater}),define("cockpit/commands/basic",["require","exports","module","pilot/canon"],function(a,b,c){var d=a("pilot/canon"),e={name:"sh",description:"Execute a system command (requires server support)",params:[{name:"command",type:"text",description:"The string to send to the os shell."}],exec:function(a,b,c){var d=new XMLHttpRequest;d.open("GET","/exec?args="+b.command,!0),d.onreadystatechange=function(a){d.readyState==4&&d.status==200&&c.done("<pre>"+d.responseText+"</pre>")},d.send(null)}},d=a("pilot/canon");b.startup=function(a,b){d.addCommand(e)},b.shutdown=function(a,b){d.removeCommand(e)}}),define("text!cockpit/ui/cli_view.css",[],"#cockpitInput { padding-left: 16px; }.cptOutput { overflow: auto; position: absolute; z-index: 999; display: none; }.cptCompletion { padding: 0; position: absolute; z-index: -1000; }.cptCompletion.VALID { background: #FFF; }.cptCompletion.INCOMPLETE { background: #DDD; }.cptCompletion.INVALID { background: #DDD; }.cptCompletion span { color: #FFF; }.cptCompletion span.INCOMPLETE { color: #DDD; border-bottom: 2px dotted #F80; }.cptCompletion span.INVALID { color: #DDD; border-bottom: 2px dotted #F00; }span.cptPrompt { color: #66F; font-weight: bold; }.cptHints { color: #000; position: absolute; border: 1px solid rgba(230, 230, 230, 0.8); background: rgba(250, 250, 250, 0.8); -moz-border-radius-topleft: 10px; -moz-border-radius-topright: 10px; border-top-left-radius: 10px; border-top-right-radius: 10px; z-index: 1000; padding: 8px; display: none;}.cptFocusPopup { display: block; }.cptFocusPopup.cptNoPopup { display: none; }.cptHints ul { margin: 0; padding: 0 15px; }.cptGt { font-weight: bold; font-size: 120%; }"),define("text!cockpit/ui/request_view.css",[],".cptRowIn { display: box; display: -moz-box; display: -webkit-box; box-orient: horizontal; -moz-box-orient: horizontal; -webkit-box-orient: horizontal; box-align: center; -moz-box-align: center; -webkit-box-align: center; color: #333; background-color: #EEE; width: 100%; font-family: consolas, courier, monospace;}.cptRowIn > * { padding-left: 2px; padding-right: 2px; }.cptRowIn > img { cursor: pointer; }.cptHover { display: none; }.cptRowIn:hover > .cptHover { display: block; }.cptRowIn:hover > .cptHover.cptHidden { display: none; }.cptOutTyped { box-flex: 1; -moz-box-flex: 1; -webkit-box-flex: 1; font-weight: bold; color: #000; font-size: 120%;}.cptRowOutput { padding-left: 10px; line-height: 1.2em; }.cptRowOutput strong,.cptRowOutput b,.cptRowOutput th,.cptRowOutput h1,.cptRowOutput h2,.cptRowOutput h3 { color: #000; }.cptRowOutput a { font-weight: bold; color: #666; text-decoration: none; }.cptRowOutput a: hover { text-decoration: underline; cursor: pointer; }.cptRowOutput input[type=password],.cptRowOutput input[type=text],.cptRowOutput textarea { color: #000; font-size: 120%; background: transparent; padding: 3px; border-radius: 5px; -moz-border-radius: 5px; -webkit-border-radius: 5px;}.cptRowOutput table,.cptRowOutput td,.cptRowOutput th { border: 0; padding: 0 2px; }.cptRowOutput .right { text-align: right; }"),define("text!cockpit/ui/request_view.html",[],'<div class=cptRow> <!-- The div for the input (i.e. what was typed) --> <div class="cptRowIn" save="${rowin}" onclick="${copyToInput}" ondblclick="${executeRequest}"> <!-- What the user actually typed --> <div class="cptGt">&gt; </div> <div class="cptOutTyped">${request.typed}</div> <!-- The extra details that appear on hover --> <div class=cptHover save="${duration}"></div> <img class=cptHover onclick="${hideOutput}" save="${hide}" alt="Hide command output" _src="${imageUrl(\'images/minus.png\')}"/> <img class="cptHover cptHidden" onclick="${showOutput}" save="${show}" alt="Show command output" _src="${imageUrl(\'images/plus.png\')}"/> <img class=cptHover onclick="${remove}" alt="Remove this command from the history" _src="${imageUrl(\'images/closer.png\')}"/> </div> <!-- The div for the command output --> <div class="cptRowOut" save="${rowout}"> <div class="cptRowOutput" save="${output}"></div> <img _src="${imageUrl(\'images/throbber.gif\')}" save="${throb}"/> </div></div>'),define("text!cockpit/ui/images/pinaction.png",[],"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC7mlDQ1BJQ0MgUHJvZmlsZQAAeAGFVM9rE0EU/jZuqdAiCFprDrJ4kCJJWatoRdQ2/RFiawzbH7ZFkGQzSdZuNuvuJrWliOTi0SreRe2hB/+AHnrwZC9KhVpFKN6rKGKhFy3xzW5MtqXqwM5+8943731vdt8ADXLSNPWABOQNx1KiEWlsfEJq/IgAjqIJQTQlVdvsTiQGQYNz+Xvn2HoPgVtWw3v7d7J3rZrStpoHhP1A4Eea2Sqw7xdxClkSAog836Epx3QI3+PY8uyPOU55eMG1Dys9xFkifEA1Lc5/TbhTzSXTQINIOJT1cVI+nNeLlNcdB2luZsbIEL1PkKa7zO6rYqGcTvYOkL2d9H5Os94+wiHCCxmtP0a4jZ71jNU/4mHhpObEhj0cGDX0+GAVtxqp+DXCFF8QTSeiVHHZLg3xmK79VvJKgnCQOMpkYYBzWkhP10xu+LqHBX0m1xOv4ndWUeF5jxNn3tTd70XaAq8wDh0MGgyaDUhQEEUEYZiwUECGPBoxNLJyPyOrBhuTezJ1JGq7dGJEsUF7Ntw9t1Gk3Tz+KCJxlEO1CJL8Qf4qr8lP5Xn5y1yw2Fb3lK2bmrry4DvF5Zm5Gh7X08jjc01efJXUdpNXR5aseXq8muwaP+xXlzHmgjWPxHOw+/EtX5XMlymMFMXjVfPqS4R1WjE3359sfzs94i7PLrXWc62JizdWm5dn/WpI++6qvJPmVflPXvXx/GfNxGPiKTEmdornIYmXxS7xkthLqwviYG3HCJ2VhinSbZH6JNVgYJq89S9dP1t4vUZ/DPVRlBnM0lSJ93/CKmQ0nbkOb/qP28f8F+T3iuefKAIvbODImbptU3HvEKFlpW5zrgIXv9F98LZua6N+OPwEWDyrFq1SNZ8gvAEcdod6HugpmNOWls05Uocsn5O66cpiUsxQ20NSUtcl12VLFrOZVWLpdtiZ0x1uHKE5QvfEp0plk/qv8RGw/bBS+fmsUtl+ThrWgZf6b8C8/UXAeIuJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAClklEQVQ4EX1TXUhUQRQ+Z3Zmd+9uN1q2P3UpZaEwcikKekkqLKggKHJ96MHe9DmLkCDa9U198Id8kErICmIlRAN96UdE6QdBW/tBA5Uic7E0zN297L17p5mb1zYjD3eYc+d83zlnON8g5xzWNUSEdUBkHTJasRWySPP7fw3hfwkk2GoNsc0vOaJRHo1GV/GiMctkTIJRFlpZli8opK+htmf83gXeG63oteOtra0u25e7TYJIJELb26vYCACTgUe1lXV86BTn745l+MsyHqs53S/Aq4VEUa9Y6ko14eYY4u3AyM3HYwdKU35DZyblGR2+qq6W0X2Nnh07xynnVYpHORx/E1/GvvqaAZUayjMjdM2f/Lgr5E+fV93zR4u3zKCLughsZqKwAzAxaz6dPY6JgjLUF+eSP5OpjmAw2E8DvldHSvJMKPg08aRor1tc4BuALu6mOwGWdQC3mKIqRsC8mKd8wYfD78/earzSYzdMDW9QgKb0Is8CBY1mQXOiaXAHEpMDE5XTJqIq4EiyxUqKlpfkF0pyV1OTAoFAhmTmyCCoDsZNZvIkUjELQpipo0sQqYZAswZHwsEEE10M0pq2SSZY9HqNcDicJcNTpBvQJz40UbSOTh1B8bDpuY0w9Hb3kkn9lPAlBLfhfD39XTtX/blFJqiqrjbkTi63Hbofj2uL4GMsmzFgbDJ/vmMgv/lB4syJ0oXO7d3j++vio6GFsYmD6cHJreWc3/jRVVHhsOYvM8iZ36mtjPDBk/xDZE8CoHlbrlAssbTxDdDJvdb536L7I6S7Vy++6Gi4Xi9BsUthJRaLOYSPz4XALKI4j4iObd/e5UtDKUjZzYyYRyGAJv01Zj8kC5cbs5WY83hQnv0DzCXl+r8APElkq0RU6oMAAAAASUVORK5CYII="),define("text!cockpit/ui/images/throbber.gif",[],"data:image/gif;base64,R0lGODlh3AATAPQAAP///wAAAL6+vqamppycnLi4uLKyssjIyNjY2MTExNTU1Nzc3ODg4OTk5LCwsLy8vOjo6Ozs7MrKyvLy8vT09M7Ozvb29sbGxtDQ0O7u7tbW1sLCwqqqqvj4+KCgoJaWliH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAA3AATAAAF/yAgjmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgECAaEpHLJbDqf0Kh0Sq1ar9isdjoQtAQFg8PwKIMHnLF63N2438f0mv1I2O8buXjvaOPtaHx7fn96goR4hmuId4qDdX95c4+RG4GCBoyAjpmQhZN0YGYFXitdZBIVGAoKoq4CG6Qaswi1CBtkcG6ytrYJubq8vbfAcMK9v7q7D8O1ycrHvsW6zcTKsczNz8HZw9vG3cjTsMIYqQgDLAQGCQoLDA0QCwUHqfYSFw/xEPz88/X38Onr14+Bp4ADCco7eC8hQYMAEe57yNCew4IVBU7EGNDiRn8Z831cGLHhSIgdE/9chIeBgDoB7gjaWUWTlYAFE3LqzDCTlc9WOHfm7PkTqNCh54rePDqB6M+lR536hCpUqs2gVZM+xbrTqtGoWqdy1emValeXKwgcWABB5y1acFNZmEvXwoJ2cGfJrTv3bl69Ffj2xZt3L1+/fw3XRVw4sGDGcR0fJhxZsF3KtBTThZxZ8mLMgC3fRatCLYMIFCzwLEprg84OsDus/tvqdezZf13Hvr2B9Szdu2X3pg18N+68xXn7rh1c+PLksI/Dhe6cuO3ow3NfV92bdArTqC2Ebc3A8vjf5QWf15Bg7Nz17c2fj69+fnq+8N2Lty+fuP78/eV2X13neIcCeBRwxorbZrAxAJoCDHbgoG8RTshahQ9iSKEEzUmYIYfNWViUhheCGJyIP5E4oom7WWjgCeBBAJNv1DVV01MZdJhhjdkplWNzO/5oXI846njjVEIqR2OS2B1pE5PVscajkxhMycqLJgxQCwT40PjfAV4GqNSXYdZXJn5gSkmmmmJu1aZYb14V51do+pTOCmA00AqVB4hG5IJ9PvYnhIFOxmdqhpaI6GeHCtpooisuutmg+Eg62KOMKuqoTaXgicQWoIYq6qiklmoqFV0UoeqqrLbq6quwxirrrLTWauutJ4QAACH5BAkKAAAALAAAAADcABMAAAX/ICCOZGmeaKqubOu+cCzPdG3feK7vfO//wKBwSAQIBoSkcslsOp/QqHRKrVqv2Kx2OhC0BAXHx/EoCzboAcdhcLDdgwJ6nua03YZ8PMFPoBMca215eg98G36IgYNvDgOGh4lqjHd7fXOTjYV9nItvhJaIfYF4jXuIf4CCbHmOBZySdoOtj5eja59wBmYFXitdHhwSFRgKxhobBgUPAmdoyxoI0tPJaM5+u9PaCQZzZ9gP2tPcdM7L4tLVznPn6OQb18nh6NV0fu3i5OvP8/nd1qjwaasHcIPAcf/gBSyAAMMwBANYEAhWYQGDBhAyLihwYJiEjx8fYMxIcsGDAxVA/yYIOZIkBAaGPIK8INJlRpgrPeasaRPmx5QgJfB0abLjz50tSeIM+pFmUo0nQQIV+vRlTJUSnNq0KlXCSq09ozIFexEBAYkeNiwgOaEtn2LFpGEQsKCtXbcSjOmVlqDuhAx3+eg1Jo3u37sZBA9GoMAw4MB5FyMwfLht4sh7G/utPGHlYAV8Nz9OnOBz4c2VFWem/Pivar0aKCP2LFn2XwhnVxBwsPbuBAQbEGiIFg1BggoWkidva5z4cL7IlStfkED48OIYoiufYIH68+cKPkqfnsB58ePjmZd3Dj199/XE20tv6/27XO3S6z9nPCz9BP3FISDefL/Bt192/uWmAv8BFzAQAQUWWFaaBgqA11hbHWTIXWIVXifNhRlq6FqF1sm1QQYhdiAhbNEYc2KKK1pXnAIvhrjhBh0KxxiINlqQAY4UXjdcjSJyeAx2G2BYJJD7NZQkjCPKuCORKnbAIXsuKhlhBxEomAIBBzgIYXIfHfmhAAyMR2ZkHk62gJoWlNlhi33ZJZ2cQiKTJoG05Wjcm3xith9dcOK5X51tLRenoHTuud2iMnaolp3KGXrdBo7eKYF5p/mXgJcogClmcgzAR5gCKymXYqlCgmacdhp2UCqL96mq4nuDBTmgBasaCFp4sHaQHHUsGvNRiiGyep1exyIra2mS7dprrtA5++z/Z8ZKYGuGsy6GqgTIDvupRGE+6CO0x3xI5Y2mOTkBjD4ySeGU79o44mcaSEClhglgsKyJ9S5ZTGY0Bnzrj+3SiKK9Rh5zjAALCywZBk/ayCWO3hYM5Y8Dn6qxxRFsgAGoJwwgDQRtYXAAragyQOmaLKNZKGaEuUlpyiub+ad/KtPqpntypvvnzR30DBtjMhNodK6Eqrl0zU0/GjTUgG43wdN6Ra2pAhGtAAZGE5Ta8TH6wknd2IytNKaiZ+Or79oR/tcvthIcAPe7DGAs9Edwk6r3qWoTaNzY2fb9HuHh2S343Hs1VIHhYtOt+Hh551rh24vP5YvXSGzh+eeghy76GuikU9FFEainrvrqrLfu+uuwxy777LTXfkIIACH5BAkKAAAALAAAAADcABMAAAX/ICCOZGmeaKqubOu+cCzPdG3feK7vfO//wKBwSAQIBoSkcslsOp/QqHRKrVqv2Kx2OhC0BAWHB2l4CDZo9IDjcBja7UEhTV+3DXi3PJFA8xMcbHiDBgMPG31pgHBvg4Z9iYiBjYx7kWocb26OD398mI2EhoiegJlud4UFiZ5sm6Kdn2mBr5t7pJ9rlG0cHg5gXitdaxwFGArIGgoaGwYCZ3QFDwjU1AoIzdCQzdPV1c0bZ9vS3tUJBmjQaGXl1OB0feze1+faiBvk8wjnimn55e/o4OtWjp+4NPIKogsXjaA3g/fiGZBQAcEAFgQGOChgYEEDCCBBLihwQILJkxIe/3wMKfJBSQkJYJpUyRIkgwcVUJq8QLPmTYoyY6ZcyfJmTp08iYZc8MBkhZgxk9aEcPOlzp5FmwI9KdWn1qASurJkClRoWKwhq6IUqpJBAwQEMBYroAHkhLt3+RyzhgCDgAV48Wbgg+waAnoLMgTOm6DwQ8CLBzdGdvjw38V5JTg2lzhyTMeUEwBWHPgzZc4TSOM1bZia6LuqJxCmnOxv7NSsl1mGHHiw5tOuIWeAEHcFATwJME/ApgFBc3MVLEgPvE+Ddb4JokufPmFBAuvPXWu3MIF89wTOmxvOvp179evQtwf2nr6aApPyzVd3jn089e/8xdfeXe/xdZ9/d1ngHf98lbHH3V0LMrgPgsWpcFwBEFBgHmyNXWeYAgLc1UF5sG2wTHjIhNjBiIKZCN81GGyQwYq9uajeMiBOQGOLJ1KjTI40kmfBYNfc2NcGIpI4pI0vyrhjiT1WFqOOLEIZnjVOVpmajYfBiCSNLGbA5YdOkjdihSkQwIEEEWg4nQUmvYhYe+bFKaFodN5lp3rKvJYfnBKAJ+gGDMi3mmbwWYfng7IheuWihu5p32XcSWdSj+stkF95dp64jJ+RBipocHkCCp6PCiRQ6INookCAAwy0yd2CtNET3Yo7RvihBjFZAOaKDHT43DL4BQnsZMo8xx6uI1oQrHXXhHZrB28G62n/YSYxi+uzP2IrgbbHbiaer7hCiOxDFWhrbmGnLVuus5NFexhFuHLX6gkEECorlLpZo0CWJG4pLjIACykmBsp0eSSVeC15TDJeUhlkowlL+SWLNJpW2WEF87urXzNWSZ6JOEb7b8g1brZMjCg3ezBtWKKc4MvyEtwybPeaMAA1ECRoAQYHYLpbeYYCLfQ+mtL5c9CnfQpYpUtHOSejEgT9ogZ/GSqd0f2m+LR5WzOtHqlQX1pYwpC+WbXKqSYtpJ5Mt4a01lGzS3akF60AxkcTaLgAyRBPWCoDgHfJqwRuBuzdw/1ml3iCwTIeLUWJN0v4McMe7uasCTxseNWPSxc5RbvIgD7geZLbGrqCG3jepUmbbze63Y6fvjiOylbwOITPfIHEFsAHL/zwxBdvPBVdFKH88sw37/zz0Ecv/fTUV2/99SeEAAAh+QQJCgAAACwAAAAA3AATAAAF/yAgjmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgECAaEpHLJbDqf0Kh0Sq1ar9isdjoQtAQFh2cw8BQEm3T6yHEYHHD4oKCuD9qGvNsxT6QTgAkcHHmFeX11fm17hXwPG35qgnhxbwMPkXaLhgZ9gWp3bpyegX4DcG+inY+Qn6eclpiZkHh6epetgLSUcBxlD2csXXdvBQrHGgoaGhsGaIkFDwjTCArTzX+QadHU3c1ofpHc3dcGG89/4+TYktvS1NYI7OHu3fEJ5tpqBu/k+HX7+nXDB06SuoHm0KXhR65cQT8P3FRAMIAFgVMPwDCAwLHjggIHJIgceeFBg44eC/+ITCCBZYKSJ1FCWPBgpE2YMmc+qNCypwScMmnaXAkUJYOaFVyKLOqx5tCXJnMelcBzJNSYKIX2ZPkzqsyjPLku9Zr1QciVErYxaICAgEUOBRJIgzChbt0MLOPFwyBggV27eCUcmxZvg9+/dfPGo5bg8N/Ag61ZM4w4seDF1fpWhizZmoa+GSortgcaMWd/fkP/HY0MgWbTipVV++wY8GhvqSG4XUEgoYTKE+Qh0OCvggULiBckWEZ4Ggbjx5HXVc58IPQJ0idQJ66XanTpFraTe348+XLizRNcz658eHMN3rNPT+C+G/nodqk3t6a+fN3j+u0Xn3nVTQPfdRPspkL/b+dEIN8EeMm2GAYbTNABdrbJ1hyFFv5lQYTodSZABhc+loCEyhxTYYkZopdMMiNeiBxyIFajV4wYHpfBBspUl8yKHu6ooV5APsZjQxyyeNeJ3N1IYod38cgdPBUid6GCKfRWgAYU4IccSyHew8B3doGJHmMLkGkZcynKk2Z50Ym0zJzLbDCmfBbI6eIyCdyJmJmoqZmnBAXy9+Z/yOlZDZpwYihnj7IZpuYEevrYJ5mJEuqiof4l+NYDEXQpXQcMnNjZNDx1oGqJ4S2nF3EsqWrhqqVWl6JIslpAK5MaIqDeqjJq56qN1aTaQaPbHTPYr8Be6Gsyyh6Da7OkmmqP/7GyztdrNVQBm5+pgw3X7aoYKhfZosb6hyUKBHCgQKij1rghkOAJuZg1SeYIIY+nIpDvf/sqm4yNG5CY64f87qdAwSXKGqFkhPH1ZHb2EgYtw3bpKGVkPz5pJAav+gukjB1UHE/HLNJobWcSX8jiuicMMBFd2OmKwQFs2tjXpDfnPE1j30V3c7iRHlrzBD2HONzODyZtsQJMI4r0AUNaE3XNHQw95c9GC001MpIxDacFQ+ulTNTZlU3O1eWVHa6vb/pnQUUrgHHSBKIuwG+bCPyEqbAg25gMVV1iOB/IGh5YOKLKIQ6xBAcUHmzjIcIqgajZ+Ro42DcvXl7j0U4WOUd+2IGu7DWjI1pt4DYq8BPm0entuGSQY/4tBi9Ss0HqfwngBQtHbCH88MQXb/zxyFfRRRHMN+/889BHL/301Fdv/fXYZ39CCAAh+QQJCgAAACwAAAAA3AATAAAF/yAgjmRpnmiqrmzrvnAsz3Rt33iu73zv/8CgcEgECAaEpHLJbDqf0Kh0Sq1ar9isdjoQtAQFh2fAKXsKm7R6Q+Y43vABep0mGwwOPH7w2CT+gHZ3d3lyagl+CQNvg4yGh36LcHoGfHR/ZYOElQ9/a4ocmoRygIiRk5p8pYmZjXePaYBujHoOqp5qZHBlHAUFXitddg8PBg8KGsgayxvGkAkFDwgICtPTzX2mftHW3QnOpojG3dbYkNjk1waxsdDS1N7ga9zw1t/aifTk35fu6Qj3numL14fOuHTNECHqU4DDgQEsCCwidiHBAwYQMmpcUOCAhI8gJVzUuLGThAQnP/9abEAyI4MCIVOKZNnyJUqUJxNcGNlywYOQgHZirGkSJ8gHNEky+AkS58qWEJYC/bMzacmbQHkqNdlUJ1KoSz2i9COhmQYCEXtVrCBgwYS3cCf8qTcNQ9u4cFFOq2bPLV65Cf7dxZthbjW+CgbjnWtNgWPFcAsHdoxgWWK/iyV045sAc2S96SDn1exYw17REwpLQEYt2eW/qtPZRQAB7QoC61RW+GsBwYZ/CXb/XRCYLsAKFizEtUAc+G7lcZsjroscOvTmsoUvx15PwccJ0N8yL17N9PG/E7jv9S4hOV7pdIPDdZ+ePDzv2qMXn2b5+wTbKuAWnF3oZbABZY0lVmD/ApQd9thybxno2GGuCVDggaUpoyBsB1bGGgIYbJCBcuFJiOAyGohIInQSmmdeiBnMF2GHfNUlIoc1rncjYRjW6NgGf3VQGILWwNjBfxEZcAFbC7gHXQcfUYOYdwzQNxo5yUhQZXhvRYlMeVSuSOJHKJa5AQMQThBlZWZ6Bp4Fa1qzTAJbijcBlJrtxeaZ4lnnpZwpukWieGQmYx5ATXIplwTL8DdNZ07CtWYybNIJF4Ap4NZHe0920AEDk035kafieQrqXofK5ympn5JHKYjPrfoWcR8WWQGp4Ul32KPVgXdnqxM6OKqspjIYrGPDrlrsZtRIcOuR86nHFwbPvmes/6PH4frrqbvySh+mKGhaAARPzjjdhCramdoGGOhp44i+zogBkSDuWC5KlE4r4pHJkarXrj++Raq5iLmWLlxHBteavjG+6amJrUkJJI4Ro5sBv9AaOK+jAau77sbH7nspCwNIYIACffL7J4JtWQnen421nNzMcB6AqpRa9klonmBSiR4GNi+cJZpvwgX0ejj71W9yR+eIgaVvQgf0l/A8nWjUFhwtZYWC4hVnkZ3p/PJqNQ5NnwUQrQCGBBBMQIGTtL7abK+5JjAv1fi9bS0GLlJHgdjEgYzzARTwC1fgEWdJuKKBZzj331Y23qB3i9v5aY/rSUC4w7PaLeWXmr9NszMFoN79eeiM232o33EJAIzaSGwh++y012777bhT0UURvPfu++/ABy/88MQXb/zxyCd/QggAIfkECQoAAAAsAAAAANwAEwAABf8gII5kaZ5oqq5s675wLM90bd94ru987//AoHBIBAgGhKRyyWw6n9CodEqtWq/YrHY6ELQEBY5nwCk7xIWNer0hO95wziC9Ttg5b4ND/+Y87IBqZAaEe29zGwmJigmDfHoGiImTjXiQhJEPdYyWhXwDmpuVmHwOoHZqjI6kZ3+MqhyemJKAdo6Ge3OKbEd4ZRwFBV4rc4MPrgYPChrMzAgbyZSJBcoI1tfQoYsJydfe2amT3d7W0OGp1OTl0YtqyQrq0Lt11PDk3KGoG+nxBpvTD9QhwCctm0BzbOyMIwdOUwEDEgawIOCB2oMLgB4wgMCx44IHBySIHClBY0ePfyT/JCB5weRJCAwejFw58kGDlzBTqqTZcuPLmCIBiWx58+VHmiRLFj0JVCVLl0xl7qSZwCbOo0lFWv0pdefQrVFDJtr5gMBEYBgxqBWwYILbtxPsqMPAFu7blfa81bUbN4HAvXAzyLWnoDBguHIRFF6m4LBbwQngMYPXuC3fldbyPrMcGLM3w5wRS1iWWUNlvnElKDZtz/EEwaqvYahQoexEfyILi4RrYYKFZwJ3810QWZ2ECrx9Ew+O3K6F5Yq9zXbb+y30a7olJJ+wnLC16W97Py+uwdtx1NcLWzs/3G9e07stVPc9kHJ0BcLtQp+c3ewKAgYkUAFpCaAmmHqKLSYA/18WHEiZPRhsQF1nlLFWmIR8ZbDBYs0YZuCGpGXWmG92aWiPMwhEOOEEHXRwIALlwXjhio+BeE15IzpnInaLbZBBhhti9x2GbnVQo2Y9ZuCfCgBeMCB+DJDIolt4iVhOaNSJdCOBUfIlkmkyMpPAAvKJ59aXzTQzJo0WoJnmQF36Jp6W1qC4gWW9GZladCiyJd+KnsHImgRRVjfnaDEKuiZvbcYWo5htzefbl5LFWNeSKQAo1QXasdhiiwwUl2B21H3aQaghXnPcp1NagCqYslXAqnV+zYWcpNwVp9l5eepJnHqL4SdBi56CGlmw2Zn6aaiZjZqfb8Y2m+Cz1O0n3f+tnvrGbF6kToApCgAWoNWPeh754JA0vmajiAr4iOuOW7abQXVGNriBWoRdOK8FxNqLwX3oluubhv8yluRbegqGb536ykesuoXhyJqPQJIGbLvQhkcwjKs1zBvBwSZIsbcsDCCBAAf4ya+UEhyQoIiEJtfoZ7oxUOafE2BwgMWMqUydfC1LVtiArk0QtGkWEopzlqM9aJrKHfw5c6wKjFkmXDrbhwFockodtMGFLWpXy9JdiXN1ZDNszV4WSLQCGBKoQYHUyonqrHa4ErewAgMmcAAF7f2baIoVzC2p3gUvJtLcvIWqloy6/R04mIpLwDhciI8qLOB5yud44pHPLbA83hFDWPjNbuk9KnySN57Av+TMBvgEAgzzNhJb5K777rz37vvvVHRRxPDEF2/88cgnr/zyzDfv/PPQnxACACH5BAkKAAAALAAAAADcABMAAAX/ICCOZGmeaKqubOu+cCzPdG3feK7vfO//wKBwSAQIBoSkcslsOp/QqHRKrVqv2Kx2OhC0BIUCwcMpO84OT2HDbm8GHLQjnn6wE3g83SA3DB55G3llfHxnfnZ4gglvew6Gf4ySgmYGlpCJknochWiId3kJcZZyDn93i6KPl4eniopwq6SIoZKxhpenbhtHZRxhXisDopwPgHkGDxrLGgjLG8mC0gkFDwjX2AgJ0bXJ2djbgNJsAtbfCNB2oOnn6MmKbeXt226K1fMGi6j359D69ua+QZskjd+3cOvY9XNgp4ABCQNYEDBl7EIeCQkeMIDAseOCBwckiBSZ4ILGjh4B/40kaXIjSggMHmBcifHky5gYE6zM2OAlzGM6Z5rs+fIjTZ0tfcYMSlLCUJ8fL47kCVXmTjwPiKJkUCDnyqc3CxzQmYeAxAEGLGJYiwCDgAUT4sqdgOebArdw507IUNfuW71xdZ7DC5iuhGsKErf9CxhPYgUaEhPWyzfBMgUIJDPW6zhb5M1y+R5GjFkBaLmCM0dOfHqvztXYJnMejaFCBQlmVxAYsEGkYnQV4lqYMNyCtnYSggNekAC58uJxmTufW5w55mwKkg+nLp105uTC53a/nhg88fMTmDfDVl65Xum/IZt/3/zaag3a5W63nll1dvfiWbaaZLmpQIABCVQA2f9lAhTG112PQWYadXE9+FtmEwKWwQYQJrZagxomsOCAGVImInsSbpCBhhwug6KKcXXQQYUcYuDMggrASFmNzjjzzIrh7cUhhhHqONeGpSEW2QYxHsmjhxpgUGAKB16g4IIbMNCkXMlhaJ8GWVJo2I3NyKclYF1GxgyYDEAnXHJrMpNAm/rFBSczPiYAlwXF8ZnmesvoOdyMbx7m4o0S5LWdn4bex2Z4xYmEzaEb5EUcnxbA+WWglqIn6aHPTInCgVbdlZyMqMrIQHMRSiaBBakS1903p04w434n0loBoQFOt1yu2YAnY68RXiNsqh2s2qqxuyKb7Imtmgcrqsp6h8D/fMSpapldx55nwayK/SfqCQd2hcFdAgDp5GMvqhvakF4mZuS710WGIYy30khekRkMu92GNu6bo7r/ttjqwLaua5+HOdrKq5Cl3dcwi+xKiLBwwwom4b0E6xvuYyqOa8IAEghwQAV45VvovpkxBl2mo0W7AKbCZXoAhgMmWnOkEqx2JX5nUufbgJHpXCfMOGu2QAd8eitpW1eaNrNeMGN27mNz0swziYnpSbXN19gYtstzfXrdYjNHtAIYGFVwwAEvR1dfxdjKxVzAP0twAAW/ir2w3nzTd3W4yQWO3t0DfleB4XYnEHCEhffdKgaA29p0eo4fHLng9qoG+OVyXz0gMeWGY7qq3xhiRIEAwayNxBawxy777LTXbjsVXRSh++689+7778AHL/zwxBdv/PEnhAAAIfkECQoAAAAsAAAAANwAEwAABf8gII5kaZ5oqq5s675wLM90bd94ru987//AoHBIBAgGhKRyyWw6n9CodEqtWq/YrHY6ELQEhYLD4BlwHGg0ubBpuzdm9Dk9eCTu+MTZkDb4PXYbeIIcHHxqf4F3gnqGY2kOdQmCjHCGfpCSjHhmh2N+knmEkJmKg3uHfgaaeY2qn6t2i4t7sKAPbwIJD2VhXisDCQZgDrKDBQ8aGgjKyhvDlJMJyAjV1gjCunkP1NfVwpRtk93e2ZVt5NfCk27jD97f0LPP7/Dr4pTp1veLgvrx7AL+Q/BM25uBegoYkDCABYFhEobhkUBRwoMGEDJqXPDgQMUEFC9c1LjxQUUJICX/iMRIEgIDkycrjmzJMSXFlDNJvkwJsmdOjQwKfDz5M+PLoSGLQqgZU6XSoB/voHxawGbFlS2XGktAwKEADB0xiEWAodqGBRPSqp1wx5qCamDRrp2Qoa3bagLkzrULF4GCvHPTglRAmKxZvWsHayBcliDitHUlvGWM97FgCdYWVw4c2e/kw4HZJlCwmDBhwHPrjraGYTHqtaoxVKggoesKAgd2SX5rbUMFCxOAC8cGDwHFwBYWJCgu4XfwtcqZV0grPHj0u2SnqwU+IXph3rK5b1fOu7Bx5+K7L6/2/Xhg8uyXnQ8dvfRiDe7TwyfNuzlybKYpgIFtKhAgwEKkKcOf/wChZbBBgMucRh1so5XH3wbI1WXafRJy9iCErmX4IWHNaIAhZ6uxBxeGHXQA24P3yYfBBhmgSBozESpwongWOBhggn/N1aKG8a1YY2oVAklgCgQUUwGJ8iXAgItrWUARbwpqIOWEal0ZoYJbzmWlZCWSlsAC6VkwZonNbMAAl5cpg+NiZwpnJ0Xylegmlc+tWY1mjnGnZnB4QukMA9UJRxGOf5r4ppqDjjmnfKilh2ejGiyJAgF1XNmYbC2GmhZ5AcJVgajcXecNqM9Rx8B6bingnlotviqdkB3YCg+rtOaapFsUhSrsq6axJ6sEwoZK7I/HWpCsr57FBxJ1w8LqV/81zbkoXK3LfVeNpic0KRQG4NHoIW/XEmZuaiN6tti62/moWbk18uhjqerWS6GFpe2YVotskVssWfBOAHACrZHoWcGQwQhlvmsdXBZ/F9YLMF2jzUuYBP4a7CLCnoEHrgkDSCDAARUILAGaVVqAwQHR8pZXomm9/ONhgjrbgc2lyYxmpIRK9uSNjrXs8gEbTrYyl2ryTJmsLCdKkWzFQl1lWlOXGmifal6p9VnbQfpyY2SZyXKVV7JmZkMrgIFSyrIeUJ2r7YKnXdivUg1kAgdQ8B7IzJjGsd9zKSdwyBL03WpwDGxwuOASEP5vriO2F3nLjQdIrpaRDxqcBdgIHGA74pKrZXiR2ZWuZt49m+o3pKMC3p4Av7SNxBa456777rz37jsVXRQh/PDEF2/88cgnr/zyzDfv/PMnhAAAIfkECQoAAAAsAAAAANwAEwAABf8gII5kaZ5oqq5s675wLM90bd94ru987//AoHBIBAgGhKRyyWw6n9CodEqtWq/YrHY6ELQEhYLDUPAMHGi0weEpbN7wI8cxTzsGj4R+n+DUxwaBeBt7hH1/gYIPhox+Y3Z3iwmGk36BkIN8egOIl3h8hBuOkAaZhQlna4BrpnyWa4mleZOFjrGKcXoFA2ReKwMJBgISDw6abwUPGggazc0bBqG0G8kI1tcIwZp51djW2nC03d7BjG8J49jl4cgP3t/RetLp1+vT6O7v5fKhAvnk0UKFogeP3zmCCIoZkDCABQFhChQYuKBHgkUJkxpA2MhxQYEDFhNcvPBAI8eNCx7/gMQYckPJkxsZPLhIM8FLmDJrYiRp8mTKkCwT8IQJwSPQkENhpgQpEunNkzlpWkwKdSbGihKocowqVSvKWQkIOBSgQOYFDBgQpI0oYMGEt3AzTLKm4BqGtnDjirxW95vbvG/nWlub8G9euRsiqqWLF/AEkRoiprX2wLDeDQgkW9PQGLDgyNc665WguK8C0XAnRY6oGPUEuRLsgk5g+a3cCxUqSBC7gsCBBXcVq6swwULx4hayvctGPK8FCwsSLE9A3Hje6NOrHzeOnW695sffRi/9HfDz7sIVSNB+XXrmugo0rHcM3X388o6jr44ceb51uNjF1xcC8zk3wXiS8aYC/wESaLABBs7ch0ECjr2WAGvLsLZBeHqVFl9kGxooV0T81TVhBo6NiOEyJ4p4IYnNRBQiYCN6x4wCG3ZAY2If8jXjYRcyk2FmG/5nXAY8wqhWAii+1YGOSGLoY4VRfqiAgikwmIeS1gjAgHkWYLQZf9m49V9gDWYWY5nmTYCRM2TS5pxxb8IZGV5nhplmhJyZadxzbrpnZ2d/6rnZgHIid5xIMDaDgJfbLdrgMkKW+Rygz1kEZz1mehabkBpgiQIByVikwGTqVfDkk2/Vxxqiqur4X3fksHccre8xlxerDLiHjQIVUAgXr77yFeyuOvYqXGbMrbrqBMqaFpFFzhL7qv9i1FX7ZLR0LUNdcc4e6Cus263KbV+inkAAHhJg0BeITR6WmHcaxhvXg/AJiKO9R77ILF1FwmVdAu6WBu+ZFua72mkZWMfqBElKu0G8rFZ5n4ATp5jkmvsOq+Nj7u63ZMMPv4bveyYy6fDH+C6brgnACHBABQUrkGirz2FwAHnM4Mmhzq9yijOrOi/MKabH6VwBiYwZdukEQAvILKTWXVq0ZvH5/CfUM7M29Zetthp1eht0eqkFYw8IKXKA6mzXfTeH7fZg9zW0AhgY0TwthUa6Ch9dBeIsbsFrYkRBfgTfiG0FhwMWnbsoq3cABUYOnu/ejU/A6uNeT8u4wMb1WnBCyJJTLjjnr8o3OeJrUcpc5oCiPqAEkz8tXuLkPeDL3Uhs4fvvwAcv/PDEU9FFEcgnr/zyzDfv/PPQRy/99NRXf0IIACH5BAkKAAAALAAAAADcABMAAAX/ICCOZGmeaKqubOu+cCzPdG3feK7vfO//wKBwSAQIBoSkcslsOp/QqHRKrVqv2Kx2OhC0BIWCw/AoDziOtCHt8BQ28PjmzK57Hom8fo42+P8DeAkbeYQcfX9+gYOFg4d1bIGEjQmPbICClI9/YwaLjHAJdJeKmZOViGtpn3qOqZineoeJgG8CeWUbBV4rAwkGAhIVGL97hGACGsrKCAgbBoTRhLvN1c3PepnU1s2/oZO6AtzdBoPf4eMI3tIJyOnF0YwFD+nY8e3z7+Xfefnj9uz8cVsXCh89axgk7BrAggAwBQsYIChwQILFixIeNIDAseOCBwcSXMy2sSPHjxJE/6a0eEGjSY4MQGK86PIlypUJEmYsaTKmyJ8JW/Ls6HMkzaEn8YwMWtPkx4pGd76E4DMPRqFTY860OGhogwYagBFoKEABA46DEGBAoEBB0AUT4sqdIFKBNbcC4M6dkEEk22oYFOTdG9fvWrtsBxM23MytYL17666t9phwXwlum2lIDHmuSA2IGyuOLOHv38qLMbdFjHruZbWgRXeOe1nC2BUEDiyAMMHZuwoTLAQX3nvDOAUW5Vogru434d4JnAsnPmFB9NBshQXfa9104+Rxl8e13rZxN+CEydtVsFkd+vDjE7C/q52wOvb4s7+faz025frbxefWbSoQIAEDEUCwgf9j7bUlwHN9ZVaegxDK1xYzFMJH24L5saXABhlYxiEzHoKoIV8LYqAMaw9aZqFmJUK4YHuNfRjiXhmk+NcyJgaIolvM8BhiBx3IleN8lH1IWAcRgkZgCgYiaBGJojGgHHFTgtagAFYSZhF7/qnTpY+faVlNAnqJN0EHWa6ozAZjBtgmmBokwMB01LW5jAZwbqfmlNips4B4eOqJgDJ2+imXRZpthuigeC6XZTWIxilXmRo8iYKBCwiWmWkJVEAkfB0w8KI1IvlIpKnOkVpqdB5+h96o8d3lFnijrgprjbfGRSt0lH0nAZG5vsprWxYRW6Suq4UWqrLEsspWg8Io6yv/q6EhK0Fw0GLbjKYn5CZYBYht1laPrnEY67kyrhYbuyceiR28Pso7bYwiXjihjWsWuWF5p/H765HmNoiur3RJsGKNG/jq748XMrwmjhwCfO6QD9v7LQsDxPTAMKsFpthyJCdkmgYiw0VdXF/Om9dyv7YMWGXTLYpZg5wNR11C78oW3p8HSGgul4qyrJppgllJHJZHn0Y0yUwDXCXUNquFZNLKyYXBAVZvxtAKYIQEsmPgDacr0tltO1y/DMwYpkgUpJfTasLGzd3cdCN3gN3UWRcY3epIEPevfq+3njBxq/kqBoGBduvea8f393zICS63ivRBTqgFpgaWZEIUULdcK+frIfAAL2AjscXqrLfu+uuwx05FF0XUbvvtuOeu++689+7778AHL/wJIQAAOwAAAAAAAAAAAA=="),define("text!cockpit/ui/images/closer.png",[],"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAj9JREFUeNp0ks+LUlEUx7/vV1o8Z8wUx3IEHcQmiBiQlomjRNCiZpEuEqF/oEUwq/6EhvoHggmRcJUQBM1CRJAW0aLIaGQimZJxJsWxyV/P9/R1zzWlFl04vPvOPZ9z7rnnK5imidmKRCIq+zxgdoPZ1T/ut8xeM3tcKpW6s1hhBkaj0Qj7bDebTX+324WmadxvsVigqipcLleN/d4rFoulORiLxTZY8ItOp8MBCpYkiYPj8Xjus9vtlORWoVB4KcTjcQc732dLpSRXvCZaAws6Q4WDdqsO52kNH+oCRFGEz+f7ydwBKRgMPmTXi49GI1x2D/DsznesB06ws2eDbI7w9HYN6bVjvGss4KAjwDAMq81mM2SW5Wa/3weBbz42UL9uYnVpiO2Nr9ANHSGXib2Wgm9tCYIggGKJEVkvlwgi5/FQRmTLxO6hgJVzI1x0T/fJrBtHJxPeL6tI/fsZLA6ot8lkQi8HRVbw94gkWYI5MaHrOjcCGSNRxZosy9y5cErDzn0Dqx7gcwO8WtBp4PndI35GMYqiUMUvBL5yOBz8yRfFNpbPmqgcCFh/IuHa1nR/YXGM8+oUpFhihEQiwcdRLpfVRqOBtWXWq34Gra6AXq8Hp2piZcmKT4cKnE4nwuHwdByVSmWQz+d32WCTlHG/qaHHREN9kgi0sYQfv0R4PB4EAgESQDKXy72fSy6VSnHJVatVf71eR7vd5n66mtfrRSgU4pLLZrOlf7RKK51Ok8g3/yPyR5lMZi7y3wIMAME4EigHWgKnAAAAAElFTkSuQmCC"),define("text!cockpit/ui/images/pinin.png",[],"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC7mlDQ1BJQ0MgUHJvZmlsZQAAeAGFVM9rE0EU/jZuqdAiCFprDrJ4kCJJWatoRdQ2/RFiawzbH7ZFkGQzSdZuNuvuJrWliOTi0SreRe2hB/+AHnrwZC9KhVpFKN6rKGKhFy3xzW5MtqXqwM5+8943731vdt8ADXLSNPWABOQNx1KiEWlsfEJq/IgAjqIJQTQlVdvsTiQGQYNz+Xvn2HoPgVtWw3v7d7J3rZrStpoHhP1A4Eea2Sqw7xdxClkSAog836Epx3QI3+PY8uyPOU55eMG1Dys9xFkifEA1Lc5/TbhTzSXTQINIOJT1cVI+nNeLlNcdB2luZsbIEL1PkKa7zO6rYqGcTvYOkL2d9H5Os94+wiHCCxmtP0a4jZ71jNU/4mHhpObEhj0cGDX0+GAVtxqp+DXCFF8QTSeiVHHZLg3xmK79VvJKgnCQOMpkYYBzWkhP10xu+LqHBX0m1xOv4ndWUeF5jxNn3tTd70XaAq8wDh0MGgyaDUhQEEUEYZiwUECGPBoxNLJyPyOrBhuTezJ1JGq7dGJEsUF7Ntw9t1Gk3Tz+KCJxlEO1CJL8Qf4qr8lP5Xn5y1yw2Fb3lK2bmrry4DvF5Zm5Gh7X08jjc01efJXUdpNXR5aseXq8muwaP+xXlzHmgjWPxHOw+/EtX5XMlymMFMXjVfPqS4R1WjE3359sfzs94i7PLrXWc62JizdWm5dn/WpI++6qvJPmVflPXvXx/GfNxGPiKTEmdornIYmXxS7xkthLqwviYG3HCJ2VhinSbZH6JNVgYJq89S9dP1t4vUZ/DPVRlBnM0lSJ93/CKmQ0nbkOb/qP28f8F+T3iuefKAIvbODImbptU3HvEKFlpW5zrgIXv9F98LZua6N+OPwEWDyrFq1SNZ8gvAEcdod6HugpmNOWls05Uocsn5O66cpiUsxQ20NSUtcl12VLFrOZVWLpdtiZ0x1uHKE5QvfEp0plk/qv8RGw/bBS+fmsUtl+ThrWgZf6b8C8/UXAeIuJAAAACXBIWXMAAAsTAAALEwEAmpwYAAABZ0lEQVQ4Ea2TPUsDQRCGZ89Eo4FACkULEQs1CH4Uamfjn7GxEYJFIFXgChFsbPwzNnZioREkaiHBQtEiEEiMRm/dZ8OEGAxR4sBxx877Pju7M2estTJIxLrNuVwuMxQEx0ZkzcFHyRtjXt02559RtB2GYanTYzoryOfz+6l4Nbszf2niwffKmpGRo9sVW22mDgqFwp5C2gDMm+P32a3JB1N+n5JifUGeP9JeNxGryPLYjcwMP8rJ07Q9fZltQzyAstOJ2vVu5sKc1ZZkRBrOcKeb+HexPidvkpCN5JUcllZtpZFc5DgBWc5M2eysZuMuofMBSA4NWjx4PUCsXefMlI0QY3ewRg4NWi4ZTQsgrjYXema+e4VqtEMK6KXvu+4B9Bklt90vVKMeD2BI6DOt4rZ/Gk7WyKFBi4fNPIAJY0joM61SCCZ9tI1o0OIB8D+DBIkYaJRbCBH9mZgNt+bb++ufSSF/eX8BYcDeAzuQJVUAAAAASUVORK5CYII="),define("text!cockpit/ui/images/plus.png",[],"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAAZiS0dEANIA0gDS7KbF4AAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9kFGw4yFTwuJTkAAAH7SURBVCjPdZKxa1NRFMZ/956XZMgFyyMlCZRA4hBx6lBcQ00GoYi4tEstFPwLAs7iLDi7FWuHThaUggihBDI5OWRoQAmBQFISQgvvpbwX3rsOaR4K+o2H8zvfOZxPWWtZqVarGaAJPAEe3ZW/A1+Bd+1221v1qhW4vb1dA44mk0nZ8zyCIAAgk8lgjGF9fb0PHF5cXLQTsF6vP/c879P19TVBEJDJZBARAKIoSmpra2sYY561Wq3PqtFouMBgMBgYay3ZbJZ/yfd9tNaUSqUboOKISPPq6sqsVvZ9H4AvL34B8PTj/QSO45jpdHovn883Ha31znw+JwzDpCEMQx4UloM8zyOdTif3zudztNY7jog8DMMQpRRxHPPt5TCBAEZvxlyOFTsfykRRBICIlB2t9a21Nh3HMXEc8+d7VhJHWCwWyzcohdZaHBHpO46z6fs+IsLj94XECaD4unCHL8FsNouI/HRE5Nx13c3ZbIbWOnG5HKtl+53TSq7rIiLnand31wUGnU7HjEYjlFLJZN/3yRnL1FMYY8jlcmxtbd0AFel2u7dnZ2eXxpi9xWJBEASkUimstYgIQSSkUimKxSKVSgVjzN7p6emPJHL7+/s14KjX65WHwyGz2SxZbWNjg2q12gcOT05O2n9lFeDg4MAAr/4T8rfHx8dJyH8DvvbYGzKvWukAAAAASUVORK5CYII="),define("text!cockpit/ui/images/minus.png",[],"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAAXNSR0IArs4c6QAAAAZiS0dEANIA0gDS7KbF4AAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9kFGw4xMrIJw5EAAAHcSURBVCjPhZIxSxtxGMZ/976XhJA/RA5EAyJcFksnp64hjUPBoXRyCYLQTyD0UxScu0nFwalCQSgFCVk7dXAwUAiBDA2RO4W7yN1x9+9gcyhU+pteHt4H3pfncay1LOl0OgY4BN4Ar/7KP4BvwNFwOIyWu87S2O12O8DxfD73oygiSRIAarUaxhhWV1fHwMFgMBiWxl6v9y6Koi+3t7ckSUKtVkNVAcjzvNRWVlYwxry9vLz86uzs7HjAZDKZGGstjUaDfxHHMSLC5ubmHdB2VfVwNpuZ5clxHPMcRVFwc3PTXFtbO3RFZHexWJCmabnweAaoVqvlv4vFAhHZdVX1ZZqmOI5DURR8fz/lxbp9Yrz+7bD72SfPcwBU1XdF5N5aWy2KgqIoeBzPEnWVLMseYnAcRERdVR27rrsdxzGqyutP6898+GBsNBqo6i9XVS88z9sOggAR4X94noeqXoiIHPm+H9XrdYIgIAxDwjAkTVPCMESzBy3LMprNJr7v34nIkV5dXd2fn59fG2P2siwjSRIqlQrWWlSVJFcqlQqtVot2u40xZu/s7OxnWbl+v98BjkejkT+dTgmCoDxtY2ODra2tMXBweno6fNJVgP39fQN8eKbkH09OTsqS/wHFRdHPfTSfjwAAAABJRU5ErkJggg=="),define("text!cockpit/ui/images/dot_clear.gif",[],"data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAEBMgA7"),define("text!cockpit/ui/images/pins.png",[],"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAQCAYAAABQrvyxAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGYklEQVRIDbVWe0yURxCf/R735o6DO0FBe0RFsaL4iLXGIKa2SY3P6JGa2GpjlJjUV9NosbU++tYUbEnaQIrVaKJBG7WiNFQFUWO1UUEsVg2CAgoeHHLewcH32O58cBdQsX9Y5+7LfrszOzO/2ZnZj1BKgTBiIwVGVvKd49OVVYunDlXn6wdBKh+ogXrv+DOz1melIb+3LM5fNv2XPYE5EHY+L3PJljN5zavHpJjsQNsA/JJEgyC2+WTjy3b0GfoJW8O4aoHtDwiHQrj5lw1LLyyb1bp5zAjJTus9klrVpdD6TqH2ngVO+0dsRJnp06cLIYU4fx7NnRI3bu7UIYOeJ/McnuY88q3k62gc0S4Dgf5qhICQtIXS2lqD7BhSduPk3YfyzXaANhBBJDxYdUqCywB2qS4RdyUuSkTF/VJxcbH5j8N7/75RuFrN3Zh8OS8zqf5m4UpPeenOyP42dbtBeuvVnCdkK1e4PfPouX03mo9se+c33M8wqDk5Ofqed8REUTicQhbySUxp9u3KlMSHTtrFU6Kyn03lz15PPpW25vsZeYSIKyiVURcqeZJOH9lTNZLfnxRjU/uwrjbEUBWsapcSO2Hq4k0VfZg9EzxdDNCEjDxgNqRDme9umz/btwlsHRIEePHgAf73RdnHZ6LTuIUBN7OBQ+c1Fdnp6cZ1BQUdeRuWZi97o3ktDQQkVeFFzqJARd1A5a0Vr7ta6Kp6TZjtZ+NTIOoKF6qDrL7e0QQIUCiqMMKk8Z1Q/SCSKvzocf2B6NEN0SQn/kTO6fKJ0zqjZUlQBSpJ0GjR77w0aoc1Pr6S5/kVJrNpakV5hR+LWKN4t7sLX+p0rx2vqSta64olIulUKUgCSXLWE1R4KPPSj+5vhm2hdDOG+CkQBmhhyyKq6SaFYWTn5bB3QJRNz54AuXKn8TJjhu0Wbv+wNEKQjVhnmKopjo4FxXmetCRnC4F7BhCiCUepqAepRh0TM/gjjzOOSK2NgWZPc05qampRWJHb7dbOffep2ednzLzgczlbrQA6gHYF9BYDh9GY+FjddMweHMscmMuep07gXlMQoqw9ALoYu5MJsak9QmJA2IvAgVmoCRciooyPujJtNCv1uHt3TmK9gegFKrG9kh6oXwZiIEAtBIjORGKNTWR/WeW8XVkbjuJepLAyloM8LmTN//njKZPbraATZaLjCHEww9Ei4FFiPg6Ja5gT6gxYgLgnRDHRQwJXbz2GOw0d4A3K4GXlUtMahJjYVxiYbrwOmxIS10bFnIBOSi6Tl9Jgs0zbOEX18wyEwgLPMrxD1Y4aCK8kmTpgYcpAF27Mzs42Hjx4kA8BICUlJfKArR7LcEvTB1xEC9AoEw9OPagWkVU/D1oesmK6U911zEczMVe01oZjiMggg6ux2Qk379qh4rYKet4GjrhhwEteBgBrH8BssoXEtbHzPpSBRRSpqlNpgAiUoxzHKxLRszoVuggIisxaDQWZqkQvQjAoax3NbDbLLGuUEABNGedXqSyLRupXgDT5JfAGZNLio9B0X8Uiwk4w77MDc1D4yejjWtykPS3DX01UDCY/GPQcVDe0QYT0CIxGFvUorfvBxZsRfVrUuWruMBAb/lXCUofoFNZfzGJtowXOX0vwUSFK4BgyMKm6P6s9wQUZld+jrYyMDC0iIQDaJdG4IyZQfL3RfbFcCBIlRgc+u3CjaTApuZ9KsANgG8PNzHlWWD3tCxd6kafNNiFp5HAalAkkJ0SCV2H3CgOD9Nc/FqrXuyb0Eocvfhq171p5eyuJ1omKJEP5rQGe/FOOnXtq335z8YmvYo9cHb2t8spIb3lVSseZW46FlGY/Sk9P50P2w20UlWJUkUHIushfc5PXGAzCo0PlD2pnpCYfCXga3lu+fPlevEhWrVrFyrN/Orfv87FOW9tlqb2Kc9pV8DzioMk3UNUbXM+8B/ATBr8C8CKdvGXWGD/9sqm3dkxtzA4McMjHMB8D2ftheYXo+qzt3pXvz8/PP/vk+v8537V+yYW87Zu+RZ1ZbrexoKAA/SBpaWn4+aL5w5zGk+/jW59JiMkESW5urpiVlWXENRb1H/Yf2I9txIxz5IdkX3TsraukpsbQjz6090yb4XsAvQoRE0YvJdamtIIbOnRoUVlZ2ftsLVQzIdEXHntsaZdimssVfCpFui109+BnWPsXaWLI/zactygAAAAASUVORK5CYII="),define("text!cockpit/ui/images/pinout.png",[],"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC7mlDQ1BJQ0MgUHJvZmlsZQAAeAGFVM9rE0EU/jZuqdAiCFprDrJ4kCJJWatoRdQ2/RFiawzbH7ZFkGQzSdZuNuvuJrWliOTi0SreRe2hB/+AHnrwZC9KhVpFKN6rKGKhFy3xzW5MtqXqwM5+8943731vdt8ADXLSNPWABOQNx1KiEWlsfEJq/IgAjqIJQTQlVdvsTiQGQYNz+Xvn2HoPgVtWw3v7d7J3rZrStpoHhP1A4Eea2Sqw7xdxClkSAog836Epx3QI3+PY8uyPOU55eMG1Dys9xFkifEA1Lc5/TbhTzSXTQINIOJT1cVI+nNeLlNcdB2luZsbIEL1PkKa7zO6rYqGcTvYOkL2d9H5Os94+wiHCCxmtP0a4jZ71jNU/4mHhpObEhj0cGDX0+GAVtxqp+DXCFF8QTSeiVHHZLg3xmK79VvJKgnCQOMpkYYBzWkhP10xu+LqHBX0m1xOv4ndWUeF5jxNn3tTd70XaAq8wDh0MGgyaDUhQEEUEYZiwUECGPBoxNLJyPyOrBhuTezJ1JGq7dGJEsUF7Ntw9t1Gk3Tz+KCJxlEO1CJL8Qf4qr8lP5Xn5y1yw2Fb3lK2bmrry4DvF5Zm5Gh7X08jjc01efJXUdpNXR5aseXq8muwaP+xXlzHmgjWPxHOw+/EtX5XMlymMFMXjVfPqS4R1WjE3359sfzs94i7PLrXWc62JizdWm5dn/WpI++6qvJPmVflPXvXx/GfNxGPiKTEmdornIYmXxS7xkthLqwviYG3HCJ2VhinSbZH6JNVgYJq89S9dP1t4vUZ/DPVRlBnM0lSJ93/CKmQ0nbkOb/qP28f8F+T3iuefKAIvbODImbptU3HvEKFlpW5zrgIXv9F98LZua6N+OPwEWDyrFq1SNZ8gvAEcdod6HugpmNOWls05Uocsn5O66cpiUsxQ20NSUtcl12VLFrOZVWLpdtiZ0x1uHKE5QvfEp0plk/qv8RGw/bBS+fmsUtl+ThrWgZf6b8C8/UXAeIuJAAAACXBIWXMAAAsTAAALEwEAmpwYAAACyUlEQVQ4EW1TXUgUURQ+Z3ZmnVV3QV2xJbVSEIowQbAfLQx8McLoYX2qjB58MRSkP3vZppceYhGxgrZaIughlYpE7CHFWiiKyj9II0qxWmwlNh1Xtp2f27mz7GDlZX7uuXO+73zfuXeQMQYIgAyALppgyBtse32stsw86txkHhATn+FbfPfzxnPB+vR3RMJYuTwW6bbB4a6WS5O3Yu2VlXIesDiAamiQNKVlVXfx5I0GJ7DY7p0/+erU4dgeMJIA31WNxZmAgibOreXDqF55sY4SFUURqbi+nkjgwTyAbHhLX8yOLsSM2QRA3JRAAgd4RGPbVhkKEp8qeJ7PFyW3fw++YHtC7CkaD0amqyqihSwlMQQ0wa07IjPVI/vbexreIUrVaQV2D4RMQ/o7m12Mdfx4H3PfB9FNzTR1U2cO0Bi45aV6xNvFBNaoIAfbSiwLlqi9/hR/R3Nrhua+Oqi9TEKiB02C7YXz+Pba4MTDrpbLiMAxNgmXb+HpwVkZdoIrkn9isW7nRw/TZYaagZArAWyhfqsSDL/c9aTx7JUjGZCtYExRqCzAwGblwr6aFQ84nTo6qZ7XCeCVQNckE/KSWolvoQnxeoFFgIh8G/nA+kBAxxuQO5m9eFrwLIGJHgcyM63VFMhRSgNVyJr7og8y1vbTQpH8DIEVgxuYuexw0QECIalq5FYgEmpkgoFYltU/lnrqDz5osirSFpF7lrHAFKSWHYfEs+mY/82UnAStyMlW8sUPsVIciTZgz3jV1ebg0CEOpgPF22s1z1YQYKSXPJ1hbAhR8T26WdLhkuVfAzPR+YO1Ox5n58SmCcF6e3uzAoHA77RkevJdWH/3+f2O9TGf3w3fWQ2Hw5F/13mcsWAT+vv6DK4kFApJ/d3d1k+kJtbCrmxXHS3n8ER6b3CQbAqaEHVra6sGxcXW4SovLx+empxapS//FfwD9kpMJjMMBBAAAAAASUVORK5CYII=") \ No newline at end of file
diff --git a/www/js/ace/keybinding-emacs.js b/www/js/ace/keybinding-emacs.js
new file mode 100644
index 0000000..da4aa9d
--- /dev/null
+++ b/www/js/ace/keybinding-emacs.js
@@ -0,0 +1 @@
+define("ace/keyboard/keybinding/emacs",["require","exports","module","ace/keyboard/state_handler"],function(a,b,c){var d=a("ace/keyboard/state_handler").StateHandler,e=a("ace/keyboard/state_handler").matchCharacterOnly,f={start:[{key:"ctrl-x",then:"c-x"},{regex:["(?:command-([0-9]*))*","(down|ctrl-n)"],exec:"golinedown",params:[{name:"times",match:1,type:"number",defaultValue:1}]},{regex:["(?:command-([0-9]*))*","(right|ctrl-f)"],exec:"gotoright",params:[{name:"times",match:1,type:"number",defaultValue:1}]},{regex:["(?:command-([0-9]*))*","(up|ctrl-p)"],exec:"golineup",params:[{name:"times",match:1,type:"number",defaultValue:1}]},{regex:["(?:command-([0-9]*))*","(left|ctrl-b)"],exec:"gotoleft",params:[{name:"times",match:1,type:"number",defaultValue:1}]},{comment:"This binding matches all printable characters except numbers as long as they are no numbers and print them n times.",regex:["(?:command-([0-9]*))","([^0-9]+)*"],match:e,exec:"inserttext",params:[{name:"times",match:1,type:"number",defaultValue:"1"},{name:"text",match:2}]},{comment:"This binding matches numbers as long as there is no meta_number in the buffer.",regex:["(command-[0-9]*)*","([0-9]+)"],match:e,disallowMatches:[1],exec:"inserttext",params:[{name:"text",match:2,type:"text"}]},{regex:["command-([0-9]*)","(command-[0-9]|[0-9])"],comment:"Stops execution if the regex /meta_[0-9]+/ matches to avoid resetting the buffer."}],"c-x":[{key:"ctrl-g",then:"start"},{key:"ctrl-s",exec:"save",then:"start"}]};b.Emacs=new d(f)}),define("ace/keyboard/state_handler",["require","exports","module"],function(a,b,c){function e(a){this.keymapping=this.$buildKeymappingRegex(a)}var d=!1;e.prototype={$buildKeymappingRegex:function(a){for(state in a)this.$buildBindingsRegex(a[state]);return a},$buildBindingsRegex:function(a){a.forEach(function(a){a.key?a.key=new RegExp("^"+a.key+"$"):Array.isArray(a.regex)?(a.key=new RegExp("^"+a.regex[1]+"$"),a.regex=new RegExp(a.regex.join("")+"$")):a.regex&&(a.regex=new RegExp(a.regex+"$"))})},$composeBuffer:function(a,b,c){if(a.state==null||a.buffer==null)a.state="start",a.buffer="";var d=[];b&1&&d.push("ctrl"),b&8&&d.push("command"),b&2&&d.push("option"),b&4&&d.push("shift"),c&&d.push(c);var e=d.join("-"),f=a.buffer+e;b!=2&&(a.buffer=f);return{bufferToUse:f,symbolicName:e}},$find:function(a,b,c,e,f){var g={};this.keymapping[a.state].some(function(h){var i;if(h.key&&!h.key.test(c))return!1;if(h.regex&&!(i=h.regex.exec(b)))return!1;if(h.match&&!h.match(b,e,f,c))return!1;if(h.disallowMatches)for(var j=0;j<h.disallowMatches.length;j++)if(!!i[h.disallowMatches[j]])return!1;if(h.exec){g.command=h.exec;if(h.params){var k;g.args={},h.params.forEach(function(a){a.match!=null&&i!=null?k=i[a.match]||a.defaultValue:k=a.defaultValue,a.type==="number"&&(k=parseInt(k)),g.args[a.name]=k})}a.buffer=""}h.then&&(a.state=h.then,a.buffer=""),g.command==null&&(g.command="null"),d&&console.log("KeyboardStateMapper#find",h);return!0});if(g.command)return g;a.buffer="";return!1},handleKeyboard:function(a,b,c){if(b!=0&&(c==""||c==String.fromCharCode(0)))return null;var e=this.$composeBuffer(a,b,c),f=e.bufferToUse,g=e.symbolicName;e=this.$find(a,f,g,b,c),d&&console.log("KeyboardStateMapper#match",f,g,e);return e}},b.matchCharacterOnly=function(a,b,c,d){return b==0?!0:b==4&&c.length==1?!0:!1},b.StateHandler=e}) \ No newline at end of file
diff --git a/www/js/ace/keybinding-vim.js b/www/js/ace/keybinding-vim.js
new file mode 100644
index 0000000..9f6ae87
--- /dev/null
+++ b/www/js/ace/keybinding-vim.js
@@ -0,0 +1 @@
+define("ace/keyboard/keybinding/vim",["require","exports","module","ace/keyboard/state_handler"],function(a,b,c){var d=a("ace/keyboard/state_handler").StateHandler,e=a("ace/keyboard/state_handler").matchCharacterOnly,f={start:[{key:"i",then:"insertMode"},{key:"a",exec:"gotoright",then:"insertMode"},{key:"shift-i",exec:"gotolinestart",then:"insertMode"},{key:"shift-a",exec:"gotolineend",then:"insertMode"},{key:"shift-c",exec:"removetolineend",then:"insertMode"},{key:"shift-r",exec:"overwrite",then:"replaceMode"},{regex:["([0-9]*)","(k|up)"],exec:"golineup",params:[{name:"times",match:1,type:"number",defaultValue:1}]},{regex:["([0-9]*)","(j|down|enter)"],exec:"golinedown",params:[{name:"times",match:1,type:"number",defaultValue:1}]},{regex:["([0-9]*)","(l|right)"],exec:"gotoright",params:[{name:"times",match:1,type:"number",defaultValue:1}]},{regex:["([0-9]*)","(h|left)"],exec:"gotoleft",params:[{name:"times",match:1,type:"number",defaultValue:1}]},{key:"shift-g",exec:"gotoend"},{key:"b",exec:"gotowordleft"},{key:"e",exec:"gotowordright"},{key:"x",exec:"del"},{key:"shift-x",exec:"backspace"},{key:"shift-d",exec:"removetolineend"},{comment:"Catch some keyboard input to stop it here",match:e}],insertMode:[{key:"esc",then:"start"}],replaceMode:[{key:"esc",exec:"overwrite",then:"start"}]};b.Vim=new d(f)}),define("ace/keyboard/state_handler",["require","exports","module"],function(a,b,c){function e(a){this.keymapping=this.$buildKeymappingRegex(a)}var d=!1;e.prototype={$buildKeymappingRegex:function(a){for(state in a)this.$buildBindingsRegex(a[state]);return a},$buildBindingsRegex:function(a){a.forEach(function(a){a.key?a.key=new RegExp("^"+a.key+"$"):Array.isArray(a.regex)?(a.key=new RegExp("^"+a.regex[1]+"$"),a.regex=new RegExp(a.regex.join("")+"$")):a.regex&&(a.regex=new RegExp(a.regex+"$"))})},$composeBuffer:function(a,b,c){if(a.state==null||a.buffer==null)a.state="start",a.buffer="";var d=[];b&1&&d.push("ctrl"),b&8&&d.push("command"),b&2&&d.push("option"),b&4&&d.push("shift"),c&&d.push(c);var e=d.join("-"),f=a.buffer+e;b!=2&&(a.buffer=f);return{bufferToUse:f,symbolicName:e}},$find:function(a,b,c,e,f){var g={};this.keymapping[a.state].some(function(h){var i;if(h.key&&!h.key.test(c))return!1;if(h.regex&&!(i=h.regex.exec(b)))return!1;if(h.match&&!h.match(b,e,f,c))return!1;if(h.disallowMatches)for(var j=0;j<h.disallowMatches.length;j++)if(!!i[h.disallowMatches[j]])return!1;if(h.exec){g.command=h.exec;if(h.params){var k;g.args={},h.params.forEach(function(a){a.match!=null&&i!=null?k=i[a.match]||a.defaultValue:k=a.defaultValue,a.type==="number"&&(k=parseInt(k)),g.args[a.name]=k})}a.buffer=""}h.then&&(a.state=h.then,a.buffer=""),g.command==null&&(g.command="null"),d&&console.log("KeyboardStateMapper#find",h);return!0});if(g.command)return g;a.buffer="";return!1},handleKeyboard:function(a,b,c){if(b!=0&&(c==""||c==String.fromCharCode(0)))return null;var e=this.$composeBuffer(a,b,c),f=e.bufferToUse,g=e.symbolicName;e=this.$find(a,f,g,b,c),d&&console.log("KeyboardStateMapper#match",f,g,e);return e}},b.matchCharacterOnly=function(a,b,c,d){return b==0?!0:b==4&&c.length==1?!0:!1},b.StateHandler=e}) \ No newline at end of file
diff --git a/www/js/ace/mode-c_cpp.js b/www/js/ace/mode-c_cpp.js
new file mode 100644
index 0000000..b19fc07
--- /dev/null
+++ b/www/js/ace/mode-c_cpp.js
@@ -0,0 +1 @@
+define("ace/mode/c_cpp",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/c_cpp_highlight_rules","ace/mode/matching_brace_outdent","ace/range","ace/mode/behaviour/cstyle"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/c_cpp_highlight_rules").c_cppHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=a("ace/range").Range,j=a("ace/mode/behaviour/cstyle").CstyleBehaviour,k=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h,this.$behaviour=new j};d.inherits(k,e),function(){this.toggleCommentLines=function(a,b,c,d){var e=!0,f=[],g=/^(\s*)\/\//;for(var h=c;h<=d;h++)if(!g.test(b.getLine(h))){e=!1;break}if(e){var j=new i(0,0,0,0);for(var h=c;h<=d;h++){var k=b.getLine(h),l=k.match(g);j.start.row=h,j.end.row=h,j.end.column=l[0].length,b.replace(j,l[1])}}else b.indentRows(c,d,"//")},this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=this.$tokenizer.getLineTokens(b,a),f=e.tokens,g=e.state;if(f.length&&f[f.length-1].type=="comment")return d;if(a=="start"){var h=b.match(/^.*[\{\(\[]\s*$/);h&&(d+=c)}else if(a=="doc-start"){if(g=="start")return"";var h=b.match(/^\s*(\/?)\*/);h&&(h[1]&&(d+=" "),d+="* ")}return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)}}.call(k.prototype),b.Mode=k}),define("ace/mode/c_cpp_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/doc_comment_highlight_rules").DocCommentHighlightRules,g=a("ace/mode/text_highlight_rules").TextHighlightRules,h=function(){var a=e.arrayToMap("and|double|not_eq|throw|and_eq|dynamic_cast|operator|true|asm|else|or|try|auto|enum|or_eq|typedef|bitand|explicit|private|typeid|bitor|extern|protected|typename|bool|false|public|union|break|float|register|unsigned|case|fro|reinterpret-cast|using|catch|friend|return|virtual|char|goto|short|void|class|if|signed|volatile|compl|inline|sizeof|wchar_t|const|int|static|while|const-cast|long|static_cast|xor|continue|mutable|struct|xor_eq|default|namespace|switch|delete|new|template|do|not|this|for".split("|")),b=e.arrayToMap("NULL".split("|"));this.$rules={start:[{token:"comment",regex:"\\/\\/.*$"},(new f).getStartRule("doc-start"),{token:"comment",regex:"\\/\\*",next:"comment"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:'["].*\\\\$',next:"qqstring"},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"string",regex:"['].*\\\\$",next:"qstring"},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"},{token:"constant",regex:"<[a-zA-Z0-9.]+>"},{token:"keyword",regex:"(?:#include|#pragma|#line|#define|#undef|#ifdef|#else|#elif|#endif|#ifndef)"},{token:function(c){return c=="this"?"variable.language":a.hasOwnProperty(c)?"keyword":b.hasOwnProperty(c)?"constant.language":"identifier"},regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"keyword.operator",regex:"!|\\$|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|==|=|!=|<=|>=|<<=|>>=|>>>=|<>|<|>|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|new|delete|typeof|void)"},{token:"lparen",regex:"[[({]"},{token:"rparen",regex:"[\\])}]"},{token:"text",regex:"\\s+"}],comment:[{token:"comment",regex:".*?\\*\\/",next:"start"},{token:"comment",regex:".+"}],qqstring:[{token:"string",regex:'(?:(?:\\\\.)|(?:[^"\\\\]))*?"',next:"start"},{token:"string",regex:".+"}],qstring:[{token:"string",regex:"(?:(?:\\\\.)|(?:[^'\\\\]))*?'",next:"start"},{token:"string",regex:".+"}]},this.embedRules(f,"doc-",[(new f).getEndRule("start")])};d.inherits(h,g),b.c_cppHighlightRules=h}),define("ace/mode/doc_comment_highlight_rules",["require","exports","module","pilot/oop","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text_highlight_rules").TextHighlightRules,f=function(){this.$rules={start:[{token:"comment.doc.tag",regex:"@[\\w\\d_]+"},{token:"comment.doc",regex:"\\s+"},{token:"comment.doc",regex:"TODO"},{token:"comment.doc",regex:"[^@\\*]+"},{token:"comment.doc",regex:"."}]}};d.inherits(f,e),function(){this.getStartRule=function(a){return{token:"comment.doc",regex:"\\/\\*(?=\\*)",next:a}},this.getEndRule=function(a){return{token:"comment.doc",regex:"\\*\\/",next:a}}}.call(f.prototype),b.DocCommentHighlightRules=f}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\}/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\})/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingBraceOutdent=e}),define("ace/mode/behaviour/cstyle",["require","exports","module","pilot/oop","ace/mode/behaviour"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/behaviour").Behaviour,f=function(){this.add("braces","insertion",function(a,b,c,d,e){if(e=="{"){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"{"+g+"}",selection:!1}:{text:"{}",selection:[1,1]}}if(e=="}"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var k=d.$findOpeningBracket("}",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}else if(e=="\n"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var l=d.findMatchingBracket({row:h.row,column:h.column}),m=this.getNextLineIndent(a,i.substring(0,i.length-1),d.getTabString()),n=this.$getIndent(d.doc.getLine(l.row));return{text:"\n"+m+"\n"+n,selection:[1,m.length,1,m.length]}}}return!1}),this.add("braces","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="{"){var g=d.doc.getLine(e.start.row),h=g.substring(e.end.column,e.end.column+1);if(h=="}")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("parens","insertion",function(a,b,c,d,e){if(e=="("){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"("+g+")",selection:!1}:{text:"()",selection:[1,1]}}if(e==")"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j==")"){var k=d.$findOpeningBracket(")",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}return!1}),this.add("parens","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="("){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h==")")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("string_dquotes","insertion",function(a,b,c,d,e){if(e=='"'){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);if(g!=="")return{text:'"'+g+'"',selection:!1};var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column-1,h.column);if(j=="\\")return!1;var k=d.getTokens(f.start.row,f.start.row)[0].tokens,l=0,m,n=-1;for(var o=0;o<k.length;o++){m=k[o],m.type=="string"?n=-1:n<0&&(n=m.value.indexOf('"'));if(m.value.length+l>f.start.column)break;l+=k[o].value.length}if(!m||n<0&&m.type!=="comment"&&(m.type!=="string"||f.start.column!==m.value.length+l-1&&m.value.lastIndexOf('"')===m.value.length-1))return{text:'""',selection:[1,1]};if(m&&m.type==="string"){var p=i.substring(h.column,h.column+1);if(p=='"')return{text:"",selection:[1,1]}}}return!1}),this.add("string_dquotes","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=='"'){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h=='"')return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1})};d.inherits(f,e),b.CstyleBehaviour=f}) \ No newline at end of file
diff --git a/www/js/ace/mode-clojure.js b/www/js/ace/mode-clojure.js
new file mode 100644
index 0000000..91338d2
--- /dev/null
+++ b/www/js/ace/mode-clojure.js
@@ -0,0 +1 @@
+define("ace/mode/clojure",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/clojure_highlight_rules","ace/mode/matching_parens_outdent","ace/range"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/clojure_highlight_rules").ClojureHighlightRules,h=a("ace/mode/matching_parens_outdent").MatchingParensOutdent,i=a("ace/range").Range,j=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h};d.inherits(j,e),function(){this.toggleCommentLines=function(a,b,c,d){var e=!0,f=[],g=/^(\s*)#/;for(var h=c;h<=d;h++)if(!g.test(b.getLine(h))){e=!1;break}if(e){var j=new i(0,0,0,0);for(var h=c;h<=d;h++){var k=b.getLine(h),l=k.match(g);j.start.row=h,j.end.row=h,j.end.column=l[0].length,b.replace(j,l[1])}}else b.indentRows(c,d,";")},this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=d,f=this.$tokenizer.getLineTokens(b,a),g=f.tokens,h=f.state;if(g.length&&g[g.length-1].type=="comment")return d;if(a=="start"){var i=b.match(/[\(\[]/);i&&(d+=" "),i=b.match(/[\)]/),i&&(d="")}return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)}}.call(j.prototype),b.Mode=j}),define("ace/mode/clojure_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/text_highlight_rules").TextHighlightRules,g=function(){var a=e.arrayToMap("* *1 *2 *3 *agent* *allow-unresolved-vars* *assert* *clojure-version* *command-line-args* *compile-files* *compile-path* *e *err* *file* *flush-on-newline* *in* *macro-meta* *math-context* *ns* *out* *print-dup* *print-length* *print-level* *print-meta* *print-readably* *read-eval* *source-path* *use-context-classloader* *warn-on-reflection* + - -> -&gt; ->> -&gt;&gt; .. / < &lt; <= &lt;= = == > &gt; >= &gt;= accessor aclone add-classpath add-watch agent agent-errors aget alength alias all-ns alter alter-meta! alter-var-root amap ancestors and apply areduce array-map aset aset-boolean aset-byte aset-char aset-double aset-float aset-int aset-long aset-short assert assoc assoc! assoc-in associative? atom await await-for await1 bases bean bigdec bigint binding bit-and bit-and-not bit-clear bit-flip bit-not bit-or bit-set bit-shift-left bit-shift-right bit-test bit-xor boolean boolean-array booleans bound-fn bound-fn* butlast byte byte-array bytes cast char char-array char-escape-string char-name-string char? chars chunk chunk-append chunk-buffer chunk-cons chunk-first chunk-next chunk-rest chunked-seq? class class? clear-agent-errors clojure-version coll? comment commute comp comparator compare compare-and-set! compile complement concat cond condp conj conj! cons constantly construct-proxy contains? count counted? create-ns create-struct cycle dec decimal? declare definline defmacro defmethod defmulti defn defn- defonce defstruct delay delay? deliver deref derive descendants destructure disj disj! dissoc dissoc! distinct distinct? doall doc dorun doseq dosync dotimes doto double double-array doubles drop drop-last drop-while empty empty? ensure enumeration-seq eval even? every? false? ffirst file-seq filter find find-doc find-ns find-var first float float-array float? floats flush fn fn? fnext for force format future future-call future-cancel future-cancelled? future-done? future? gen-class gen-interface gensym get get-in get-method get-proxy-class get-thread-bindings get-validator hash hash-map hash-set identical? identity if-let if-not ifn? import in-ns inc init-proxy instance? int int-array integer? interleave intern interpose into into-array ints io! isa? iterate iterator-seq juxt key keys keyword keyword? last lazy-cat lazy-seq let letfn line-seq list list* list? load load-file load-reader load-string loaded-libs locking long long-array longs loop macroexpand macroexpand-1 make-array make-hierarchy map map? mapcat max max-key memfn memoize merge merge-with meta method-sig methods min min-key mod name namespace neg? newline next nfirst nil? nnext not not-any? not-empty not-every? not= ns ns-aliases ns-imports ns-interns ns-map ns-name ns-publics ns-refers ns-resolve ns-unalias ns-unmap nth nthnext num number? odd? or parents partial partition pcalls peek persistent! pmap pop pop! pop-thread-bindings pos? pr pr-str prefer-method prefers primitives-classnames print print-ctor print-doc print-dup print-method print-namespace-doc print-simple print-special-doc print-str printf println println-str prn prn-str promise proxy proxy-call-with-super proxy-mappings proxy-name proxy-super push-thread-bindings pvalues quot rand rand-int range ratio? rational? rationalize re-find re-groups re-matcher re-matches re-pattern re-seq read read-line read-string reduce ref ref-history-count ref-max-history ref-min-history ref-set refer refer-clojure release-pending-sends rem remove remove-method remove-ns remove-watch repeat repeatedly replace replicate require reset! reset-meta! resolve rest resultset-seq reverse reversible? rseq rsubseq second select-keys send send-off seq seq? seque sequence sequential? set set-validator! set? short short-array shorts shutdown-agents slurp some sort sort-by sorted-map sorted-map-by sorted-set sorted-set-by sorted? special-form-anchor special-symbol? split-at split-with str stream? string? struct struct-map subs subseq subvec supers swap! symbol symbol? sync syntax-symbol-anchor take take-last take-nth take-while test the-ns time to-array to-array-2d trampoline transient tree-seq true? type unchecked-add unchecked-dec unchecked-divide unchecked-inc unchecked-multiply unchecked-negate unchecked-remainder unchecked-subtract underive unquote unquote-splicing update-in update-proxy use val vals var-get var-set var? vary-meta vec vector vector? when when-first when-let when-not while with-bindings with-bindings* with-in-str with-loading-context with-local-vars with-meta with-open with-out-str with-precision xml-seq zero? zipmap ".split(" ")),b=e.arrayToMap("def do fn if let loop monitor-enter monitor-exit new quote recur set! throw try var".split(" ")),c=e.arrayToMap("true false nil".split(" "));this.$rules={start:[{token:"comment",regex:";.*$"},{token:"comment",regex:"^=begin$",next:"comment"},{token:"keyword",regex:"[\\(|\\)]"},{token:"keyword",regex:"[\\'\\(]"},{token:"keyword",regex:"[\\[|\\]]"},{token:"keyword",regex:"[\\{|\\}|\\#\\{|\\#\\}]"},{token:"keyword",regex:"[\\&]"},{token:"keyword",regex:"[\\#\\^\\{]"},{token:"keyword",regex:"[\\%]"},{token:"keyword",regex:"[@]"},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"},{token:"constant.language",regex:"[!|\\$|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+||=|!=|<=|>=|<>|<|>|!|&&]"},{token:function(d){return b.hasOwnProperty(d)?"keyword":c.hasOwnProperty(d)?"constant.language":a.hasOwnProperty(d)?"support.function":"identifier"},regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:"[:](?:[a-zA-Z]|d)+"},{token:"string.regexp",regex:'/#"(?:.|(\\")|[^""\n])*"/g'}],comment:[{token:"comment",regex:"^=end$",next:"start"},{token:"comment",regex:".+"}]}};d.inherits(g,f),b.ClojureHighlightRules=g}),define("ace/mode/matching_parens_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\)/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\))/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingParensOutdent=e}) \ No newline at end of file
diff --git a/www/js/ace/mode-coffee.js b/www/js/ace/mode-coffee.js
new file mode 100644
index 0000000..ca47332
--- /dev/null
+++ b/www/js/ace/mode-coffee.js
@@ -0,0 +1 @@
+define("ace/mode/coffee",["require","exports","module","ace/tokenizer","ace/mode/coffee_highlight_rules","ace/mode/matching_brace_outdent","ace/range","ace/mode/text","ace/worker/worker_client","pilot/oop"],function(a,b,c){function k(){this.$tokenizer=new d((new e).getRules()),this.$outdent=new f}var d=a("ace/tokenizer").Tokenizer,e=a("ace/mode/coffee_highlight_rules").CoffeeHighlightRules,f=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,g=a("ace/range").Range,h=a("ace/mode/text").Mode,i=a("ace/worker/worker_client").WorkerClient,j=a("pilot/oop");j.inherits(k,h),function(){var a=/(?:[({[=:]|[-=]>|\b(?:else|switch|try|catch(?:\s*[$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]*)?|finally))\s*$/,b=/^(\s*)#/,c=/^\s*###(?!#)/,d=/^\s*/;this.getNextLineIndent=function(b,c,d){var e=this.$getIndent(c),f=this.$tokenizer.getLineTokens(c,b).tokens;(!f.length||f[f.length-1].type!=="comment")&&b==="start"&&a.test(c)&&(e+=d);return e},this.toggleCommentLines=function(a,e,f,h){console.log("toggle");var i=new g(0,0,0,0);for(var j=f;j<=h;++j){var k=e.getLine(j);if(c.test(k))continue;b.test(k)?k=k.replace(b,"$1"):k=k.replace(d,"$&#"),i.end.row=i.start.row=j,i.end.column=k.length+1,e.replace(i,k)}},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)},this.createWorker=function(a){var b=a.getDocument(),c=new i(["ace","pilot"],"worker-coffee.js","ace/mode/coffee_worker","Worker");c.call("setValue",[b.getValue()]),b.on("change",function(a){a.range={start:a.data.range.start,end:a.data.range.end},c.emit("change",a)}),c.on("error",function(b){a.setAnnotations([b.data])}),c.on("ok",function(b){a.clearAnnotations()})}}.call(k.prototype),b.Mode=k}),define("ace/mode/coffee_highlight_rules",["require","exports","module","pilot/oop","ace/mode/text_highlight_rules"],function(a,b,c){function d(){var a="[$A-Za-z_\\x7f-\\uffff][$\\w\\x7f-\\uffff]*",b="(?![$\\w]|\\s*:)",c={token:"string",regex:".+"};this.$rules={start:[{token:"identifier",regex:"(?:@|(?:\\.|::)\\s*)"+a},{token:"keyword",regex:"(?:t(?:h(?:is|row|en)|ry|ypeof)|s(?:uper|witch)|return|b(?:reak|y)|c(?:ontinue|atch|lass)|i(?:n(?:stanceof)?|s(?:nt)?|f)|e(?:lse|xtends)|f(?:or (?:own)?|inally|unction)|wh(?:ile|en)|n(?:ew|ot?)|d(?:e(?:lete|bugger)|o)|loop|o(?:ff?|[rn])|un(?:less|til)|and|yes)"+b},{token:"constant.language",regex:"(?:true|false|null|undefined)"+b},{token:"invalid.illegal",regex:"(?:c(?:ase|onst)|default|function|v(?:ar|oid)|with|e(?:num|xport)|i(?:mplements|nterface)|let|p(?:ackage|r(?:ivate|otected)|ublic)|static|yield|__(?:hasProp|extends|slice|bind|indexOf))"+b},{token:"language.support.class",regex:"(?:Array|Boolean|Date|Function|Number|Object|R(?:e(?:gExp|ferenceError)|angeError)|S(?:tring|yntaxError)|E(?:rror|valError)|TypeError|URIError)"+b},{token:"language.support.function",regex:"(?:Math|JSON|is(?:NaN|Finite)|parse(?:Int|Float)|encodeURI(?:Component)?|decodeURI(?:Component)?)"+b},{token:"identifier",regex:a},{token:"constant.numeric",regex:"(?:0x[\\da-fA-F]+|(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:[eE][+-]?\\d+)?)"},{token:"string",regex:"'''",next:"qdoc"},{token:"string",regex:'"""',next:"qqdoc"},{token:"string",regex:"'",next:"qstring"},{token:"string",regex:'"',next:"qqstring"},{token:"string",regex:"`",next:"js"},{token:"string.regex",regex:"///",next:"heregex"},{token:"string.regex",regex:"/(?!\\s)[^[/\\n\\\\]*(?: (?:\\\\.|\\[[^\\]\\n\\\\]*(?:\\\\.[^\\]\\n\\\\]*)*\\])[^[/\\n\\\\]*)*/[imgy]{0,4}(?!\\w)"},{token:"comment",regex:"###(?!#)",next:"comment"},{token:"comment",regex:"#.*"},{token:"lparen",regex:"[({[]"},{token:"rparen",regex:"[\\]})]"},{token:"keyword.operator",regex:"\\S+"},{token:"text",regex:"\\s+"}],qdoc:[{token:"string",regex:".*?'''",next:"start"},c],qqdoc:[{token:"string",regex:'.*?"""',next:"start"},c],qstring:[{token:"string",regex:"[^\\\\']*(?:\\\\.[^\\\\']*)*'",next:"start"},c],qqstring:[{token:"string",regex:'[^\\\\"]*(?:\\\\.[^\\\\"]*)*"',next:"start"},c],js:[{token:"string",regex:"[^\\\\`]*(?:\\\\.[^\\\\`]*)*`",next:"start"},c],heregex:[{token:"string.regex",regex:".*?///[imgy]{0,4}",next:"start"},{token:"comment.regex",regex:"\\s+(?:#.*)?"},{token:"string.regex",regex:"\\S+"}],comment:[{token:"comment",regex:".*?###",next:"start"},{token:"comment",regex:".+"}]}}a("pilot/oop").inherits(d,a("ace/mode/text_highlight_rules").TextHighlightRules),b.CoffeeHighlightRules=d}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\}/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\})/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingBraceOutdent=e}),define("ace/worker/worker_client",["require","exports","module","pilot/oop","pilot/event_emitter"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=function(b,c,d,e){this.callbacks=[];if(a.packaged)var f=this.$guessBasePath(),g=this.$worker=new Worker(f+c);else{var h=a.nameToUrl("ace/worker/worker",null,"_"),g=this.$worker=new Worker(h),i={};for(var j=0;j<b.length;j++){var k=b[j];i[k]=a.nameToUrl(k,null,"_").replace(/.js$/,"")}}this.$worker.postMessage({init:!0,tlns:i,module:d,classname:e}),this.callbackId=1,this.callbacks={};var l=this;this.$worker.onerror=function(a){window.console&&console.log&&console.log(a);throw a},this.$worker.onmessage=function(a){var b=a.data;switch(b.type){case"log":window.console&&console.log&&console.log(b.data);break;case"event":l._dispatchEvent(b.name,{data:b.data});break;case"call":var c=l.callbacks[b.id];c&&(c(b.data),delete l.callbacks[b.id])}}};(function(){d.implement(this,e),this.$guessBasePath=function(){if(a.aceBaseUrl)return a.aceBaseUrl;var b=document.getElementsByTagName("script");for(var c=0;c<b.length;c++){var d=b[c].src||b[c].getAttribute("src");if(!d)continue;var e=d.match(/^(?:(.*\/)ace\.js|(.*\/)ace-uncompressed\.js)(?:\?|$)/);if(e)return e[1]||e[2]}return""},this.terminate=function(){this._dispatchEvent("terminate",{}),this.$worker.terminate()},this.send=function(a,b){this.$worker.postMessage({command:a,args:b})},this.call=function(a,b,c){if(c){var d=this.callbackId++;this.callbacks[d]=c,b.push(d)}this.send(a,b)},this.emit=function(a,b){this.$worker.postMessage({event:a,data:b})}}).call(f.prototype),b.WorkerClient=f}) \ No newline at end of file
diff --git a/www/js/ace/mode-csharp.js b/www/js/ace/mode-csharp.js
new file mode 100644
index 0000000..4c3c12d
--- /dev/null
+++ b/www/js/ace/mode-csharp.js
@@ -0,0 +1 @@
+define("ace/mode/csharp",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/csharp_highlight_rules","ace/mode/matching_brace_outdent","ace/mode/behaviour/cstyle"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/csharp_highlight_rules").CSharpHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=a("ace/mode/behaviour/cstyle").CstyleBehaviour,j=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h,this.$behaviour=new i};d.inherits(j,e),function(){this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=this.$tokenizer.getLineTokens(b,a),f=e.tokens,g=e.state;if(f.length&&f[f.length-1].type=="comment")return d;if(a=="start"){var h=b.match(/^.*[\{\(\[]\s*$/);h&&(d+=c)}return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)},this.createWorker=function(a){return null}}.call(j.prototype),b.Mode=j}),define("ace/mode/csharp_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/doc_comment_highlight_rules").DocCommentHighlightRules,g=a("ace/mode/text_highlight_rules").TextHighlightRules,h=function(){var a=e.arrayToMap("abstract|event|new|struct|as|explicit|null|switch|base|extern|object|this|bool|false|operator|throw|break|finally|out|true|byte|fixed|override|try|case|float|params|typeof|catch|for|private|uint|char|foreach|protected|ulong|checked|goto|public|unchecked|class|if|readonly|unsafe|const|implicit|ref|ushort|continue|in|return|using|decimal|int|sbyte|virtual|default|interface|sealed|volatile|delegate|internal|short|void|do|is|sizeof|while|double|lock|stackalloc|else|long|static|enum|namespace|string|var|dynamic".split("|")),b=e.arrayToMap("null|true|false".split("|"));this.$rules={start:[{token:"comment",regex:"\\/\\/.*$"},(new f).getStartRule("doc-start"),{token:"comment",regex:"\\/\\*",next:"comment"},{token:"comment",regex:"\\/\\*\\*",next:"comment"},{token:"string.regexp",regex:"[/](?:(?:\\[(?:\\\\]|[^\\]])+\\])|(?:\\\\/|[^\\]/]))*[/]\\w*\\s*(?=[).,;]|$)"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"},{token:"constant.language.boolean",regex:"(?:true|false)\\b"},{token:function(c){return c=="this"?"variable.language":a.hasOwnProperty(c)?"keyword":b.hasOwnProperty(c)?"constant.language":"identifier"},regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"keyword.operator",regex:"!|\\$|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|instanceof|new|delete|typeof|void)"},{token:"lparen",regex:"[[({]"},{token:"rparen",regex:"[\\])}]"},{token:"text",regex:"\\s+"}],comment:[{token:"comment",regex:".*?\\*\\/",next:"start"},{token:"comment",regex:".+"}],qqstring:[{token:"string",regex:'(?:(?:\\\\.)|(?:[^"\\\\]))*?"',next:"start"},{token:"string",regex:".+"}],qstring:[{token:"string",regex:"(?:(?:\\\\.)|(?:[^'\\\\]))*?'",next:"start"},{token:"string",regex:".+"}]},this.embedRules(f,"doc-",[(new f).getEndRule("start")])};d.inherits(h,g),b.CSharpHighlightRules=h}),define("ace/mode/doc_comment_highlight_rules",["require","exports","module","pilot/oop","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text_highlight_rules").TextHighlightRules,f=function(){this.$rules={start:[{token:"comment.doc.tag",regex:"@[\\w\\d_]+"},{token:"comment.doc",regex:"\\s+"},{token:"comment.doc",regex:"TODO"},{token:"comment.doc",regex:"[^@\\*]+"},{token:"comment.doc",regex:"."}]}};d.inherits(f,e),function(){this.getStartRule=function(a){return{token:"comment.doc",regex:"\\/\\*(?=\\*)",next:a}},this.getEndRule=function(a){return{token:"comment.doc",regex:"\\*\\/",next:a}}}.call(f.prototype),b.DocCommentHighlightRules=f}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\}/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\})/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingBraceOutdent=e}),define("ace/mode/behaviour/cstyle",["require","exports","module","pilot/oop","ace/mode/behaviour"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/behaviour").Behaviour,f=function(){this.add("braces","insertion",function(a,b,c,d,e){if(e=="{"){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"{"+g+"}",selection:!1}:{text:"{}",selection:[1,1]}}if(e=="}"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var k=d.$findOpeningBracket("}",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}else if(e=="\n"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var l=d.findMatchingBracket({row:h.row,column:h.column}),m=this.getNextLineIndent(a,i.substring(0,i.length-1),d.getTabString()),n=this.$getIndent(d.doc.getLine(l.row));return{text:"\n"+m+"\n"+n,selection:[1,m.length,1,m.length]}}}return!1}),this.add("braces","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="{"){var g=d.doc.getLine(e.start.row),h=g.substring(e.end.column,e.end.column+1);if(h=="}")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("parens","insertion",function(a,b,c,d,e){if(e=="("){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"("+g+")",selection:!1}:{text:"()",selection:[1,1]}}if(e==")"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j==")"){var k=d.$findOpeningBracket(")",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}return!1}),this.add("parens","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="("){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h==")")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("string_dquotes","insertion",function(a,b,c,d,e){if(e=='"'){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);if(g!=="")return{text:'"'+g+'"',selection:!1};var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column-1,h.column);if(j=="\\")return!1;var k=d.getTokens(f.start.row,f.start.row)[0].tokens,l=0,m,n=-1;for(var o=0;o<k.length;o++){m=k[o],m.type=="string"?n=-1:n<0&&(n=m.value.indexOf('"'));if(m.value.length+l>f.start.column)break;l+=k[o].value.length}if(!m||n<0&&m.type!=="comment"&&(m.type!=="string"||f.start.column!==m.value.length+l-1&&m.value.lastIndexOf('"')===m.value.length-1))return{text:'""',selection:[1,1]};if(m&&m.type==="string"){var p=i.substring(h.column,h.column+1);if(p=='"')return{text:"",selection:[1,1]}}}return!1}),this.add("string_dquotes","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=='"'){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h=='"')return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1})};d.inherits(f,e),b.CstyleBehaviour=f}) \ No newline at end of file
diff --git a/www/js/ace/mode-css.js b/www/js/ace/mode-css.js
new file mode 100644
index 0000000..c27ad88
--- /dev/null
+++ b/www/js/ace/mode-css.js
@@ -0,0 +1 @@
+define("ace/mode/css",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/css_highlight_rules","ace/mode/matching_brace_outdent","ace/worker/worker_client"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/css_highlight_rules").CssHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=a("ace/worker/worker_client").WorkerClient,j=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h};d.inherits(j,e),function(){this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=this.$tokenizer.getLineTokens(b,a).tokens;if(e.length&&e[e.length-1].type=="comment")return d;var f=b.match(/^.*\{\s*$/);f&&(d+=c);return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)},this.createWorker=function(a){var b=a.getDocument(),c=new i(["ace","pilot"],"worker-css.js","ace/mode/css_worker","Worker");c.call("setValue",[b.getValue()]),b.on("change",function(a){a.range={start:a.data.range.start,end:a.data.range.end},c.emit("change",a)}),c.on("csslint",function(b){var c=[];b.data.forEach(function(a){c.push({row:a.line-1,column:a.col-1,text:a.message,type:a.type,lint:a})}),a.setAnnotations(c)})}}.call(j.prototype),b.Mode=j}),define("ace/mode/css_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/text_highlight_rules").TextHighlightRules,g=function(){function g(a){var b=[],c=a.split("");for(var d=0;d<c.length;d++)b.push("[",c[d].toLowerCase(),c[d].toUpperCase(),"]");return b.join("")}var a=e.arrayToMap("-moz-box-sizing|-webkit-box-sizing|appearance|azimuth|background-attachment|background-color|background-image|background-position|background-repeat|background|border-bottom-color|border-bottom-style|border-bottom-width|border-bottom|border-collapse|border-color|border-left-color|border-left-style|border-left-width|border-left|border-right-color|border-right-style|border-right-width|border-right|border-spacing|border-style|border-top-color|border-top-style|border-top-width|border-top|border-width|border|bottom|box-sizing|caption-side|clear|clip|color|content|counter-increment|counter-reset|cue-after|cue-before|cue|cursor|direction|display|elevation|empty-cells|float|font-family|font-size-adjust|font-size|font-stretch|font-style|font-variant|font-weight|font|height|left|letter-spacing|line-height|list-style-image|list-style-position|list-style-type|list-style|margin-bottom|margin-left|margin-right|margin-top|marker-offset|margin|marks|max-height|max-width|min-height|min-width|-moz-border-radius|opacity|orphans|outline-color|outline-style|outline-width|outline|overflow|overflow-x|overflow-y|padding-bottom|padding-left|padding-right|padding-top|padding|page-break-after|page-break-before|page-break-inside|page|pause-after|pause-before|pause|pitch-range|pitch|play-during|position|quotes|richness|right|size|speak-header|speak-numeral|speak-punctuation|speech-rate|speak|stress|table-layout|text-align|text-decoration|text-indent|text-shadow|text-transform|top|unicode-bidi|vertical-align|visibility|voice-family|volume|white-space|widows|width|word-spacing|z-index".split("|")),b=e.arrayToMap("rgb|rgba|url|attr|counter|counters".split("|")),c=e.arrayToMap("absolute|all-scroll|always|armenian|auto|baseline|below|bidi-override|block|bold|bolder|border-box|both|bottom|break-all|break-word|capitalize|center|char|circle|cjk-ideographic|col-resize|collapse|content-box|crosshair|dashed|decimal-leading-zero|decimal|default|disabled|disc|distribute-all-lines|distribute-letter|distribute-space|distribute|dotted|double|e-resize|ellipsis|fixed|georgian|groove|hand|hebrew|help|hidden|hiragana-iroha|hiragana|horizontal|ideograph-alpha|ideograph-numeric|ideograph-parenthesis|ideograph-space|inactive|inherit|inline-block|inline|inset|inside|inter-ideograph|inter-word|italic|justify|katakana-iroha|katakana|keep-all|left|lighter|line-edge|line-through|line|list-item|loose|lower-alpha|lower-greek|lower-latin|lower-roman|lowercase|lr-tb|ltr|medium|middle|move|n-resize|ne-resize|newspaper|no-drop|no-repeat|nw-resize|none|normal|not-allowed|nowrap|oblique|outset|outside|overline|pointer|progress|relative|repeat-x|repeat-y|repeat|right|ridge|row-resize|rtl|s-resize|scroll|se-resize|separate|small-caps|solid|square|static|strict|super|sw-resize|table-footer-group|table-header-group|tb-rl|text-bottom|text-top|text|thick|thin|top|transparent|underline|upper-alpha|upper-latin|upper-roman|uppercase|vertical-ideographic|vertical-text|visible|w-resize|wait|whitespace|zero".split("|")),d=e.arrayToMap("aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow".split("|")),f="\\-?(?:(?:[0-9]+)|(?:[0-9]*\\.[0-9]+))",h=[{token:"comment",regex:"\\/\\*",next:"ruleset_comment"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"constant.numeric",regex:f+g("em")},{token:"constant.numeric",regex:f+g("ex")},{token:"constant.numeric",regex:f+g("px")},{token:"constant.numeric",regex:f+g("cm")},{token:"constant.numeric",regex:f+g("mm")},{token:"constant.numeric",regex:f+g("in")},{token:"constant.numeric",regex:f+g("pt")},{token:"constant.numeric",regex:f+g("pc")},{token:"constant.numeric",regex:f+g("deg")},{token:"constant.numeric",regex:f+g("rad")},{token:"constant.numeric",regex:f+g("grad")},{token:"constant.numeric",regex:f+g("ms")},{token:"constant.numeric",regex:f+g("s")},{token:"constant.numeric",regex:f+g("hz")},{token:"constant.numeric",regex:f+g("khz")},{token:"constant.numeric",regex:f+"%"},{token:"constant.numeric",regex:f},{token:"constant.numeric",regex:"#[a-fA-F0-9]{6}"},{token:"constant.numeric",regex:"#[a-fA-F0-9]{3}"},{token:function(e){return a.hasOwnProperty(e.toLowerCase())?"support.type":b.hasOwnProperty(e.toLowerCase())?"support.function":c.hasOwnProperty(e.toLowerCase())?"support.constant":d.hasOwnProperty(e.toLowerCase())?"support.constant.color":"text"},regex:"\\-?[a-zA-Z_][a-zA-Z0-9_\\-]*"}],i=e.copyArray(h);i.unshift({token:"rparen",regex:"}",next:"start"});var j=e.copyArray(h);j.unshift({token:"rparen",regex:"}",next:"media"});var k=[{token:"comment",regex:".+"}],l=e.copyArray(k);l.unshift({token:"comment",regex:".*?\\*\\/",next:"start"});var m=e.copyArray(k);m.unshift({token:"comment",regex:".*?\\*\\/",next:"media"});var n=e.copyArray(k);n.unshift({token:"comment",regex:".*?\\*\\/",next:"ruleset"}),this.$rules={start:[{token:"comment",regex:"\\/\\*",next:"comment"},{token:"lparen",regex:"{",next:"ruleset"},{token:"string",regex:"@media.*?{",next:"media"},{token:"keyword",regex:"#[a-zA-Z0-9-_]+"},{token:"variable",regex:"\\.[a-zA-Z0-9-_]+"},{token:"string",regex:":[a-zA-Z0-9-_]+"},{token:"constant",regex:"[a-zA-Z0-9-_]+"}],media:[{token:"comment",regex:"\\/\\*",next:"media_comment"},{token:"lparen",regex:"{",next:"media_ruleset"},{token:"string",regex:"}",next:"start"},{token:"keyword",regex:"#[a-zA-Z0-9-_]+"},{token:"variable",regex:"\\.[a-zA-Z0-9-_]+"},{token:"string",regex:":[a-zA-Z0-9-_]+"},{token:"constant",regex:"[a-zA-Z0-9-_]+"}],comment:l,ruleset:i,ruleset_comment:n,media_ruleset:j,media_comment:m}};d.inherits(g,f),b.CssHighlightRules=g}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\}/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\})/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingBraceOutdent=e}),define("ace/worker/worker_client",["require","exports","module","pilot/oop","pilot/event_emitter"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=function(b,c,d,e){this.callbacks=[];if(a.packaged)var f=this.$guessBasePath(),g=this.$worker=new Worker(f+c);else{var h=a.nameToUrl("ace/worker/worker",null,"_"),g=this.$worker=new Worker(h),i={};for(var j=0;j<b.length;j++){var k=b[j];i[k]=a.nameToUrl(k,null,"_").replace(/.js$/,"")}}this.$worker.postMessage({init:!0,tlns:i,module:d,classname:e}),this.callbackId=1,this.callbacks={};var l=this;this.$worker.onerror=function(a){window.console&&console.log&&console.log(a);throw a},this.$worker.onmessage=function(a){var b=a.data;switch(b.type){case"log":window.console&&console.log&&console.log(b.data);break;case"event":l._dispatchEvent(b.name,{data:b.data});break;case"call":var c=l.callbacks[b.id];c&&(c(b.data),delete l.callbacks[b.id])}}};(function(){d.implement(this,e),this.$guessBasePath=function(){if(a.aceBaseUrl)return a.aceBaseUrl;var b=document.getElementsByTagName("script");for(var c=0;c<b.length;c++){var d=b[c].src||b[c].getAttribute("src");if(!d)continue;var e=d.match(/^(?:(.*\/)ace\.js|(.*\/)ace-uncompressed\.js)(?:\?|$)/);if(e)return e[1]||e[2]}return""},this.terminate=function(){this._dispatchEvent("terminate",{}),this.$worker.terminate()},this.send=function(a,b){this.$worker.postMessage({command:a,args:b})},this.call=function(a,b,c){if(c){var d=this.callbackId++;this.callbacks[d]=c,b.push(d)}this.send(a,b)},this.emit=function(a,b){this.$worker.postMessage({event:a,data:b})}}).call(f.prototype),b.WorkerClient=f}) \ No newline at end of file
diff --git a/www/js/ace/mode-html.js b/www/js/ace/mode-html.js
new file mode 100644
index 0000000..526a1cb
--- /dev/null
+++ b/www/js/ace/mode-html.js
@@ -0,0 +1 @@
+define("ace/mode/html",["require","exports","module","pilot/oop","ace/mode/text","ace/mode/javascript","ace/mode/css","ace/tokenizer","ace/mode/html_highlight_rules","ace/mode/behaviour/xml"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/mode/javascript").Mode,g=a("ace/mode/css").Mode,h=a("ace/tokenizer").Tokenizer,i=a("ace/mode/html_highlight_rules").HtmlHighlightRules,j=a("ace/mode/behaviour/xml").XmlBehaviour,k=function(){var a=new i;this.$tokenizer=new h(a.getRules()),this.$behaviour=new j,this.$embeds=a.getEmbeds(),this.createModeDelegates({"js-":f,"css-":g})};d.inherits(k,e),function(){this.toggleCommentLines=function(a,b,c,d){return 0},this.getNextLineIndent=function(a,b,c){return this.$getIndent(b)},this.checkOutdent=function(a,b,c){return!1}}.call(k.prototype),b.Mode=k}),define("ace/mode/javascript",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/javascript_highlight_rules","ace/mode/matching_brace_outdent","ace/range","ace/worker/worker_client","ace/mode/behaviour/cstyle"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/javascript_highlight_rules").JavaScriptHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=a("ace/range").Range,j=a("ace/worker/worker_client").WorkerClient,k=a("ace/mode/behaviour/cstyle").CstyleBehaviour,l=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h,this.$behaviour=new k};d.inherits(l,e),function(){this.toggleCommentLines=function(a,b,c,d){var e=!0,f=[],g=/^(\s*)\/\//;for(var h=c;h<=d;h++)if(!g.test(b.getLine(h))){e=!1;break}if(e){var j=new i(0,0,0,0);for(var h=c;h<=d;h++){var k=b.getLine(h),l=k.match(g);j.start.row=h,j.end.row=h,j.end.column=l[0].length,b.replace(j,l[1])}}else b.indentRows(c,d,"//")},this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=this.$tokenizer.getLineTokens(b,a),f=e.tokens,g=e.state;if(f.length&&f[f.length-1].type=="comment")return d;if(a=="start"){var h=b.match(/^.*[\{\(\[]\s*$/);h&&(d+=c)}else if(a=="doc-start"){if(g=="start")return"";var h=b.match(/^\s*(\/?)\*/);h&&(h[1]&&(d+=" "),d+="* ")}return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)},this.createWorker=function(a){var b=a.getDocument(),c=new j(["ace","pilot"],"worker-javascript.js","ace/mode/javascript_worker","JavaScriptWorker");c.call("setValue",[b.getValue()]),b.on("change",function(a){a.range={start:a.data.range.start,end:a.data.range.end},c.emit("change",a)}),c.on("jslint",function(b){var c=[];for(var d=0;d<b.data.length;d++){var e=b.data[d];e&&c.push({row:e.line-1,column:e.character-1,text:e.reason,type:"warning",lint:e})}a.setAnnotations(c)}),c.on("narcissus",function(b){a.setAnnotations([b.data])}),c.on("terminate",function(){a.clearAnnotations()});return c}}.call(l.prototype),b.Mode=l}),define("ace/mode/javascript_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/doc_comment_highlight_rules").DocCommentHighlightRules,g=a("ace/mode/text_highlight_rules").TextHighlightRules,h=function(){var a=e.arrayToMap("break|case|catch|continue|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|throw|try|typeof|let|var|while|with|const|yield|import|get|set".split("|")),b=e.arrayToMap("null|Infinity|NaN|undefined".split("|")),c=e.arrayToMap("class|enum|extends|super|export|implements|private|public|interface|package|protected|static".split("|"));this.$rules={start:[{token:"comment",regex:"\\/\\/.*$"},(new f).getStartRule("doc-start"),{token:"comment",regex:"\\/\\*",next:"comment"},{token:"string.regexp",regex:"[/](?:(?:\\[(?:\\\\]|[^\\]])+\\])|(?:\\\\/|[^\\]/]))*[/]\\w*\\s*(?=[).,;]|$)"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:'["].*\\\\$',next:"qqstring"},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"string",regex:"['].*\\\\$",next:"qstring"},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"},{token:"constant.language.boolean",regex:"(?:true|false)\\b"},{token:function(d){return d=="this"?"variable.language":a.hasOwnProperty(d)?"keyword":b.hasOwnProperty(d)?"constant.language":c.hasOwnProperty(d)?"invalid.illegal":d=="debugger"?"invalid.deprecated":"identifier"},regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"keyword.operator",regex:"!|\\$|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|instanceof|new|delete|typeof|void)"},{token:"lparen",regex:"[[({]"},{token:"rparen",regex:"[\\])}]"},{token:"comment",regex:"^#!.*$"},{token:"text",regex:"\\s+"}],comment:[{token:"comment",regex:".*?\\*\\/",next:"start"},{token:"comment",regex:".+"}],qqstring:[{token:"string",regex:'(?:(?:\\\\.)|(?:[^"\\\\]))*?"',next:"start"},{token:"string",regex:".+"}],qstring:[{token:"string",regex:"(?:(?:\\\\.)|(?:[^'\\\\]))*?'",next:"start"},{token:"string",regex:".+"}]},this.embedRules(f,"doc-",[(new f).getEndRule("start")])};d.inherits(h,g),b.JavaScriptHighlightRules=h}),define("ace/mode/doc_comment_highlight_rules",["require","exports","module","pilot/oop","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text_highlight_rules").TextHighlightRules,f=function(){this.$rules={start:[{token:"comment.doc.tag",regex:"@[\\w\\d_]+"},{token:"comment.doc",regex:"\\s+"},{token:"comment.doc",regex:"TODO"},{token:"comment.doc",regex:"[^@\\*]+"},{token:"comment.doc",regex:"."}]}};d.inherits(f,e),function(){this.getStartRule=function(a){return{token:"comment.doc",regex:"\\/\\*(?=\\*)",next:a}},this.getEndRule=function(a){return{token:"comment.doc",regex:"\\*\\/",next:a}}}.call(f.prototype),b.DocCommentHighlightRules=f}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\}/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\})/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingBraceOutdent=e}),define("ace/worker/worker_client",["require","exports","module","pilot/oop","pilot/event_emitter"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=function(b,c,d,e){this.callbacks=[];if(a.packaged)var f=this.$guessBasePath(),g=this.$worker=new Worker(f+c);else{var h=a.nameToUrl("ace/worker/worker",null,"_"),g=this.$worker=new Worker(h),i={};for(var j=0;j<b.length;j++){var k=b[j];i[k]=a.nameToUrl(k,null,"_").replace(/.js$/,"")}}this.$worker.postMessage({init:!0,tlns:i,module:d,classname:e}),this.callbackId=1,this.callbacks={};var l=this;this.$worker.onerror=function(a){window.console&&console.log&&console.log(a);throw a},this.$worker.onmessage=function(a){var b=a.data;switch(b.type){case"log":window.console&&console.log&&console.log(b.data);break;case"event":l._dispatchEvent(b.name,{data:b.data});break;case"call":var c=l.callbacks[b.id];c&&(c(b.data),delete l.callbacks[b.id])}}};(function(){d.implement(this,e),this.$guessBasePath=function(){if(a.aceBaseUrl)return a.aceBaseUrl;var b=document.getElementsByTagName("script");for(var c=0;c<b.length;c++){var d=b[c].src||b[c].getAttribute("src");if(!d)continue;var e=d.match(/^(?:(.*\/)ace\.js|(.*\/)ace-uncompressed\.js)(?:\?|$)/);if(e)return e[1]||e[2]}return""},this.terminate=function(){this._dispatchEvent("terminate",{}),this.$worker.terminate()},this.send=function(a,b){this.$worker.postMessage({command:a,args:b})},this.call=function(a,b,c){if(c){var d=this.callbackId++;this.callbacks[d]=c,b.push(d)}this.send(a,b)},this.emit=function(a,b){this.$worker.postMessage({event:a,data:b})}}).call(f.prototype),b.WorkerClient=f}),define("ace/mode/behaviour/cstyle",["require","exports","module","pilot/oop","ace/mode/behaviour"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/behaviour").Behaviour,f=function(){this.add("braces","insertion",function(a,b,c,d,e){if(e=="{"){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"{"+g+"}",selection:!1}:{text:"{}",selection:[1,1]}}if(e=="}"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var k=d.$findOpeningBracket("}",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}else if(e=="\n"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var l=d.findMatchingBracket({row:h.row,column:h.column}),m=this.getNextLineIndent(a,i.substring(0,i.length-1),d.getTabString()),n=this.$getIndent(d.doc.getLine(l.row));return{text:"\n"+m+"\n"+n,selection:[1,m.length,1,m.length]}}}return!1}),this.add("braces","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="{"){var g=d.doc.getLine(e.start.row),h=g.substring(e.end.column,e.end.column+1);if(h=="}")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("parens","insertion",function(a,b,c,d,e){if(e=="("){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"("+g+")",selection:!1}:{text:"()",selection:[1,1]}}if(e==")"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j==")"){var k=d.$findOpeningBracket(")",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}return!1}),this.add("parens","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="("){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h==")")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("string_dquotes","insertion",function(a,b,c,d,e){if(e=='"'){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);if(g!=="")return{text:'"'+g+'"',selection:!1};var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column-1,h.column);if(j=="\\")return!1;var k=d.getTokens(f.start.row,f.start.row)[0].tokens,l=0,m,n=-1;for(var o=0;o<k.length;o++){m=k[o],m.type=="string"?n=-1:n<0&&(n=m.value.indexOf('"'));if(m.value.length+l>f.start.column)break;l+=k[o].value.length}if(!m||n<0&&m.type!=="comment"&&(m.type!=="string"||f.start.column!==m.value.length+l-1&&m.value.lastIndexOf('"')===m.value.length-1))return{text:'""',selection:[1,1]};if(m&&m.type==="string"){var p=i.substring(h.column,h.column+1);if(p=='"')return{text:"",selection:[1,1]}}}return!1}),this.add("string_dquotes","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=='"'){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h=='"')return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1})};d.inherits(f,e),b.CstyleBehaviour=f}),define("ace/mode/css",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/css_highlight_rules","ace/mode/matching_brace_outdent","ace/worker/worker_client"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/css_highlight_rules").CssHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=a("ace/worker/worker_client").WorkerClient,j=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h};d.inherits(j,e),function(){this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=this.$tokenizer.getLineTokens(b,a).tokens;if(e.length&&e[e.length-1].type=="comment")return d;var f=b.match(/^.*\{\s*$/);f&&(d+=c);return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)},this.createWorker=function(a){var b=a.getDocument(),c=new i(["ace","pilot"],"worker-css.js","ace/mode/css_worker","Worker");c.call("setValue",[b.getValue()]),b.on("change",function(a){a.range={start:a.data.range.start,end:a.data.range.end},c.emit("change",a)}),c.on("csslint",function(b){var c=[];b.data.forEach(function(a){c.push({row:a.line-1,column:a.col-1,text:a.message,type:a.type,lint:a})}),a.setAnnotations(c)})}}.call(j.prototype),b.Mode=j}),define("ace/mode/css_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/text_highlight_rules").TextHighlightRules,g=function(){function g(a){var b=[],c=a.split("");for(var d=0;d<c.length;d++)b.push("[",c[d].toLowerCase(),c[d].toUpperCase(),"]");return b.join("")}var a=e.arrayToMap("-moz-box-sizing|-webkit-box-sizing|appearance|azimuth|background-attachment|background-color|background-image|background-position|background-repeat|background|border-bottom-color|border-bottom-style|border-bottom-width|border-bottom|border-collapse|border-color|border-left-color|border-left-style|border-left-width|border-left|border-right-color|border-right-style|border-right-width|border-right|border-spacing|border-style|border-top-color|border-top-style|border-top-width|border-top|border-width|border|bottom|box-sizing|caption-side|clear|clip|color|content|counter-increment|counter-reset|cue-after|cue-before|cue|cursor|direction|display|elevation|empty-cells|float|font-family|font-size-adjust|font-size|font-stretch|font-style|font-variant|font-weight|font|height|left|letter-spacing|line-height|list-style-image|list-style-position|list-style-type|list-style|margin-bottom|margin-left|margin-right|margin-top|marker-offset|margin|marks|max-height|max-width|min-height|min-width|-moz-border-radius|opacity|orphans|outline-color|outline-style|outline-width|outline|overflow|overflow-x|overflow-y|padding-bottom|padding-left|padding-right|padding-top|padding|page-break-after|page-break-before|page-break-inside|page|pause-after|pause-before|pause|pitch-range|pitch|play-during|position|quotes|richness|right|size|speak-header|speak-numeral|speak-punctuation|speech-rate|speak|stress|table-layout|text-align|text-decoration|text-indent|text-shadow|text-transform|top|unicode-bidi|vertical-align|visibility|voice-family|volume|white-space|widows|width|word-spacing|z-index".split("|")),b=e.arrayToMap("rgb|rgba|url|attr|counter|counters".split("|")),c=e.arrayToMap("absolute|all-scroll|always|armenian|auto|baseline|below|bidi-override|block|bold|bolder|border-box|both|bottom|break-all|break-word|capitalize|center|char|circle|cjk-ideographic|col-resize|collapse|content-box|crosshair|dashed|decimal-leading-zero|decimal|default|disabled|disc|distribute-all-lines|distribute-letter|distribute-space|distribute|dotted|double|e-resize|ellipsis|fixed|georgian|groove|hand|hebrew|help|hidden|hiragana-iroha|hiragana|horizontal|ideograph-alpha|ideograph-numeric|ideograph-parenthesis|ideograph-space|inactive|inherit|inline-block|inline|inset|inside|inter-ideograph|inter-word|italic|justify|katakana-iroha|katakana|keep-all|left|lighter|line-edge|line-through|line|list-item|loose|lower-alpha|lower-greek|lower-latin|lower-roman|lowercase|lr-tb|ltr|medium|middle|move|n-resize|ne-resize|newspaper|no-drop|no-repeat|nw-resize|none|normal|not-allowed|nowrap|oblique|outset|outside|overline|pointer|progress|relative|repeat-x|repeat-y|repeat|right|ridge|row-resize|rtl|s-resize|scroll|se-resize|separate|small-caps|solid|square|static|strict|super|sw-resize|table-footer-group|table-header-group|tb-rl|text-bottom|text-top|text|thick|thin|top|transparent|underline|upper-alpha|upper-latin|upper-roman|uppercase|vertical-ideographic|vertical-text|visible|w-resize|wait|whitespace|zero".split("|")),d=e.arrayToMap("aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow".split("|")),f="\\-?(?:(?:[0-9]+)|(?:[0-9]*\\.[0-9]+))",h=[{token:"comment",regex:"\\/\\*",next:"ruleset_comment"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"constant.numeric",regex:f+g("em")},{token:"constant.numeric",regex:f+g("ex")},{token:"constant.numeric",regex:f+g("px")},{token:"constant.numeric",regex:f+g("cm")},{token:"constant.numeric",regex:f+g("mm")},{token:"constant.numeric",regex:f+g("in")},{token:"constant.numeric",regex:f+g("pt")},{token:"constant.numeric",regex:f+g("pc")},{token:"constant.numeric",regex:f+g("deg")},{token:"constant.numeric",regex:f+g("rad")},{token:"constant.numeric",regex:f+g("grad")},{token:"constant.numeric",regex:f+g("ms")},{token:"constant.numeric",regex:f+g("s")},{token:"constant.numeric",regex:f+g("hz")},{token:"constant.numeric",regex:f+g("khz")},{token:"constant.numeric",regex:f+"%"},{token:"constant.numeric",regex:f},{token:"constant.numeric",regex:"#[a-fA-F0-9]{6}"},{token:"constant.numeric",regex:"#[a-fA-F0-9]{3}"},{token:function(e){return a.hasOwnProperty(e.toLowerCase())?"support.type":b.hasOwnProperty(e.toLowerCase())?"support.function":c.hasOwnProperty(e.toLowerCase())?"support.constant":d.hasOwnProperty(e.toLowerCase())?"support.constant.color":"text"},regex:"\\-?[a-zA-Z_][a-zA-Z0-9_\\-]*"}],i=e.copyArray(h);i.unshift({token:"rparen",regex:"}",next:"start"});var j=e.copyArray(h);j.unshift({token:"rparen",regex:"}",next:"media"});var k=[{token:"comment",regex:".+"}],l=e.copyArray(k);l.unshift({token:"comment",regex:".*?\\*\\/",next:"start"});var m=e.copyArray(k);m.unshift({token:"comment",regex:".*?\\*\\/",next:"media"});var n=e.copyArray(k);n.unshift({token:"comment",regex:".*?\\*\\/",next:"ruleset"}),this.$rules={start:[{token:"comment",regex:"\\/\\*",next:"comment"},{token:"lparen",regex:"{",next:"ruleset"},{token:"string",regex:"@media.*?{",next:"media"},{token:"keyword",regex:"#[a-zA-Z0-9-_]+"},{token:"variable",regex:"\\.[a-zA-Z0-9-_]+"},{token:"string",regex:":[a-zA-Z0-9-_]+"},{token:"constant",regex:"[a-zA-Z0-9-_]+"}],media:[{token:"comment",regex:"\\/\\*",next:"media_comment"},{token:"lparen",regex:"{",next:"media_ruleset"},{token:"string",regex:"}",next:"start"},{token:"keyword",regex:"#[a-zA-Z0-9-_]+"},{token:"variable",regex:"\\.[a-zA-Z0-9-_]+"},{token:"string",regex:":[a-zA-Z0-9-_]+"},{token:"constant",regex:"[a-zA-Z0-9-_]+"}],comment:l,ruleset:i,ruleset_comment:n,media_ruleset:j,media_comment:m}};d.inherits(g,f),b.CssHighlightRules=g}),define("ace/mode/html_highlight_rules",["require","exports","module","pilot/oop","ace/mode/css_highlight_rules","ace/mode/javascript_highlight_rules","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/css_highlight_rules").CssHighlightRules,f=a("ace/mode/javascript_highlight_rules").JavaScriptHighlightRules,g=a("ace/mode/text_highlight_rules").TextHighlightRules,h=function(){function b(a,b){return[{token:"string",regex:".*"+a,next:b},{token:"string",regex:".+"}]}function a(a){return[{token:"string",regex:'".*?"'},{token:"string",regex:'["].*$',next:a+"-qqstring"},{token:"string",regex:"'.*?'"},{token:"string",regex:"['].*$",next:a+"-qstring"}]}this.$rules={start:[{token:"text",regex:"<\\!\\[CDATA\\[",next:"cdata"},{token:"xml_pe",regex:"<\\?.*?\\?>"},{token:"comment",regex:"<\\!--",next:"comment"},{token:"text",regex:"<(?=s*script)",next:"script"},{token:"text",regex:"<(?=s*style)",next:"css"},{token:"text",regex:"<\\/?",next:"tag"},{token:"text",regex:"\\s+"},{token:"text",regex:"[^<]+"}],script:[{token:"text",regex:">",next:"js-start"},{token:"keyword",regex:"[-_a-zA-Z0-9:]+"},{token:"text",regex:"\\s+"}].concat(a("script")),css:[{token:"text",regex:">",next:"css-start"},{token:"keyword",regex:"[-_a-zA-Z0-9:]+"},{token:"text",regex:"\\s+"}].concat(a("style")),tag:[{token:"text",regex:">",next:"start"},{token:"keyword",regex:"[-_a-zA-Z0-9:]+"},{token:"text",regex:"\\s+"}].concat(a("tag")),"style-qstring":b("'","style"),"style-qqstring":b('"',"style"),"script-qstring":b("'","script"),"script-qqstring":b('"',"script"),"tag-qstring":b("'","tag"),"tag-qqstring":b('"',"tag"),cdata:[{token:"text",regex:"\\]\\]>",next:"start"},{token:"text",regex:"\\s+"},{token:"text",regex:".+"}],comment:[{token:"comment",regex:".*?-->",next:"start"},{token:"comment",regex:".+"}]},this.embedRules(f,"js-",[{token:"comment",regex:"\\/\\/.*(?=<\\/script>)",next:"tag"},{token:"text",regex:"<\\/(?=script)",next:"tag"}]),this.embedRules(e,"css-",[{token:"text",regex:"<\\/(?=style)",next:"tag"}])};d.inherits(h,g),b.HtmlHighlightRules=h}),define("ace/mode/behaviour/xml",["require","exports","module","pilot/oop","ace/mode/behaviour","ace/mode/behaviour/cstyle"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/behaviour").Behaviour,f=a("ace/mode/behaviour/cstyle").CstyleBehaviour,g=function(){this.inherit(f,["string_dquotes"]),this.add("brackets","insertion",function(a,b,c,d,e){if(e=="<"){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?!1:{text:"<>",selection:[1,1]}}if(e==">"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j==">")return{text:"",selection:[1,1]}}else if(e=="\n"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),k=i.substring(h.column,h.column+2);if(k=="</"){var l=this.$getIndent(d.doc.getLine(h.row))+d.getTabString(),m=this.$getIndent(d.doc.getLine(h.row));return{text:"\n"+l+"\n"+m,selection:[1,l.length,1,l.length]}}}return!1})};d.inherits(g,e),b.XmlBehaviour=g}) \ No newline at end of file
diff --git a/www/js/ace/mode-java.js b/www/js/ace/mode-java.js
new file mode 100644
index 0000000..93fc3b9
--- /dev/null
+++ b/www/js/ace/mode-java.js
@@ -0,0 +1 @@
+define("ace/mode/java",["require","exports","module","pilot/oop","ace/mode/javascript","ace/tokenizer","ace/mode/java_highlight_rules","ace/mode/matching_brace_outdent","ace/mode/behaviour/cstyle"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/javascript").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/java_highlight_rules").JavaHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=a("ace/mode/behaviour/cstyle").CstyleBehaviour,j=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h,this.$behaviour=new i};d.inherits(j,e),function(){this.createWorker=function(a){return null}}.call(j.prototype),b.Mode=j}),define("ace/mode/javascript",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/javascript_highlight_rules","ace/mode/matching_brace_outdent","ace/range","ace/worker/worker_client","ace/mode/behaviour/cstyle"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/javascript_highlight_rules").JavaScriptHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=a("ace/range").Range,j=a("ace/worker/worker_client").WorkerClient,k=a("ace/mode/behaviour/cstyle").CstyleBehaviour,l=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h,this.$behaviour=new k};d.inherits(l,e),function(){this.toggleCommentLines=function(a,b,c,d){var e=!0,f=[],g=/^(\s*)\/\//;for(var h=c;h<=d;h++)if(!g.test(b.getLine(h))){e=!1;break}if(e){var j=new i(0,0,0,0);for(var h=c;h<=d;h++){var k=b.getLine(h),l=k.match(g);j.start.row=h,j.end.row=h,j.end.column=l[0].length,b.replace(j,l[1])}}else b.indentRows(c,d,"//")},this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=this.$tokenizer.getLineTokens(b,a),f=e.tokens,g=e.state;if(f.length&&f[f.length-1].type=="comment")return d;if(a=="start"){var h=b.match(/^.*[\{\(\[]\s*$/);h&&(d+=c)}else if(a=="doc-start"){if(g=="start")return"";var h=b.match(/^\s*(\/?)\*/);h&&(h[1]&&(d+=" "),d+="* ")}return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)},this.createWorker=function(a){var b=a.getDocument(),c=new j(["ace","pilot"],"worker-javascript.js","ace/mode/javascript_worker","JavaScriptWorker");c.call("setValue",[b.getValue()]),b.on("change",function(a){a.range={start:a.data.range.start,end:a.data.range.end},c.emit("change",a)}),c.on("jslint",function(b){var c=[];for(var d=0;d<b.data.length;d++){var e=b.data[d];e&&c.push({row:e.line-1,column:e.character-1,text:e.reason,type:"warning",lint:e})}a.setAnnotations(c)}),c.on("narcissus",function(b){a.setAnnotations([b.data])}),c.on("terminate",function(){a.clearAnnotations()});return c}}.call(l.prototype),b.Mode=l}),define("ace/mode/javascript_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/doc_comment_highlight_rules").DocCommentHighlightRules,g=a("ace/mode/text_highlight_rules").TextHighlightRules,h=function(){var a=e.arrayToMap("break|case|catch|continue|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|throw|try|typeof|let|var|while|with|const|yield|import|get|set".split("|")),b=e.arrayToMap("null|Infinity|NaN|undefined".split("|")),c=e.arrayToMap("class|enum|extends|super|export|implements|private|public|interface|package|protected|static".split("|"));this.$rules={start:[{token:"comment",regex:"\\/\\/.*$"},(new f).getStartRule("doc-start"),{token:"comment",regex:"\\/\\*",next:"comment"},{token:"string.regexp",regex:"[/](?:(?:\\[(?:\\\\]|[^\\]])+\\])|(?:\\\\/|[^\\]/]))*[/]\\w*\\s*(?=[).,;]|$)"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:'["].*\\\\$',next:"qqstring"},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"string",regex:"['].*\\\\$",next:"qstring"},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"},{token:"constant.language.boolean",regex:"(?:true|false)\\b"},{token:function(d){return d=="this"?"variable.language":a.hasOwnProperty(d)?"keyword":b.hasOwnProperty(d)?"constant.language":c.hasOwnProperty(d)?"invalid.illegal":d=="debugger"?"invalid.deprecated":"identifier"},regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"keyword.operator",regex:"!|\\$|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|instanceof|new|delete|typeof|void)"},{token:"lparen",regex:"[[({]"},{token:"rparen",regex:"[\\])}]"},{token:"comment",regex:"^#!.*$"},{token:"text",regex:"\\s+"}],comment:[{token:"comment",regex:".*?\\*\\/",next:"start"},{token:"comment",regex:".+"}],qqstring:[{token:"string",regex:'(?:(?:\\\\.)|(?:[^"\\\\]))*?"',next:"start"},{token:"string",regex:".+"}],qstring:[{token:"string",regex:"(?:(?:\\\\.)|(?:[^'\\\\]))*?'",next:"start"},{token:"string",regex:".+"}]},this.embedRules(f,"doc-",[(new f).getEndRule("start")])};d.inherits(h,g),b.JavaScriptHighlightRules=h}),define("ace/mode/doc_comment_highlight_rules",["require","exports","module","pilot/oop","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text_highlight_rules").TextHighlightRules,f=function(){this.$rules={start:[{token:"comment.doc.tag",regex:"@[\\w\\d_]+"},{token:"comment.doc",regex:"\\s+"},{token:"comment.doc",regex:"TODO"},{token:"comment.doc",regex:"[^@\\*]+"},{token:"comment.doc",regex:"."}]}};d.inherits(f,e),function(){this.getStartRule=function(a){return{token:"comment.doc",regex:"\\/\\*(?=\\*)",next:a}},this.getEndRule=function(a){return{token:"comment.doc",regex:"\\*\\/",next:a}}}.call(f.prototype),b.DocCommentHighlightRules=f}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\}/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\})/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingBraceOutdent=e}),define("ace/worker/worker_client",["require","exports","module","pilot/oop","pilot/event_emitter"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=function(b,c,d,e){this.callbacks=[];if(a.packaged)var f=this.$guessBasePath(),g=this.$worker=new Worker(f+c);else{var h=a.nameToUrl("ace/worker/worker",null,"_"),g=this.$worker=new Worker(h),i={};for(var j=0;j<b.length;j++){var k=b[j];i[k]=a.nameToUrl(k,null,"_").replace(/.js$/,"")}}this.$worker.postMessage({init:!0,tlns:i,module:d,classname:e}),this.callbackId=1,this.callbacks={};var l=this;this.$worker.onerror=function(a){window.console&&console.log&&console.log(a);throw a},this.$worker.onmessage=function(a){var b=a.data;switch(b.type){case"log":window.console&&console.log&&console.log(b.data);break;case"event":l._dispatchEvent(b.name,{data:b.data});break;case"call":var c=l.callbacks[b.id];c&&(c(b.data),delete l.callbacks[b.id])}}};(function(){d.implement(this,e),this.$guessBasePath=function(){if(a.aceBaseUrl)return a.aceBaseUrl;var b=document.getElementsByTagName("script");for(var c=0;c<b.length;c++){var d=b[c].src||b[c].getAttribute("src");if(!d)continue;var e=d.match(/^(?:(.*\/)ace\.js|(.*\/)ace-uncompressed\.js)(?:\?|$)/);if(e)return e[1]||e[2]}return""},this.terminate=function(){this._dispatchEvent("terminate",{}),this.$worker.terminate()},this.send=function(a,b){this.$worker.postMessage({command:a,args:b})},this.call=function(a,b,c){if(c){var d=this.callbackId++;this.callbacks[d]=c,b.push(d)}this.send(a,b)},this.emit=function(a,b){this.$worker.postMessage({event:a,data:b})}}).call(f.prototype),b.WorkerClient=f}),define("ace/mode/behaviour/cstyle",["require","exports","module","pilot/oop","ace/mode/behaviour"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/behaviour").Behaviour,f=function(){this.add("braces","insertion",function(a,b,c,d,e){if(e=="{"){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"{"+g+"}",selection:!1}:{text:"{}",selection:[1,1]}}if(e=="}"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var k=d.$findOpeningBracket("}",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}else if(e=="\n"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var l=d.findMatchingBracket({row:h.row,column:h.column}),m=this.getNextLineIndent(a,i.substring(0,i.length-1),d.getTabString()),n=this.$getIndent(d.doc.getLine(l.row));return{text:"\n"+m+"\n"+n,selection:[1,m.length,1,m.length]}}}return!1}),this.add("braces","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="{"){var g=d.doc.getLine(e.start.row),h=g.substring(e.end.column,e.end.column+1);if(h=="}")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("parens","insertion",function(a,b,c,d,e){if(e=="("){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"("+g+")",selection:!1}:{text:"()",selection:[1,1]}}if(e==")"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j==")"){var k=d.$findOpeningBracket(")",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}return!1}),this.add("parens","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="("){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h==")")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("string_dquotes","insertion",function(a,b,c,d,e){if(e=='"'){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);if(g!=="")return{text:'"'+g+'"',selection:!1};var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column-1,h.column);if(j=="\\")return!1;var k=d.getTokens(f.start.row,f.start.row)[0].tokens,l=0,m,n=-1;for(var o=0;o<k.length;o++){m=k[o],m.type=="string"?n=-1:n<0&&(n=m.value.indexOf('"'));if(m.value.length+l>f.start.column)break;l+=k[o].value.length}if(!m||n<0&&m.type!=="comment"&&(m.type!=="string"||f.start.column!==m.value.length+l-1&&m.value.lastIndexOf('"')===m.value.length-1))return{text:'""',selection:[1,1]};if(m&&m.type==="string"){var p=i.substring(h.column,h.column+1);if(p=='"')return{text:"",selection:[1,1]}}}return!1}),this.add("string_dquotes","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=='"'){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h=='"')return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1})};d.inherits(f,e),b.CstyleBehaviour=f}),define("ace/mode/java_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/doc_comment_highlight_rules").DocCommentHighlightRules,g=a("ace/mode/text_highlight_rules").TextHighlightRules,h=function(){var a=e.arrayToMap("abstract|continue|for|new|switch|assert|default|goto|package|synchronized|boolean|do|if|private|this|break|double|implements|protected|throw|byte|else|import|public|throws|case|enum|instanceof|return|transient|catch|extends|int|short|try|char|final|interface|static|void|class|finally|long|strictfp|volatile|const|float|native|super|while".split("|")),b=e.arrayToMap("null|Infinity|NaN|undefined".split("|")),c=e.arrayToMap("AbstractMethodError|AssertionError|ClassCircularityError|ClassFormatError|Deprecated|EnumConstantNotPresentException|ExceptionInInitializerError|IllegalAccessError|IllegalThreadStateException|InstantiationError|InternalError|NegativeArraySizeException|NoSuchFieldError|Override|Process|ProcessBuilder|SecurityManager|StringIndexOutOfBoundsException|SuppressWarnings|TypeNotPresentException|UnknownError|UnsatisfiedLinkError|UnsupportedClassVersionError|VerifyError|InstantiationException|IndexOutOfBoundsException|ArrayIndexOutOfBoundsException|CloneNotSupportedException|NoSuchFieldException|IllegalArgumentException|NumberFormatException|SecurityException|Void|InheritableThreadLocal|IllegalStateException|InterruptedException|NoSuchMethodException|IllegalAccessException|UnsupportedOperationException|Enum|StrictMath|Package|Compiler|Readable|Runtime|StringBuilder|Math|IncompatibleClassChangeError|NoSuchMethodError|ThreadLocal|RuntimePermission|ArithmeticException|NullPointerException|Long|Integer|Short|Byte|Double|Number|Float|Character|Boolean|StackTraceElement|Appendable|StringBuffer|Iterable|ThreadGroup|Runnable|Thread|IllegalMonitorStateException|StackOverflowError|OutOfMemoryError|VirtualMachineError|ArrayStoreException|ClassCastException|LinkageError|NoClassDefFoundError|ClassNotFoundException|RuntimeException|Exception|ThreadDeath|Error|Throwable|System|ClassLoader|Cloneable|Class|CharSequence|Comparable|String|Object".split("|")),d=e.arrayToMap("".split("|"));this.$rules={start:[{token:"comment",regex:"\\/\\/.*$"},(new f).getStartRule("doc-start"),{token:"comment",regex:"\\/\\*",next:"comment"},{token:"comment",regex:"\\/\\*\\*",next:"comment"},{token:"string.regexp",regex:"[/](?:(?:\\[(?:\\\\]|[^\\]])+\\])|(?:\\\\/|[^\\]/]))*[/]\\w*\\s*(?=[).,;]|$)"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"},{token:"constant.language.boolean",regex:"(?:true|false)\\b"},{token:function(e){return e=="this"?"variable.language":a.hasOwnProperty(e)?"keyword":c.hasOwnProperty(e)?"support.function":d.hasOwnProperty(e)?"support.function":b.hasOwnProperty(e)?"constant.language":"identifier"},regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"keyword.operator",regex:"!|\\$|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|instanceof|new|delete|typeof|void)"},{token:"lparen",regex:"[[({]"},{token:"rparen",regex:"[\\])}]"},{token:"text",regex:"\\s+"}],comment:[{token:"comment",regex:".*?\\*\\/",next:"start"},{token:"comment",regex:".+"}],qqstring:[{token:"string",regex:'(?:(?:\\\\.)|(?:[^"\\\\]))*?"',next:"start"},{token:"string",regex:".+"}],qstring:[{token:"string",regex:"(?:(?:\\\\.)|(?:[^'\\\\]))*?'",next:"start"},{token:"string",regex:".+"}]},this.embedRules(f,"doc-",[(new f).getEndRule("start")])};d.inherits(h,g),b.JavaHighlightRules=h}) \ No newline at end of file
diff --git a/www/js/ace/mode-javascript.js b/www/js/ace/mode-javascript.js
new file mode 100644
index 0000000..f365db4
--- /dev/null
+++ b/www/js/ace/mode-javascript.js
@@ -0,0 +1 @@
+define("ace/mode/javascript",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/javascript_highlight_rules","ace/mode/matching_brace_outdent","ace/range","ace/worker/worker_client","ace/mode/behaviour/cstyle"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/javascript_highlight_rules").JavaScriptHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=a("ace/range").Range,j=a("ace/worker/worker_client").WorkerClient,k=a("ace/mode/behaviour/cstyle").CstyleBehaviour,l=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h,this.$behaviour=new k};d.inherits(l,e),function(){this.toggleCommentLines=function(a,b,c,d){var e=!0,f=[],g=/^(\s*)\/\//;for(var h=c;h<=d;h++)if(!g.test(b.getLine(h))){e=!1;break}if(e){var j=new i(0,0,0,0);for(var h=c;h<=d;h++){var k=b.getLine(h),l=k.match(g);j.start.row=h,j.end.row=h,j.end.column=l[0].length,b.replace(j,l[1])}}else b.indentRows(c,d,"//")},this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=this.$tokenizer.getLineTokens(b,a),f=e.tokens,g=e.state;if(f.length&&f[f.length-1].type=="comment")return d;if(a=="start"){var h=b.match(/^.*[\{\(\[]\s*$/);h&&(d+=c)}else if(a=="doc-start"){if(g=="start")return"";var h=b.match(/^\s*(\/?)\*/);h&&(h[1]&&(d+=" "),d+="* ")}return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)},this.createWorker=function(a){var b=a.getDocument(),c=new j(["ace","pilot"],"worker-javascript.js","ace/mode/javascript_worker","JavaScriptWorker");c.call("setValue",[b.getValue()]),b.on("change",function(a){a.range={start:a.data.range.start,end:a.data.range.end},c.emit("change",a)}),c.on("jslint",function(b){var c=[];for(var d=0;d<b.data.length;d++){var e=b.data[d];e&&c.push({row:e.line-1,column:e.character-1,text:e.reason,type:"warning",lint:e})}a.setAnnotations(c)}),c.on("narcissus",function(b){a.setAnnotations([b.data])}),c.on("terminate",function(){a.clearAnnotations()});return c}}.call(l.prototype),b.Mode=l}),define("ace/mode/javascript_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/doc_comment_highlight_rules").DocCommentHighlightRules,g=a("ace/mode/text_highlight_rules").TextHighlightRules,h=function(){var a=e.arrayToMap("break|case|catch|continue|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|throw|try|typeof|let|var|while|with|const|yield|import|get|set".split("|")),b=e.arrayToMap("null|Infinity|NaN|undefined".split("|")),c=e.arrayToMap("class|enum|extends|super|export|implements|private|public|interface|package|protected|static".split("|"));this.$rules={start:[{token:"comment",regex:"\\/\\/.*$"},(new f).getStartRule("doc-start"),{token:"comment",regex:"\\/\\*",next:"comment"},{token:"string.regexp",regex:"[/](?:(?:\\[(?:\\\\]|[^\\]])+\\])|(?:\\\\/|[^\\]/]))*[/]\\w*\\s*(?=[).,;]|$)"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:'["].*\\\\$',next:"qqstring"},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"string",regex:"['].*\\\\$",next:"qstring"},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"},{token:"constant.language.boolean",regex:"(?:true|false)\\b"},{token:function(d){return d=="this"?"variable.language":a.hasOwnProperty(d)?"keyword":b.hasOwnProperty(d)?"constant.language":c.hasOwnProperty(d)?"invalid.illegal":d=="debugger"?"invalid.deprecated":"identifier"},regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"keyword.operator",regex:"!|\\$|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|instanceof|new|delete|typeof|void)"},{token:"lparen",regex:"[[({]"},{token:"rparen",regex:"[\\])}]"},{token:"comment",regex:"^#!.*$"},{token:"text",regex:"\\s+"}],comment:[{token:"comment",regex:".*?\\*\\/",next:"start"},{token:"comment",regex:".+"}],qqstring:[{token:"string",regex:'(?:(?:\\\\.)|(?:[^"\\\\]))*?"',next:"start"},{token:"string",regex:".+"}],qstring:[{token:"string",regex:"(?:(?:\\\\.)|(?:[^'\\\\]))*?'",next:"start"},{token:"string",regex:".+"}]},this.embedRules(f,"doc-",[(new f).getEndRule("start")])};d.inherits(h,g),b.JavaScriptHighlightRules=h}),define("ace/mode/doc_comment_highlight_rules",["require","exports","module","pilot/oop","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text_highlight_rules").TextHighlightRules,f=function(){this.$rules={start:[{token:"comment.doc.tag",regex:"@[\\w\\d_]+"},{token:"comment.doc",regex:"\\s+"},{token:"comment.doc",regex:"TODO"},{token:"comment.doc",regex:"[^@\\*]+"},{token:"comment.doc",regex:"."}]}};d.inherits(f,e),function(){this.getStartRule=function(a){return{token:"comment.doc",regex:"\\/\\*(?=\\*)",next:a}},this.getEndRule=function(a){return{token:"comment.doc",regex:"\\*\\/",next:a}}}.call(f.prototype),b.DocCommentHighlightRules=f}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\}/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\})/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingBraceOutdent=e}),define("ace/worker/worker_client",["require","exports","module","pilot/oop","pilot/event_emitter"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=function(b,c,d,e){this.callbacks=[];if(a.packaged)var f=this.$guessBasePath(),g=this.$worker=new Worker(f+c);else{var h=a.nameToUrl("ace/worker/worker",null,"_"),g=this.$worker=new Worker(h),i={};for(var j=0;j<b.length;j++){var k=b[j];i[k]=a.nameToUrl(k,null,"_").replace(/.js$/,"")}}this.$worker.postMessage({init:!0,tlns:i,module:d,classname:e}),this.callbackId=1,this.callbacks={};var l=this;this.$worker.onerror=function(a){window.console&&console.log&&console.log(a);throw a},this.$worker.onmessage=function(a){var b=a.data;switch(b.type){case"log":window.console&&console.log&&console.log(b.data);break;case"event":l._dispatchEvent(b.name,{data:b.data});break;case"call":var c=l.callbacks[b.id];c&&(c(b.data),delete l.callbacks[b.id])}}};(function(){d.implement(this,e),this.$guessBasePath=function(){if(a.aceBaseUrl)return a.aceBaseUrl;var b=document.getElementsByTagName("script");for(var c=0;c<b.length;c++){var d=b[c].src||b[c].getAttribute("src");if(!d)continue;var e=d.match(/^(?:(.*\/)ace\.js|(.*\/)ace-uncompressed\.js)(?:\?|$)/);if(e)return e[1]||e[2]}return""},this.terminate=function(){this._dispatchEvent("terminate",{}),this.$worker.terminate()},this.send=function(a,b){this.$worker.postMessage({command:a,args:b})},this.call=function(a,b,c){if(c){var d=this.callbackId++;this.callbacks[d]=c,b.push(d)}this.send(a,b)},this.emit=function(a,b){this.$worker.postMessage({event:a,data:b})}}).call(f.prototype),b.WorkerClient=f}),define("ace/mode/behaviour/cstyle",["require","exports","module","pilot/oop","ace/mode/behaviour"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/behaviour").Behaviour,f=function(){this.add("braces","insertion",function(a,b,c,d,e){if(e=="{"){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"{"+g+"}",selection:!1}:{text:"{}",selection:[1,1]}}if(e=="}"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var k=d.$findOpeningBracket("}",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}else if(e=="\n"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var l=d.findMatchingBracket({row:h.row,column:h.column}),m=this.getNextLineIndent(a,i.substring(0,i.length-1),d.getTabString()),n=this.$getIndent(d.doc.getLine(l.row));return{text:"\n"+m+"\n"+n,selection:[1,m.length,1,m.length]}}}return!1}),this.add("braces","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="{"){var g=d.doc.getLine(e.start.row),h=g.substring(e.end.column,e.end.column+1);if(h=="}")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("parens","insertion",function(a,b,c,d,e){if(e=="("){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"("+g+")",selection:!1}:{text:"()",selection:[1,1]}}if(e==")"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j==")"){var k=d.$findOpeningBracket(")",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}return!1}),this.add("parens","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="("){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h==")")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("string_dquotes","insertion",function(a,b,c,d,e){if(e=='"'){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);if(g!=="")return{text:'"'+g+'"',selection:!1};var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column-1,h.column);if(j=="\\")return!1;var k=d.getTokens(f.start.row,f.start.row)[0].tokens,l=0,m,n=-1;for(var o=0;o<k.length;o++){m=k[o],m.type=="string"?n=-1:n<0&&(n=m.value.indexOf('"'));if(m.value.length+l>f.start.column)break;l+=k[o].value.length}if(!m||n<0&&m.type!=="comment"&&(m.type!=="string"||f.start.column!==m.value.length+l-1&&m.value.lastIndexOf('"')===m.value.length-1))return{text:'""',selection:[1,1]};if(m&&m.type==="string"){var p=i.substring(h.column,h.column+1);if(p=='"')return{text:"",selection:[1,1]}}}return!1}),this.add("string_dquotes","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=='"'){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h=='"')return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1})};d.inherits(f,e),b.CstyleBehaviour=f}) \ No newline at end of file
diff --git a/www/js/ace/mode-json.js b/www/js/ace/mode-json.js
new file mode 100644
index 0000000..b4561f6
--- /dev/null
+++ b/www/js/ace/mode-json.js
@@ -0,0 +1 @@
+define("ace/mode/json",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/json_highlight_rules","ace/mode/matching_brace_outdent","ace/range","ace/mode/behaviour/cstyle"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/json_highlight_rules").JsonHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=a("ace/range").Range,j=a("ace/mode/behaviour/cstyle").CstyleBehaviour,k=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h,this.$behaviour=new j};d.inherits(k,e),function(){this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=this.$tokenizer.getLineTokens(b,a),f=e.tokens,g=e.state;if(a=="start"){var h=b.match(/^.*[\{\(\[]\s*$/);h&&(d+=c)}return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)}}.call(k.prototype),b.Mode=k}),define("ace/mode/json_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/text_highlight_rules").TextHighlightRules,g=function(){this.$rules={start:[{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"},{token:"constant.language.boolean",regex:"(?:true|false)\\b"},{token:"invalid.illegal",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"invalid.illegal",regex:"\\/\\/.*$"},{token:"lparen",regex:"[[({]"},{token:"rparen",regex:"[\\])}]"},{token:"text",regex:"\\s+"}]}};d.inherits(g,f),b.JsonHighlightRules=g}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\}/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\})/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingBraceOutdent=e}),define("ace/mode/behaviour/cstyle",["require","exports","module","pilot/oop","ace/mode/behaviour"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/behaviour").Behaviour,f=function(){this.add("braces","insertion",function(a,b,c,d,e){if(e=="{"){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"{"+g+"}",selection:!1}:{text:"{}",selection:[1,1]}}if(e=="}"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var k=d.$findOpeningBracket("}",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}else if(e=="\n"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var l=d.findMatchingBracket({row:h.row,column:h.column}),m=this.getNextLineIndent(a,i.substring(0,i.length-1),d.getTabString()),n=this.$getIndent(d.doc.getLine(l.row));return{text:"\n"+m+"\n"+n,selection:[1,m.length,1,m.length]}}}return!1}),this.add("braces","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="{"){var g=d.doc.getLine(e.start.row),h=g.substring(e.end.column,e.end.column+1);if(h=="}")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("parens","insertion",function(a,b,c,d,e){if(e=="("){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"("+g+")",selection:!1}:{text:"()",selection:[1,1]}}if(e==")"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j==")"){var k=d.$findOpeningBracket(")",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}return!1}),this.add("parens","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="("){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h==")")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("string_dquotes","insertion",function(a,b,c,d,e){if(e=='"'){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);if(g!=="")return{text:'"'+g+'"',selection:!1};var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column-1,h.column);if(j=="\\")return!1;var k=d.getTokens(f.start.row,f.start.row)[0].tokens,l=0,m,n=-1;for(var o=0;o<k.length;o++){m=k[o],m.type=="string"?n=-1:n<0&&(n=m.value.indexOf('"'));if(m.value.length+l>f.start.column)break;l+=k[o].value.length}if(!m||n<0&&m.type!=="comment"&&(m.type!=="string"||f.start.column!==m.value.length+l-1&&m.value.lastIndexOf('"')===m.value.length-1))return{text:'""',selection:[1,1]};if(m&&m.type==="string"){var p=i.substring(h.column,h.column+1);if(p=='"')return{text:"",selection:[1,1]}}}return!1}),this.add("string_dquotes","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=='"'){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h=='"')return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1})};d.inherits(f,e),b.CstyleBehaviour=f}) \ No newline at end of file
diff --git a/www/js/ace/mode-perl.js b/www/js/ace/mode-perl.js
new file mode 100644
index 0000000..3ec8330
--- /dev/null
+++ b/www/js/ace/mode-perl.js
@@ -0,0 +1 @@
+define("ace/mode/perl",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/perl_highlight_rules","ace/mode/matching_brace_outdent","ace/range"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/perl_highlight_rules").PerlHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=a("ace/range").Range,j=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h};d.inherits(j,e),function(){this.toggleCommentLines=function(a,b,c,d){var e=!0,f=[],g=/^(\s*)#/;for(var h=c;h<=d;h++)if(!g.test(b.getLine(h))){e=!1;break}if(e){var j=new i(0,0,0,0);for(var h=c;h<=d;h++){var k=b.getLine(h),l=k.match(g);j.start.row=h,j.end.row=h,j.end.column=l[0].length,b.replace(j,l[1])}}else b.indentRows(c,d,"#")},this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=this.$tokenizer.getLineTokens(b,a),f=e.tokens,g=e.state;if(f.length&&f[f.length-1].type=="comment")return d;if(a=="start"){var h=b.match(/^.*[\{\(\[\:]\s*$/);h&&(d+=c)}return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)}}.call(j.prototype),b.Mode=j}),define("ace/mode/perl_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/text_highlight_rules").TextHighlightRules,g=function(){var a=e.arrayToMap("base|constant|continue|else|elsif|for|foreach|format|goto|if|last|local|my|next|no|package|parent|redo|require|scalar|sub|unless|until|while|use|vars".split("|")),b=e.arrayToMap("ARGV|ENV|INC|SIG".split("|")),c=e.arrayToMap("getprotobynumber|getprotobyname|getservbyname|gethostbyaddr|gethostbyname|getservbyport|getnetbyaddr|getnetbyname|getsockname|getpeername|setpriority|getprotoent|setprotoent|getpriority|endprotoent|getservent|setservent|endservent|sethostent|socketpair|getsockopt|gethostent|endhostent|setsockopt|setnetent|quotemeta|localtime|prototype|getnetent|endnetent|rewinddir|wantarray|getpwuid|closedir|getlogin|readlink|endgrent|getgrgid|getgrnam|shmwrite|shutdown|readline|endpwent|setgrent|readpipe|formline|truncate|dbmclose|syswrite|setpwent|getpwnam|getgrent|getpwent|ucfirst|sysread|setpgrp|shmread|sysseek|sysopen|telldir|defined|opendir|connect|lcfirst|getppid|binmode|syscall|sprintf|getpgrp|readdir|seekdir|waitpid|reverse|unshift|symlink|dbmopen|semget|msgrcv|rename|listen|chroot|msgsnd|shmctl|accept|unpack|exists|fileno|shmget|system|unlink|printf|gmtime|msgctl|semctl|values|rindex|substr|splice|length|msgget|select|socket|return|caller|delete|alarm|ioctl|index|undef|lstat|times|srand|chown|fcntl|close|write|umask|rmdir|study|sleep|chomp|untie|print|utime|mkdir|atan2|split|crypt|flock|chmod|BEGIN|bless|chdir|semop|shift|reset|link|stat|chop|grep|fork|dump|join|open|tell|pipe|exit|glob|warn|each|bind|sort|pack|eval|push|keys|getc|kill|seek|sqrt|send|wait|rand|tied|read|time|exec|recv|eof|chr|int|ord|exp|pos|pop|sin|log|abs|oct|hex|tie|cos|vec|END|ref|map|die|uc|lc|do".split("|"));this.$rules={start:[{token:"comment",regex:"#.*$"},{token:"string.regexp",regex:"[/](?:(?:\\[(?:\\\\]|[^\\]])+\\])|(?:\\\\/|[^\\]/]))*[/]\\w*\\s*(?=[).,;]|$)"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:'["].*\\\\$',next:"qqstring"},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"string",regex:"['].*\\\\$",next:"qstring"},{token:"constant.numeric",regex:"0x[0-9a-fA-F]+\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"},{token:function(d){return a.hasOwnProperty(d)?"keyword":b.hasOwnProperty(d)?"constant.language":c.hasOwnProperty(d)?"support.function":"identifier"},regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"keyword.operator",regex:"\\.\\.\\.|\\|\\|=|>>=|<<=|<=>|&&=|=>|!~|\\^=|&=|\\|=|\\.=|x=|%=|\\/=|\\*=|\\-=|\\+=|=~|\\*\\*|\\-\\-|\\.\\.|\\|\\||&&|\\+\\+|\\->|!=|==|>=|<=|>>|<<|,|=|\\?\\:|\\^|\\||x|%|\\/|\\*|<|&|\\\\|~|!|>|\\.|\\-|\\+|\\-C|\\-b|\\-S|\\-u|\\-t|\\-p|\\-l|\\-d|\\-f|\\-g|\\-s|\\-z|\\-k|\\-e|\\-O|\\-T|\\-B|\\-M|\\-A|\\-X|\\-W|\\-c|\\-R|\\-o|\\-x|\\-w|\\-r|\\b(?:and|cmp|eq|ge|gt|le|lt|ne|not|or|xor)"},{token:"lparen",regex:"[[({]"},{token:"rparen",regex:"[\\])}]"},{token:"text",regex:"\\s+"}],qqstring:[{token:"string",regex:'(?:(?:\\\\.)|(?:[^"\\\\]))*?"',next:"start"},{token:"string",regex:".+"}],qstring:[{token:"string",regex:"(?:(?:\\\\.)|(?:[^'\\\\]))*?'",next:"start"},{token:"string",regex:".+"}]}};d.inherits(g,f),b.PerlHighlightRules=g}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\}/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\})/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingBraceOutdent=e}) \ No newline at end of file
diff --git a/www/js/ace/mode-php.js b/www/js/ace/mode-php.js
new file mode 100644
index 0000000..161648b
--- /dev/null
+++ b/www/js/ace/mode-php.js
@@ -0,0 +1 @@
+define("ace/mode/php",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/php_highlight_rules","ace/mode/matching_brace_outdent","ace/range","ace/mode/behaviour/cstyle"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/php_highlight_rules").PhpHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=a("ace/range").Range,j=a("ace/mode/behaviour/cstyle").CstyleBehaviour,k=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h,this.$behaviour=new j};d.inherits(k,e),function(){this.toggleCommentLines=function(a,b,c,d){var e=!0,f=[],g=/^(\s*)#/;for(var h=c;h<=d;h++)if(!g.test(b.getLine(h))){e=!1;break}if(e){var j=new i(0,0,0,0);for(var h=c;h<=d;h++){var k=b.getLine(h),l=k.match(g);j.start.row=h,j.end.row=h,j.end.column=l[0].length,b.replace(j,l[1])}}else b.indentRows(c,d,"#")},this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=this.$tokenizer.getLineTokens(b,a),f=e.tokens,g=e.state;if(f.length&&f[f.length-1].type=="comment")return d;if(a=="start"){var h=b.match(/^.*[\{\(\[\:]\s*$/);h&&(d+=c)}return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)}}.call(k.prototype),b.Mode=k}),define("ace/mode/php_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/doc_comment_highlight_rules").DocCommentHighlightRules,g=a("ace/mode/text_highlight_rules").TextHighlightRules,h=function(){var a=e.arrayToMap("abs|acos|acosh|addcslashes|addslashes|aggregate|aggregate_info|aggregate_methods|aggregate_methods_by_list|aggregate_methods_by_regexp|aggregate_properties|aggregate_properties_by_list|aggregate_properties_by_regexp|aggregation_info|apache_child_terminate|apache_get_modules|apache_get_version|apache_getenv|apache_lookup_uri|apache_note|apache_request_headers|apache_response_headers|apache_setenv|array|array_change_key_case|array_chunk|array_combine|array_count_values|array_diff|array_diff_assoc|array_diff_uassoc|array_fill|array_filter|array_flip|array_intersect|array_intersect_assoc|array_key_exists|array_keys|array_map|array_merge|array_merge_recursive|array_multisort|array_pad|array_pop|array_push|array_rand|array_reduce|array_reverse|array_search|array_shift|array_slice|array_splice|array_sum|array_udiff|array_udiff_assoc|array_udiff_uassoc|array_unique|array_unshift|array_values|array_walk|arsort|ascii2ebcdic|asin|asinh|asort|aspell_check|aspell_check_raw|aspell_new|aspell_suggest|assert|assert_options|atan|atan2|atanh|base64_decode|base64_encode|base_convert|basename|bcadd|bccomp|bcdiv|bcmod|bcmul|bcpow|bcpowmod|bcscale|bcsqrt|bcsub|bin2hex|bind_textdomain_codeset|bindec|bindtextdomain|bzclose|bzcompress|bzdecompress|bzerrno|bzerror|bzerrstr|bzflush|bzopen|bzread|bzwrite|cal_days_in_month|cal_from_jd|cal_info|cal_to_jd|call_user_func|call_user_func_array|call_user_method|call_user_method_array|ccvs_add|ccvs_auth|ccvs_command|ccvs_count|ccvs_delete|ccvs_done|ccvs_init|ccvs_lookup|ccvs_new|ccvs_report|ccvs_return|ccvs_reverse|ccvs_sale|ccvs_status|ccvs_textvalue|ccvs_void|ceil|chdir|checkdate|checkdnsrr|chgrp|chmod|chop|chown|chr|chroot|chunk_split|class_exists|clearstatcache|closedir|closelog|com|com_addref|com_get|com_invoke|com_isenum|com_load|com_load_typelib|com_propget|com_propput|com_propset|com_release|com_set|compact|connection_aborted|connection_status|connection_timeout|constant|convert_cyr_string|copy|cos|cosh|count|count_chars|cpdf_add_annotation|cpdf_add_outline|cpdf_arc|cpdf_begin_text|cpdf_circle|cpdf_clip|cpdf_close|cpdf_closepath|cpdf_closepath_fill_stroke|cpdf_closepath_stroke|cpdf_continue_text|cpdf_curveto|cpdf_end_text|cpdf_fill|cpdf_fill_stroke|cpdf_finalize|cpdf_finalize_page|cpdf_global_set_document_limits|cpdf_import_jpeg|cpdf_lineto|cpdf_moveto|cpdf_newpath|cpdf_open|cpdf_output_buffer|cpdf_page_init|cpdf_place_inline_image|cpdf_rect|cpdf_restore|cpdf_rlineto|cpdf_rmoveto|cpdf_rotate|cpdf_rotate_text|cpdf_save|cpdf_save_to_file|cpdf_scale|cpdf_set_action_url|cpdf_set_char_spacing|cpdf_set_creator|cpdf_set_current_page|cpdf_set_font|cpdf_set_font_directories|cpdf_set_font_map_file|cpdf_set_horiz_scaling|cpdf_set_keywords|cpdf_set_leading|cpdf_set_page_animation|cpdf_set_subject|cpdf_set_text_matrix|cpdf_set_text_pos|cpdf_set_text_rendering|cpdf_set_text_rise|cpdf_set_title|cpdf_set_viewer_preferences|cpdf_set_word_spacing|cpdf_setdash|cpdf_setflat|cpdf_setgray|cpdf_setgray_fill|cpdf_setgray_stroke|cpdf_setlinecap|cpdf_setlinejoin|cpdf_setlinewidth|cpdf_setmiterlimit|cpdf_setrgbcolor|cpdf_setrgbcolor_fill|cpdf_setrgbcolor_stroke|cpdf_show|cpdf_show_xy|cpdf_stringwidth|cpdf_stroke|cpdf_text|cpdf_translate|crack_check|crack_closedict|crack_getlastmessage|crack_opendict|crc32|create_function|crypt|ctype_alnum|ctype_alpha|ctype_cntrl|ctype_digit|ctype_graph|ctype_lower|ctype_print|ctype_punct|ctype_space|ctype_upper|ctype_xdigit|curl_close|curl_errno|curl_error|curl_exec|curl_getinfo|curl_init|curl_multi_add_handle|curl_multi_close|curl_multi_exec|curl_multi_getcontent|curl_multi_info_read|curl_multi_init|curl_multi_remove_handle|curl_multi_select|curl_setopt|curl_version|current|cybercash_base64_decode|cybercash_base64_encode|cybercash_decr|cybercash_encr|cyrus_authenticate|cyrus_bind|cyrus_close|cyrus_connect|cyrus_query|cyrus_unbind|date|dba_close|dba_delete|dba_exists|dba_fetch|dba_firstkey|dba_handlers|dba_insert|dba_key_split|dba_list|dba_nextkey|dba_open|dba_optimize|dba_popen|dba_replace|dba_sync|dbase_add_record|dbase_close|dbase_create|dbase_delete_record|dbase_get_header_info|dbase_get_record|dbase_get_record_with_names|dbase_numfields|dbase_numrecords|dbase_open|dbase_pack|dbase_replace_record|dblist|dbmclose|dbmdelete|dbmexists|dbmfetch|dbmfirstkey|dbminsert|dbmnextkey|dbmopen|dbmreplace|dbplus_add|dbplus_aql|dbplus_chdir|dbplus_close|dbplus_curr|dbplus_errcode|dbplus_errno|dbplus_find|dbplus_first|dbplus_flush|dbplus_freealllocks|dbplus_freelock|dbplus_freerlocks|dbplus_getlock|dbplus_getunique|dbplus_info|dbplus_last|dbplus_lockrel|dbplus_next|dbplus_open|dbplus_prev|dbplus_rchperm|dbplus_rcreate|dbplus_rcrtexact|dbplus_rcrtlike|dbplus_resolve|dbplus_restorepos|dbplus_rkeys|dbplus_ropen|dbplus_rquery|dbplus_rrename|dbplus_rsecindex|dbplus_runlink|dbplus_rzap|dbplus_savepos|dbplus_setindex|dbplus_setindexbynumber|dbplus_sql|dbplus_tcl|dbplus_tremove|dbplus_undo|dbplus_undoprepare|dbplus_unlockrel|dbplus_unselect|dbplus_update|dbplus_xlockrel|dbplus_xunlockrel|dbx_close|dbx_compare|dbx_connect|dbx_error|dbx_escape_string|dbx_fetch_row|dbx_query|dbx_sort|dcgettext|dcngettext|deaggregate|debug_backtrace|debug_print_backtrace|debugger_off|debugger_on|decbin|dechex|decoct|define|define_syslog_variables|defined|deg2rad|delete|dgettext|die|dio_close|dio_fcntl|dio_open|dio_read|dio_seek|dio_stat|dio_tcsetattr|dio_truncate|dio_write|dir|dirname|disk_free_space|disk_total_space|diskfreespace|dl|dngettext|dns_check_record|dns_get_mx|dns_get_record|domxml_new_doc|domxml_open_file|domxml_open_mem|domxml_version|domxml_xmltree|domxml_xslt_stylesheet|domxml_xslt_stylesheet_doc|domxml_xslt_stylesheet_file|dotnet_load|doubleval|each|easter_date|easter_days|ebcdic2ascii|echo|empty|end|ereg|ereg_replace|eregi|eregi_replace|error_log|error_reporting|escapeshellarg|escapeshellcmd|eval|exec|exif_imagetype|exif_read_data|exif_thumbnail|exit|exp|explode|expm1|extension_loaded|extract|ezmlm_hash|fam_cancel_monitor|fam_close|fam_monitor_collection|fam_monitor_directory|fam_monitor_file|fam_next_event|fam_open|fam_pending|fam_resume_monitor|fam_suspend_monitor|fbsql_affected_rows|fbsql_autocommit|fbsql_blob_size|fbsql_change_user|fbsql_clob_size|fbsql_close|fbsql_commit|fbsql_connect|fbsql_create_blob|fbsql_create_clob|fbsql_create_db|fbsql_data_seek|fbsql_database|fbsql_database_password|fbsql_db_query|fbsql_db_status|fbsql_drop_db|fbsql_errno|fbsql_error|fbsql_fetch_array|fbsql_fetch_assoc|fbsql_fetch_field|fbsql_fetch_lengths|fbsql_fetch_object|fbsql_fetch_row|fbsql_field_flags|fbsql_field_len|fbsql_field_name|fbsql_field_seek|fbsql_field_table|fbsql_field_type|fbsql_free_result|fbsql_get_autostart_info|fbsql_hostname|fbsql_insert_id|fbsql_list_dbs|fbsql_list_fields|fbsql_list_tables|fbsql_next_result|fbsql_num_fields|fbsql_num_rows|fbsql_password|fbsql_pconnect|fbsql_query|fbsql_read_blob|fbsql_read_clob|fbsql_result|fbsql_rollback|fbsql_select_db|fbsql_set_lob_mode|fbsql_set_password|fbsql_set_transaction|fbsql_start_db|fbsql_stop_db|fbsql_tablename|fbsql_username|fbsql_warnings|fclose|fdf_add_doc_javascript|fdf_add_template|fdf_close|fdf_create|fdf_enum_values|fdf_errno|fdf_error|fdf_get_ap|fdf_get_attachment|fdf_get_encoding|fdf_get_file|fdf_get_flags|fdf_get_opt|fdf_get_status|fdf_get_value|fdf_get_version|fdf_header|fdf_next_field_name|fdf_open|fdf_open_string|fdf_remove_item|fdf_save|fdf_save_string|fdf_set_ap|fdf_set_encoding|fdf_set_file|fdf_set_flags|fdf_set_javascript_action|fdf_set_opt|fdf_set_status|fdf_set_submit_form_action|fdf_set_target_frame|fdf_set_value|fdf_set_version|feof|fflush|fgetc|fgetcsv|fgets|fgetss|file|file_exists|file_get_contents|file_put_contents|fileatime|filectime|filegroup|fileinode|filemtime|fileowner|fileperms|filepro|filepro_fieldcount|filepro_fieldname|filepro_fieldtype|filepro_fieldwidth|filepro_retrieve|filepro_rowcount|filesize|filetype|floatval|flock|floor|flush|fmod|fnmatch|fopen|fpassthru|fprintf|fputs|fread|frenchtojd|fribidi_log2vis|fscanf|fseek|fsockopen|fstat|ftell|ftok|ftp_alloc|ftp_cdup|ftp_chdir|ftp_chmod|ftp_close|ftp_connect|ftp_delete|ftp_exec|ftp_fget|ftp_fput|ftp_get|ftp_get_option|ftp_login|ftp_mdtm|ftp_mkdir|ftp_nb_continue|ftp_nb_fget|ftp_nb_fput|ftp_nb_get|ftp_nb_put|ftp_nlist|ftp_pasv|ftp_put|ftp_pwd|ftp_quit|ftp_raw|ftp_rawlist|ftp_rename|ftp_rmdir|ftp_set_option|ftp_site|ftp_size|ftp_ssl_connect|ftp_systype|ftruncate|func_get_arg|func_get_args|func_num_args|function_exists|fwrite|gd_info|get_browser|get_cfg_var|get_class|get_class_methods|get_class_vars|get_current_user|get_declared_classes|get_declared_interfaces|get_defined_constants|get_defined_functions|get_defined_vars|get_extension_funcs|get_headers|get_html_translation_table|get_include_path|get_included_files|get_loaded_extensions|get_magic_quotes_gpc|get_magic_quotes_runtime|get_meta_tags|get_object_vars|get_parent_class|get_required_files|get_resource_type|getallheaders|getcwd|getdate|getenv|gethostbyaddr|gethostbyname|gethostbynamel|getimagesize|getlastmod|getmxrr|getmygid|getmyinode|getmypid|getmyuid|getopt|getprotobyname|getprotobynumber|getrandmax|getrusage|getservbyname|getservbyport|gettext|gettimeofday|gettype|glob|gmdate|gmmktime|gmp_abs|gmp_add|gmp_and|gmp_clrbit|gmp_cmp|gmp_com|gmp_div|gmp_div_q|gmp_div_qr|gmp_div_r|gmp_divexact|gmp_fact|gmp_gcd|gmp_gcdext|gmp_hamdist|gmp_init|gmp_intval|gmp_invert|gmp_jacobi|gmp_legendre|gmp_mod|gmp_mul|gmp_neg|gmp_or|gmp_perfect_square|gmp_popcount|gmp_pow|gmp_powm|gmp_prob_prime|gmp_random|gmp_scan0|gmp_scan1|gmp_setbit|gmp_sign|gmp_sqrt|gmp_sqrtrem|gmp_strval|gmp_sub|gmp_xor|gmstrftime|gregoriantojd|gzclose|gzcompress|gzdeflate|gzencode|gzeof|gzfile|gzgetc|gzgets|gzgetss|gzinflate|gzopen|gzpassthru|gzputs|gzread|gzrewind|gzseek|gztell|gzuncompress|gzwrite|header|headers_list|headers_sent|hebrev|hebrevc|hexdec|highlight_file|highlight_string|html_entity_decode|htmlentities|htmlspecialchars|http_build_query|hw_api_attribute|hw_api_content|hw_api_object|hw_array2objrec|hw_changeobject|hw_children|hw_childrenobj|hw_close|hw_connect|hw_connection_info|hw_cp|hw_deleteobject|hw_docbyanchor|hw_docbyanchorobj|hw_document_attributes|hw_document_bodytag|hw_document_content|hw_document_setcontent|hw_document_size|hw_dummy|hw_edittext|hw_error|hw_errormsg|hw_free_document|hw_getanchors|hw_getanchorsobj|hw_getandlock|hw_getchildcoll|hw_getchildcollobj|hw_getchilddoccoll|hw_getchilddoccollobj|hw_getobject|hw_getobjectbyquery|hw_getobjectbyquerycoll|hw_getobjectbyquerycollobj|hw_getobjectbyqueryobj|hw_getparents|hw_getparentsobj|hw_getrellink|hw_getremote|hw_getremotechildren|hw_getsrcbydestobj|hw_gettext|hw_getusername|hw_identify|hw_incollections|hw_info|hw_inscoll|hw_insdoc|hw_insertanchors|hw_insertdocument|hw_insertobject|hw_mapid|hw_modifyobject|hw_mv|hw_new_document|hw_objrec2array|hw_output_document|hw_pconnect|hw_pipedocument|hw_root|hw_setlinkroot|hw_stat|hw_unlock|hw_who|hwapi_hgcsp|hypot|ibase_add_user|ibase_affected_rows|ibase_backup|ibase_blob_add|ibase_blob_cancel|ibase_blob_close|ibase_blob_create|ibase_blob_echo|ibase_blob_get|ibase_blob_import|ibase_blob_info|ibase_blob_open|ibase_close|ibase_commit|ibase_commit_ret|ibase_connect|ibase_db_info|ibase_delete_user|ibase_drop_db|ibase_errcode|ibase_errmsg|ibase_execute|ibase_fetch_assoc|ibase_fetch_object|ibase_fetch_row|ibase_field_info|ibase_free_event_handler|ibase_free_query|ibase_free_result|ibase_gen_id|ibase_maintain_db|ibase_modify_user|ibase_name_result|ibase_num_fields|ibase_num_params|ibase_param_info|ibase_pconnect|ibase_prepare|ibase_query|ibase_restore|ibase_rollback|ibase_rollback_ret|ibase_server_info|ibase_service_attach|ibase_service_detach|ibase_set_event_handler|ibase_timefmt|ibase_trans|ibase_wait_event|iconv|iconv_get_encoding|iconv_mime_decode|iconv_mime_decode_headers|iconv_mime_encode|iconv_set_encoding|iconv_strlen|iconv_strpos|iconv_strrpos|iconv_substr|idate|ifx_affected_rows|ifx_blobinfile_mode|ifx_byteasvarchar|ifx_close|ifx_connect|ifx_copy_blob|ifx_create_blob|ifx_create_char|ifx_do|ifx_error|ifx_errormsg|ifx_fetch_row|ifx_fieldproperties|ifx_fieldtypes|ifx_free_blob|ifx_free_char|ifx_free_result|ifx_get_blob|ifx_get_char|ifx_getsqlca|ifx_htmltbl_result|ifx_nullformat|ifx_num_fields|ifx_num_rows|ifx_pconnect|ifx_prepare|ifx_query|ifx_textasvarchar|ifx_update_blob|ifx_update_char|ifxus_close_slob|ifxus_create_slob|ifxus_free_slob|ifxus_open_slob|ifxus_read_slob|ifxus_seek_slob|ifxus_tell_slob|ifxus_write_slob|ignore_user_abort|image2wbmp|image_type_to_mime_type|imagealphablending|imageantialias|imagearc|imagechar|imagecharup|imagecolorallocate|imagecolorallocatealpha|imagecolorat|imagecolorclosest|imagecolorclosestalpha|imagecolorclosesthwb|imagecolordeallocate|imagecolorexact|imagecolorexactalpha|imagecolormatch|imagecolorresolve|imagecolorresolvealpha|imagecolorset|imagecolorsforindex|imagecolorstotal|imagecolortransparent|imagecopy|imagecopymerge|imagecopymergegray|imagecopyresampled|imagecopyresized|imagecreate|imagecreatefromgd|imagecreatefromgd2|imagecreatefromgd2part|imagecreatefromgif|imagecreatefromjpeg|imagecreatefrompng|imagecreatefromstring|imagecreatefromwbmp|imagecreatefromxbm|imagecreatefromxpm|imagecreatetruecolor|imagedashedline|imagedestroy|imageellipse|imagefill|imagefilledarc|imagefilledellipse|imagefilledpolygon|imagefilledrectangle|imagefilltoborder|imagefilter|imagefontheight|imagefontwidth|imageftbbox|imagefttext|imagegammacorrect|imagegd|imagegd2|imagegif|imageinterlace|imageistruecolor|imagejpeg|imagelayereffect|imageline|imageloadfont|imagepalettecopy|imagepng|imagepolygon|imagepsbbox|imagepscopyfont|imagepsencodefont|imagepsextendfont|imagepsfreefont|imagepsloadfont|imagepsslantfont|imagepstext|imagerectangle|imagerotate|imagesavealpha|imagesetbrush|imagesetpixel|imagesetstyle|imagesetthickness|imagesettile|imagestring|imagestringup|imagesx|imagesy|imagetruecolortopalette|imagettfbbox|imagettftext|imagetypes|imagewbmp|imagexbm|imap_8bit|imap_alerts|imap_append|imap_base64|imap_binary|imap_body|imap_bodystruct|imap_check|imap_clearflag_full|imap_close|imap_createmailbox|imap_delete|imap_deletemailbox|imap_errors|imap_expunge|imap_fetch_overview|imap_fetchbody|imap_fetchheader|imap_fetchstructure|imap_get_quota|imap_get_quotaroot|imap_getacl|imap_getmailboxes|imap_getsubscribed|imap_header|imap_headerinfo|imap_headers|imap_last_error|imap_list|imap_listmailbox|imap_listscan|imap_listsubscribed|imap_lsub|imap_mail|imap_mail_compose|imap_mail_copy|imap_mail_move|imap_mailboxmsginfo|imap_mime_header_decode|imap_msgno|imap_num_msg|imap_num_recent|imap_open|imap_ping|imap_qprint|imap_renamemailbox|imap_reopen|imap_rfc822_parse_adrlist|imap_rfc822_parse_headers|imap_rfc822_write_address|imap_scanmailbox|imap_search|imap_set_quota|imap_setacl|imap_setflag_full|imap_sort|imap_status|imap_subscribe|imap_thread|imap_timeout|imap_uid|imap_undelete|imap_unsubscribe|imap_utf7_decode|imap_utf7_encode|imap_utf8|implode|import_request_variables|in_array|ingres_autocommit|ingres_close|ingres_commit|ingres_connect|ingres_fetch_array|ingres_fetch_object|ingres_fetch_row|ingres_field_length|ingres_field_name|ingres_field_nullable|ingres_field_precision|ingres_field_scale|ingres_field_type|ingres_num_fields|ingres_num_rows|ingres_pconnect|ingres_query|ingres_rollback|ini_alter|ini_get|ini_get_all|ini_restore|ini_set|intval|ip2long|iptcembed|iptcparse|ircg_channel_mode|ircg_disconnect|ircg_fetch_error_msg|ircg_get_username|ircg_html_encode|ircg_ignore_add|ircg_ignore_del|ircg_invite|ircg_is_conn_alive|ircg_join|ircg_kick|ircg_list|ircg_lookup_format_messages|ircg_lusers|ircg_msg|ircg_nick|ircg_nickname_escape|ircg_nickname_unescape|ircg_notice|ircg_oper|ircg_part|ircg_pconnect|ircg_register_format_messages|ircg_set_current|ircg_set_file|ircg_set_on_die|ircg_topic|ircg_who|ircg_whois|is_a|is_array|is_bool|is_callable|is_dir|is_double|is_executable|is_file|is_finite|is_float|is_infinite|is_int|is_integer|is_link|is_long|is_nan|is_null|is_numeric|is_object|is_readable|is_real|is_resource|is_scalar|is_soap_fault|is_string|is_subclass_of|is_uploaded_file|is_writable|is_writeable|isset|java_last_exception_clear|java_last_exception_get|jddayofweek|jdmonthname|jdtofrench|jdtogregorian|jdtojewish|jdtojulian|jdtounix|jewishtojd|join|jpeg2wbmp|juliantojd|key|krsort|ksort|lcg_value|ldap_8859_to_t61|ldap_add|ldap_bind|ldap_close|ldap_compare|ldap_connect|ldap_count_entries|ldap_delete|ldap_dn2ufn|ldap_err2str|ldap_errno|ldap_error|ldap_explode_dn|ldap_first_attribute|ldap_first_entry|ldap_first_reference|ldap_free_result|ldap_get_attributes|ldap_get_dn|ldap_get_entries|ldap_get_option|ldap_get_values|ldap_get_values_len|ldap_list|ldap_mod_add|ldap_mod_del|ldap_mod_replace|ldap_modify|ldap_next_attribute|ldap_next_entry|ldap_next_reference|ldap_parse_reference|ldap_parse_result|ldap_read|ldap_rename|ldap_search|ldap_set_option|ldap_set_rebind_proc|ldap_sort|ldap_start_tls|ldap_t61_to_8859|ldap_unbind|levenshtein|link|linkinfo|list|localeconv|localtime|log|log10|log1p|long2ip|lstat|ltrim|lzf_compress|lzf_decompress|lzf_optimized_for|mail|mailparse_determine_best_xfer_encoding|mailparse_msg_create|mailparse_msg_extract_part|mailparse_msg_extract_part_file|mailparse_msg_free|mailparse_msg_get_part|mailparse_msg_get_part_data|mailparse_msg_get_structure|mailparse_msg_parse|mailparse_msg_parse_file|mailparse_rfc822_parse_addresses|mailparse_stream_encode|mailparse_uudecode_all|main|max|mb_convert_case|mb_convert_encoding|mb_convert_kana|mb_convert_variables|mb_decode_mimeheader|mb_decode_numericentity|mb_detect_encoding|mb_detect_order|mb_encode_mimeheader|mb_encode_numericentity|mb_ereg|mb_ereg_match|mb_ereg_replace|mb_ereg_search|mb_ereg_search_getpos|mb_ereg_search_getregs|mb_ereg_search_init|mb_ereg_search_pos|mb_ereg_search_regs|mb_ereg_search_setpos|mb_eregi|mb_eregi_replace|mb_get_info|mb_http_input|mb_http_output|mb_internal_encoding|mb_language|mb_output_handler|mb_parse_str|mb_preferred_mime_name|mb_regex_encoding|mb_regex_set_options|mb_send_mail|mb_split|mb_strcut|mb_strimwidth|mb_strlen|mb_strpos|mb_strrpos|mb_strtolower|mb_strtoupper|mb_strwidth|mb_substitute_character|mb_substr|mb_substr_count|mcal_append_event|mcal_close|mcal_create_calendar|mcal_date_compare|mcal_date_valid|mcal_day_of_week|mcal_day_of_year|mcal_days_in_month|mcal_delete_calendar|mcal_delete_event|mcal_event_add_attribute|mcal_event_init|mcal_event_set_alarm|mcal_event_set_category|mcal_event_set_class|mcal_event_set_description|mcal_event_set_end|mcal_event_set_recur_daily|mcal_event_set_recur_monthly_mday|mcal_event_set_recur_monthly_wday|mcal_event_set_recur_none|mcal_event_set_recur_weekly|mcal_event_set_recur_yearly|mcal_event_set_start|mcal_event_set_title|mcal_expunge|mcal_fetch_current_stream_event|mcal_fetch_event|mcal_is_leap_year|mcal_list_alarms|mcal_list_events|mcal_next_recurrence|mcal_open|mcal_popen|mcal_rename_calendar|mcal_reopen|mcal_snooze|mcal_store_event|mcal_time_valid|mcal_week_of_year|mcrypt_cbc|mcrypt_cfb|mcrypt_create_iv|mcrypt_decrypt|mcrypt_ecb|mcrypt_enc_get_algorithms_name|mcrypt_enc_get_block_size|mcrypt_enc_get_iv_size|mcrypt_enc_get_key_size|mcrypt_enc_get_modes_name|mcrypt_enc_get_supported_key_sizes|mcrypt_enc_is_block_algorithm|mcrypt_enc_is_block_algorithm_mode|mcrypt_enc_is_block_mode|mcrypt_enc_self_test|mcrypt_encrypt|mcrypt_generic|mcrypt_generic_deinit|mcrypt_generic_end|mcrypt_generic_init|mcrypt_get_block_size|mcrypt_get_cipher_name|mcrypt_get_iv_size|mcrypt_get_key_size|mcrypt_list_algorithms|mcrypt_list_modes|mcrypt_module_close|mcrypt_module_get_algo_block_size|mcrypt_module_get_algo_key_size|mcrypt_module_get_supported_key_sizes|mcrypt_module_is_block_algorithm|mcrypt_module_is_block_algorithm_mode|mcrypt_module_is_block_mode|mcrypt_module_open|mcrypt_module_self_test|mcrypt_ofb|mcve_adduser|mcve_adduserarg|mcve_bt|mcve_checkstatus|mcve_chkpwd|mcve_chngpwd|mcve_completeauthorizations|mcve_connect|mcve_connectionerror|mcve_deleteresponse|mcve_deletetrans|mcve_deleteusersetup|mcve_deluser|mcve_destroyconn|mcve_destroyengine|mcve_disableuser|mcve_edituser|mcve_enableuser|mcve_force|mcve_getcell|mcve_getcellbynum|mcve_getcommadelimited|mcve_getheader|mcve_getuserarg|mcve_getuserparam|mcve_gft|mcve_gl|mcve_gut|mcve_initconn|mcve_initengine|mcve_initusersetup|mcve_iscommadelimited|mcve_liststats|mcve_listusers|mcve_maxconntimeout|mcve_monitor|mcve_numcolumns|mcve_numrows|mcve_override|mcve_parsecommadelimited|mcve_ping|mcve_preauth|mcve_preauthcompletion|mcve_qc|mcve_responseparam|mcve_return|mcve_returncode|mcve_returnstatus|mcve_sale|mcve_setblocking|mcve_setdropfile|mcve_setip|mcve_setssl|mcve_setssl_files|mcve_settimeout|mcve_settle|mcve_text_avs|mcve_text_code|mcve_text_cv|mcve_transactionauth|mcve_transactionavs|mcve_transactionbatch|mcve_transactioncv|mcve_transactionid|mcve_transactionitem|mcve_transactionssent|mcve_transactiontext|mcve_transinqueue|mcve_transnew|mcve_transparam|mcve_transsend|mcve_ub|mcve_uwait|mcve_verifyconnection|mcve_verifysslcert|mcve_void|md5|md5_file|mdecrypt_generic|memory_get_usage|metaphone|method_exists|mhash|mhash_count|mhash_get_block_size|mhash_get_hash_name|mhash_keygen_s2k|microtime|mime_content_type|min|ming_setcubicthreshold|ming_setscale|ming_useswfversion|mkdir|mktime|money_format|move_uploaded_file|msession_connect|msession_count|msession_create|msession_destroy|msession_disconnect|msession_find|msession_get|msession_get_array|msession_getdata|msession_inc|msession_list|msession_listvar|msession_lock|msession_plugin|msession_randstr|msession_set|msession_set_array|msession_setdata|msession_timeout|msession_uniq|msession_unlock|msg_get_queue|msg_receive|msg_remove_queue|msg_send|msg_set_queue|msg_stat_queue|msql|msql|msql_affected_rows|msql_close|msql_connect|msql_create_db|msql_createdb|msql_data_seek|msql_dbname|msql_drop_db|msql_error|msql_fetch_array|msql_fetch_field|msql_fetch_object|msql_fetch_row|msql_field_flags|msql_field_len|msql_field_name|msql_field_seek|msql_field_table|msql_field_type|msql_fieldflags|msql_fieldlen|msql_fieldname|msql_fieldtable|msql_fieldtype|msql_free_result|msql_list_dbs|msql_list_fields|msql_list_tables|msql_num_fields|msql_num_rows|msql_numfields|msql_numrows|msql_pconnect|msql_query|msql_regcase|msql_result|msql_select_db|msql_tablename|mssql_bind|mssql_close|mssql_connect|mssql_data_seek|mssql_execute|mssql_fetch_array|mssql_fetch_assoc|mssql_fetch_batch|mssql_fetch_field|mssql_fetch_object|mssql_fetch_row|mssql_field_length|mssql_field_name|mssql_field_seek|mssql_field_type|mssql_free_result|mssql_free_statement|mssql_get_last_message|mssql_guid_string|mssql_init|mssql_min_error_severity|mssql_min_message_severity|mssql_next_result|mssql_num_fields|mssql_num_rows|mssql_pconnect|mssql_query|mssql_result|mssql_rows_affected|mssql_select_db|mt_getrandmax|mt_rand|mt_srand|muscat_close|muscat_get|muscat_give|muscat_setup|muscat_setup_net|mysql_affected_rows|mysql_change_user|mysql_client_encoding|mysql_close|mysql_connect|mysql_create_db|mysql_data_seek|mysql_db_name|mysql_db_query|mysql_drop_db|mysql_errno|mysql_error|mysql_escape_string|mysql_fetch_array|mysql_fetch_assoc|mysql_fetch_field|mysql_fetch_lengths|mysql_fetch_object|mysql_fetch_row|mysql_field_flags|mysql_field_len|mysql_field_name|mysql_field_seek|mysql_field_table|mysql_field_type|mysql_free_result|mysql_get_client_info|mysql_get_host_info|mysql_get_proto_info|mysql_get_server_info|mysql_info|mysql_insert_id|mysql_list_dbs|mysql_list_fields|mysql_list_processes|mysql_list_tables|mysql_num_fields|mysql_num_rows|mysql_pconnect|mysql_ping|mysql_query|mysql_real_escape_string|mysql_result|mysql_select_db|mysql_stat|mysql_tablename|mysql_thread_id|mysql_unbuffered_query|mysqli_affected_rows|mysqli_autocommit|mysqli_bind_param|mysqli_bind_result|mysqli_change_user|mysqli_character_set_name|mysqli_client_encoding|mysqli_close|mysqli_commit|mysqli_connect|mysqli_connect_errno|mysqli_connect_error|mysqli_data_seek|mysqli_debug|mysqli_disable_reads_from_master|mysqli_disable_rpl_parse|mysqli_dump_debug_info|mysqli_embedded_connect|mysqli_enable_reads_from_master|mysqli_enable_rpl_parse|mysqli_errno|mysqli_error|mysqli_escape_string|mysqli_execute|mysqli_fetch|mysqli_fetch_array|mysqli_fetch_assoc|mysqli_fetch_field|mysqli_fetch_field_direct|mysqli_fetch_fields|mysqli_fetch_lengths|mysqli_fetch_object|mysqli_fetch_row|mysqli_field_count|mysqli_field_seek|mysqli_field_tell|mysqli_free_result|mysqli_get_client_info|mysqli_get_client_version|mysqli_get_host_info|mysqli_get_metadata|mysqli_get_proto_info|mysqli_get_server_info|mysqli_get_server_version|mysqli_info|mysqli_init|mysqli_insert_id|mysqli_kill|mysqli_master_query|mysqli_more_results|mysqli_multi_query|mysqli_next_result|mysqli_num_fields|mysqli_num_rows|mysqli_options|mysqli_param_count|mysqli_ping|mysqli_prepare|mysqli_query|mysqli_real_connect|mysqli_real_escape_string|mysqli_real_query|mysqli_report|mysqli_rollback|mysqli_rpl_parse_enabled|mysqli_rpl_probe|mysqli_rpl_query_type|mysqli_select_db|mysqli_send_long_data|mysqli_send_query|mysqli_server_end|mysqli_server_init|mysqli_set_opt|mysqli_sqlstate|mysqli_ssl_set|mysqli_stat|mysqli_stmt_init|mysqli_stmt_affected_rows|mysqli_stmt_bind_param|mysqli_stmt_bind_result|mysqli_stmt_close|mysqli_stmt_data_seek|mysqli_stmt_errno|mysqli_stmt_error|mysqli_stmt_execute|mysqli_stmt_fetch|mysqli_stmt_free_result|mysqli_stmt_num_rows|mysqli_stmt_param_count|mysqli_stmt_prepare|mysqli_stmt_result_metadata|mysqli_stmt_send_long_data|mysqli_stmt_sqlstate|mysqli_stmt_store_result|mysqli_store_result|mysqli_thread_id|mysqli_thread_safe|mysqli_use_result|mysqli_warning_count|natcasesort|natsort|ncurses_addch|ncurses_addchnstr|ncurses_addchstr|ncurses_addnstr|ncurses_addstr|ncurses_assume_default_colors|ncurses_attroff|ncurses_attron|ncurses_attrset|ncurses_baudrate|ncurses_beep|ncurses_bkgd|ncurses_bkgdset|ncurses_border|ncurses_bottom_panel|ncurses_can_change_color|ncurses_cbreak|ncurses_clear|ncurses_clrtobot|ncurses_clrtoeol|ncurses_color_content|ncurses_color_set|ncurses_curs_set|ncurses_def_prog_mode|ncurses_def_shell_mode|ncurses_define_key|ncurses_del_panel|ncurses_delay_output|ncurses_delch|ncurses_deleteln|ncurses_delwin|ncurses_doupdate|ncurses_echo|ncurses_echochar|ncurses_end|ncurses_erase|ncurses_erasechar|ncurses_filter|ncurses_flash|ncurses_flushinp|ncurses_getch|ncurses_getmaxyx|ncurses_getmouse|ncurses_getyx|ncurses_halfdelay|ncurses_has_colors|ncurses_has_ic|ncurses_has_il|ncurses_has_key|ncurses_hide_panel|ncurses_hline|ncurses_inch|ncurses_init|ncurses_init_color|ncurses_init_pair|ncurses_insch|ncurses_insdelln|ncurses_insertln|ncurses_insstr|ncurses_instr|ncurses_isendwin|ncurses_keyok|ncurses_keypad|ncurses_killchar|ncurses_longname|ncurses_meta|ncurses_mouse_trafo|ncurses_mouseinterval|ncurses_mousemask|ncurses_move|ncurses_move_panel|ncurses_mvaddch|ncurses_mvaddchnstr|ncurses_mvaddchstr|ncurses_mvaddnstr|ncurses_mvaddstr|ncurses_mvcur|ncurses_mvdelch|ncurses_mvgetch|ncurses_mvhline|ncurses_mvinch|ncurses_mvvline|ncurses_mvwaddstr|ncurses_napms|ncurses_new_panel|ncurses_newpad|ncurses_newwin|ncurses_nl|ncurses_nocbreak|ncurses_noecho|ncurses_nonl|ncurses_noqiflush|ncurses_noraw|ncurses_pair_content|ncurses_panel_above|ncurses_panel_below|ncurses_panel_window|ncurses_pnoutrefresh|ncurses_prefresh|ncurses_putp|ncurses_qiflush|ncurses_raw|ncurses_refresh|ncurses_replace_panel|ncurses_reset_prog_mode|ncurses_reset_shell_mode|ncurses_resetty|ncurses_savetty|ncurses_scr_dump|ncurses_scr_init|ncurses_scr_restore|ncurses_scr_set|ncurses_scrl|ncurses_show_panel|ncurses_slk_attr|ncurses_slk_attroff|ncurses_slk_attron|ncurses_slk_attrset|ncurses_slk_clear|ncurses_slk_color|ncurses_slk_init|ncurses_slk_noutrefresh|ncurses_slk_refresh|ncurses_slk_restore|ncurses_slk_set|ncurses_slk_touch|ncurses_standend|ncurses_standout|ncurses_start_color|ncurses_termattrs|ncurses_termname|ncurses_timeout|ncurses_top_panel|ncurses_typeahead|ncurses_ungetch|ncurses_ungetmouse|ncurses_update_panels|ncurses_use_default_colors|ncurses_use_env|ncurses_use_extended_names|ncurses_vidattr|ncurses_vline|ncurses_waddch|ncurses_waddstr|ncurses_wattroff|ncurses_wattron|ncurses_wattrset|ncurses_wborder|ncurses_wclear|ncurses_wcolor_set|ncurses_werase|ncurses_wgetch|ncurses_whline|ncurses_wmouse_trafo|ncurses_wmove|ncurses_wnoutrefresh|ncurses_wrefresh|ncurses_wstandend|ncurses_wstandout|ncurses_wvline|next|ngettext|nl2br|nl_langinfo|notes_body|notes_copy_db|notes_create_db|notes_create_note|notes_drop_db|notes_find_note|notes_header_info|notes_list_msgs|notes_mark_read|notes_mark_unread|notes_nav_create|notes_search|notes_unread|notes_version|nsapi_request_headers|nsapi_response_headers|nsapi_virtual|number_format|ob_clean|ob_end_clean|ob_end_flush|ob_flush|ob_get_clean|ob_get_contents|ob_get_flush|ob_get_length|ob_get_level|ob_get_status|ob_gzhandler|ob_iconv_handler|ob_implicit_flush|ob_list_handlers|ob_start|ob_tidyhandler|oci_bind_by_name|oci_cancel|oci_close|oci_commit|oci_connect|oci_define_by_name|oci_error|oci_execute|oci_fetch|oci_fetch_all|oci_fetch_array|oci_fetch_assoc|oci_fetch_object|oci_fetch_row|oci_field_is_null|oci_field_name|oci_field_precision|oci_field_scale|oci_field_size|oci_field_type|oci_field_type_raw|oci_free_statement|oci_internal_debug|oci_lob_copy|oci_lob_is_equal|oci_new_collection|oci_new_connect|oci_new_cursor|oci_new_descriptor|oci_num_fields|oci_num_rows|oci_parse|oci_password_change|oci_pconnect|oci_result|oci_rollback|oci_server_version|oci_set_prefetch|oci_statement_type|ocibindbyname|ocicancel|ocicloselob|ocicollappend|ocicollassign|ocicollassignelem|ocicollgetelem|ocicollmax|ocicollsize|ocicolltrim|ocicolumnisnull|ocicolumnname|ocicolumnprecision|ocicolumnscale|ocicolumnsize|ocicolumntype|ocicolumntyperaw|ocicommit|ocidefinebyname|ocierror|ociexecute|ocifetch|ocifetchinto|ocifetchstatement|ocifreecollection|ocifreecursor|ocifreedesc|ocifreestatement|ociinternaldebug|ociloadlob|ocilogoff|ocilogon|ocinewcollection|ocinewcursor|ocinewdescriptor|ocinlogon|ocinumcols|ociparse|ociplogon|ociresult|ocirollback|ocirowcount|ocisavelob|ocisavelobfile|ociserverversion|ocisetprefetch|ocistatementtype|ociwritelobtofile|ociwritetemporarylob|octdec|odbc_autocommit|odbc_binmode|odbc_close|odbc_close_all|odbc_columnprivileges|odbc_columns|odbc_commit|odbc_connect|odbc_cursor|odbc_data_source|odbc_do|odbc_error|odbc_errormsg|odbc_exec|odbc_execute|odbc_fetch_array|odbc_fetch_into|odbc_fetch_object|odbc_fetch_row|odbc_field_len|odbc_field_name|odbc_field_num|odbc_field_precision|odbc_field_scale|odbc_field_type|odbc_foreignkeys|odbc_free_result|odbc_gettypeinfo|odbc_longreadlen|odbc_next_result|odbc_num_fields|odbc_num_rows|odbc_pconnect|odbc_prepare|odbc_primarykeys|odbc_procedurecolumns|odbc_procedures|odbc_result|odbc_result_all|odbc_rollback|odbc_setoption|odbc_specialcolumns|odbc_statistics|odbc_tableprivileges|odbc_tables|opendir|openlog|openssl_csr_export|openssl_csr_export_to_file|openssl_csr_new|openssl_csr_sign|openssl_error_string|openssl_free_key|openssl_get_privatekey|openssl_get_publickey|openssl_open|openssl_pkcs7_decrypt|openssl_pkcs7_encrypt|openssl_pkcs7_sign|openssl_pkcs7_verify|openssl_pkey_export|openssl_pkey_export_to_file|openssl_pkey_get_private|openssl_pkey_get_public|openssl_pkey_new|openssl_private_decrypt|openssl_private_encrypt|openssl_public_decrypt|openssl_public_encrypt|openssl_seal|openssl_sign|openssl_verify|openssl_x509_check_private_key|openssl_x509_checkpurpose|openssl_x509_export|openssl_x509_export_to_file|openssl_x509_free|openssl_x509_parse|openssl_x509_read|ora_bind|ora_close|ora_columnname|ora_columnsize|ora_columntype|ora_commit|ora_commitoff|ora_commiton|ora_do|ora_error|ora_errorcode|ora_exec|ora_fetch|ora_fetch_into|ora_getcolumn|ora_logoff|ora_logon|ora_numcols|ora_numrows|ora_open|ora_parse|ora_plogon|ora_rollback|ord|output_add_rewrite_var|output_reset_rewrite_vars|overload|ovrimos_close|ovrimos_commit|ovrimos_connect|ovrimos_cursor|ovrimos_exec|ovrimos_execute|ovrimos_fetch_into|ovrimos_fetch_row|ovrimos_field_len|ovrimos_field_name|ovrimos_field_num|ovrimos_field_type|ovrimos_free_result|ovrimos_longreadlen|ovrimos_num_fields|ovrimos_num_rows|ovrimos_prepare|ovrimos_result|ovrimos_result_all|ovrimos_rollback|pack|parse_ini_file|parse_str|parse_url|passthru|pathinfo|pclose|pcntl_alarm|pcntl_exec|pcntl_fork|pcntl_getpriority|pcntl_setpriority|pcntl_signal|pcntl_wait|pcntl_waitpid|pcntl_wexitstatus|pcntl_wifexited|pcntl_wifsignaled|pcntl_wifstopped|pcntl_wstopsig|pcntl_wtermsig|pdf_add_annotation|pdf_add_bookmark|pdf_add_launchlink|pdf_add_locallink|pdf_add_note|pdf_add_outline|pdf_add_pdflink|pdf_add_thumbnail|pdf_add_weblink|pdf_arc|pdf_arcn|pdf_attach_file|pdf_begin_page|pdf_begin_pattern|pdf_begin_template|pdf_circle|pdf_clip|pdf_close|pdf_close_image|pdf_close_pdi|pdf_close_pdi_page|pdf_closepath|pdf_closepath_fill_stroke|pdf_closepath_stroke|pdf_concat|pdf_continue_text|pdf_curveto|pdf_delete|pdf_end_page|pdf_end_pattern|pdf_end_template|pdf_endpath|pdf_fill|pdf_fill_stroke|pdf_findfont|pdf_get_buffer|pdf_get_font|pdf_get_fontname|pdf_get_fontsize|pdf_get_image_height|pdf_get_image_width|pdf_get_majorversion|pdf_get_minorversion|pdf_get_parameter|pdf_get_pdi_parameter|pdf_get_pdi_value|pdf_get_value|pdf_initgraphics|pdf_lineto|pdf_makespotcolor|pdf_moveto|pdf_new|pdf_open|pdf_open_ccitt|pdf_open_file|pdf_open_gif|pdf_open_image|pdf_open_image_file|pdf_open_jpeg|pdf_open_memory_image|pdf_open_pdi|pdf_open_pdi_page|pdf_open_png|pdf_open_tiff|pdf_place_image|pdf_place_pdi_page|pdf_rect|pdf_restore|pdf_rotate|pdf_save|pdf_scale|pdf_set_border_color|pdf_set_border_dash|pdf_set_border_style|pdf_set_char_spacing|pdf_set_duration|pdf_set_font|pdf_set_horiz_scaling|pdf_set_info|pdf_set_info_author|pdf_set_info_creator|pdf_set_info_keywords|pdf_set_info_subject|pdf_set_info_title|pdf_set_leading|pdf_set_parameter|pdf_set_text_matrix|pdf_set_text_pos|pdf_set_text_rendering|pdf_set_text_rise|pdf_set_value|pdf_set_word_spacing|pdf_setcolor|pdf_setdash|pdf_setflat|pdf_setfont|pdf_setgray|pdf_setgray_fill|pdf_setgray_stroke|pdf_setlinecap|pdf_setlinejoin|pdf_setlinewidth|pdf_setmatrix|pdf_setmiterlimit|pdf_setpolydash|pdf_setrgbcolor|pdf_setrgbcolor_fill|pdf_setrgbcolor_stroke|pdf_show|pdf_show_boxed|pdf_show_xy|pdf_skew|pdf_stringwidth|pdf_stroke|pdf_translate|pfpro_cleanup|pfpro_init|pfpro_process|pfpro_process_raw|pfpro_version|pfsockopen|pg_affected_rows|pg_cancel_query|pg_client_encoding|pg_close|pg_connect|pg_connection_busy|pg_connection_reset|pg_connection_status|pg_convert|pg_copy_from|pg_copy_to|pg_dbname|pg_delete|pg_end_copy|pg_escape_bytea|pg_escape_string|pg_fetch_all|pg_fetch_array|pg_fetch_assoc|pg_fetch_object|pg_fetch_result|pg_fetch_row|pg_field_is_null|pg_field_name|pg_field_num|pg_field_prtlen|pg_field_size|pg_field_type|pg_free_result|pg_get_notify|pg_get_pid|pg_get_result|pg_host|pg_insert|pg_last_error|pg_last_notice|pg_last_oid|pg_lo_close|pg_lo_create|pg_lo_export|pg_lo_import|pg_lo_open|pg_lo_read|pg_lo_read_all|pg_lo_seek|pg_lo_tell|pg_lo_unlink|pg_lo_write|pg_meta_data|pg_num_fields|pg_num_rows|pg_options|pg_pconnect|pg_ping|pg_port|pg_put_line|pg_query|pg_result_error|pg_result_seek|pg_result_status|pg_select|pg_send_query|pg_set_client_encoding|pg_trace|pg_tty|pg_unescape_bytea|pg_untrace|pg_update|php_ini_scanned_files|php_logo_guid|php_sapi_name|php_uname|phpcredits|phpinfo|phpversion|pi|png2wbmp|popen|pos|posix_ctermid|posix_get_last_error|posix_getcwd|posix_getegid|posix_geteuid|posix_getgid|posix_getgrgid|posix_getgrnam|posix_getgroups|posix_getlogin|posix_getpgid|posix_getpgrp|posix_getpid|posix_getppid|posix_getpwnam|posix_getpwuid|posix_getrlimit|posix_getsid|posix_getuid|posix_isatty|posix_kill|posix_mkfifo|posix_setegid|posix_seteuid|posix_setgid|posix_setpgid|posix_setsid|posix_setuid|posix_strerror|posix_times|posix_ttyname|posix_uname|pow|preg_grep|preg_match|preg_match_all|preg_quote|preg_replace|preg_replace_callback|preg_split|prev|print|print_r|printer_abort|printer_close|printer_create_brush|printer_create_dc|printer_create_font|printer_create_pen|printer_delete_brush|printer_delete_dc|printer_delete_font|printer_delete_pen|printer_draw_bmp|printer_draw_chord|printer_draw_elipse|printer_draw_line|printer_draw_pie|printer_draw_rectangle|printer_draw_roundrect|printer_draw_text|printer_end_doc|printer_end_page|printer_get_option|printer_list|printer_logical_fontheight|printer_open|printer_select_brush|printer_select_font|printer_select_pen|printer_set_option|printer_start_doc|printer_start_page|printer_write|printf|proc_close|proc_get_status|proc_nice|proc_open|proc_terminate|pspell_add_to_personal|pspell_add_to_session|pspell_check|pspell_clear_session|pspell_config_create|pspell_config_ignore|pspell_config_mode|pspell_config_personal|pspell_config_repl|pspell_config_runtogether|pspell_config_save_repl|pspell_new|pspell_new_config|pspell_new_personal|pspell_save_wordlist|pspell_store_replacement|pspell_suggest|putenv|qdom_error|qdom_tree|quoted_printable_decode|quotemeta|rad2deg|rand|range|rawurldecode|rawurlencode|read_exif_data|readdir|readfile|readgzfile|readline|readline_add_history|readline_clear_history|readline_completion_function|readline_info|readline_list_history|readline_read_history|readline_write_history|readlink|realpath|recode|recode_file|recode_string|register_shutdown_function|register_tick_function|rename|reset|restore_error_handler|restore_include_path|rewind|rewinddir|rmdir|round|rsort|rtrim|scandir|sem_acquire|sem_get|sem_release|sem_remove|serialize|sesam_affected_rows|sesam_commit|sesam_connect|sesam_diagnostic|sesam_disconnect|sesam_errormsg|sesam_execimm|sesam_fetch_array|sesam_fetch_result|sesam_fetch_row|sesam_field_array|sesam_field_name|sesam_free_result|sesam_num_fields|sesam_query|sesam_rollback|sesam_seek_row|sesam_settransaction|session_cache_expire|session_cache_limiter|session_commit|session_decode|session_destroy|session_encode|session_get_cookie_params|session_id|session_is_registered|session_module_name|session_name|session_regenerate_id|session_register|session_save_path|session_set_cookie_params|session_set_save_handler|session_start|session_unregister|session_unset|session_write_close|set_error_handler|set_file_buffer|set_include_path|set_magic_quotes_runtime|set_time_limit|setcookie|setlocale|setrawcookie|settype|sha1|sha1_file|shell_exec|shm_attach|shm_detach|shm_get_var|shm_put_var|shm_remove|shm_remove_var|shmop_close|shmop_delete|shmop_open|shmop_read|shmop_size|shmop_write|show_source|shuffle|similar_text|simplexml_import_dom|simplexml_load_file|simplexml_load_string|sin|sinh|sizeof|sleep|snmp_get_quick_print|snmp_set_quick_print|snmpget|snmprealwalk|snmpset|snmpwalk|snmpwalkoid|socket_accept|socket_bind|socket_clear_error|socket_close|socket_connect|socket_create|socket_create_listen|socket_create_pair|socket_get_option|socket_get_status|socket_getpeername|socket_getsockname|socket_iovec_add|socket_iovec_alloc|socket_iovec_delete|socket_iovec_fetch|socket_iovec_free|socket_iovec_set|socket_last_error|socket_listen|socket_read|socket_readv|socket_recv|socket_recvfrom|socket_recvmsg|socket_select|socket_send|socket_sendmsg|socket_sendto|socket_set_block|socket_set_blocking|socket_set_nonblock|socket_set_option|socket_set_timeout|socket_shutdown|socket_strerror|socket_write|socket_writev|sort|soundex|split|spliti|sprintf|sql_regcase|sqlite_array_query|sqlite_busy_timeout|sqlite_changes|sqlite_close|sqlite_column|sqlite_create_aggregate|sqlite_create_function|sqlite_current|sqlite_error_string|sqlite_escape_string|sqlite_fetch_array|sqlite_fetch_single|sqlite_fetch_string|sqlite_field_name|sqlite_has_more|sqlite_last_error|sqlite_last_insert_rowid|sqlite_libencoding|sqlite_libversion|sqlite_next|sqlite_num_fields|sqlite_num_rows|sqlite_open|sqlite_popen|sqlite_query|sqlite_rewind|sqlite_seek|sqlite_udf_decode_binary|sqlite_udf_encode_binary|sqlite_unbuffered_query|sqrt|srand|sscanf|stat|str_ireplace|str_pad|str_repeat|str_replace|str_rot13|str_shuffle|str_split|str_word_count|strcasecmp|strchr|strcmp|strcoll|strcspn|stream_context_create|stream_context_get_options|stream_context_set_option|stream_context_set_params|stream_copy_to_stream|stream_filter_append|stream_filter_prepend|stream_filter_register|stream_get_contents|stream_get_filters|stream_get_line|stream_get_meta_data|stream_get_transports|stream_get_wrappers|stream_register_wrapper|stream_select|stream_set_blocking|stream_set_timeout|stream_set_write_buffer|stream_socket_accept|stream_socket_client|stream_socket_get_name|stream_socket_recvfrom|stream_socket_sendto|stream_socket_server|stream_wrapper_register|strftime|strip_tags|stripcslashes|stripos|stripslashes|stristr|strlen|strnatcasecmp|strnatcmp|strncasecmp|strncmp|strpos|strrchr|strrev|strripos|strrpos|strspn|strstr|strtok|strtolower|strtotime|strtoupper|strtr|strval|substr|substr_compare|substr_count|substr_replace|swf_actiongeturl|swf_actiongotoframe|swf_actiongotolabel|swf_actionnextframe|swf_actionplay|swf_actionprevframe|swf_actionsettarget|swf_actionstop|swf_actiontogglequality|swf_actionwaitforframe|swf_addbuttonrecord|swf_addcolor|swf_closefile|swf_definebitmap|swf_definefont|swf_defineline|swf_definepoly|swf_definerect|swf_definetext|swf_endbutton|swf_enddoaction|swf_endshape|swf_endsymbol|swf_fontsize|swf_fontslant|swf_fonttracking|swf_getbitmapinfo|swf_getfontinfo|swf_getframe|swf_labelframe|swf_lookat|swf_modifyobject|swf_mulcolor|swf_nextid|swf_oncondition|swf_openfile|swf_ortho|swf_ortho2|swf_perspective|swf_placeobject|swf_polarview|swf_popmatrix|swf_posround|swf_pushmatrix|swf_removeobject|swf_rotate|swf_scale|swf_setfont|swf_setframe|swf_shapearc|swf_shapecurveto|swf_shapecurveto3|swf_shapefillbitmapclip|swf_shapefillbitmaptile|swf_shapefilloff|swf_shapefillsolid|swf_shapelinesolid|swf_shapelineto|swf_shapemoveto|swf_showframe|swf_startbutton|swf_startdoaction|swf_startshape|swf_startsymbol|swf_textwidth|swf_translate|swf_viewport|swfaction|swfbitmap|swfbutton|swfbutton_keypress|swfdisplayitem|swffill|swffont|swfgradient|swfmorph|swfmovie|swfshape|swfsprite|swftext|swftextfield|sybase_affected_rows|sybase_close|sybase_connect|sybase_data_seek|sybase_deadlock_retry_count|sybase_fetch_array|sybase_fetch_assoc|sybase_fetch_field|sybase_fetch_object|sybase_fetch_row|sybase_field_seek|sybase_free_result|sybase_get_last_message|sybase_min_client_severity|sybase_min_error_severity|sybase_min_message_severity|sybase_min_server_severity|sybase_num_fields|sybase_num_rows|sybase_pconnect|sybase_query|sybase_result|sybase_select_db|sybase_set_message_handler|sybase_unbuffered_query|symlink|syslog|system|tan|tanh|tcpwrap_check|tempnam|textdomain|tidy_access_count|tidy_clean_repair|tidy_config_count|tidy_diagnose|tidy_error_count|tidy_get_body|tidy_get_config|tidy_get_error_buffer|tidy_get_head|tidy_get_html|tidy_get_html_ver|tidy_get_output|tidy_get_release|tidy_get_root|tidy_get_status|tidy_getopt|tidy_is_xhtml|tidy_is_xml|tidy_load_config|tidy_parse_file|tidy_parse_string|tidy_repair_file|tidy_repair_string|tidy_reset_config|tidy_save_config|tidy_set_encoding|tidy_setopt|tidy_warning_count|time|tmpfile|token_get_all|token_name|touch|trigger_error|trim|uasort|ucfirst|ucwords|udm_add_search_limit|udm_alloc_agent|udm_alloc_agent_array|udm_api_version|udm_cat_list|udm_cat_path|udm_check_charset|udm_check_stored|udm_clear_search_limits|udm_close_stored|udm_crc32|udm_errno|udm_error|udm_find|udm_free_agent|udm_free_ispell_data|udm_free_res|udm_get_doc_count|udm_get_res_field|udm_get_res_param|udm_hash32|udm_load_ispell_data|udm_open_stored|udm_set_agent_param|uksort|umask|uniqid|unixtojd|unlink|unpack|unregister_tick_function|unserialize|unset|urldecode|urlencode|user_error|usleep|usort|utf8_decode|utf8_encode|var_dump|var_export|variant|version_compare|virtual|vpopmail_add_alias_domain|vpopmail_add_alias_domain_ex|vpopmail_add_domain|vpopmail_add_domain_ex|vpopmail_add_user|vpopmail_alias_add|vpopmail_alias_del|vpopmail_alias_del_domain|vpopmail_alias_get|vpopmail_alias_get_all|vpopmail_auth_user|vpopmail_del_domain|vpopmail_del_domain_ex|vpopmail_del_user|vpopmail_error|vpopmail_passwd|vpopmail_set_user_quota|vprintf|vsprintf|w32api_deftype|w32api_init_dtype|w32api_invoke_function|w32api_register_function|w32api_set_call_method|wddx_add_vars|wddx_deserialize|wddx_packet_end|wddx_packet_start|wddx_serialize_value|wddx_serialize_vars|wordwrap|xdiff_file_diff|xdiff_file_diff_binary|xdiff_file_merge3|xdiff_file_patch|xdiff_file_patch_binary|xdiff_string_diff|xdiff_string_diff_binary|xdiff_string_merge3|xdiff_string_patch|xdiff_string_patch_binary|xml_error_string|xml_get_current_byte_index|xml_get_current_column_number|xml_get_current_line_number|xml_get_error_code|xml_parse|xml_parse_into_struct|xml_parser_create|xml_parser_create_ns|xml_parser_free|xml_parser_get_option|xml_parser_set_option|xml_set_character_data_handler|xml_set_default_handler|xml_set_element_handler|xml_set_end_namespace_decl_handler|xml_set_external_entity_ref_handler|xml_set_notation_decl_handler|xml_set_object|xml_set_processing_instruction_handler|xml_set_start_namespace_decl_handler|xml_set_unparsed_entity_decl_handler|xmlrpc_decode|xmlrpc_decode_request|xmlrpc_encode|xmlrpc_encode_request|xmlrpc_get_type|xmlrpc_parse_method_descriptions|xmlrpc_server_add_introspection_data|xmlrpc_server_call_method|xmlrpc_server_create|xmlrpc_server_destroy|xmlrpc_server_register_introspection_callback|xmlrpc_server_register_method|xmlrpc_set_type|xpath_eval|xpath_eval_expression|xpath_new_context|xptr_eval|xptr_new_context|xsl_xsltprocessor_get_parameter|xsl_xsltprocessor_has_exslt_support|xsl_xsltprocessor_import_stylesheet|xsl_xsltprocessor_register_php_functions|xsl_xsltprocessor_remove_parameter|xsl_xsltprocessor_set_parameter|xsl_xsltprocessor_transform_to_doc|xsl_xsltprocessor_transform_to_uri|xsl_xsltprocessor_transform_to_xml|xslt_create|xslt_errno|xslt_error|xslt_free|xslt_process|xslt_set_base|xslt_set_encoding|xslt_set_error_handler|xslt_set_log|xslt_set_sax_handler|xslt_set_sax_handlers|xslt_set_scheme_handler|xslt_set_scheme_handlers|yaz_addinfo|yaz_ccl_conf|yaz_ccl_parse|yaz_close|yaz_connect|yaz_database|yaz_element|yaz_errno|yaz_error|yaz_es_result|yaz_get_option|yaz_hits|yaz_itemorder|yaz_present|yaz_range|yaz_record|yaz_scan|yaz_scan_result|yaz_schema|yaz_search|yaz_set_option|yaz_sort|yaz_syntax|yaz_wait|yp_all|yp_cat|yp_err_string|yp_errno|yp_first|yp_get_default_domain|yp_master|yp_match|yp_next|yp_order|zend_logo_guid|zend_version|zip_close|zip_entry_close|zip_entry_compressedsize|zip_entry_compressionmethod|zip_entry_filesize|zip_entry_name|zip_entry_open|zip_entry_read|zip_open|zip_read|zlib_get_coding_type".split("|")),b=e.arrayToMap("abstract|and|array|as|break|case|catch|cfunction|class|clone|const|continue|declare|default|die|do|else|elseif|enddeclare|endfor|endforeach|endif|endswitch|endwhile|extends|final|for|foreach|function|include|include_once|global|goto|if|implements|interface|instanceof|namespace|new|old_function|or|private|protected|public|return|require|require_once|static|switch|throw|try|use|var|while|xor".split("|")),c=e.arrayToMap("true|false|null|__FILE__|__LINE__|__METHOD__|__FUNCTION__|__CLASS__".split("|")),d=e.arrayToMap("$GLOBALS|$_SERVER|$_GET|$_POST|$_FILES|$_REQUEST|$_SESSION|$_ENV|$_COOKIE|$php_errormsg|$HTTP_RAW_POST_DATA|$http_response_header|$argc|$argv".split("|")),g=e.arrayToMap([]);this.$rules={start:[{token:"support",regex:"<\\?(?:php|\\=)"},{token:"support",regex:"\\?>"},{token:"comment",regex:"\\/\\/.*$"},{token:"comment",regex:"#.*$"},(new f).getStartRule("doc-start"),{token:"comment",regex:"\\/\\*",next:"comment"},{token:"string.regexp",regex:"[/](?:(?:\\[(?:\\\\]|[^\\]])+\\])|(?:\\\\/|[^\\]/]))*[/][gimy]*\\s*(?=[).,;]|$)"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:'["].*\\\\$',next:"qqstring"},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"string",regex:"['].*\\\\$",next:"qstring"},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"},{token:"constant.language",regex:"\\b(?:DEFAULT_INCLUDE_PATH|E_(?:ALL|CO(?:MPILE_(?:ERROR|WARNING)|RE_(?:ERROR|WARNING))|ERROR|NOTICE|PARSE|STRICT|USER_(?:ERROR|NOTICE|WARNING)|WARNING)|P(?:EAR_(?:EXTENSION_DIR|INSTALL_DIR)|HP_(?:BINDIR|CONFIG_FILE_(?:PATH|SCAN_DIR)|DATADIR|E(?:OL|XTENSION_DIR)|INT_(?:MAX|SIZE)|L(?:IBDIR|OCALSTATEDIR)|O(?:S|UTPUT_HANDLER_(?:CONT|END|START))|PREFIX|S(?:API|HLIB_SUFFIX|YSCONFDIR)|VERSION))|__COMPILER_HALT_OFFSET__)\\b"},{token:"constant.language",regex:"\\b(?:A(?:B(?:DAY_(?:1|2|3|4|5|6|7)|MON_(?:1(?:0|1|2|)|2|3|4|5|6|7|8|9))|LT_DIGITS|M_STR|SSERT_(?:ACTIVE|BAIL|CALLBACK|QUIET_EVAL|WARNING))|C(?:ASE_(?:LOWER|UPPER)|HAR_MAX|O(?:DESET|NNECTION_(?:ABORTED|NORMAL|TIMEOUT)|UNT_(?:NORMAL|RECURSIVE))|R(?:EDITS_(?:ALL|DOCS|FULLPAGE|G(?:ENERAL|ROUP)|MODULES|QA|SAPI)|NCYSTR|YPT_(?:BLOWFISH|EXT_DES|MD5|S(?:ALT_LENGTH|TD_DES)))|URRENCY_SYMBOL)|D(?:AY_(?:1|2|3|4|5|6|7)|ECIMAL_POINT|IRECTORY_SEPARATOR|_(?:FMT|T_FMT))|E(?:NT_(?:COMPAT|NOQUOTES|QUOTES)|RA(?:_(?:D_(?:FMT|T_FMT)|T_FMT|YEAR)|)|XTR_(?:IF_EXISTS|OVERWRITE|PREFIX_(?:ALL|I(?:F_EXISTS|NVALID)|SAME)|SKIP))|FRAC_DIGITS|GROUPING|HTML_(?:ENTITIES|SPECIALCHARS)|IN(?:FO_(?:ALL|C(?:ONFIGURATION|REDITS)|ENVIRONMENT|GENERAL|LICENSE|MODULES|VARIABLES)|I_(?:ALL|PERDIR|SYSTEM|USER)|T_(?:CURR_SYMBOL|FRAC_DIGITS))|L(?:C_(?:ALL|C(?:OLLATE|TYPE)|M(?:ESSAGES|ONETARY)|NUMERIC|TIME)|O(?:CK_(?:EX|NB|SH|UN)|G_(?:A(?:LERT|UTH(?:PRIV|))|C(?:ONS|R(?:IT|ON))|D(?:AEMON|EBUG)|E(?:MERG|RR)|INFO|KERN|L(?:OCAL(?:0|1|2|3|4|5|6|7)|PR)|MAIL|N(?:DELAY|EWS|O(?:TICE|WAIT))|ODELAY|P(?:ERROR|ID)|SYSLOG|U(?:SER|UCP)|WARNING)))|M(?:ON_(?:1(?:0|1|2|)|2|3|4|5|6|7|8|9|DECIMAL_POINT|GROUPING|THOUSANDS_SEP)|_(?:1_PI|2_(?:PI|SQRTPI)|E|L(?:N(?:10|2)|OG(?:10E|2E))|PI(?:_(?:2|4)|)|SQRT(?:1_2|2)))|N(?:EGATIVE_SIGN|O(?:EXPR|STR)|_(?:CS_PRECEDES|S(?:EP_BY_SPACE|IGN_POSN)))|P(?:ATH(?:INFO_(?:BASENAME|DIRNAME|EXTENSION)|_SEPARATOR)|M_STR|OSITIVE_SIGN|_(?:CS_PRECEDES|S(?:EP_BY_SPACE|IGN_POSN)))|RADIXCHAR|S(?:EEK_(?:CUR|END|SET)|ORT_(?:ASC|DESC|NUMERIC|REGULAR|STRING)|TR_PAD_(?:BOTH|LEFT|RIGHT))|T(?:HOUS(?:ANDS_SEP|EP)|_FMT(?:_AMPM|))|YES(?:EXPR|STR)|STD(?:IN|OUT|ERR))\\b"},{token:function(e){if(b.hasOwnProperty(e))return"keyword";if(c.hasOwnProperty(e))return"constant.language";if(d.hasOwnProperty(e))return"variable.language";if(g.hasOwnProperty(e))return"invalid.illegal";if(a.hasOwnProperty(e))return"support.function";if(e=="debugger")return"invalid.deprecated";if(e.match(/^(\$[a-zA-Z_][a-zA-Z0-9_]*|self|parent)$/))return"variable";return"identifier"},regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"keyword.operator",regex:"!|\\$|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|instanceof|new|delete|typeof|void)"},{token:"lparen",regex:"[[({]"},{token:"rparen",regex:"[\\])}]"},{token:"text",regex:"\\s+"}],comment:[{token:"comment",regex:".*?\\*\\/",next:"start"},{token:"comment",regex:".+"}],qqstring:[{token:"string",regex:'(?:(?:\\\\.)|(?:[^"\\\\]))*?"',next:"start"},{token:"string",regex:".+"}],qstring:[{token:"string",regex:"(?:(?:\\\\.)|(?:[^'\\\\]))*?'",next:"start"},{token:"string",regex:".+"}]},this.embedRules(f,"doc-",[(new f).getEndRule("start")])};d.inherits(h,g),b.PhpHighlightRules=h}),define("ace/mode/doc_comment_highlight_rules",["require","exports","module","pilot/oop","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text_highlight_rules").TextHighlightRules,f=function(){this.$rules={start:[{token:"comment.doc.tag",regex:"@[\\w\\d_]+"},{token:"comment.doc",regex:"\\s+"},{token:"comment.doc",regex:"TODO"},{token:"comment.doc",regex:"[^@\\*]+"},{token:"comment.doc",regex:"."}]}};d.inherits(f,e),function(){this.getStartRule=function(a){return{token:"comment.doc",regex:"\\/\\*(?=\\*)",next:a}},this.getEndRule=function(a){return{token:"comment.doc",regex:"\\*\\/",next:a}}}.call(f.prototype),b.DocCommentHighlightRules=f}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\}/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\})/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingBraceOutdent=e}),define("ace/mode/behaviour/cstyle",["require","exports","module","pilot/oop","ace/mode/behaviour"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/behaviour").Behaviour,f=function(){this.add("braces","insertion",function(a,b,c,d,e){if(e=="{"){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"{"+g+"}",selection:!1}:{text:"{}",selection:[1,1]}}if(e=="}"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var k=d.$findOpeningBracket("}",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}else if(e=="\n"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var l=d.findMatchingBracket({row:h.row,column:h.column}),m=this.getNextLineIndent(a,i.substring(0,i.length-1),d.getTabString()),n=this.$getIndent(d.doc.getLine(l.row));return{text:"\n"+m+"\n"+n,selection:[1,m.length,1,m.length]}}}return!1}),this.add("braces","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="{"){var g=d.doc.getLine(e.start.row),h=g.substring(e.end.column,e.end.column+1);if(h=="}")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("parens","insertion",function(a,b,c,d,e){if(e=="("){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"("+g+")",selection:!1}:{text:"()",selection:[1,1]}}if(e==")"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j==")"){var k=d.$findOpeningBracket(")",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}return!1}),this.add("parens","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="("){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h==")")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("string_dquotes","insertion",function(a,b,c,d,e){if(e=='"'){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);if(g!=="")return{text:'"'+g+'"',selection:!1};var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column-1,h.column);if(j=="\\")return!1;var k=d.getTokens(f.start.row,f.start.row)[0].tokens,l=0,m,n=-1;for(var o=0;o<k.length;o++){m=k[o],m.type=="string"?n=-1:n<0&&(n=m.value.indexOf('"'));if(m.value.length+l>f.start.column)break;l+=k[o].value.length}if(!m||n<0&&m.type!=="comment"&&(m.type!=="string"||f.start.column!==m.value.length+l-1&&m.value.lastIndexOf('"')===m.value.length-1))return{text:'""',selection:[1,1]};if(m&&m.type==="string"){var p=i.substring(h.column,h.column+1);if(p=='"')return{text:"",selection:[1,1]}}}return!1}),this.add("string_dquotes","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=='"'){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h=='"')return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1})};d.inherits(f,e),b.CstyleBehaviour=f}) \ No newline at end of file
diff --git a/www/js/ace/mode-python.js b/www/js/ace/mode-python.js
new file mode 100644
index 0000000..4c680fa
--- /dev/null
+++ b/www/js/ace/mode-python.js
@@ -0,0 +1 @@
+define("ace/mode/python",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/python_highlight_rules","ace/mode/matching_brace_outdent","ace/range"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/python_highlight_rules").PythonHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=a("ace/range").Range,j=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h};d.inherits(j,e),function(){this.toggleCommentLines=function(a,b,c,d){var e=!0,f=[],g=/^(\s*)#/;for(var h=c;h<=d;h++)if(!g.test(b.getLine(h))){e=!1;break}if(e){var j=new i(0,0,0,0);for(var h=c;h<=d;h++){var k=b.getLine(h),l=k.match(g);j.start.row=h,j.end.row=h,j.end.column=l[0].length,b.replace(j,l[1])}}else b.indentRows(c,d,"#")},this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=this.$tokenizer.getLineTokens(b,a),f=e.tokens,g=e.state;if(f.length&&f[f.length-1].type=="comment")return d;if(a=="start"){var h=b.match(/^.*[\{\(\[\:]\s*$/);h&&(d+=c)}return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)}}.call(j.prototype),b.Mode=j}),define("ace/mode/python_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/text_highlight_rules").TextHighlightRules,g=function(){var a=e.arrayToMap("and|as|assert|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|not|or|pass|print|raise|return|try|while|with|yield".split("|")),b=e.arrayToMap("True|False|None|NotImplemented|Ellipsis|__debug__".split("|")),c=e.arrayToMap("abs|divmod|input|open|staticmethod|all|enumerate|int|ord|str|any|eval|isinstance|pow|sum|basestring|execfile|issubclass|print|super|binfile|iter|property|tuple|bool|filter|len|range|type|bytearray|float|list|raw_input|unichr|callable|format|locals|reduce|unicode|chr|frozenset|long|reload|vars|classmethod|getattr|map|repr|xrange|cmp|globals|max|reversed|zip|compile|hasattr|memoryview|round|__import__|complex|hash|min|set|apply|delattr|help|next|setattr|buffer|dict|hex|object|slice|coerce|dir|id|oct|sorted|intern".split("|")),d=e.arrayToMap("".split("|")),f="(?:r|u|ur|R|U|UR|Ur|uR)?",g="(?:(?:[1-9]\\d*)|(?:0))",h="(?:0[oO]?[0-7]+)",i="(?:0[xX][\\dA-Fa-f]+)",j="(?:0[bB][01]+)",k="(?:"+g+"|"+h+"|"+i+"|"+j+")",l="(?:[eE][+-]?\\d+)",m="(?:\\.\\d+)",n="(?:\\d+)",o="(?:(?:"+n+"?"+m+")|(?:"+n+"\\.))",p="(?:(?:"+o+"|"+n+")"+l+")",q="(?:"+p+"|"+o+")";this.$rules={start:[{token:"comment",regex:"#.*$"},{token:"string",regex:f+'"{3}(?:[^\\\\]|\\\\.)*?"{3}'},{token:"string",regex:f+'"{3}.*$',next:"qqstring"},{token:"string",regex:f+'"(?:[^\\\\]|\\\\.)*?"'},{token:"string",regex:f+"'{3}(?:[^\\\\]|\\\\.)*?'{3}"},{token:"string",regex:f+"'{3}.*$",next:"qstring"},{token:"string",regex:f+"'(?:[^\\\\]|\\\\.)*?'"},{token:"constant.numeric",regex:"(?:"+q+"|\\d+)[jJ]\\b"},{token:"constant.numeric",regex:q},{token:"constant.numeric",regex:k+"[lL]\\b"},{token:"constant.numeric",regex:k+"\\b"},{token:function(e){return a.hasOwnProperty(e)?"keyword":b.hasOwnProperty(e)?"constant.language":d.hasOwnProperty(e)?"invalid.illegal":c.hasOwnProperty(e)?"support.function":e=="debugger"?"invalid.deprecated":"identifier"},regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"keyword.operator",regex:"\\+|\\-|\\*|\\*\\*|\\/|\\/\\/|%|<<|>>|&|\\||\\^|~|<|>|<=|=>|==|!=|<>|="},{token:"lparen",regex:"[\\[\\(\\{]"},{token:"rparen",regex:"[\\]\\)\\}]"},{token:"text",regex:"\\s+"}],qqstring:[{token:"string",regex:'(?:[^\\\\]|\\\\.)*?"{3}',next:"start"},{token:"string",regex:".+"}],qstring:[{token:"string",regex:"(?:[^\\\\]|\\\\.)*?'{3}",next:"start"},{token:"string",regex:".+"}]}};d.inherits(g,f),b.PythonHighlightRules=g}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\}/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\})/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingBraceOutdent=e}) \ No newline at end of file
diff --git a/www/js/ace/mode-ruby.js b/www/js/ace/mode-ruby.js
new file mode 100644
index 0000000..7b185e0
--- /dev/null
+++ b/www/js/ace/mode-ruby.js
@@ -0,0 +1 @@
+define("ace/mode/ruby",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/ruby_highlight_rules","ace/mode/matching_brace_outdent","ace/range"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/ruby_highlight_rules").RubyHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=a("ace/range").Range,j=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h};d.inherits(j,e),function(){this.toggleCommentLines=function(a,b,c,d){var e=!0,f=[],g=/^(\s*)#/;for(var h=c;h<=d;h++)if(!g.test(b.getLine(h))){e=!1;break}if(e){var j=new i(0,0,0,0);for(var h=c;h<=d;h++){var k=b.getLine(h),l=k.match(g);j.start.row=h,j.end.row=h,j.end.column=l[0].length,b.replace(j,l[1])}}else b.indentRows(c,d,"#")},this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=this.$tokenizer.getLineTokens(b,a),f=e.tokens,g=e.state;if(f.length&&f[f.length-1].type=="comment")return d;if(a=="start"){var h=b.match(/^.*[\{\(\[]\s*$/);h&&(d+=c)}return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)}}.call(j.prototype),b.Mode=j}),define("ace/mode/ruby_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/text_highlight_rules").TextHighlightRules,g=function(){var a=e.arrayToMap("abort|Array|assert|assert_equal|assert_not_equal|assert_same|assert_not_same|assert_nil|assert_not_nil|assert_match|assert_no_match|assert_in_delta|assert_throws|assert_raise|assert_nothing_raised|assert_instance_of|assert_kind_of|assert_respond_to|assert_operator|assert_send|assert_difference|assert_no_difference|assert_recognizes|assert_generates|assert_response|assert_redirected_to|assert_template|assert_select|assert_select_email|assert_select_rjs|assert_select_encoded|css_select|at_exit|attr|attr_writer|attr_reader|attr_accessor|attr_accessible|autoload|binding|block_given?|callcc|caller|catch|chomp|chomp!|chop|chop!|defined?|delete_via_redirect|eval|exec|exit|exit!|fail|Float|flunk|follow_redirect!|fork|form_for|form_tag|format|gets|global_variables|gsub|gsub!|get_via_redirect|h|host!|https?|https!|include|Integer|lambda|link_to|link_to_unless_current|link_to_function|link_to_remote|load|local_variables|loop|open|open_session|p|print|printf|proc|putc|puts|post_via_redirect|put_via_redirect|raise|rand|raw|readline|readlines|redirect?|request_via_redirect|require|scan|select|set_trace_func|sleep|split|sprintf|srand|String|stylesheet_link_tag|syscall|system|sub|sub!|test|throw|trace_var|trap|untrace_var|atan2|cos|exp|frexp|ldexp|log|log10|sin|sqrt|tan|render|javascript_include_tag|csrf_meta_tag|label_tag|text_field_tag|submit_tag|check_box_tag|content_tag|radio_button_tag|text_area_tag|password_field_tag|hidden_field_tag|fields_for|select_tag|options_for_select|options_from_collection_for_select|collection_select|time_zone_select|select_date|select_time|select_datetime|date_select|time_select|datetime_select|select_year|select_month|select_day|select_hour|select_minute|select_second|file_field_tag|file_field|respond_to|skip_before_filter|around_filter|after_filter|verify|protect_from_forgery|rescue_from|helper_method|redirect_to|before_filter|send_data|send_file|validates_presence_of|validates_uniqueness_of|validates_length_of|validates_format_of|validates_acceptance_of|validates_associated|validates_exclusion_of|validates_inclusion_of|validates_numericality_of|validates_with|validates_each|authenticate_or_request_with_http_basic|authenticate_or_request_with_http_digest|filter_parameter_logging|match|get|post|resources|redirect|scope|assert_routing|translate|localize|extract_locale_from_tld|t|l|caches_page|expire_page|caches_action|expire_action|cache|expire_fragment|expire_cache_for|observe|cache_sweeper|has_many|has_one|belongs_to|has_and_belongs_to_many".split("|")),b=e.arrayToMap("alias|and|BEGIN|begin|break|case|class|def|defined|do|else|elsif|END|end|ensure|__FILE__|finally|for|gem|if|in|__LINE__|module|next|not|or|private|protected|public|redo|rescue|retry|return|super|then|undef|unless|until|when|while|yield".split("|")),c=e.arrayToMap("true|TRUE|false|FALSE|nil|NIL|ARGF|ARGV|DATA|ENV|RUBY_PLATFORM|RUBY_RELEASE_DATE|RUBY_VERSION|STDERR|STDIN|STDOUT|TOPLEVEL_BINDING".split("|")),d=e.arrayToMap("$DEBUG|$defout|$FILENAME|$LOAD_PATH|$SAFE|$stdin|$stdout|$stderr|$VERBOSE|$!|root_url|flash|session|cookies|params|request|response|logger".split("|"));this.$rules={start:[{token:"comment",regex:"#.*$"},{token:"comment",regex:"^=begin$",next:"comment"},{token:"string.regexp",regex:"[/](?:(?:\\[(?:\\\\]|[^\\]])+\\])|(?:\\\\/|[^\\]/]))*[/]\\w*\\s*(?=[).,;]|$)"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"string",regex:"[`](?:(?:\\\\.)|(?:[^'\\\\]))*?[`]"},{token:"variable.instancce",regex:"@{1,2}(?:[a-zA-Z_]|d)+"},{token:"variable.class",regex:"[A-Z](?:[a-zA-Z_]|d)+"},{token:"string",regex:"[:](?:[a-zA-Z]|d)+"},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"},{token:"constant.language.boolean",regex:"(?:true|false)\\b"},{token:function(e){return e=="self"?"variable.language":b.hasOwnProperty(e)?"keyword":c.hasOwnProperty(e)?"constant.language":d.hasOwnProperty(e)?"variable.language":a.hasOwnProperty(e)?"support.function":e=="debugger"?"invalid.deprecated":"identifier"},regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"keyword.operator",regex:"!|\\$|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|instanceof|new|delete|typeof|void)"},{token:"lparen",regex:"[[({]"},{token:"rparen",regex:"[\\])}]"},{token:"text",regex:"\\s+"}],comment:[{token:"comment",regex:"^=end$",next:"start"},{token:"comment",regex:".+"}]}};d.inherits(g,f),b.RubyHighlightRules=g}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\}/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\})/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingBraceOutdent=e}) \ No newline at end of file
diff --git a/www/js/ace/mode-scss.js b/www/js/ace/mode-scss.js
new file mode 100644
index 0000000..49a9eb1
--- /dev/null
+++ b/www/js/ace/mode-scss.js
@@ -0,0 +1 @@
+define("ace/mode/scss",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/scss_highlight_rules","ace/mode/matching_brace_outdent"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/scss_highlight_rules").ScssHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h};d.inherits(i,e),function(){this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=this.$tokenizer.getLineTokens(b,a).tokens;if(e.length&&e[e.length-1].type=="comment")return d;var f=b.match(/^.*\{\s*$/);f&&(d+=c);return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)}}.call(i.prototype),b.Mode=i}),define("ace/mode/scss_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/text_highlight_rules").TextHighlightRules,g=function(){function i(a){var b=[],c=a.split("");for(var d=0;d<c.length;d++)b.push("[",c[d].toLowerCase(),c[d].toUpperCase(),"]");return b.join("")}var a=e.arrayToMap(function(){var a="-webkit-|-moz-|-o-|-ms-|-svg-|-pie-|-khtml-".split("|"),b="appearance|background-clip|background-inline-policy|background-origin|background-size|binding|border-bottom-colors|border-left-colors|border-right-colors|border-top-colors|border-end|border-end-color|border-end-style|border-end-width|border-image|border-start|border-start-color|border-start-style|border-start-width|box-align|box-direction|box-flex|box-flexgroup|box-ordinal-group|box-orient|box-pack|box-sizing|column-count|column-gap|column-width|column-rule|column-rule-width|column-rule-style|column-rule-color|float-edge|font-feature-settings|font-language-override|force-broken-image-icon|image-region|margin-end|margin-start|opacity|outline|outline-color|outline-offset|outline-radius|outline-radius-bottomleft|outline-radius-bottomright|outline-radius-topleft|outline-radius-topright|outline-style|outline-width|padding-end|padding-start|stack-sizing|tab-size|text-blink|text-decoration-color|text-decoration-line|text-decoration-style|transform|transform-origin|transition|transition-delay|transition-duration|transition-property|transition-timing-function|user-focus|user-input|user-modify|user-select|window-shadow|border-radius".split("|"),c="azimuth|background-attachment|background-color|background-image|background-position|background-repeat|background|border-bottom-color|border-bottom-style|border-bottom-width|border-bottom|border-collapse|border-color|border-left-color|border-left-style|border-left-width|border-left|border-right-color|border-right-style|border-right-width|border-right|border-spacing|border-style|border-top-color|border-top-style|border-top-width|border-top|border-width|border|bottom|box-sizing|caption-side|clear|clip|color|content|counter-increment|counter-reset|cue-after|cue-before|cue|cursor|direction|display|elevation|empty-cells|float|font-family|font-size-adjust|font-size|font-stretch|font-style|font-variant|font-weight|font|height|left|letter-spacing|line-height|list-style-image|list-style-position|list-style-type|list-style|margin-bottom|margin-left|margin-right|margin-top|marker-offset|margin|marks|max-height|max-width|min-height|min-width|opacity|orphans|outline-color|outline-style|outline-width|outline|overflow|overflow-x|overflow-y|padding-bottom|padding-left|padding-right|padding-top|padding|page-break-after|page-break-before|page-break-inside|page|pause-after|pause-before|pause|pitch-range|pitch|play-during|position|quotes|richness|right|size|speak-header|speak-numeral|speak-punctuation|speech-rate|speak|stress|table-layout|text-align|text-decoration|text-indent|text-shadow|text-transform|top|unicode-bidi|vertical-align|visibility|voice-family|volume|white-space|widows|width|word-spacing|z-index".split("|"),d=[];for(var e=0,f=a.length;e<f;e++)Array.prototype.push.apply(d,(a[e]+b.join("|"+a[e])).split("|"));Array.prototype.push.apply(d,b),Array.prototype.push.apply(d,c);return d}()),b=e.arrayToMap("hsl|hsla|rgb|rgba|url|attr|counter|counters|abs|adjust_color|adjust_hue|alpha|join|blue|ceil|change_color|comparable|complement|darken|desaturate|floor|grayscale|green|hue|if|invert|join|length|lighten|lightness|mix|nth|opacify|opacity|percentage|quote|red|round|saturate|saturation|scale_color|transparentize|type_of|unit|unitless|unqoute".split("|")),c=e.arrayToMap("absolute|all-scroll|always|armenian|auto|baseline|below|bidi-override|block|bold|bolder|border-box|both|bottom|break-all|break-word|capitalize|center|char|circle|cjk-ideographic|col-resize|collapse|content-box|crosshair|dashed|decimal-leading-zero|decimal|default|disabled|disc|distribute-all-lines|distribute-letter|distribute-space|distribute|dotted|double|e-resize|ellipsis|fixed|georgian|groove|hand|hebrew|help|hidden|hiragana-iroha|hiragana|horizontal|ideograph-alpha|ideograph-numeric|ideograph-parenthesis|ideograph-space|inactive|inherit|inline-block|inline|inset|inside|inter-ideograph|inter-word|italic|justify|katakana-iroha|katakana|keep-all|left|lighter|line-edge|line-through|line|list-item|loose|lower-alpha|lower-greek|lower-latin|lower-roman|lowercase|lr-tb|ltr|medium|middle|move|n-resize|ne-resize|newspaper|no-drop|no-repeat|nw-resize|none|normal|not-allowed|nowrap|oblique|outset|outside|overline|pointer|progress|relative|repeat-x|repeat-y|repeat|right|ridge|row-resize|rtl|s-resize|scroll|se-resize|separate|small-caps|solid|square|static|strict|super|sw-resize|table-footer-group|table-header-group|tb-rl|text-bottom|text-top|text|thick|thin|top|transparent|underline|upper-alpha|upper-latin|upper-roman|uppercase|vertical-ideographic|vertical-text|visible|w-resize|wait|whitespace|zero".split("|")),d=e.arrayToMap("aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow".split("|")),f=e.arrayToMap("@mixin|@extend|@include|@import|@media|@debug|@warn|@if|@for|@each|@while|@else|@font-face|@-webkit-keyframes|if|and|!default|module|def|end|declare".split("|")),g=e.arrayToMap("a|abbr|acronym|address|applet|area|article|aside|audio|b|base|basefont|bdo|big|blockquote|body|br|button|canvas|caption|center|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|dir|div|dl|dt|em|embed|fieldset|figcaption|figure|font|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hgroup|hr|html|i|iframe|img|input|ins|keygen|kbd|label|legend|li|link|map|mark|menu|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|s|samp|script|section|select|small|source|span|strike|strong|style|sub|summary|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|u|ul|var|video|wbr|xmp".split("|")),h="\\-?(?:(?:[0-9]+)|(?:[0-9]*\\.[0-9]+))";this.$rules={start:[{token:"comment",regex:"\\/\\/.*$"},{token:"comment",regex:"\\/\\*",next:"comment"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:'["].*\\\\$',next:"qqstring"},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"string",regex:"['].*\\\\$",next:"qstring"},{token:"constant.numeric",regex:h+i("em")},{token:"constant.numeric",regex:h+i("ex")},{token:"constant.numeric",regex:h+i("px")},{token:"constant.numeric",regex:h+i("cm")},{token:"constant.numeric",regex:h+i("mm")},{token:"constant.numeric",regex:h+i("in")},{token:"constant.numeric",regex:h+i("pt")},{token:"constant.numeric",regex:h+i("pc")},{token:"constant.numeric",regex:h+i("deg")},{token:"constant.numeric",regex:h+i("rad")},{token:"constant.numeric",regex:h+i("grad")},{token:"constant.numeric",regex:h+i("ms")},{token:"constant.numeric",regex:h+i("s")},{token:"constant.numeric",regex:h+i("hz")},{token:"constant.numeric",regex:h+i("khz")},{token:"constant.numeric",regex:h+"%"},{token:"constant.numeric",regex:"#[a-fA-F0-9]{6}"},{token:"constant.numeric",regex:"#[a-fA-F0-9]{3}"},{token:"constant.numeric",regex:h},{token:function(e){if(a.hasOwnProperty(e.toLowerCase()))return"support.type";return f.hasOwnProperty(e)?"keyword":c.hasOwnProperty(e)?"constant.language":b.hasOwnProperty(e)?"support.function":d.hasOwnProperty(e.toLowerCase())?"support.constant.color":g.hasOwnProperty(e.toLowerCase())?"variable.language":"text"},regex:"\\-?[@a-zA-Z_][@a-zA-Z0-9_\\-]*"},{token:"variable",regex:"[a-zA-Z_\\-$][a-zA-Z0-9_\\-$]*\\b"},{token:"variable.language",regex:"#[a-zA-Z0-9-_]+"},{token:"variable.language",regex:"\\.[a-zA-Z0-9-_]+"},{token:"variable.language",regex:":[a-zA-Z0-9-_]+"},{token:"constant",regex:"[a-zA-Z0-9-_]+"},{token:"keyword.operator",regex:"<|>|<=|>=|==|!=|-|%|#|\\+|\\$|\\+|\\*"},{token:"lparen",regex:"[[({]"},{token:"rparen",regex:"[\\])}]"},{token:"text",regex:"\\s+"}],comment:[{token:"comment",regex:".*?\\*\\/",next:"start"},{token:"comment",regex:".+"}],qqstring:[{token:"string",regex:'(?:(?:\\\\.)|(?:[^"\\\\]))*?"',next:"start"},{token:"string",regex:".+"}],qstring:[{token:"string",regex:"(?:(?:\\\\.)|(?:[^'\\\\]))*?'",next:"start"},{token:"string",regex:".+"}]}};d.inherits(g,f),b.ScssHighlightRules=g}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\}/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\})/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingBraceOutdent=e}) \ No newline at end of file
diff --git a/www/js/ace/mode-svg.js b/www/js/ace/mode-svg.js
new file mode 100644
index 0000000..700e4bb
--- /dev/null
+++ b/www/js/ace/mode-svg.js
@@ -0,0 +1 @@
+define("ace/mode/svg",["require","exports","module","pilot/oop","ace/mode/text","ace/mode/javascript","ace/tokenizer","ace/mode/svg_highlight_rules","ace/mode/behaviour/xml"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/mode/javascript").Mode,g=a("ace/tokenizer").Tokenizer,h=a("ace/mode/svg_highlight_rules").SvgHighlightRules,i=a("ace/mode/behaviour/xml").XmlBehaviour,j=function(){this.highlighter=new h,this.$tokenizer=new g(this.highlighter.getRules()),this.$behaviour=new i,this.$embeds=this.highlighter.getEmbeds(),this.createModeDelegates({"js-":f})};d.inherits(j,e),function(){this.toggleCommentLines=function(a,b,c,d){return 0},this.getNextLineIndent=function(a,b,c){return this.$getIndent(b)},this.checkOutdent=function(a,b,c){return!1}}.call(j.prototype),b.Mode=j}),define("ace/mode/javascript",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/javascript_highlight_rules","ace/mode/matching_brace_outdent","ace/range","ace/worker/worker_client","ace/mode/behaviour/cstyle"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/javascript_highlight_rules").JavaScriptHighlightRules,h=a("ace/mode/matching_brace_outdent").MatchingBraceOutdent,i=a("ace/range").Range,j=a("ace/worker/worker_client").WorkerClient,k=a("ace/mode/behaviour/cstyle").CstyleBehaviour,l=function(){this.$tokenizer=new f((new g).getRules()),this.$outdent=new h,this.$behaviour=new k};d.inherits(l,e),function(){this.toggleCommentLines=function(a,b,c,d){var e=!0,f=[],g=/^(\s*)\/\//;for(var h=c;h<=d;h++)if(!g.test(b.getLine(h))){e=!1;break}if(e){var j=new i(0,0,0,0);for(var h=c;h<=d;h++){var k=b.getLine(h),l=k.match(g);j.start.row=h,j.end.row=h,j.end.column=l[0].length,b.replace(j,l[1])}}else b.indentRows(c,d,"//")},this.getNextLineIndent=function(a,b,c){var d=this.$getIndent(b),e=this.$tokenizer.getLineTokens(b,a),f=e.tokens,g=e.state;if(f.length&&f[f.length-1].type=="comment")return d;if(a=="start"){var h=b.match(/^.*[\{\(\[]\s*$/);h&&(d+=c)}else if(a=="doc-start"){if(g=="start")return"";var h=b.match(/^\s*(\/?)\*/);h&&(h[1]&&(d+=" "),d+="* ")}return d},this.checkOutdent=function(a,b,c){return this.$outdent.checkOutdent(b,c)},this.autoOutdent=function(a,b,c){this.$outdent.autoOutdent(b,c)},this.createWorker=function(a){var b=a.getDocument(),c=new j(["ace","pilot"],"worker-javascript.js","ace/mode/javascript_worker","JavaScriptWorker");c.call("setValue",[b.getValue()]),b.on("change",function(a){a.range={start:a.data.range.start,end:a.data.range.end},c.emit("change",a)}),c.on("jslint",function(b){var c=[];for(var d=0;d<b.data.length;d++){var e=b.data[d];e&&c.push({row:e.line-1,column:e.character-1,text:e.reason,type:"warning",lint:e})}a.setAnnotations(c)}),c.on("narcissus",function(b){a.setAnnotations([b.data])}),c.on("terminate",function(){a.clearAnnotations()});return c}}.call(l.prototype),b.Mode=l}),define("ace/mode/javascript_highlight_rules",["require","exports","module","pilot/oop","pilot/lang","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/lang"),f=a("ace/mode/doc_comment_highlight_rules").DocCommentHighlightRules,g=a("ace/mode/text_highlight_rules").TextHighlightRules,h=function(){var a=e.arrayToMap("break|case|catch|continue|default|delete|do|else|finally|for|function|if|in|instanceof|new|return|switch|throw|try|typeof|let|var|while|with|const|yield|import|get|set".split("|")),b=e.arrayToMap("null|Infinity|NaN|undefined".split("|")),c=e.arrayToMap("class|enum|extends|super|export|implements|private|public|interface|package|protected|static".split("|"));this.$rules={start:[{token:"comment",regex:"\\/\\/.*$"},(new f).getStartRule("doc-start"),{token:"comment",regex:"\\/\\*",next:"comment"},{token:"string.regexp",regex:"[/](?:(?:\\[(?:\\\\]|[^\\]])+\\])|(?:\\\\/|[^\\]/]))*[/]\\w*\\s*(?=[).,;]|$)"},{token:"string",regex:'["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]'},{token:"string",regex:'["].*\\\\$',next:"qqstring"},{token:"string",regex:"['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']"},{token:"string",regex:"['].*\\\\$",next:"qstring"},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"},{token:"constant.language.boolean",regex:"(?:true|false)\\b"},{token:function(d){return d=="this"?"variable.language":a.hasOwnProperty(d)?"keyword":b.hasOwnProperty(d)?"constant.language":c.hasOwnProperty(d)?"invalid.illegal":d=="debugger"?"invalid.deprecated":"identifier"},regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"keyword.operator",regex:"!|\\$|%|&|\\*|\\-\\-|\\-|\\+\\+|\\+|~|===|==|=|!=|!==|<=|>=|<<=|>>=|>>>=|<>|<|>|!|&&|\\|\\||\\?\\:|\\*=|%=|\\+=|\\-=|&=|\\^=|\\b(?:in|instanceof|new|delete|typeof|void)"},{token:"lparen",regex:"[[({]"},{token:"rparen",regex:"[\\])}]"},{token:"comment",regex:"^#!.*$"},{token:"text",regex:"\\s+"}],comment:[{token:"comment",regex:".*?\\*\\/",next:"start"},{token:"comment",regex:".+"}],qqstring:[{token:"string",regex:'(?:(?:\\\\.)|(?:[^"\\\\]))*?"',next:"start"},{token:"string",regex:".+"}],qstring:[{token:"string",regex:"(?:(?:\\\\.)|(?:[^'\\\\]))*?'",next:"start"},{token:"string",regex:".+"}]},this.embedRules(f,"doc-",[(new f).getEndRule("start")])};d.inherits(h,g),b.JavaScriptHighlightRules=h}),define("ace/mode/doc_comment_highlight_rules",["require","exports","module","pilot/oop","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text_highlight_rules").TextHighlightRules,f=function(){this.$rules={start:[{token:"comment.doc.tag",regex:"@[\\w\\d_]+"},{token:"comment.doc",regex:"\\s+"},{token:"comment.doc",regex:"TODO"},{token:"comment.doc",regex:"[^@\\*]+"},{token:"comment.doc",regex:"."}]}};d.inherits(f,e),function(){this.getStartRule=function(a){return{token:"comment.doc",regex:"\\/\\*(?=\\*)",next:a}},this.getEndRule=function(a){return{token:"comment.doc",regex:"\\*\\/",next:a}}}.call(f.prototype),b.DocCommentHighlightRules=f}),define("ace/mode/matching_brace_outdent",["require","exports","module","ace/range"],function(a,b,c){var d=a("ace/range").Range,e=function(){};(function(){this.checkOutdent=function(a,b){if(!/^\s+$/.test(a))return!1;return/^\s*\}/.test(b)},this.autoOutdent=function(a,b){var c=a.getLine(b),e=c.match(/^(\s*\})/);if(!e)return 0;var f=e[1].length,g=a.findMatchingBracket({row:b,column:f});if(!g||g.row==b)return 0;var h=this.$getIndent(a.getLine(g.row));a.replace(new d(b,0,b,f-1),h)},this.$getIndent=function(a){var b=a.match(/^(\s+)/);if(b)return b[1];return""}}).call(e.prototype),b.MatchingBraceOutdent=e}),define("ace/worker/worker_client",["require","exports","module","pilot/oop","pilot/event_emitter"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=function(b,c,d,e){this.callbacks=[];if(a.packaged)var f=this.$guessBasePath(),g=this.$worker=new Worker(f+c);else{var h=a.nameToUrl("ace/worker/worker",null,"_"),g=this.$worker=new Worker(h),i={};for(var j=0;j<b.length;j++){var k=b[j];i[k]=a.nameToUrl(k,null,"_").replace(/.js$/,"")}}this.$worker.postMessage({init:!0,tlns:i,module:d,classname:e}),this.callbackId=1,this.callbacks={};var l=this;this.$worker.onerror=function(a){window.console&&console.log&&console.log(a);throw a},this.$worker.onmessage=function(a){var b=a.data;switch(b.type){case"log":window.console&&console.log&&console.log(b.data);break;case"event":l._dispatchEvent(b.name,{data:b.data});break;case"call":var c=l.callbacks[b.id];c&&(c(b.data),delete l.callbacks[b.id])}}};(function(){d.implement(this,e),this.$guessBasePath=function(){if(a.aceBaseUrl)return a.aceBaseUrl;var b=document.getElementsByTagName("script");for(var c=0;c<b.length;c++){var d=b[c].src||b[c].getAttribute("src");if(!d)continue;var e=d.match(/^(?:(.*\/)ace\.js|(.*\/)ace-uncompressed\.js)(?:\?|$)/);if(e)return e[1]||e[2]}return""},this.terminate=function(){this._dispatchEvent("terminate",{}),this.$worker.terminate()},this.send=function(a,b){this.$worker.postMessage({command:a,args:b})},this.call=function(a,b,c){if(c){var d=this.callbackId++;this.callbacks[d]=c,b.push(d)}this.send(a,b)},this.emit=function(a,b){this.$worker.postMessage({event:a,data:b})}}).call(f.prototype),b.WorkerClient=f}),define("ace/mode/behaviour/cstyle",["require","exports","module","pilot/oop","ace/mode/behaviour"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/behaviour").Behaviour,f=function(){this.add("braces","insertion",function(a,b,c,d,e){if(e=="{"){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"{"+g+"}",selection:!1}:{text:"{}",selection:[1,1]}}if(e=="}"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var k=d.$findOpeningBracket("}",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}else if(e=="\n"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var l=d.findMatchingBracket({row:h.row,column:h.column}),m=this.getNextLineIndent(a,i.substring(0,i.length-1),d.getTabString()),n=this.$getIndent(d.doc.getLine(l.row));return{text:"\n"+m+"\n"+n,selection:[1,m.length,1,m.length]}}}return!1}),this.add("braces","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="{"){var g=d.doc.getLine(e.start.row),h=g.substring(e.end.column,e.end.column+1);if(h=="}")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("parens","insertion",function(a,b,c,d,e){if(e=="("){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"("+g+")",selection:!1}:{text:"()",selection:[1,1]}}if(e==")"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j==")"){var k=d.$findOpeningBracket(")",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}return!1}),this.add("parens","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="("){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h==")")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("string_dquotes","insertion",function(a,b,c,d,e){if(e=='"'){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);if(g!=="")return{text:'"'+g+'"',selection:!1};var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column-1,h.column);if(j=="\\")return!1;var k=d.getTokens(f.start.row,f.start.row)[0].tokens,l=0,m,n=-1;for(var o=0;o<k.length;o++){m=k[o],m.type=="string"?n=-1:n<0&&(n=m.value.indexOf('"'));if(m.value.length+l>f.start.column)break;l+=k[o].value.length}if(!m||n<0&&m.type!=="comment"&&(m.type!=="string"||f.start.column!==m.value.length+l-1&&m.value.lastIndexOf('"')===m.value.length-1))return{text:'""',selection:[1,1]};if(m&&m.type==="string"){var p=i.substring(h.column,h.column+1);if(p=='"')return{text:"",selection:[1,1]}}}return!1}),this.add("string_dquotes","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=='"'){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h=='"')return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1})};d.inherits(f,e),b.CstyleBehaviour=f}),define("ace/mode/svg_highlight_rules",["require","exports","module","pilot/oop","ace/mode/javascript_highlight_rules","ace/mode/xml_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/javascript_highlight_rules").JavaScriptHighlightRules,f=a("ace/mode/xml_highlight_rules").XmlHighlightRules,g=function(){f.call(this),this.$rules.start.splice(3,0,{token:"text",regex:"<(?=s*script)",next:"script"}),this.$rules.script=[{token:"text",regex:">",next:"js-start"},{token:"keyword",regex:"[-_a-zA-Z0-9:]+"},{token:"text",regex:"\\s+"},{token:"string",regex:'".*?"'},{token:"string",regex:"'.*?'"}],this.embedRules(e,"js-",[{token:"comment",regex:"\\/\\/.*(?=<\\/script>)",next:"tag"},{token:"text",regex:"<\\/(?=script)",next:"tag"}])};d.inherits(g,f),b.SvgHighlightRules=g}),define("ace/mode/xml_highlight_rules",["require","exports","module","pilot/oop","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text_highlight_rules").TextHighlightRules,f=function(){this.$rules={start:[{token:"text",regex:"<\\!\\[CDATA\\[",next:"cdata"},{token:"xml_pe",regex:"<\\?.*?\\?>"},{token:"comment",regex:"<\\!--",next:"comment"},{token:"text",regex:"<\\/?",next:"tag"},{token:"text",regex:"\\s+"},{token:"text",regex:"[^<]+"}],tag:[{token:"text",regex:">",next:"start"},{token:"keyword",regex:"[-_a-zA-Z0-9:]+"},{token:"text",regex:"\\s+"},{token:"string",regex:'".*?"'},{token:"string",regex:'["].*$',next:"qqstring"},{token:"string",regex:"'.*?'"},{token:"string",regex:"['].*$",next:"qstring"}],qstring:[{token:"string",regex:".*'",next:"tag"},{token:"string",regex:".+"}],qqstring:[{token:"string",regex:'.*"',next:"tag"},{token:"string",regex:".+"}],cdata:[{token:"text",regex:"\\]\\]>",next:"start"},{token:"text",regex:"\\s+"},{token:"text",regex:"(?:[^\\]]|\\](?!\\]>))+"}],comment:[{token:"comment",regex:".*?-->",next:"start"},{token:"comment",regex:".+"}]}};d.inherits(f,e),b.XmlHighlightRules=f}),define("ace/mode/behaviour/xml",["require","exports","module","pilot/oop","ace/mode/behaviour","ace/mode/behaviour/cstyle"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/behaviour").Behaviour,f=a("ace/mode/behaviour/cstyle").CstyleBehaviour,g=function(){this.inherit(f,["string_dquotes"]),this.add("brackets","insertion",function(a,b,c,d,e){if(e=="<"){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?!1:{text:"<>",selection:[1,1]}}if(e==">"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j==">")return{text:"",selection:[1,1]}}else if(e=="\n"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),k=i.substring(h.column,h.column+2);if(k=="</"){var l=this.$getIndent(d.doc.getLine(h.row))+d.getTabString(),m=this.$getIndent(d.doc.getLine(h.row));return{text:"\n"+l+"\n"+m,selection:[1,l.length,1,l.length]}}}return!1})};d.inherits(g,e),b.XmlBehaviour=g}) \ No newline at end of file
diff --git a/www/js/ace/mode-xml.js b/www/js/ace/mode-xml.js
new file mode 100644
index 0000000..bca8943
--- /dev/null
+++ b/www/js/ace/mode-xml.js
@@ -0,0 +1 @@
+define("ace/mode/xml",["require","exports","module","pilot/oop","ace/mode/text","ace/tokenizer","ace/mode/xml_highlight_rules","ace/mode/behaviour/xml"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text").Mode,f=a("ace/tokenizer").Tokenizer,g=a("ace/mode/xml_highlight_rules").XmlHighlightRules,h=a("ace/mode/behaviour/xml").XmlBehaviour,i=function(){this.$tokenizer=new f((new g).getRules()),this.$behaviour=new h};d.inherits(i,e),function(){this.getNextLineIndent=function(a,b,c){return this.$getIndent(b)}}.call(i.prototype),b.Mode=i}),define("ace/mode/xml_highlight_rules",["require","exports","module","pilot/oop","ace/mode/text_highlight_rules"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/text_highlight_rules").TextHighlightRules,f=function(){this.$rules={start:[{token:"text",regex:"<\\!\\[CDATA\\[",next:"cdata"},{token:"xml_pe",regex:"<\\?.*?\\?>"},{token:"comment",regex:"<\\!--",next:"comment"},{token:"text",regex:"<\\/?",next:"tag"},{token:"text",regex:"\\s+"},{token:"text",regex:"[^<]+"}],tag:[{token:"text",regex:">",next:"start"},{token:"keyword",regex:"[-_a-zA-Z0-9:]+"},{token:"text",regex:"\\s+"},{token:"string",regex:'".*?"'},{token:"string",regex:'["].*$',next:"qqstring"},{token:"string",regex:"'.*?'"},{token:"string",regex:"['].*$",next:"qstring"}],qstring:[{token:"string",regex:".*'",next:"tag"},{token:"string",regex:".+"}],qqstring:[{token:"string",regex:'.*"',next:"tag"},{token:"string",regex:".+"}],cdata:[{token:"text",regex:"\\]\\]>",next:"start"},{token:"text",regex:"\\s+"},{token:"text",regex:"(?:[^\\]]|\\](?!\\]>))+"}],comment:[{token:"comment",regex:".*?-->",next:"start"},{token:"comment",regex:".+"}]}};d.inherits(f,e),b.XmlHighlightRules=f}),define("ace/mode/behaviour/xml",["require","exports","module","pilot/oop","ace/mode/behaviour","ace/mode/behaviour/cstyle"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/behaviour").Behaviour,f=a("ace/mode/behaviour/cstyle").CstyleBehaviour,g=function(){this.inherit(f,["string_dquotes"]),this.add("brackets","insertion",function(a,b,c,d,e){if(e=="<"){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?!1:{text:"<>",selection:[1,1]}}if(e==">"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j==">")return{text:"",selection:[1,1]}}else if(e=="\n"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),k=i.substring(h.column,h.column+2);if(k=="</"){var l=this.$getIndent(d.doc.getLine(h.row))+d.getTabString(),m=this.$getIndent(d.doc.getLine(h.row));return{text:"\n"+l+"\n"+m,selection:[1,l.length,1,l.length]}}}return!1})};d.inherits(g,e),b.XmlBehaviour=g}),define("ace/mode/behaviour/cstyle",["require","exports","module","pilot/oop","ace/mode/behaviour"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/mode/behaviour").Behaviour,f=function(){this.add("braces","insertion",function(a,b,c,d,e){if(e=="{"){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"{"+g+"}",selection:!1}:{text:"{}",selection:[1,1]}}if(e=="}"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var k=d.$findOpeningBracket("}",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}else if(e=="\n"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j=="}"){var l=d.findMatchingBracket({row:h.row,column:h.column}),m=this.getNextLineIndent(a,i.substring(0,i.length-1),d.getTabString()),n=this.$getIndent(d.doc.getLine(l.row));return{text:"\n"+m+"\n"+n,selection:[1,m.length,1,m.length]}}}return!1}),this.add("braces","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="{"){var g=d.doc.getLine(e.start.row),h=g.substring(e.end.column,e.end.column+1);if(h=="}")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("parens","insertion",function(a,b,c,d,e){if(e=="("){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);return g!==""?{text:"("+g+")",selection:!1}:{text:"()",selection:[1,1]}}if(e==")"){var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column,h.column+1);if(j==")"){var k=d.$findOpeningBracket(")",{column:h.column+1,row:h.row});if(k!==null)return{text:"",selection:[1,1]}}}return!1}),this.add("parens","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=="("){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h==")")return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1}),this.add("string_dquotes","insertion",function(a,b,c,d,e){if(e=='"'){var f=c.getSelectionRange(),g=d.doc.getTextRange(f);if(g!=="")return{text:'"'+g+'"',selection:!1};var h=c.getCursorPosition(),i=d.doc.getLine(h.row),j=i.substring(h.column-1,h.column);if(j=="\\")return!1;var k=d.getTokens(f.start.row,f.start.row)[0].tokens,l=0,m,n=-1;for(var o=0;o<k.length;o++){m=k[o],m.type=="string"?n=-1:n<0&&(n=m.value.indexOf('"'));if(m.value.length+l>f.start.column)break;l+=k[o].value.length}if(!m||n<0&&m.type!=="comment"&&(m.type!=="string"||f.start.column!==m.value.length+l-1&&m.value.lastIndexOf('"')===m.value.length-1))return{text:'""',selection:[1,1]};if(m&&m.type==="string"){var p=i.substring(h.column,h.column+1);if(p=='"')return{text:"",selection:[1,1]}}}return!1}),this.add("string_dquotes","deletion",function(a,b,c,d,e){var f=d.doc.getTextRange(e);if(!e.isMultiLine()&&f=='"'){var g=d.doc.getLine(e.start.row),h=g.substring(e.start.column+1,e.start.column+2);if(h=='"')return new Range(e.start.row,e.start.column,e.start.row,e.end.column+1)}return!1})};d.inherits(f,e),b.CstyleBehaviour=f}) \ No newline at end of file
diff --git a/www/js/ace/theme-clouds.js b/www/js/ace/theme-clouds.js
new file mode 100644
index 0000000..c14d3cb
--- /dev/null
+++ b/www/js/ace/theme-clouds.js
@@ -0,0 +1 @@
+define("ace/theme/clouds",["require","exports","module"],function(a,b,c){var d=a("pilot/dom"),e=".ace-clouds .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-clouds .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-clouds .ace_gutter {\n width: 50px;\n background: #e8e8e8;\n color: #333;\n overflow : hidden;\n}\n\n.ace-clouds .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-clouds .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-clouds .ace_print_margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-clouds .ace_scroller {\n background-color: #FFFFFF;\n}\n\n.ace-clouds .ace_text-layer {\n cursor: text;\n color: #000000;\n}\n\n.ace-clouds .ace_cursor {\n border-left: 2px solid #000000;\n}\n\n.ace-clouds .ace_cursor.ace_overwrite {\n border-left: 0px;\n border-bottom: 1px solid #000000;\n}\n \n.ace-clouds .ace_marker-layer .ace_selection {\n background: #BDD5FC;\n}\n\n.ace-clouds .ace_marker-layer .ace_step {\n background: rgb(198, 219, 174);\n}\n\n.ace-clouds .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid #BFBFBF;\n}\n\n.ace-clouds .ace_marker-layer .ace_active_line {\n background: #FFFBD1;\n}\n\n \n.ace-clouds .ace_invisible {\n color: #BFBFBF;\n}\n\n.ace-clouds .ace_keyword {\n color:#AF956F;\n}\n\n.ace-clouds .ace_keyword.ace_operator {\n color:#484848;\n}\n\n.ace-clouds .ace_constant {\n \n}\n\n.ace-clouds .ace_constant.ace_language {\n color:#39946A;\n}\n\n.ace-clouds .ace_constant.ace_library {\n \n}\n\n.ace-clouds .ace_constant.ace_numeric {\n color:#46A609;\n}\n\n.ace-clouds .ace_invalid {\n background-color:#FF002A;\n}\n\n.ace-clouds .ace_invalid.ace_illegal {\n \n}\n\n.ace-clouds .ace_invalid.ace_deprecated {\n \n}\n\n.ace-clouds .ace_support {\n \n}\n\n.ace-clouds .ace_support.ace_function {\n color:#C52727;\n}\n\n.ace-clouds .ace_function.ace_buildin {\n \n}\n\n.ace-clouds .ace_string {\n color:#5D90CD;\n}\n\n.ace-clouds .ace_string.ace_regexp {\n \n}\n\n.ace-clouds .ace_comment {\n color:#BCC8BA;\n}\n\n.ace-clouds .ace_comment.ace_doc {\n \n}\n\n.ace-clouds .ace_comment.ace_doc.ace_tag {\n \n}\n\n.ace-clouds .ace_variable {\n \n}\n\n.ace-clouds .ace_variable.ace_language {\n \n}\n\n.ace-clouds .ace_xml_pe {\n \n}";d.importCssString(e),b.cssClass="ace-clouds"}) \ No newline at end of file
diff --git a/www/js/ace/theme-clouds_midnight.js b/www/js/ace/theme-clouds_midnight.js
new file mode 100644
index 0000000..52137fb
--- /dev/null
+++ b/www/js/ace/theme-clouds_midnight.js
@@ -0,0 +1 @@
+define("ace/theme/clouds_midnight",["require","exports","module"],function(a,b,c){var d=a("pilot/dom"),e=".ace-clouds-midnight .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-clouds-midnight .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-clouds-midnight .ace_gutter {\n width: 50px;\n background: #e8e8e8;\n color: #333;\n overflow : hidden;\n}\n\n.ace-clouds-midnight .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-clouds-midnight .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-clouds-midnight .ace_print_margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-clouds-midnight .ace_scroller {\n background-color: #191919;\n}\n\n.ace-clouds-midnight .ace_text-layer {\n cursor: text;\n color: #929292;\n}\n\n.ace-clouds-midnight .ace_cursor {\n border-left: 2px solid #7DA5DC;\n}\n\n.ace-clouds-midnight .ace_cursor.ace_overwrite {\n border-left: 0px;\n border-bottom: 1px solid #7DA5DC;\n}\n \n.ace-clouds-midnight .ace_marker-layer .ace_selection {\n background: #000000;\n}\n\n.ace-clouds-midnight .ace_marker-layer .ace_step {\n background: rgb(198, 219, 174);\n}\n\n.ace-clouds-midnight .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid #BFBFBF;\n}\n\n.ace-clouds-midnight .ace_marker-layer .ace_active_line {\n background: rgba(215, 215, 215, 0.031);\n}\n\n \n.ace-clouds-midnight .ace_invisible {\n color: #BFBFBF;\n}\n\n.ace-clouds-midnight .ace_keyword {\n color:#927C5D;\n}\n\n.ace-clouds-midnight .ace_keyword.ace_operator {\n color:#4B4B4B;\n}\n\n.ace-clouds-midnight .ace_constant {\n \n}\n\n.ace-clouds-midnight .ace_constant.ace_language {\n color:#39946A;\n}\n\n.ace-clouds-midnight .ace_constant.ace_library {\n \n}\n\n.ace-clouds-midnight .ace_constant.ace_numeric {\n color:#46A609;\n}\n\n.ace-clouds-midnight .ace_invalid {\n color:#FFFFFF;\nbackground-color:#E92E2E;\n}\n\n.ace-clouds-midnight .ace_invalid.ace_illegal {\n \n}\n\n.ace-clouds-midnight .ace_invalid.ace_deprecated {\n \n}\n\n.ace-clouds-midnight .ace_support {\n \n}\n\n.ace-clouds-midnight .ace_support.ace_function {\n color:#E92E2E;\n}\n\n.ace-clouds-midnight .ace_function.ace_buildin {\n \n}\n\n.ace-clouds-midnight .ace_string {\n color:#5D90CD;\n}\n\n.ace-clouds-midnight .ace_string.ace_regexp {\n \n}\n\n.ace-clouds-midnight .ace_comment {\n color:#3C403B;\n}\n\n.ace-clouds-midnight .ace_comment.ace_doc {\n \n}\n\n.ace-clouds-midnight .ace_comment.ace_doc.ace_tag {\n \n}\n\n.ace-clouds-midnight .ace_variable {\n \n}\n\n.ace-clouds-midnight .ace_variable.ace_language {\n \n}\n\n.ace-clouds-midnight .ace_xml_pe {\n \n}";d.importCssString(e),b.cssClass="ace-clouds-midnight"}) \ No newline at end of file
diff --git a/www/js/ace/theme-cobalt.js b/www/js/ace/theme-cobalt.js
new file mode 100644
index 0000000..51be7b2
--- /dev/null
+++ b/www/js/ace/theme-cobalt.js
@@ -0,0 +1 @@
+define("ace/theme/cobalt",["require","exports","module"],function(a,b,c){var d=a("pilot/dom"),e=".ace-cobalt .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-cobalt .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-cobalt .ace_gutter {\n width: 50px;\n background: #e8e8e8;\n color: #333;\n overflow : hidden;\n}\n\n.ace-cobalt .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-cobalt .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-cobalt .ace_print_margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-cobalt .ace_scroller {\n background-color: #002240;\n}\n\n.ace-cobalt .ace_text-layer {\n cursor: text;\n color: #FFFFFF;\n}\n\n.ace-cobalt .ace_cursor {\n border-left: 2px solid #FFFFFF;\n}\n\n.ace-cobalt .ace_cursor.ace_overwrite {\n border-left: 0px;\n border-bottom: 1px solid #FFFFFF;\n}\n \n.ace-cobalt .ace_marker-layer .ace_selection {\n background: rgba(179, 101, 57, 0.75);\n}\n\n.ace-cobalt .ace_marker-layer .ace_step {\n background: rgb(198, 219, 174);\n}\n\n.ace-cobalt .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid rgba(255, 255, 255, 0.15);\n}\n\n.ace-cobalt .ace_marker-layer .ace_active_line {\n background: rgba(0, 0, 0, 0.35);\n}\n\n \n.ace-cobalt .ace_invisible {\n color: rgba(255, 255, 255, 0.15);\n}\n\n.ace-cobalt .ace_keyword {\n color:#FF9D00;\n}\n\n.ace-cobalt .ace_keyword.ace_operator {\n \n}\n\n.ace-cobalt .ace_constant {\n color:#FF628C;\n}\n\n.ace-cobalt .ace_constant.ace_language {\n \n}\n\n.ace-cobalt .ace_constant.ace_library {\n \n}\n\n.ace-cobalt .ace_constant.ace_numeric {\n \n}\n\n.ace-cobalt .ace_invalid {\n color:#F8F8F8;\nbackground-color:#800F00;\n}\n\n.ace-cobalt .ace_invalid.ace_illegal {\n \n}\n\n.ace-cobalt .ace_invalid.ace_deprecated {\n \n}\n\n.ace-cobalt .ace_support {\n color:#80FFBB;\n}\n\n.ace-cobalt .ace_support.ace_function {\n color:#FFB054;\n}\n\n.ace-cobalt .ace_function.ace_buildin {\n \n}\n\n.ace-cobalt .ace_string {\n \n}\n\n.ace-cobalt .ace_string.ace_regexp {\n color:#80FFC2;\n}\n\n.ace-cobalt .ace_comment {\n font-style:italic;\ncolor:#0088FF;\n}\n\n.ace-cobalt .ace_comment.ace_doc {\n \n}\n\n.ace-cobalt .ace_comment.ace_doc.ace_tag {\n \n}\n\n.ace-cobalt .ace_variable {\n color:#CCCCCC;\n}\n\n.ace-cobalt .ace_variable.ace_language {\n color:#FF80E1;\n}\n\n.ace-cobalt .ace_xml_pe {\n \n}";d.importCssString(e),b.cssClass="ace-cobalt"}) \ No newline at end of file
diff --git a/www/js/ace/theme-dawn.js b/www/js/ace/theme-dawn.js
new file mode 100644
index 0000000..3b98416
--- /dev/null
+++ b/www/js/ace/theme-dawn.js
@@ -0,0 +1 @@
+define("ace/theme/dawn",["require","exports","module"],function(a,b,c){var d=a("pilot/dom"),e=".ace-dawn .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-dawn .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-dawn .ace_gutter {\n width: 50px;\n background: #e8e8e8;\n color: #333;\n overflow : hidden;\n}\n\n.ace-dawn .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-dawn .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-dawn .ace_print_margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-dawn .ace_scroller {\n background-color: #F9F9F9;\n}\n\n.ace-dawn .ace_text-layer {\n cursor: text;\n color: #080808;\n}\n\n.ace-dawn .ace_cursor {\n border-left: 2px solid #000000;\n}\n\n.ace-dawn .ace_cursor.ace_overwrite {\n border-left: 0px;\n border-bottom: 1px solid #000000;\n}\n \n.ace-dawn .ace_marker-layer .ace_selection {\n background: rgba(39, 95, 255, 0.30);\n}\n\n.ace-dawn .ace_marker-layer .ace_step {\n background: rgb(198, 219, 174);\n}\n\n.ace-dawn .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid rgba(75, 75, 126, 0.50);\n}\n\n.ace-dawn .ace_marker-layer .ace_active_line {\n background: rgba(36, 99, 180, 0.12);\n}\n\n \n.ace-dawn .ace_invisible {\n color: rgba(75, 75, 126, 0.50);\n}\n\n.ace-dawn .ace_keyword {\n color:#794938;\n}\n\n.ace-dawn .ace_keyword.ace_operator {\n \n}\n\n.ace-dawn .ace_constant {\n color:#811F24;\n}\n\n.ace-dawn .ace_constant.ace_language {\n \n}\n\n.ace-dawn .ace_constant.ace_library {\n \n}\n\n.ace-dawn .ace_constant.ace_numeric {\n \n}\n\n.ace-dawn .ace_invalid {\n \n}\n\n.ace-dawn .ace_invalid.ace_illegal {\n text-decoration:underline;\nfont-style:italic;\ncolor:#F8F8F8;\nbackground-color:#B52A1D;\n}\n\n.ace-dawn .ace_invalid.ace_deprecated {\n text-decoration:underline;\nfont-style:italic;\ncolor:#B52A1D;\n}\n\n.ace-dawn .ace_support {\n color:#691C97;\n}\n\n.ace-dawn .ace_support.ace_function {\n color:#693A17;\n}\n\n.ace-dawn .ace_function.ace_buildin {\n \n}\n\n.ace-dawn .ace_string {\n color:#0B6125;\n}\n\n.ace-dawn .ace_string.ace_regexp {\n color:#CF5628;\n}\n\n.ace-dawn .ace_comment {\n font-style:italic;\ncolor:#5A525F;\n}\n\n.ace-dawn .ace_comment.ace_doc {\n \n}\n\n.ace-dawn .ace_comment.ace_doc.ace_tag {\n \n}\n\n.ace-dawn .ace_variable {\n color:#234A97;\n}\n\n.ace-dawn .ace_variable.ace_language {\n \n}\n\n.ace-dawn .ace_xml_pe {\n \n}";d.importCssString(e),b.cssClass="ace-dawn"}) \ No newline at end of file
diff --git a/www/js/ace/theme-eclipse.js b/www/js/ace/theme-eclipse.js
new file mode 100644
index 0000000..e74e173
--- /dev/null
+++ b/www/js/ace/theme-eclipse.js
@@ -0,0 +1 @@
+define("ace/theme/eclipse",["require","exports","module"],function(a,b,c){var d=a("pilot/dom"),e=".ace-eclipse .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-eclipse .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-eclipse .ace_gutter {\n width: 50px;\n background: rgb(227, 227, 227);\n border-right: 1px solid rgb(159, 159, 159);\t \n color: rgb(136, 136, 136);\n}\n\n.ace-eclipse .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-eclipse .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-eclipse .ace_text-layer {\n cursor: text;\n}\n\n.ace-eclipse .ace_cursor {\n border-left: 1px solid black;\n}\n\n.ace-eclipse .ace_line .ace_keyword, .ace-eclipse .ace_line .ace_variable {\n color: rgb(127, 0, 85);\n}\n\n.ace-eclipse .ace_line .ace_constant.ace_buildin {\n color: rgb(88, 72, 246);\n}\n\n.ace-eclipse .ace_line .ace_constant.ace_library {\n color: rgb(6, 150, 14);\n}\n\n.ace-eclipse .ace_line .ace_function {\n color: rgb(60, 76, 114);\n}\n\n.ace-eclipse .ace_line .ace_string {\n color: rgb(42, 0, 255);\n}\n\n.ace-eclipse .ace_line .ace_comment {\n color: rgb(63, 127, 95);\n}\n\n.ace-eclipse .ace_line .ace_comment.ace_doc {\n color: rgb(63, 95, 191);\n}\n\n.ace-eclipse .ace_line .ace_comment.ace_doc.ace_tag {\n color: rgb(127, 159, 191);\n}\n\n.ace-eclipse .ace_line .ace_constant.ace_numeric {\n}\n\n.ace-eclipse .ace_line .ace_tag {\n\tcolor: rgb(63, 127, 127);\n}\n\n.ace-eclipse .ace_line .ace_xml_pe {\n color: rgb(104, 104, 91);\n}\n\n.ace-eclipse .ace_marker-layer .ace_selection {\n background: rgb(181, 213, 255);\n}\n\n.ace-eclipse .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid rgb(192, 192, 192);\n}\n\n.ace-eclipse .ace_marker-layer .ace_active_line {\n background: rgb(232, 242, 254);\n}";d.importCssString(e),b.cssClass="ace-eclipse"}) \ No newline at end of file
diff --git a/www/js/ace/theme-idle_fingers.js b/www/js/ace/theme-idle_fingers.js
new file mode 100644
index 0000000..40c7b95
--- /dev/null
+++ b/www/js/ace/theme-idle_fingers.js
@@ -0,0 +1 @@
+define("ace/theme/idle_fingers",["require","exports","module"],function(a,b,c){var d=a("pilot/dom"),e=".ace-idle-fingers .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-idle-fingers .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-idle-fingers .ace_gutter {\n width: 50px;\n background: #e8e8e8;\n color: #333;\n overflow : hidden;\n}\n\n.ace-idle-fingers .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-idle-fingers .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-idle-fingers .ace_print_margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-idle-fingers .ace_scroller {\n background-color: #323232;\n}\n\n.ace-idle-fingers .ace_text-layer {\n cursor: text;\n color: #FFFFFF;\n}\n\n.ace-idle-fingers .ace_cursor {\n border-left: 2px solid #91FF00;\n}\n\n.ace-idle-fingers .ace_cursor.ace_overwrite {\n border-left: 0px;\n border-bottom: 1px solid #91FF00;\n}\n \n.ace-idle-fingers .ace_marker-layer .ace_selection {\n background: rgba(90, 100, 126, 0.88);\n}\n\n.ace-idle-fingers .ace_marker-layer .ace_step {\n background: rgb(198, 219, 174);\n}\n\n.ace-idle-fingers .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid #404040;\n}\n\n.ace-idle-fingers .ace_marker-layer .ace_active_line {\n background: #353637;\n}\n\n \n.ace-idle-fingers .ace_invisible {\n color: #404040;\n}\n\n.ace-idle-fingers .ace_keyword {\n color:#CC7833;\n}\n\n.ace-idle-fingers .ace_keyword.ace_operator {\n \n}\n\n.ace-idle-fingers .ace_constant {\n color:#6C99BB;\n}\n\n.ace-idle-fingers .ace_constant.ace_language {\n \n}\n\n.ace-idle-fingers .ace_constant.ace_library {\n \n}\n\n.ace-idle-fingers .ace_constant.ace_numeric {\n \n}\n\n.ace-idle-fingers .ace_invalid {\n color:#FFFFFF;\nbackground-color:#FF0000;\n}\n\n.ace-idle-fingers .ace_invalid.ace_illegal {\n \n}\n\n.ace-idle-fingers .ace_invalid.ace_deprecated {\n \n}\n\n.ace-idle-fingers .ace_support {\n \n}\n\n.ace-idle-fingers .ace_support.ace_function {\n color:#B83426;\n}\n\n.ace-idle-fingers .ace_function.ace_buildin {\n \n}\n\n.ace-idle-fingers .ace_string {\n color:#A5C261;\n}\n\n.ace-idle-fingers .ace_string.ace_regexp {\n color:#CCCC33;\n}\n\n.ace-idle-fingers .ace_comment {\n font-style:italic;\ncolor:#BC9458;\n}\n\n.ace-idle-fingers .ace_comment.ace_doc {\n \n}\n\n.ace-idle-fingers .ace_comment.ace_doc.ace_tag {\n \n}\n\n.ace-idle-fingers .ace_variable {\n \n}\n\n.ace-idle-fingers .ace_variable.ace_language {\n \n}\n\n.ace-idle-fingers .ace_xml_pe {\n \n}\n\n.ace-idle-fingers .ace_collab.ace_user1 {\n color:#323232;\nbackground-color:#FFF980; \n}";d.importCssString(e),b.cssClass="ace-idle-fingers"}) \ No newline at end of file
diff --git a/www/js/ace/theme-kr_theme.js b/www/js/ace/theme-kr_theme.js
new file mode 100644
index 0000000..2fac8ba
--- /dev/null
+++ b/www/js/ace/theme-kr_theme.js
@@ -0,0 +1 @@
+define("ace/theme/kr_theme",["require","exports","module"],function(a,b,c){var d=a("pilot/dom"),e=".ace-kr-theme .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-kr-theme .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-kr-theme .ace_gutter {\n width: 50px;\n background: #e8e8e8;\n color: #333;\n overflow : hidden;\n}\n\n.ace-kr-theme .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-kr-theme .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-kr-theme .ace_print_margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-kr-theme .ace_scroller {\n background-color: #0B0A09;\n}\n\n.ace-kr-theme .ace_text-layer {\n cursor: text;\n color: #FCFFE0;\n}\n\n.ace-kr-theme .ace_cursor {\n border-left: 2px solid #FF9900;\n}\n\n.ace-kr-theme .ace_cursor.ace_overwrite {\n border-left: 0px;\n border-bottom: 1px solid #FF9900;\n}\n \n.ace-kr-theme .ace_marker-layer .ace_selection {\n background: rgba(170, 0, 255, 0.45);\n}\n\n.ace-kr-theme .ace_marker-layer .ace_step {\n background: rgb(198, 219, 174);\n}\n\n.ace-kr-theme .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid rgba(255, 177, 111, 0.32);\n}\n\n.ace-kr-theme .ace_marker-layer .ace_active_line {\n background: #38403D;\n}\n\n \n.ace-kr-theme .ace_invisible {\n color: rgba(255, 177, 111, 0.32);\n}\n\n.ace-kr-theme .ace_keyword {\n color:#949C8B;\n}\n\n.ace-kr-theme .ace_keyword.ace_operator {\n \n}\n\n.ace-kr-theme .ace_constant {\n color:rgba(210, 117, 24, 0.76);\n}\n\n.ace-kr-theme .ace_constant.ace_language {\n \n}\n\n.ace-kr-theme .ace_constant.ace_library {\n \n}\n\n.ace-kr-theme .ace_constant.ace_numeric {\n \n}\n\n.ace-kr-theme .ace_invalid {\n color:#F8F8F8;\nbackground-color:#A41300;\n}\n\n.ace-kr-theme .ace_invalid.ace_illegal {\n \n}\n\n.ace-kr-theme .ace_invalid.ace_deprecated {\n \n}\n\n.ace-kr-theme .ace_support {\n color:#9FC28A;\n}\n\n.ace-kr-theme .ace_support.ace_function {\n color:#85873A;\n}\n\n.ace-kr-theme .ace_function.ace_buildin {\n \n}\n\n.ace-kr-theme .ace_string {\n \n}\n\n.ace-kr-theme .ace_string.ace_regexp {\n color:rgba(125, 255, 192, 0.65);\n}\n\n.ace-kr-theme .ace_comment {\n font-style:italic;\ncolor:#706D5B;\n}\n\n.ace-kr-theme .ace_comment.ace_doc {\n \n}\n\n.ace-kr-theme .ace_comment.ace_doc.ace_tag {\n \n}\n\n.ace-kr-theme .ace_variable {\n color:#D1A796;\n}\n\n.ace-kr-theme .ace_variable.ace_language {\n color:#FF80E1;\n}\n\n.ace-kr-theme .ace_xml_pe {\n \n}";d.importCssString(e),b.cssClass="ace-kr-theme"}) \ No newline at end of file
diff --git a/www/js/ace/theme-merbivore.js b/www/js/ace/theme-merbivore.js
new file mode 100644
index 0000000..407e6f6
--- /dev/null
+++ b/www/js/ace/theme-merbivore.js
@@ -0,0 +1 @@
+define("ace/theme/merbivore",["require","exports","module"],function(a,b,c){var d=a("pilot/dom"),e=".ace-merbivore .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-merbivore .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-merbivore .ace_gutter {\n width: 50px;\n background: #e8e8e8;\n color: #333;\n overflow : hidden;\n}\n\n.ace-merbivore .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-merbivore .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-merbivore .ace_print_margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-merbivore .ace_scroller {\n background-color: #161616;\n}\n\n.ace-merbivore .ace_text-layer {\n cursor: text;\n color: #E6E1DC;\n}\n\n.ace-merbivore .ace_cursor {\n border-left: 2px solid #FFFFFF;\n}\n\n.ace-merbivore .ace_cursor.ace_overwrite {\n border-left: 0px;\n border-bottom: 1px solid #FFFFFF;\n}\n \n.ace-merbivore .ace_marker-layer .ace_selection {\n background: #454545;\n}\n\n.ace-merbivore .ace_marker-layer .ace_step {\n background: rgb(198, 219, 174);\n}\n\n.ace-merbivore .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid #FCE94F;\n}\n\n.ace-merbivore .ace_marker-layer .ace_active_line {\n background: #333435;\n}\n\n \n.ace-merbivore .ace_invisible {\n color: #404040;\n}\n\n.ace-merbivore .ace_keyword {\n color:#FC6F09;\n}\n\n.ace-merbivore .ace_keyword.ace_operator {\n \n}\n\n.ace-merbivore .ace_constant {\n color:#1EDAFB;\n}\n\n.ace-merbivore .ace_constant.ace_language {\n color:#FDC251;\n}\n\n.ace-merbivore .ace_constant.ace_library {\n color:#8DFF0A;\n}\n\n.ace-merbivore .ace_constant.ace_numeric {\n color:#58C554;\n}\n\n.ace-merbivore .ace_invalid {\n color:#FFFFFF;\n background-color:#990000;\n}\n\n.ace-merbivore .ace_invalid.ace_illegal {\n \n}\n\n.ace-merbivore .ace_invalid.ace_deprecated {\n color:#FFFFFF;\n background-color:#990000;\n}\n\n.ace-merbivore .ace_support {\n \n}\n\n.ace-merbivore .ace_support.ace_function {\n color:#FC6F09;\n}\n\n.ace-merbivore .ace_function.ace_buildin {\n \n}\n\n.ace-merbivore .ace_string {\n color:#8DFF0A;\n}\n\n.ace-merbivore .ace_string.ace_regexp {\n \n}\n\n.ace-merbivore .ace_comment {\n color:#AD2EA4;\n}\n\n.ace-merbivore .ace_comment.ace_doc {\n \n}\n\n.ace-merbivore .ace_comment.ace_doc.ace_tag {\n \n}\n\n.ace-merbivore .ace_variable {\n \n}\n\n.ace-merbivore .ace_variable.ace_language {\n \n}\n\n.ace-merbivore .ace_xml_pe {\n \n}";d.importCssString(e),b.cssClass="ace-merbivore"}) \ No newline at end of file
diff --git a/www/js/ace/theme-merbivore_soft.js b/www/js/ace/theme-merbivore_soft.js
new file mode 100644
index 0000000..5be28c5
--- /dev/null
+++ b/www/js/ace/theme-merbivore_soft.js
@@ -0,0 +1 @@
+define("ace/theme/merbivore_soft",["require","exports","module"],function(a,b,c){var d=a("pilot/dom"),e=".ace-merbivore-soft .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-merbivore-soft .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-merbivore-soft .ace_gutter {\n width: 50px;\n background: #e8e8e8;\n color: #333;\n overflow : hidden;\n}\n\n.ace-merbivore-soft .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-merbivore-soft .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-merbivore-soft .ace_print_margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-merbivore-soft .ace_scroller {\n background-color: #1C1C1C;\n}\n\n.ace-merbivore-soft .ace_text-layer {\n cursor: text;\n color: #E6E1DC;\n}\n\n.ace-merbivore-soft .ace_cursor {\n border-left: 2px solid #FFFFFF;\n}\n\n.ace-merbivore-soft .ace_cursor.ace_overwrite {\n border-left: 0px;\n border-bottom: 1px solid #FFFFFF;\n}\n \n.ace-merbivore-soft .ace_marker-layer .ace_selection {\n background: #494949;\n}\n\n.ace-merbivore-soft .ace_marker-layer .ace_step {\n background: rgb(198, 219, 174);\n}\n\n.ace-merbivore-soft .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid #FCE94F;\n}\n\n.ace-merbivore-soft .ace_marker-layer .ace_active_line {\n background: #333435;\n}\n\n \n.ace-merbivore-soft .ace_invisible {\n color: #404040;\n}\n\n.ace-merbivore-soft .ace_keyword {\n color:#FC803A;\n}\n\n.ace-merbivore-soft .ace_keyword.ace_operator {\n \n}\n\n.ace-merbivore-soft .ace_constant {\n color:#68C1D8;\n}\n\n.ace-merbivore-soft .ace_constant.ace_language {\n color:#E1C582;\n}\n\n.ace-merbivore-soft .ace_constant.ace_library {\n color:#8EC65F;\n}\n\n.ace-merbivore-soft .ace_constant.ace_numeric {\n color:#7FC578;\n}\n\n.ace-merbivore-soft .ace_invalid {\n color:#FFFFFF;\n background-color:#FE3838;\n}\n\n.ace-merbivore-soft .ace_invalid.ace_illegal {\n \n}\n\n.ace-merbivore-soft .ace_invalid.ace_deprecated {\n color:#FFFFFF;\n background-color:#FE3838;\n}\n\n.ace-merbivore-soft .ace_support {\n \n}\n\n.ace-merbivore-soft .ace_support.ace_function {\n color:#FC803A;\n}\n\n.ace-merbivore-soft .ace_function.ace_buildin {\n \n}\n\n.ace-merbivore-soft .ace_string {\n color:#8EC65F;\n}\n\n.ace-merbivore-soft .ace_string.ace_regexp {\n \n}\n\n.ace-merbivore-soft .ace_comment {\n color:#AC4BB8;\n}\n\n.ace-merbivore-soft .ace_comment.ace_doc {\n \n}\n\n.ace-merbivore-soft .ace_comment.ace_doc.ace_tag {\n \n}\n\n.ace-merbivore-soft .ace_variable {\n \n}\n\n.ace-merbivore-soft .ace_variable.ace_language {\n \n}\n\n.ace-merbivore-soft .ace_xml_pe {\n \n}";d.importCssString(e),b.cssClass="ace-merbivore-soft"}) \ No newline at end of file
diff --git a/www/js/ace/theme-mono_industrial.js b/www/js/ace/theme-mono_industrial.js
new file mode 100644
index 0000000..354de6b
--- /dev/null
+++ b/www/js/ace/theme-mono_industrial.js
@@ -0,0 +1 @@
+define("ace/theme/mono_industrial",["require","exports","module"],function(a,b,c){var d=a("pilot/dom"),e=".ace-mono-industrial .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-mono-industrial .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-mono-industrial .ace_gutter {\n width: 50px;\n background: #e8e8e8;\n color: #333;\n overflow : hidden;\n}\n\n.ace-mono-industrial .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-mono-industrial .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-mono-industrial .ace_print_margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-mono-industrial .ace_scroller {\n background-color: #222C28;\n}\n\n.ace-mono-industrial .ace_text-layer {\n cursor: text;\n color: #FFFFFF;\n}\n\n.ace-mono-industrial .ace_cursor {\n border-left: 2px solid #FFFFFF;\n}\n\n.ace-mono-industrial .ace_cursor.ace_overwrite {\n border-left: 0px;\n border-bottom: 1px solid #FFFFFF;\n}\n \n.ace-mono-industrial .ace_marker-layer .ace_selection {\n background: rgba(145, 153, 148, 0.40);\n}\n\n.ace-mono-industrial .ace_marker-layer .ace_step {\n background: rgb(198, 219, 174);\n}\n\n.ace-mono-industrial .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid rgba(102, 108, 104, 0.50);\n}\n\n.ace-mono-industrial .ace_marker-layer .ace_active_line {\n background: rgba(12, 13, 12, 0.25);\n}\n\n \n.ace-mono-industrial .ace_invisible {\n color: rgba(102, 108, 104, 0.50);\n}\n\n.ace-mono-industrial .ace_keyword {\n color:#A39E64;\n}\n\n.ace-mono-industrial .ace_keyword.ace_operator {\n color:#A8B3AB;\n}\n\n.ace-mono-industrial .ace_constant {\n color:#E98800;\n}\n\n.ace-mono-industrial .ace_constant.ace_language {\n \n}\n\n.ace-mono-industrial .ace_constant.ace_library {\n \n}\n\n.ace-mono-industrial .ace_constant.ace_numeric {\n color:#E98800;\n}\n\n.ace-mono-industrial .ace_invalid {\n color:#FFFFFF;\nbackground-color:rgba(153, 0, 0, 0.68);\n}\n\n.ace-mono-industrial .ace_invalid.ace_illegal {\n \n}\n\n.ace-mono-industrial .ace_invalid.ace_deprecated {\n \n}\n\n.ace-mono-industrial .ace_support {\n \n}\n\n.ace-mono-industrial .ace_support.ace_function {\n color:#588E60;\n}\n\n.ace-mono-industrial .ace_function.ace_buildin {\n \n}\n\n.ace-mono-industrial .ace_string {\n \n}\n\n.ace-mono-industrial .ace_string.ace_regexp {\n \n}\n\n.ace-mono-industrial .ace_comment {\n color:#666C68;\nbackground-color:#151C19;\n}\n\n.ace-mono-industrial .ace_comment.ace_doc {\n \n}\n\n.ace-mono-industrial .ace_comment.ace_doc.ace_tag {\n \n}\n\n.ace-mono-industrial .ace_variable {\n \n}\n\n.ace-mono-industrial .ace_variable.ace_language {\n color:#648BD2;\n}\n\n.ace-mono-industrial .ace_xml_pe {\n \n}";d.importCssString(e),b.cssClass="ace-mono-industrial"}) \ No newline at end of file
diff --git a/www/js/ace/theme-monokai.js b/www/js/ace/theme-monokai.js
new file mode 100644
index 0000000..398195f
--- /dev/null
+++ b/www/js/ace/theme-monokai.js
@@ -0,0 +1 @@
+define("ace/theme/monokai",["require","exports","module"],function(a,b,c){var d=a("pilot/dom"),e=".ace-monokai .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-monokai .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-monokai .ace_gutter {\n width: 50px;\n background: #e8e8e8;\n color: #333;\n overflow : hidden;\n}\n\n.ace-monokai .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-monokai .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-monokai .ace_print_margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-monokai .ace_scroller {\n background-color: #272822;\n}\n\n.ace-monokai .ace_text-layer {\n cursor: text;\n color: #F8F8F2;\n}\n\n.ace-monokai .ace_cursor {\n border-left: 2px solid #F8F8F0;\n}\n\n.ace-monokai .ace_cursor.ace_overwrite {\n border-left: 0px;\n border-bottom: 1px solid #F8F8F0;\n}\n \n.ace-monokai .ace_marker-layer .ace_selection {\n background: #49483E;\n}\n\n.ace-monokai .ace_marker-layer .ace_step {\n background: rgb(198, 219, 174);\n}\n\n.ace-monokai .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid #49483E;\n}\n\n.ace-monokai .ace_marker-layer .ace_active_line {\n background: #49483E;\n}\n\n \n.ace-monokai .ace_invisible {\n color: #49483E;\n}\n\n.ace-monokai .ace_keyword {\n color:#F92672;\n}\n\n.ace-monokai .ace_keyword.ace_operator {\n \n}\n\n.ace-monokai .ace_constant {\n \n}\n\n.ace-monokai .ace_constant.ace_language {\n color:#AE81FF;\n}\n\n.ace-monokai .ace_constant.ace_library {\n \n}\n\n.ace-monokai .ace_constant.ace_numeric {\n color:#AE81FF;\n}\n\n.ace-monokai .ace_invalid {\n color:#F8F8F0;\nbackground-color:#F92672;\n}\n\n.ace-monokai .ace_invalid.ace_illegal {\n \n}\n\n.ace-monokai .ace_invalid.ace_deprecated {\n color:#F8F8F0;\nbackground-color:#AE81FF;\n}\n\n.ace-monokai .ace_support {\n \n}\n\n.ace-monokai .ace_support.ace_function {\n color:#66D9EF;\n}\n\n.ace-monokai .ace_function.ace_buildin {\n \n}\n\n.ace-monokai .ace_string {\n color:#E6DB74;\n}\n\n.ace-monokai .ace_string.ace_regexp {\n \n}\n\n.ace-monokai .ace_comment {\n color:#75715E;\n}\n\n.ace-monokai .ace_comment.ace_doc {\n \n}\n\n.ace-monokai .ace_comment.ace_doc.ace_tag {\n \n}\n\n.ace-monokai .ace_variable {\n \n}\n\n.ace-monokai .ace_variable.ace_language {\n \n}\n\n.ace-monokai .ace_xml_pe {\n \n}";d.importCssString(e),b.cssClass="ace-monokai"}) \ No newline at end of file
diff --git a/www/js/ace/theme-pastel_on_dark.js b/www/js/ace/theme-pastel_on_dark.js
new file mode 100644
index 0000000..79cdb10
--- /dev/null
+++ b/www/js/ace/theme-pastel_on_dark.js
@@ -0,0 +1 @@
+define("ace/theme/pastel_on_dark",["require","exports","module"],function(a,b,c){var d=a("pilot/dom"),e=".ace-pastel-on-dark .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-pastel-on-dark .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-pastel-on-dark .ace_gutter {\n width: 50px;\n background: #e8e8e8;\n color: #333;\n overflow : hidden;\n}\n\n.ace-pastel-on-dark .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-pastel-on-dark .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-pastel-on-dark .ace_print_margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-pastel-on-dark .ace_scroller {\n background-color: #2c2828;\n}\n\n.ace-pastel-on-dark .ace_text-layer {\n cursor: text;\n color: #8f938f;\n}\n\n.ace-pastel-on-dark .ace_cursor {\n border-left: 2px solid #A7A7A7;\n}\n\n.ace-pastel-on-dark .ace_cursor.ace_overwrite {\n border-left: 0px;\n border-bottom: 1px solid #A7A7A7;\n}\n \n.ace-pastel-on-dark .ace_marker-layer .ace_selection {\n background: rgba(221, 240, 255, 0.20);\n}\n\n.ace-pastel-on-dark .ace_marker-layer .ace_step {\n background: rgb(198, 219, 174);\n}\n\n.ace-pastel-on-dark .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid rgba(255, 255, 255, 0.25);\n}\n\n.ace-pastel-on-dark .ace_marker-layer .ace_active_line {\n background: rgba(255, 255, 255, 0.031);\n}\n\n \n.ace-pastel-on-dark .ace_invisible {\n color: rgba(255, 255, 255, 0.25);\n}\n\n.ace-pastel-on-dark .ace_keyword {\n color:#757ad8;\n}\n\n.ace-pastel-on-dark .ace_keyword.ace_operator {\n color:#797878;\n}\n\n.ace-pastel-on-dark .ace_constant {\n color:#4fb7c5;\n}\n\n.ace-pastel-on-dark .ace_constant.ace_language {\n \n}\n\n.ace-pastel-on-dark .ace_constant.ace_library {\n \n}\n\n.ace-pastel-on-dark .ace_constant.ace_numeric {\n \n}\n\n.ace-pastel-on-dark .ace_invalid {\n \n}\n\n.ace-pastel-on-dark .ace_invalid.ace_illegal {\n color:#F8F8F8;\nbackground-color:rgba(86, 45, 86, 0.75);\n}\n\n.ace-pastel-on-dark .ace_invalid.ace_deprecated {\n text-decoration:underline;\nfont-style:italic;\ncolor:#D2A8A1;\n}\n\n.ace-pastel-on-dark .ace_support {\n color:#9a9a9a;\n}\n\n.ace-pastel-on-dark .ace_support.ace_function {\n color:#aeb2f8;\n}\n\n.ace-pastel-on-dark .ace_function.ace_buildin {\n \n}\n\n.ace-pastel-on-dark .ace_string {\n color:#66a968;\n}\n\n.ace-pastel-on-dark .ace_string.ace_regexp {\n color:#E9C062;\n}\n\n.ace-pastel-on-dark .ace_comment {\n color:#656865;\n}\n\n.ace-pastel-on-dark .ace_comment.ace_doc {\n color:A6C6FF;\n}\n\n.ace-pastel-on-dark .ace_comment.ace_doc.ace_tag {\n color:A6C6FF;\n}\n\n.ace-pastel-on-dark .ace_variable {\n color:#bebf55;\n}\n\n.ace-pastel-on-dark .ace_variable.ace_language {\n color:#bebf55;\n}\n\n.ace-pastel-on-dark .ace_xml_pe {\n color:#494949;\n}";d.importCssString(e),b.cssClass="ace-pastel-on-dark"}) \ No newline at end of file
diff --git a/www/js/ace/theme-twilight.js b/www/js/ace/theme-twilight.js
new file mode 100644
index 0000000..4072462
--- /dev/null
+++ b/www/js/ace/theme-twilight.js
@@ -0,0 +1 @@
+define("ace/theme/twilight",["require","exports","module"],function(a,b,c){var d=a("pilot/dom"),e=".ace-twilight .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-twilight .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-twilight .ace_gutter {\n width: 50px;\n background: #e8e8e8;\n color: #333;\n overflow : hidden;\n}\n\n.ace-twilight .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-twilight .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-twilight .ace_print_margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-twilight .ace_scroller {\n background-color: #141414;\n}\n\n.ace-twilight .ace_text-layer {\n cursor: text;\n color: #F8F8F8;\n}\n\n.ace-twilight .ace_cursor {\n border-left: 2px solid #A7A7A7;\n}\n\n.ace-twilight .ace_cursor.ace_overwrite {\n border-left: 0px;\n border-bottom: 1px solid #A7A7A7;\n}\n \n.ace-twilight .ace_marker-layer .ace_selection {\n background: rgba(221, 240, 255, 0.20);\n}\n\n.ace-twilight .ace_marker-layer .ace_step {\n background: rgb(198, 219, 174);\n}\n\n.ace-twilight .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid rgba(255, 255, 255, 0.25);\n}\n\n.ace-twilight .ace_marker-layer .ace_active_line {\n background: rgba(255, 255, 255, 0.031);\n}\n\n \n.ace-twilight .ace_invisible {\n color: rgba(255, 255, 255, 0.25);\n}\n\n.ace-twilight .ace_keyword {\n color:#CDA869;\n}\n\n.ace-twilight .ace_keyword.ace_operator {\n \n}\n\n.ace-twilight .ace_constant {\n color:#CF6A4C;\n}\n\n.ace-twilight .ace_constant.ace_language {\n \n}\n\n.ace-twilight .ace_constant.ace_library {\n \n}\n\n.ace-twilight .ace_constant.ace_numeric {\n \n}\n\n.ace-twilight .ace_invalid {\n \n}\n\n.ace-twilight .ace_invalid.ace_illegal {\n color:#F8F8F8;\nbackground-color:rgba(86, 45, 86, 0.75);\n}\n\n.ace-twilight .ace_invalid.ace_deprecated {\n text-decoration:underline;\nfont-style:italic;\ncolor:#D2A8A1;\n}\n\n.ace-twilight .ace_support {\n color:#9B859D;\n}\n\n.ace-twilight .ace_support.ace_function {\n color:#DAD085;\n}\n\n.ace-twilight .ace_function.ace_buildin {\n \n}\n\n.ace-twilight .ace_string {\n color:#8F9D6A;\n}\n\n.ace-twilight .ace_string.ace_regexp {\n color:#E9C062;\n}\n\n.ace-twilight .ace_comment {\n font-style:italic;\ncolor:#5F5A60;\n}\n\n.ace-twilight .ace_comment.ace_doc {\n \n}\n\n.ace-twilight .ace_comment.ace_doc.ace_tag {\n \n}\n\n.ace-twilight .ace_variable {\n color:#7587A6;\n}\n\n.ace-twilight .ace_variable.ace_language {\n \n}\n\n.ace-twilight .ace_xml_pe {\n color:#494949;\n}";d.importCssString(e),b.cssClass="ace-twilight"}) \ No newline at end of file
diff --git a/www/js/ace/theme-vibrant_ink.js b/www/js/ace/theme-vibrant_ink.js
new file mode 100644
index 0000000..ed6ae9e
--- /dev/null
+++ b/www/js/ace/theme-vibrant_ink.js
@@ -0,0 +1 @@
+define("ace/theme/vibrant_ink",["require","exports","module"],function(a,b,c){var d=a("pilot/dom"),e=".ace-vibrant-ink .ace_editor {\n border: 2px solid rgb(159, 159, 159);\n}\n\n.ace-vibrant-ink .ace_editor.ace_focus {\n border: 2px solid #327fbd;\n}\n\n.ace-vibrant-ink .ace_gutter {\n width: 50px;\n background: #e8e8e8;\n color: #333;\n overflow : hidden;\n}\n\n.ace-vibrant-ink .ace_gutter-layer {\n width: 100%;\n text-align: right;\n}\n\n.ace-vibrant-ink .ace_gutter-layer .ace_gutter-cell {\n padding-right: 6px;\n}\n\n.ace-vibrant-ink .ace_print_margin {\n width: 1px;\n background: #e8e8e8;\n}\n\n.ace-vibrant-ink .ace_scroller {\n background-color: #0F0F0F;\n}\n\n.ace-vibrant-ink .ace_text-layer {\n cursor: text;\n color: #FFFFFF;\n}\n\n.ace-vibrant-ink .ace_cursor {\n border-left: 2px solid #FFFFFF;\n}\n\n.ace-vibrant-ink .ace_cursor.ace_overwrite {\n border-left: 0px;\n border-bottom: 1px solid #FFFFFF;\n}\n \n.ace-vibrant-ink .ace_marker-layer .ace_selection {\n background: #6699CC;\n}\n\n.ace-vibrant-ink .ace_marker-layer .ace_step {\n background: rgb(198, 219, 174);\n}\n\n.ace-vibrant-ink .ace_marker-layer .ace_bracket {\n margin: -1px 0 0 -1px;\n border: 1px solid #99CC99;\n}\n\n.ace-vibrant-ink .ace_marker-layer .ace_active_line {\n background: #333333;\n}\n\n \n.ace-vibrant-ink .ace_invisible {\n color: #404040;\n}\n\n.ace-vibrant-ink .ace_keyword {\n color:#FF6600;\n}\n\n.ace-vibrant-ink .ace_keyword.ace_operator {\n \n}\n\n.ace-vibrant-ink .ace_constant {\n \n}\n\n.ace-vibrant-ink .ace_constant.ace_language {\n color:#339999;\n}\n\n.ace-vibrant-ink .ace_constant.ace_library {\n \n}\n\n.ace-vibrant-ink .ace_constant.ace_numeric {\n color:#99CC99;\n}\n\n.ace-vibrant-ink .ace_invalid {\n color:#CCFF33;\n background-color:#000000;\n}\n\n.ace-vibrant-ink .ace_invalid.ace_illegal {\n \n}\n\n.ace-vibrant-ink .ace_invalid.ace_deprecated {\n color:#CCFF33;\n background-color:#000000;\n}\n\n.ace-vibrant-ink .ace_support {\n \n}\n\n.ace-vibrant-ink .ace_support.ace_function {\n color:#FFCC00;\n}\n\n.ace-vibrant-ink .ace_function.ace_buildin {\n \n}\n\n.ace-vibrant-ink .ace_string {\n color:#66FF00;\n}\n\n.ace-vibrant-ink .ace_string.ace_regexp {\n \n}\n\n.ace-vibrant-ink .ace_comment {\n color:#9933CC;\n}\n\n.ace-vibrant-ink .ace_comment.ace_doc {\n \n}\n\n.ace-vibrant-ink .ace_comment.ace_doc.ace_tag {\n \n}\n\n.ace-vibrant-ink .ace_variable {\n \n}\n\n.ace-vibrant-ink .ace_variable.ace_language {\n \n}\n\n.ace-vibrant-ink .ace_xml_pe {\n \n}";d.importCssString(e),b.cssClass="ace-vibrant-ink"}) \ No newline at end of file
diff --git a/www/js/ace/worker-coffee.js b/www/js/ace/worker-coffee.js
new file mode 100644
index 0000000..1ffa65f
--- /dev/null
+++ b/www/js/ace/worker-coffee.js
@@ -0,0 +1 @@
+function initSender(){var a=require("pilot/event_emitter").EventEmitter,b=require("pilot/oop"),c=function(){};(function(){b.implement(this,a),this.callback=function(a,b){postMessage({type:"call",id:b,data:a})},this.emit=function(a,b){postMessage({type:"event",name:a,data:b})}}).call(c.prototype);return new c}function initBaseUrls(a){require.tlns=a}var console={log:function(a){postMessage({type:"log",data:a})}},window={console:console},require=function(a){var b=require.modules[a];if(b){b.initialized||(b.exports=b.factory().exports,b.initialized=!0);return b.exports}var c=a.split("/");c[0]=require.tlns[c[0]]||c[0],path=c.join("/")+".js",require.id=a,importScripts(path);return require(a)};require.modules={},require.tlns={};var define=function(a,b,c){arguments.length==2?c=b:arguments.length==1&&(c=a,a=require.id);a.indexOf("text/")!==0&&(require.modules[a]={factory:function(){var a={exports:{}},b=c(require,a.exports,a);b&&(a.exports=b);return a}})},main,sender;onmessage=function(a){var b=a.data;if(b.command)main[b.command].apply(main,b.args);else if(b.init){initBaseUrls(b.tlns),require("pilot/fixoldbrowsers"),sender=initSender();var c=require(b.module)[b.classname];main=new c(sender)}else b.event&&sender&&sender._dispatchEvent(b.event,b.data)},define("pilot/fixoldbrowsers",["require","exports","module"],function(a,b,c){if(!Function.prototype.bind){var d=Array.prototype.slice;Function.prototype.bind=function(b){var c=this;if(typeof c.apply!="function"||typeof c.call!="function")return new TypeError;var e=d.call(arguments),f=function a(){if(this instanceof a){var b=Object.create(c.prototype);c.apply(b,e.concat(d.call(arguments)));return b}return c.call.apply(c,e.concat(d.call(arguments)))};f.length=typeof c=="function"?Math.max(c.length-e.length,0):0;return f}}var e=Function.prototype.call,f=Array.prototype,g=Object.prototype,h=e.bind(g.hasOwnProperty),i,j,k,l,m;if(m=h(g,"__defineGetter__"))i=e.bind(g.__defineGetter__),j=e.bind(g.__defineSetter__),k=e.bind(g.__lookupGetter__),l=e.bind(g.__lookupSetter__);Array.isArray||(Array.isArray=function(b){return Object.prototype.toString.call(b)==="[object Array]"}),Array.prototype.forEach||(Array.prototype.forEach=function(b,c){var d=+this.length;for(var e=0;e<d;e++)e in this&&b.call(c,this[e],e,this)}),Array.prototype.map||(Array.prototype.map=function(b){var c=+this.length;if(typeof b!="function")throw new TypeError;var d=Array(c),e=arguments[1];for(var f=0;f<c;f++)f in this&&(d[f]=b.call(e,this[f],f,this));return d}),Array.prototype.filter||(Array.prototype.filter=function(b){var c=[],d=arguments[1];for(var e=0;e<this.length;e++)b.call(d,this[e])&&c.push(this[e]);return c}),Array.prototype.every||(Array.prototype.every=function(b){var c=arguments[1];for(var d=0;d<this.length;d++)if(!b.call(c,this[d]))return!1;return!0}),Array.prototype.some||(Array.prototype.some=function(b){var c=arguments[1];for(var d=0;d<this.length;d++)if(b.call(c,this[d]))return!0;return!1}),Array.prototype.reduce||(Array.prototype.reduce=function(b){var c=+this.length;if(typeof b!="function")throw new TypeError;if(c===0&&arguments.length===1)throw new TypeError;var d=0;if(arguments.length>=2)var e=arguments[1];else do{if(d in this){e=this[d++];break}if(++d>=c)throw new TypeError}while(!0);for(;d<c;d++)d in this&&(e=b.call(null,e,this[d],d,this));return e}),Array.prototype.reduceRight||(Array.prototype.reduceRight=function(b){var c=+this.length;if(typeof b!="function")throw new TypeError;if(c===0&&arguments.length===1)throw new TypeError;var d=c-1;if(arguments.length>=2)var e=arguments[1];else do{if(d in this){e=this[d--];break}if(--d<0)throw new TypeError}while(!0);for(;d>=0;d--)d in this&&(e=b.call(null,e,this[d],d,this));return e}),Array.prototype.indexOf||(Array.prototype.indexOf=function(b){var c=this.length;if(!c)return-1;var d=arguments[1]||0;if(d>=c)return-1;d<0&&(d+=c);for(;d<c;d++){if(!h(this,d))continue;if(b===this[d])return d}return-1}),Array.prototype.lastIndexOf||(Array.prototype.lastIndexOf=function(b){var c=this.length;if(!c)return-1;var d=arguments[1]||c;d<0&&(d+=c),d=Math.min(d,c-1);for(;d>=0;d--){if(!h(this,d))continue;if(b===this[d])return d}return-1}),Object.getPrototypeOf||(Object.getPrototypeOf=function(b){return b.__proto__||b.constructor.prototype});if(!Object.getOwnPropertyDescriptor){var n="Object.getOwnPropertyDescriptor called on a non-object: ";Object.getOwnPropertyDescriptor=function(b,c){if(typeof b!="object"&&typeof b!="function"||b===null)throw new TypeError(n+b);if(!h(b,c))return undefined;var d,e,f;d={enumerable:!0,configurable:!0};if(m){var i=b.__proto__;b.__proto__=g;var e=k(b,c),f=l(b,c);b.__proto__=i;if(e||f){e&&(descriptor.get=e),f&&(descriptor.set=f);return descriptor}}descriptor.value=b[c];return descriptor}}Object.getOwnPropertyNames||(Object.getOwnPropertyNames=function(b){return Object.keys(b)}),Object.create||(Object.create=function(b,c){var d;if(b===null)d={"__proto__":null};else{if(typeof b!="object")throw new TypeError("typeof prototype["+typeof b+"] != 'object'");var e=function(){};e.prototype=b,d=new e,d.__proto__=b}typeof c!="undefined"&&Object.defineProperties(d,c);return d});if(!Object.defineProperty){var o="Property description must be an object: ",p="Object.defineProperty called on non-object: ",q="getters & setters can not be defined on this javascript engine";Object.defineProperty=function(b,c,d){if(typeof b!="object"&&typeof b!="function")throw new TypeError(p+b);if(typeof b!="object"||b===null)throw new TypeError(o+d);if(h(d,"value"))if(m&&(k(b,c)||l(b,c))){var e=b.__proto__;b.__proto__=g,delete b[c],b[c]=d.value,b.prototype}else b[c]=d.value;else{if(!m)throw new TypeError(q);h(d,"get")&&i(b,c,d.get),h(d,"set")&&j(b,c,d.set)}return b}}Object.defineProperties||(Object.defineProperties=function(b,c){for(var d in c)h(c,d)&&Object.defineProperty(b,d,c[d]);return b}),Object.seal||(Object.seal=function(b){return b}),Object.freeze||(Object.freeze=function(b){return b});try{Object.freeze(function(){})}catch(r){Object.freeze=function(b){return function(c){return typeof c=="function"?c:b(c)}}(Object.freeze)}Object.preventExtensions||(Object.preventExtensions=function(b){return b}),Object.isSealed||(Object.isSealed=function(b){return!1}),Object.isFrozen||(Object.isFrozen=function(b){return!1}),Object.isExtensible||(Object.isExtensible=function(b){return!0});if(!Object.keys){var s=!0,t=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],u=t.length;for(var v in{toString:null})s=!1;Object.keys=function a(b){if(typeof b!="object"&&typeof b!="function"||b===null)throw new TypeError("Object.keys called on a non-object");var a=[];for(var c in b)h(b,c)&&a.push(c);if(s)for(var d=0,e=u;d<e;d++){var f=t[d];h(b,f)&&a.push(f)}return a}}Date.prototype.toISOString||(Date.prototype.toISOString=function(){return this.getUTCFullYear()+"-"+(this.getUTCMonth()+1)+"-"+this.getUTCDate()+"T"+this.getUTCHours()+":"+this.getUTCMinutes()+":"+this.getUTCSeconds()+"Z"}),Date.now||(Date.now=function(){return(new Date).getTime()}),Date.prototype.toJSON||(Date.prototype.toJSON=function(b){if(typeof this.toISOString!="function")throw new TypeError;return this.toISOString()}),isNaN(Date.parse("T00:00"))&&(Date=function(a){var b=function(c,d,e,f,g,h,i){var j=arguments.length;if(this instanceof a){var k=j===1&&String(c)===c?new a(b.parse(c)):j>=7?new a(c,d,e,f,g,h,i):j>=6?new a(c,d,e,f,g,h):j>=5?new a(c,d,e,f,g):j>=4?new a(c,d,e,f):j>=3?new a(c,d,e):j>=2?new a(c,d):j>=1?new a(c):new a;k.constructor=b;return k}return a.apply(this,arguments)},c=new RegExp("^(?:((?:[+-]\\d\\d)?\\d\\d\\d\\d)(?:-(\\d\\d)(?:-(\\d\\d))?)?)?(?:T(\\d\\d):(\\d\\d)(?::(\\d\\d)(?:\\.(\\d\\d\\d))?)?)?(?:Z|([+-])(\\d\\d):(\\d\\d))?$");for(var d in a)b[d]=a[d];b.now=a.now,b.UTC=a.UTC,b.prototype=a.prototype,b.prototype.constructor=b,b.parse=function(d){var e=c.exec(d);if(e){e.shift();var f=e[0]===undefined;for(var g=0;g<10;g++){if(g===7)continue;e[g]=+(e[g]||(g<3?1:0)),g===1&&e[g]--}if(f)return((e[3]*60+e[4])*60+e[5])*1e3+e[6];var h=(e[8]*60+e[9])*60*1e3;e[6]==="-"&&(h=-h);return a.UTC.apply(this,e.slice(0,7))+h}return a.parse.apply(this,arguments)};return b}(Date));if(!String.prototype.trim){var w=/^\s\s*/,x=/\s\s*$/;String.prototype.trim=function(){return String(this).replace(w,"").replace(x,"")}}}),define("pilot/event_emitter",["require","exports","module"],function(a,b,c){var d={};d._emit=d._dispatchEvent=function(a,b){this._eventRegistry=this._eventRegistry||{};var c=this._eventRegistry[a];if(!!c&&!!c.length){var b=b||{};b.type=a;for(var d=0;d<c.length;d++)c[d](b)}},d.on=d.addEventListener=function(a,b){this._eventRegistry=this._eventRegistry||{};var c=this._eventRegistry[a];if(!c)var c=this._eventRegistry[a]=[];c.indexOf(b)==-1&&c.push(b)},d.removeListener=d.removeEventListener=function(a,b){this._eventRegistry=this._eventRegistry||{};var c=this._eventRegistry[a];if(!!c){var d=c.indexOf(b);d!==-1&&c.splice(d,1)}},d.removeAllListeners=function(a){this._eventRegistry&&(this._eventRegistry[a]=[])},b.EventEmitter=d}),define("pilot/oop",["require","exports","module"],function(a,b,c){b.inherits=function(){var a=function(){};return function(b,c){a.prototype=c.prototype,b.super_=c.prototype,b.prototype=new a,b.prototype.constructor=b}}(),b.mixin=function(a,b){for(var c in b)a[c]=b[c]},b.implement=function(a,c){b.mixin(a,c)}}),define("ace/mode/coffee_worker",["require","exports","module","pilot/oop","ace/worker/mirror","ace/mode/coffee/coffee-script"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/worker/mirror").Mirror,f=a("ace/mode/coffee/coffee-script");window.addEventListener=function(){};var g=b.Worker=function(a){e.call(this,a),this.setTimeout(200)};d.inherits(g,e),function(){this.onUpdate=function(){var a=this.doc.getValue();try{f.parse(a)}catch(b){var c=b.message.match(/Parse error on line (\d+): (.*)/);if(c){this.sender.emit("error",{row:parseInt(c[1])-1,column:null,text:c[2],type:"error"});return}if(b instanceof SyntaxError){var c=b.message.match(/ on line (\d+)/);c&&this.sender.emit("error",{row:parseInt(c[1])-1,column:null,text:b.message.replace(c[0],""),type:"error"})}return}this.sender.emit("ok")}}.call(g.prototype)}),define("ace/worker/mirror",["require","exports","module","ace/document","pilot/lang"],function(a,b,c){var d=a("ace/document").Document,e=a("pilot/lang"),f=b.Mirror=function(a){this.sender=a;var b=this.doc=new d(""),c=this.deferredUpdate=e.deferredCall(this.onUpdate.bind(this)),f=this;a.on("change",function(a){b.applyDeltas([a.data]),c.schedule(f.$timeout)})};(function(){this.$timeout=500,this.setTimeout=function(a){this.$timeout=a},this.setValue=function(a){this.doc.setValue(a),this.deferredUpdate.schedule(this.$timeout)},this.getValue=function(a){this.sender.callback(this.doc.getValue(),a)},this.onUpdate=function(){}}).call(f.prototype)}),define("ace/document",["require","exports","module","pilot/oop","pilot/event_emitter","ace/range","ace/anchor"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=a("ace/range").Range,g=a("ace/anchor").Anchor,h=function(a){this.$lines=[],Array.isArray(a)?this.insertLines(0,a):a.length==0?this.$lines=[""]:this.insert({row:0,column:0},a)};(function(){d.implement(this,e),this.setValue=function(a){var b=this.getLength();this.remove(new f(0,0,b,this.getLine(b-1).length)),this.insert({row:0,column:0},a)},this.getValue=function(){return this.getAllLines().join(this.getNewLineCharacter())},this.createAnchor=function(a,b){return new g(this,a,b)},"aaa".split(/a/).length==0?this.$split=function(a){return a.replace(/\r\n|\r/g,"\n").split("\n")}:this.$split=function(a){return a.split(/\r\n|\r|\n/)},this.$detectNewLine=function(a){var b=a.match(/^.*?(\r?\n)/m);b?this.$autoNewLine=b[1]:this.$autoNewLine="\n"},this.getNewLineCharacter=function(){switch(this.$newLineMode){case"windows":return"\r\n";case"unix":return"\n";case"auto":return this.$autoNewLine}},this.$autoNewLine="\n",this.$newLineMode="auto",this.setNewLineMode=function(a){this.$newLineMode!==a&&(this.$newLineMode=a)},this.getNewLineMode=function(){return this.$newLineMode},this.isNewLine=function(a){return a=="\r\n"||a=="\r"||a=="\n"},this.getLine=function(a){return this.$lines[a]||""},this.getLines=function(a,b){return this.$lines.slice(a,b+1)},this.getAllLines=function(){return this.getLines(0,this.getLength())},this.getLength=function(){return this.$lines.length},this.getTextRange=function(a){if(a.start.row==a.end.row)return this.$lines[a.start.row].substring(a.start.column,a.end.column);var b=[];b.push(this.$lines[a.start.row].substring(a.start.column)),b.push.apply(b,this.getLines(a.start.row+1,a.end.row-1)),b.push(this.$lines[a.end.row].substring(0,a.end.column));return b.join(this.getNewLineCharacter())},this.$clipPosition=function(a){var b=this.getLength();a.row>=b&&(a.row=Math.max(0,b-1),a.column=this.getLine(b-1).length);return a},this.insert=function(a,b){if(b.length==0)return a;a=this.$clipPosition(a),this.getLength()<=1&&this.$detectNewLine(b);var c=this.$split(b),d=c.splice(0,1)[0],e=c.length==0?null:c.splice(c.length-1,1)[0];this._dispatchEvent("changeStart"),a=this.insertInLine(a,d),e!==null&&(a=this.insertNewLine(a),a=this.insertLines(a.row,c),a=this.insertInLine(a,e||"")),this._dispatchEvent("changeEnd");return a},this.insertLines=function(a,b){if(b.length==0)return{row:a,column:0};var c=[a,0];c.push.apply(c,b),this.$lines.splice.apply(this.$lines,c),this._dispatchEvent("changeStart");var d=new f(a,0,a+b.length,0),e={action:"insertLines",range:d,lines:b};this._dispatchEvent("change",{data:e}),this._dispatchEvent("changeEnd");return d.end},this.insertNewLine=function(a){a=this.$clipPosition(a);var b=this.$lines[a.row]||"";this._dispatchEvent("changeStart"),this.$lines[a.row]=b.substring(0,a.column),this.$lines.splice(a.row+1,0,b.substring(a.column,b.length));var c={row:a.row+1,column:0},d={action:"insertText",range:f.fromPoints(a,c),text:this.getNewLineCharacter()};this._dispatchEvent("change",{data:d}),this._dispatchEvent("changeEnd");return c},this.insertInLine=function(a,b){if(b.length==0)return a;var c=this.$lines[a.row]||"";this._dispatchEvent("changeStart"),this.$lines[a.row]=c.substring(0,a.column)+b+c.substring(a.column);var d={row:a.row,column:a.column+b.length},e={action:"insertText",range:f.fromPoints(a,d),text:b};this._dispatchEvent("change",{data:e}),this._dispatchEvent("changeEnd");return d},this.remove=function(a){a.start=this.$clipPosition(a.start),a.end=this.$clipPosition(a.end);if(a.isEmpty())return a.start;var b=a.start.row,c=a.end.row;this._dispatchEvent("changeStart");if(a.isMultiLine()){var d=a.start.column==0?b:b+1,e=c-1;a.end.column>0&&this.removeInLine(c,0,a.end.column),e>=d&&this.removeLines(d,e),d!=b&&(this.removeInLine(b,a.start.column,this.getLine(b).length),this.removeNewLine(a.start.row))}else this.removeInLine(b,a.start.column,a.end.column);this._dispatchEvent("changeEnd");return a.start},this.removeInLine=function(a,b,c){if(b!=c){var d=new f(a,b,a,c),e=this.getLine(a),g=e.substring(b,c),h=e.substring(0,b)+e.substring(c,e.length);this._dispatchEvent("changeStart"),this.$lines.splice(a,1,h);var i={action:"removeText",range:d,text:g};this._dispatchEvent("change",{data:i}),this._dispatchEvent("changeEnd");return d.start}},this.removeLines=function(a,b){this._dispatchEvent("changeStart");var c=new f(a,0,b+1,0),d=this.$lines.splice(a,b-a+1),e={action:"removeLines",range:c,nl:this.getNewLineCharacter(),lines:d};this._dispatchEvent("change",{data:e}),this._dispatchEvent("changeEnd");return d},this.removeNewLine=function(a){var b=this.getLine(a),c=this.getLine(a+1),d=new f(a,b.length,a+1,0),e=b+c;this._dispatchEvent("changeStart"),this.$lines.splice(a,2,e);var g={action:"removeText",range:d,text:this.getNewLineCharacter()};this._dispatchEvent("change",{data:g}),this._dispatchEvent("changeEnd")},this.replace=function(a,b){if(b.length==0&&a.isEmpty())return a.start;if(b==this.getTextRange(a))return a.end;this._dispatchEvent("changeStart"),this.remove(a);if(b)var c=this.insert(a.start,b);else c=a.start;this._dispatchEvent("changeEnd");return c},this.applyDeltas=function(a){for(var b=0;b<a.length;b++){var c=a[b],d=f.fromPoints(c.range.start,c.range.end);c.action=="insertLines"?this.insertLines(d.start.row,c.lines):c.action=="insertText"?this.insert(d.start,c.text):c.action=="removeLines"?this.removeLines(d.start.row,d.end.row-1):c.action=="removeText"&&this.remove(d)}},this.revertDeltas=function(a){for(var b=a.length-1;b>=0;b--){var c=a[b],d=f.fromPoints(c.range.start,c.range.end);c.action=="insertLines"?this.removeLines(d.start.row,d.end.row-1):c.action=="insertText"?this.remove(d):c.action=="removeLines"?this.insertLines(d.start.row,c.lines):c.action=="removeText"&&this.insert(d.start,c.text)}}}).call(h.prototype),b.Document=h}),define("ace/range",["require","exports","module"],function(a,b,c){var d=function(a,b,c,d){this.start={row:a,column:b},this.end={row:c,column:d}};(function(){this.toString=function(){return"Range: ["+this.start.row+"/"+this.start.column+"] -> ["+this.end.row+"/"+this.end.column+"]"},this.contains=function(a,b){return this.compare(a,b)==0},this.compareRange=function(a){var b,c=a.end,d=a.start;b=this.compare(c.row,c.column);if(b==1){b=this.compare(d.row,d.column);return b==1?2:b==0?1:0}if(b==-1)return-2;b=this.compare(d.row,d.column);return b==-1?-1:b==1?42:0},this.containsRange=function(a){var b=this.compareRange(a);return b==-1||b==0||b==1},this.isEnd=function(a,b){return this.end.row==a&&this.end.column==b},this.isStart=function(a,b){return this.start.row==a&&this.start.column==b},this.setStart=function(a,b){typeof a=="object"?(this.start.column=a.column,this.start.row=a.row):(this.start.row=a,this.start.column=b)},this.setEnd=function(a,b){typeof a=="object"?(this.end.column=a.column,this.end.row=a.row):(this.end.row=a,this.end.column=b)},this.inside=function(a,b){if(this.compare(a,b)==0)return this.isEnd(a,b)||this.isStart(a,b)?!1:!0;return!1},this.insideStart=function(a,b){if(this.compare(a,b)==0)return this.isEnd(a,b)?!1:!0;return!1},this.insideEnd=function(a,b){if(this.compare(a,b)==0)return this.isStart(a,b)?!1:!0;return!1},this.compare=function(a,b){if(!this.isMultiLine()&&a===this.start.row)return b<this.start.column?-1:b>this.end.column?1:0;if(a<this.start.row)return-1;if(a>this.end.row)return 1;if(this.start.row===a)return b>=this.start.column?0:-1;if(this.end.row===a)return b<=this.end.column?0:1;return 0},this.compareStart=function(a,b){return this.start.row==a&&this.start.column==b?-1:this.compare(a,b)},this.compareEnd=function(a,b){return this.end.row==a&&this.end.column==b?1:this.compare(a,b)},this.compareInside=function(a,b){return this.end.row==a&&this.end.column==b?1:this.start.row==a&&this.start.column==b?-1:this.compare(a,b)},this.clipRows=function(a,b){if(this.end.row>b)var c={row:b+1,column:0};if(this.start.row>b)var e={row:b+1,column:0};if(this.start.row<a)var e={row:a,column:0};if(this.end.row<a)var c={row:a,column:0};return d.fromPoints(e||this.start,c||this.end)},this.extend=function(a,b){var c=this.compare(a,b);if(c==0)return this;if(c==-1)var e={row:a,column:b};else var f={row:a,column:b};return d.fromPoints(e||this.start,f||this.end)},this.isEmpty=function(){return this.start.row==this.end.row&&this.start.column==this.end.column},this.isMultiLine=function(){return this.start.row!==this.end.row},this.clone=function(){return d.fromPoints(this.start,this.end)},this.collapseRows=function(){return this.end.column==0?new d(this.start.row,0,Math.max(this.start.row,this.end.row-1),0):new d(this.start.row,0,this.end.row,0)},this.toScreenRange=function(a){var b=a.documentToScreenPosition(this.start),c=a.documentToScreenPosition(this.end);return new d(b.row,b.column,c.row,c.column)}}).call(d.prototype),d.fromPoints=function(a,b){return new d(a.row,a.column,b.row,b.column)},b.Range=d}),define("ace/anchor",["require","exports","module","pilot/oop","pilot/event_emitter"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=b.Anchor=function(a,b,c){this.document=a,typeof c=="undefined"?this.setPosition(b.row,b.column):this.setPosition(b,c),this.$onChange=this.onChange.bind(this),a.on("change",this.$onChange)};(function(){d.implement(this,e),this.getPosition=function(){return this.$clipPositionToDocument(this.row,this.column)},this.getDocument=function(){return this.document},this.onChange=function(a){var b=a.data,c=b.range;if(c.start.row!=c.end.row||c.start.row==this.row){if(c.start.row>this.row)return;if(c.start.row==this.row&&c.start.column>this.column)return;var d=this.row,e=this.column;b.action==="insertText"?c.start.row===d&&c.start.column<=e?c.start.row===c.end.row?e+=c.end.column-c.start.column:(e-=c.start.column,d+=c.end.row-c.start.row):c.start.row!==c.end.row&&c.start.row<d&&(d+=c.end.row-c.start.row):b.action==="insertLines"?c.start.row<=d&&(d+=c.end.row-c.start.row):b.action=="removeText"?c.start.row==d&&c.start.column<e?c.end.column>=e?e=c.start.column:e=Math.max(0,e-(c.end.column-c.start.column)):c.start.row!==c.end.row&&c.start.row<d?(c.end.row==d&&(e=Math.max(0,e-c.end.column)+c.start.column),d-=c.end.row-c.start.row):c.end.row==d&&(d-=c.end.row-c.start.row,e=Math.max(0,e-c.end.column)+c.start.column):b.action=="removeLines"&&c.start.row<=d&&(c.end.row<=d?d-=c.end.row-c.start.row:(d=c.start.row,e=0)),this.setPosition(d,e,!0)}},this.setPosition=function(a,b,c){c?pos={row:a,column:b}:pos=this.$clipPositionToDocument(a,b);if(this.row!=pos.row||this.column!=pos.column){var d={row:this.row,column:this.column};this.row=pos.row,this.column=pos.column,this._dispatchEvent("change",{old:d,value:pos})}},this.detach=function(){this.document.removeEventListener("change",this.$onChange)},this.$clipPositionToDocument=function(a,b){var c={};a>=this.document.getLength()?(c.row=Math.max(0,this.document.getLength()-1),c.column=this.document.getLine(c.row).length):a<0?(c.row=0,c.column=0):(c.row=a,c.column=Math.min(this.document.getLine(c.row).length,Math.max(0,b))),b<0&&(c.column=0);return c}}).call(f.prototype)}),define("pilot/lang",["require","exports","module"],function(a,b,c){b.stringReverse=function(a){return a.split("").reverse().join("")},b.stringRepeat=function(a,b){return Array(b+1).join(a)};var d=/^\s\s*/,e=/\s\s*$/;b.stringTrimLeft=function(a){return a.replace(d,"")},b.stringTrimRight=function(a){return a.replace(e,"")},b.copyObject=function(a){var b={};for(var c in a)b[c]=a[c];return b},b.copyArray=function(a){var b=[];for(i=0,l=a.length;i<l;i++)a[i]&&typeof a[i]=="object"?b[i]=this.copyObject(a[i]):b[i]=a[i];return b},b.deepCopy=function(a){if(typeof a!="object")return a;var b=a.constructor();for(var c in a)typeof a[c]=="object"?b[c]=this.deepCopy(a[c]):b[c]=a[c];return b},b.arrayToMap=function(a){var b={};for(var c=0;c<a.length;c++)b[a[c]]=1;return b},b.arrayRemove=function(a,b){for(var c=0;c<=a.length;c++)b===a[c]&&a.splice(c,1)},b.escapeRegExp=function(a){return a.replace(/([.*+?^${}()|[\]\/\\])/g,"\\$1")},b.deferredCall=function(a){var b=null,c=function(){b=null,a()},d=function(a){b||(b=setTimeout(c,a||0));return d};d.schedule=d,d.call=function(){this.cancel(),a();return d},d.cancel=function(){clearTimeout(b),b=null;return d};return d}}),define("ace/mode/coffee/coffee-script",["require","exports","module","ace/mode/coffee/lexer","ace/mode/coffee/parser","ace/mode/coffee/nodes"],function(a,b,c){var d=a("ace/mode/coffee/lexer").Lexer,e=a("ace/mode/coffee/parser"),f=new d;e.lexer={lex:function(){var a,b;b=this.tokens[this.pos++]||[""],a=b[0],this.yytext=b[1],this.yylineno=b[2];return a},setInput:function(a){this.tokens=a;return this.pos=0},upcomingInput:function(){return""}},e.yy=a("ace/mode/coffee/nodes"),b.parse=function(a){return e.parse(f.tokenize(a))}}),define("ace/mode/coffee/lexer",["require","exports","module","ace/mode/coffee/rewriter","ace/mode/coffee/helpers"],function(a,b,c){var d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y=Array.prototype.indexOf||function(a){for(var b=0,c=this.length;b<c;b++)if(this[b]===a)return b;return-1};M=a("ace/mode/coffee/rewriter").Rewriter,X=a("ace/mode/coffee/helpers"),T=X.count,W=X.starts,S=X.compact,V=X.last,b.Lexer=A=function(){function a(){}a.prototype.tokenize=function(a,b){var c;b==null&&(b={}),R.test(a)&&(a="\n"+a),a=a.replace(/\r/g,"").replace(P,""),this.code=a,this.line=b.line||0,this.indent=0,this.indebt=0,this.outdebt=0,this.indents=[],this.tokens=[],c=0;while(this.chunk=a.slice(c))c+=this.identifierToken()||this.commentToken()||this.whitespaceToken()||this.lineToken()||this.heredocToken()||this.stringToken()||this.numberToken()||this.regexToken()||this.jsToken()||this.literalToken();this.closeIndentation();if(b.rewrite===!1)return this.tokens;return(new M).rewrite(this.tokens)},a.prototype.identifierToken=function(){var a,b,c,d,e,f,g,k,l;if(!(e=s.exec(this.chunk)))return 0;d=e[0],c=e[1],a=e[2];if(c==="own"&&this.tag()==="FOR"){this.token("OWN",c);return c.length}b=a||(f=V(this.tokens))&&((k=f[0])==="."||k==="?."||k==="::"||!f.spaced&&f[0]==="@"),g="IDENTIFIER";if(Y.call(w,c)>=0||!b&&Y.call(j,c)>=0)g=c.toUpperCase(),g==="WHEN"&&(l=this.tag(),Y.call(x,l)>=0)?g="LEADING_WHEN":g==="FOR"?this.seenFor=!0:g==="UNLESS"?g="IF":Y.call(Q,g)>=0?g="UNARY":Y.call(K,g)>=0&&(g!=="INSTANCEOF"&&this.seenFor?(g="FOR"+g,this.seenFor=!1):(g="RELATION",this.value()==="!"&&(this.tokens.pop(),c="!"+c)));Y.call(v,c)>=0&&(b?(g="IDENTIFIER",c=new String(c),c.reserved=!0):Y.call(L,c)>=0&&this.identifierError(c)),b||(Y.call(h,c)>=0&&(c=i[c]),g=function(){switch(c){case"!":return"UNARY";case"==":case"!=":return"COMPARE";case"&&":case"||":return"LOGIC";case"true":case"false":case"null":case"undefined":return"BOOL";case"break":case"continue":case"debugger":return"STATEMENT";default:return g}}()),this.token(g,c),a&&this.token(":",":");return d.length},a.prototype.numberToken=function(){var a,b;if(!(a=H.exec(this.chunk)))return 0;b=a[0],this.token("NUMBER",b);return b.length},a.prototype.stringToken=function(){var a,b;switch(this.chunk.charAt(0)){case"'":if(!(a=O.exec(this.chunk)))return 0;this.token("STRING",(b=a[0]).replace(C,"\\\n"));break;case'"':if(!(b=this.balancedString(this.chunk,'"')))return 0;0<b.indexOf("#{",1)?this.interpolateString(b.slice(1,-1)):this.token("STRING",this.escapeLines(b));break;default:return 0}this.line+=T(b,"\n");return b.length},a.prototype.heredocToken=function(){var a,b,c,d;if(!(c=n.exec(this.chunk)))return 0;b=c[0],d=b.charAt(0),a=this.sanitizeHeredoc(c[2],{quote:d,indent:null}),d==='"'&&0<=a.indexOf("#{")?this.interpolateString(a,{heredoc:!0}):this.token("STRING",this.makeString(a,d,!0)),this.line+=T(b,"\n");return b.length},a.prototype.commentToken=function(){var a,b,c;if(!(c=this.chunk.match(k)))return 0;a=c[0],b=c[1],b&&(this.token("HERECOMMENT",this.sanitizeHeredoc(b,{herecomment:!0,indent:Array(this.indent+1).join(" ")})),this.token("TERMINATOR","\n")),this.line+=T(a,"\n");return a.length},a.prototype.jsToken=function(){var a,b;if(this.chunk.charAt(0)!=="`"||!(a=u.exec(this.chunk)))return 0;this.token("JS",(b=a[0]).slice(1,-1));return b.length},a.prototype.regexToken=function(){var a,b,c,d;if(this.chunk.charAt(0)!=="/")return 0;if(a=q.exec(this.chunk)){this.line+=T(a[0],"\n");return this.heregexToken(a)}b=V(this.tokens);if(b&&(d=b[0],Y.call(b.spaced?E:F,d)>=0))return 0;if(!(a=J.exec(this.chunk)))return 0;c=a[0],this.token("REGEX",c==="//"?"/(?:)/":c);return c.length},a.prototype.heregexToken=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n;d=a[0],b=a[1],c=a[2];if(0>b.indexOf("#{")){e=b.replace(r,"").replace(/\//g,"\\/"),this.token("REGEX","/"+(e||"(?:)")+"/"+c);return d.length}this.token("IDENTIFIER","RegExp"),this.tokens.push(["CALL_START","("]),g=[],k=this.interpolateString(b,{regex:!0});for(i=0,j=k.length;i<j;i++){l=k[i],f=l[0],h=l[1];if(f==="TOKENS")g.push.apply(g,h);else{if(!(h=h.replace(r,"")))continue;h=h.replace(/\\/g,"\\\\"),g.push(["STRING",this.makeString(h,'"',!0)])}g.push(["+","+"])}g.pop(),((m=g[0])!=null?m[0]:void 0)!=="STRING"&&this.tokens.push(["STRING",'""'],["+","+"]),(n=this.tokens).push.apply(n,g),c&&this.tokens.push([",",","],["STRING",'"'+c+'"']),this.token(")",")");return d.length},a.prototype.lineToken=function(){var a,b,c,d,e,f;if(!(c=D.exec(this.chunk)))return 0;b=c[0],this.line+=T(b,"\n"),e=V(this.tokens,1),f=b.length-1-b.lastIndexOf("\n"),d=this.unfinished();if(f-this.indebt===this.indent){d?this.suppressNewlines():this.newlineToken();return b.length}if(f>this.indent){if(d){this.indebt=f-this.indent,this.suppressNewlines();return b.length}a=f-this.indent+this.outdebt,this.token("INDENT",a),this.indents.push(a),this.outdebt=this.indebt=0}else this.indebt=0,this.outdentToken(this.indent-f,d);this.indent=f;return b.length},a.prototype.outdentToken=function(a,b,c){var d,e;while(a>0)e=this.indents.length-1,this.indents[e]===void 0?a=0:this.indents[e]===this.outdebt?(a-=this.outdebt,this.outdebt=0):this.indents[e]<this.outdebt?(this.outdebt-=this.indents[e],a-=this.indents[e]):(d=this.indents.pop()-this.outdebt,a-=d,this.outdebt=0,this.token("OUTDENT",d));d&&(this.outdebt-=a),this.tag()!=="TERMINATOR"&&!b&&this.token("TERMINATOR","\n");return this},a.prototype.whitespaceToken=function(){var a,b,c;if(!(a=R.exec(this.chunk))&&!(b=this.chunk.charAt(0)==="\n"))return 0;c=V(this.tokens),c&&(c[a?"spaced":"newLine"]=!0);return a?a[0].length:0},a.prototype.newlineToken=function(){this.tag()!=="TERMINATOR"&&this.token("TERMINATOR","\n");return this},a.prototype.suppressNewlines=function(){this.value()==="\\"&&this.tokens.pop();return this},a.prototype.literalToken=function(){var a,b,c,d,e,h,i,j;(a=I.exec(this.chunk))?(d=a[0],g.test(d)&&this.tagParameters()):d=this.chunk.charAt(0),c=d,b=V(this.tokens);if(d==="="&&b){!b[1].reserved&&(e=b[1],Y.call(v,e)>=0)&&this.assignmentError();if((h=b[1])==="||"||h==="&&"){b[0]="COMPOUND_ASSIGN",b[1]+="=";return d.length}}if(d===";")c="TERMINATOR";else if(Y.call(B,d)>=0)c="MATH";else if(Y.call(l,d)>=0)c="COMPARE";else if(Y.call(m,d)>=0)c="COMPOUND_ASSIGN";else if(Y.call(Q,d)>=0)c="UNARY";else if(Y.call(N,d)>=0)c="SHIFT";else if(Y.call(z,d)>=0||d==="?"&&(b!=null?b.spaced:void 0))c="LOGIC";else if(b&&!b.spaced)if(d==="("&&(i=b[0],Y.call(f,i)>=0))b[0]==="?"&&(b[0]="FUNC_EXIST"),c="CALL_START";else if(d==="["&&(j=b[0],Y.call(t,j)>=0)){c="INDEX_START";switch(b[0]){case"?":b[0]="INDEX_SOAK";break;case"::":b[0]="INDEX_PROTO"}}this.token(c,d);return d.length},a.prototype.sanitizeHeredoc=function(a,b){var c,d,e,f,g;e=b.indent,d=b.herecomment;if(d){if(o.test(a))throw new Error('block comment cannot contain "*/", starting on line '+(this.line+1));if(a.indexOf("\n")<=0)return a}else while(f=p.exec(a)){c=f[1];if(e===null||0<(g=c.length)&&g<e.length)e=c}e&&(a=a.replace(RegExp("\\n"+e,"g"),"\n")),d||(a=a.replace(/^\n/,""));return a},a.prototype.tagParameters=function(){var a,b,c,d;if(this.tag()!==")")return this;b=[],d=this.tokens,a=d.length,d[--a][0]="PARAM_END";while(c=d[--a])switch(c[0]){case")":b.push(c);break;case"(":case"CALL_START":if(b.length)b.pop();else if(c[0]==="("){c[0]="PARAM_START";return this}}return this},a.prototype.closeIndentation=function(){return this.outdentToken(this.indent)},a.prototype.identifierError=function(a){throw SyntaxError('Reserved word "'+a+'" on line '+(this.line+1))},a.prototype.assignmentError=function(){throw SyntaxError('Reserved word "'+this.value()+'" on line '+(this.line+1)+" can't be assigned")},a.prototype.balancedString=function(a,b){var c,d,e,f,g;f=[b];for(c=1,g=a.length;1<=g?c<g:c>g;1<=g?c++:c--){switch(d=a.charAt(c)){case"\\":c++;continue;case b:f.pop();if(!f.length)return a.slice(0,c+1);b=f[f.length-1];continue}b!=="}"||d!=='"'&&d!=="'"?b==="}"&&d==="{"?f.push(b="}"):b==='"'&&e==="#"&&d==="{"&&f.push(b="}"):f.push(b=d),e=d}throw new Error("missing "+f.pop()+", starting on line "+(this.line+1))},a.prototype.interpolateString=function(b,c){var d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t;c==null&&(c={}),e=c.heredoc,m=c.regex,o=[],l=0,f=-1;while(j=b.charAt(f+=1)){if(j==="\\"){f+=1;continue}if(j!=="#"||b.charAt(f+1)!=="{"||!(d=this.balancedString(b.slice(f+1),"}")))continue;l<f&&o.push(["NEOSTRING",b.slice(l,f)]),g=d.slice(1,-1);if(g.length){k=(new a).tokenize(g,{line:this.line,rewrite:!1}),k.pop(),((r=k[0])!=null?r[0]:void 0)==="TERMINATOR"&&k.shift();if(i=k.length)i>1&&(k.unshift(["(","("]),k.push([")",")"])),o.push(["TOKENS",k])}f+=d.length,l=f+1}f>l&&l<b.length&&o.push(["NEOSTRING",b.slice(l)]);if(m)return o;if(!o.length)return this.token("STRING",'""');o[0][0]!=="NEOSTRING"&&o.unshift(["",""]),(h=o.length>1)&&this.token("(","(");for(f=0,q=o.length;f<q;f++)s=o[f],n=s[0],p=s[1],f&&this.token("+","+"),n==="TOKENS"?(t=this.tokens).push.apply(t,p):this.token("STRING",this.makeString(p,'"',e));h&&this.token(")",")");return o},a.prototype.token=function(a,b){return this.tokens.push([a,b,this.line])},a.prototype.tag=function(a,b){var c;return(c=V(this.tokens,a))&&(b?c[0]=b:c[0])},a.prototype.value=function(a,b){var c;return(c=V(this.tokens,a))&&(b?c[1]=b:c[1])},a.prototype.unfinished=function(){var a,b;return y.test(this.chunk)||(a=V(this.tokens,1))&&a[0]!=="."&&(b=this.value())&&!b.reserved&&G.test(b)&&!g.test(b)&&!d.test(this.chunk)},a.prototype.escapeLines=function(a,b){return a.replace(C,b?"\\n":"")},a.prototype.makeString=function(a,b,c){if(!a)return b+b;a=a.replace(/\\([\s\S])/g,function(a,c){return c==="\n"||c===b?c:a}),a=a.replace(RegExp(""+b,"g"),"\\$&");return b+this.escapeLines(a,c)+b};return a}(),w=["true","false","null","this","new","delete","typeof","in","instanceof","return","throw","break","continue","debugger","if","else","switch","for","while","do","try","catch","finally","class","extends","super"],j=["undefined","then","unless","until","loop","of","by","when"],i={and:"&&",or:"||",is:"==",isnt:"!=",not:"!",yes:"true",no:"false",on:"true",off:"false"},h=function(){var a;a=[];for(U in i)a.push(U);return a}(),j=j.concat(h),L=["case","default","function","var","void","with","const","let","enum","export","import","native","__hasProp","__extends","__slice","__bind","__indexOf"],v=w.concat(L),b.RESERVED=L.concat(w).concat(j),s=/^([$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]*)([^\n\S]*:(?!:))?/,H=/^0x[\da-f]+|^(?:\d+(\.\d+)?|\.\d+)(?:e[+-]?\d+)?/i,n=/^("""|''')([\s\S]*?)(?:\n[^\n\S]*)?\1/,I=/^(?:[-=]>|[-+*\/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>])\2=?|\?\.|\.{2,3})/,R=/^[^\n\S]+/,k=/^###([^#][\s\S]*?)(?:###[^\n\S]*|(?:###)?$)|^(?:\s*#(?!##[^#]).*)+/,g=/^[-=]>/,D=/^(?:\n[^\n\S]*)+/,O=/^'[^\\']*(?:\\.[^\\']*)*'/,u=/^`[^\\`]*(?:\\.[^\\`]*)*`/,J=/^\/(?![\s=])[^[\/\n\\]*(?:(?:\\[\s\S]|\[[^\]\n\\]*(?:\\[\s\S][^\]\n\\]*)*])[^[\/\n\\]*)*\/[imgy]{0,4}(?!\w)/,q=/^\/{3}([\s\S]+?)\/{3}([imgy]{0,4})(?!\w)/,r=/\s+(?:#.*)?/g,C=/\n/g,p=/\n+([^\n\S]*)/g,o=/\*\//,d=/^\s*@?([$A-Za-z_][$\w\x7f-\uffff]*|['"].*['"])[^\n\S]*?[:=][^:=>]/,y=/^\s*(?:,|\??\.(?![.\d])|::)/,P=/\s+$/,G=/^(?:[-+*&|\/%=<>!.\\][<>=&|]*|and|or|is(?:nt)?|n(?:ot|ew)|delete|typeof|instanceof)$/,m=["-=","+=","/=","*=","%=","||=","&&=","?=","<<=",">>=",">>>=","&=","^=","|="],Q=["!","~","NEW","TYPEOF","DELETE","DO"],z=["&&","||","&","|","^"],N=["<<",">>",">>>"],l=["==","!=","<",">","<=",">="],B=["*","/","%"],K=["IN","OF","INSTANCEOF"],e=["TRUE","FALSE","NULL","UNDEFINED"],E=["NUMBER","REGEX","BOOL","++","--","]"],F=E.concat(")","}","THIS","IDENTIFIER","STRING"),f=["IDENTIFIER","STRING","REGEX",")","]","}","?","::","@","THIS","SUPER"],t=f.concat("NUMBER","BOOL"),x=["INDENT","OUTDENT","TERMINATOR"]}),define("ace/mode/coffee/rewriter",["require","exports","module"],function(a,b,c){var d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v=Array.prototype.indexOf||function(a){for(var b=0,c=this.length;b<c;b++)if(this[b]===a)return b;return-1},w=Array.prototype.slice;b.Rewriter=function(){function a(){}a.prototype.rewrite=function(a){this.tokens=a,this.removeLeadingNewlines(),this.removeMidExpressionNewlines(),this.closeOpenCalls(),this.closeOpenIndexes(),this.addImplicitIndentation(),this.tagPostfixConditionals(),this.addImplicitBraces(),this.addImplicitParentheses(),this.ensureBalance(d),this.rewriteClosingParens();return this.tokens},a.prototype.scanTokens=function(a){var b,c,d;d=this.tokens,b=0;while(c=d[b])b+=a.call(this,c,b,d);return!0},a.prototype.detectEnd=function(a,b,c){var d,e,h,i,j;h=this.tokens,d=0;while(e=h[a]){if(d===0&&b.call(this,e,a))return c.call(this,e,a);if(!e||d<0)return c.call(this,e,a-1);if(i=e[0],v.call(g,i)>=0)d+=1;else if(j=e[0],v.call(f,j)>=0)d-=1;a+=1}return a-1},a.prototype.removeLeadingNewlines=function(){var a,b,c,d;d=this.tokens;for(a=0,c=d.length;a<c;a++){b=d[a][0];if(b!=="TERMINATOR")break}if(a)return this.tokens.splice(0,a)},a.prototype.removeMidExpressionNewlines=function(){return this.scanTokens(function(a,b,c){var d;if(!(a[0]==="TERMINATOR"&&(d=this.tag(b+1),v.call(e,d)>=0)))return 1;c.splice(b,1);return 0})},a.prototype.closeOpenCalls=function(){var a,b;b=function(a,b){var c;return(c=a[0])===")"||c==="CALL_END"||a[0]==="OUTDENT"&&this.tag(b-1)===")"},a=function(a,b){return this.tokens[a[0]==="OUTDENT"?b-1:b][0]="CALL_END"};return this.scanTokens(function(c,d){c[0]==="CALL_START"&&this.detectEnd(d+1,b,a);return 1})},a.prototype.closeOpenIndexes=function(){var a,b;b=function(a,b){var c;return(c=a[0])==="]"||c==="INDEX_END"},a=function(a,b){return a[0]="INDEX_END"};return this.scanTokens(function(c,d){c[0]==="INDEX_START"&&this.detectEnd(d+1,b,a);return 1})},a.prototype.addImplicitBraces=function(){var a,b,c,d,e;c=[],d=null,e=0,b=function(a,b){var c,d,e,f,g,h;g=this.tokens.slice(b+1,b+3+1||9e9),c=g[0],f=g[1],e=g[2];if("HERECOMMENT"===(c!=null?c[0]:void 0))return!1;d=a[0];return(d==="TERMINATOR"||d==="OUTDENT")&&(f!=null?f[0]:void 0)!==":"&&((c!=null?c[0]:void 0)!=="@"||(e!=null?e[0]:void 0)!==":")||d===","&&c&&(h=c[0])!=="IDENTIFIER"&&h!=="NUMBER"&&h!=="STRING"&&h!=="@"&&h!=="TERMINATOR"&&h!=="OUTDENT"},a=function(a,b){var c;c=["}","}",a[2]],c.generated=!0;return this.tokens.splice(b,0,c)};return this.scanTokens(function(e,h,i){var j,k,l,m,n,o,p;if(o=l=e[0],v.call(g,o)>=0){c.push([l==="INDENT"&&this.tag(h-1)==="{"?"{":l,h]);return 1}if(v.call(f,l)>=0){d=c.pop();return 1}if(l!==":"||(j=this.tag(h-2))!==":"&&((p=c[c.length-1])!=null?p[0]:void 0)==="{")return 1;c.push(["{"]),k=j==="@"?h-2:h-1;while(this.tag(k-2)==="HERECOMMENT")k-=2;n=new String("{"),n.generated=!0,m=["{",n,e[2]],m.generated=!0,i.splice(k,0,m),this.detectEnd(h+2,b,a);return 2})},a.prototype.addImplicitParentheses=function(){var a,b;b=!1,a=function(a,b){var c;c=a[0]==="OUTDENT"?b+1:b;return this.tokens.splice(c,0,["CALL_END",")",a[2]])};return this.scanTokens(function(c,d,e){var f,g,m,o,p,q,r,s,t,u;r=c[0];if(r==="CLASS"||r==="IF")b=!0;s=e.slice(d-1,d+1+1||9e9),o=s[0],g=s[1],m=s[2],f=!b&&r==="INDENT"&&m&&m.generated&&m[0]==="{"&&o&&(t=o[0],v.call(k,t)>=0),q=!1,p=!1,v.call(n,r)>=0&&(b=!1),o&&!o.spaced&&r==="?"&&(c.call=!0);if(c.fromThen)return 1;if(!(f||(o!=null?o.spaced:void 0)&&(o.call||(u=o[0],v.call(k,u)>=0))&&(v.call(i,r)>=0||!c.spaced&&!c.newLine&&v.call(l,r)>=0)))return 1;e.splice(d,0,["CALL_START","(",c[2]]),this.detectEnd(d+1,function(a,b){var c,d;r=a[0];if(!q&&a.fromThen)return!0;if(r==="IF"||r==="ELSE"||r==="CATCH"||r==="->"||r==="=>")q=!0;if(r==="IF"||r==="ELSE"||r==="SWITCH"||r==="TRY")p=!0;if((r==="."||r==="?."||r==="::")&&this.tag(b-1)==="OUTDENT")return!0;return!a.generated&&this.tag(b-1)!==","&&(v.call(j,r)>=0||r==="INDENT"&&!p)&&(r!=="INDENT"||this.tag(b-2)!=="CLASS"&&(d=this.tag(b-1),v.call(h,d)<0)&&(!(c=this.tokens[b+1])||!c.generated||c[0]!=="{"))},a),o[0]==="?"&&(o[0]="FUNC_EXIST");return 2})},a.prototype.addImplicitIndentation=function(){return this.scanTokens(function(a,b,c){var d,e,f,g,h,i,j,k;i=a[0];if(i==="TERMINATOR"&&this.tag(b+1)==="THEN"){c.splice(b,1);return 0}if(i==="ELSE"&&this.tag(b-1)!=="OUTDENT"){c.splice.apply(c,[b,0].concat(w.call(this.indentation(a))));return 2}if(i==="CATCH"&&((j=this.tag(b+2))==="OUTDENT"||j==="TERMINATOR"||j==="FINALLY")){c.splice.apply(c,[b+2,0].concat(w.call(this.indentation(a))));return 4}if(v.call(p,i)>=0&&this.tag(b+1)!=="INDENT"&&(i!=="ELSE"||this.tag(b+1)!=="IF")){h=i,k=this.indentation(a),f=k[0],g=k[1],h==="THEN"&&(f.fromThen=!0),f.generated=g.generated=!0,c.splice(b+1,0,f),e=function(a,b){var c;return a[1]!==";"&&(c=a[0],v.call(o,c)>=0)&&(a[0]!=="ELSE"||h==="IF"||h==="THEN")},d=function(a,b){return this.tokens.splice(this.tag(b-1)===","?b-1:b,0,g)},this.detectEnd(b+2,e,d),i==="THEN"&&c.splice(b,1);return 1}return 1})},a.prototype.tagPostfixConditionals=function(){var a;a=function(a,b){var c;return(c=a[0])==="TERMINATOR"||c==="INDENT"};return this.scanTokens(function(b,c){var d;if(b[0]!=="IF")return 1;d=b,this.detectEnd(c+1,a,function(a,b){if(a[0]!=="INDENT")return d[0]="POST_"+d[0]});return 1})},a.prototype.ensureBalance=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n;d={},f={},m=this.tokens;for(i=0,k=m.length;i<k;i++){h=m[i],g=h[0];for(j=0,l=a.length;j<l;j++){n=a[j],e=n[0],b=n[1],d[e]|=0;if(g===e)d[e]++===0&&(f[e]=h[2]);else if(g===b&&--d[e]<0)throw Error("too many "+h[1]+" on line "+(h[2]+1))}}for(e in d){c=d[e];if(c>0)throw Error("unclosed "+e+" on line "+(f[e]+1))}return this},a.prototype.rewriteClosingParens=function(){var a,b,c;c=[],a={};for(b in m)a[b]=0;return this.scanTokens(function(b,d,e){var h,i,j,k,l,n,o;if(o=l=b[0],v.call(g,o)>=0){c.push(b);return 1}if(v.call(f,l)<0)return 1;if(a[h=m[l]]>0){a[h]-=1,e.splice(d,1);return 0}i=c.pop(),j=i[0],k=m[j];if(l===k)return 1;a[j]+=1,n=[k,j==="INDENT"?i[1]:k],this.tag(d+2)===j?(e.splice(d+3,0,n),c.push(i)):e.splice(d,0,n);return 1})},a.prototype.indentation=function(a){return[["INDENT",2,a[2]],["OUTDENT",2,a[2]]]},a.prototype.tag=function(a){var b;return(b=this.tokens[a])!=null?b[0]:void 0};return a}(),d=[["(",")"],["[","]"],["{","}"],["INDENT","OUTDENT"],["CALL_START","CALL_END"],["PARAM_START","PARAM_END"],["INDEX_START","INDEX_END"]],m={},g=[],f=[];for(s=0,t=d.length;s<t;s++)u=d[s],q=u[0],r=u[1],g.push(m[r]=q),f.push(m[q]=r);e=["CATCH","WHEN","ELSE","FINALLY"].concat(f),k=["IDENTIFIER","SUPER",")","CALL_END","]","INDEX_END","@","THIS"],i=["IDENTIFIER","NUMBER","STRING","JS","REGEX","NEW","PARAM_START","CLASS","IF","TRY","SWITCH","THIS","BOOL","UNARY","SUPER","@","->","=>","[","(","{","--","++"],l=["+","-"],h=["->","=>","{","[",","],j=["POST_IF","FOR","WHILE","UNTIL","WHEN","BY","LOOP","TERMINATOR"],p=["ELSE","->","=>","TRY","FINALLY","THEN"],o=["TERMINATOR","CATCH","FINALLY","ELSE","OUTDENT","LEADING_WHEN"],n=["TERMINATOR","INDENT","OUTDENT"]}),define("ace/mode/coffee/helpers",["require","exports","module"],function(a,b,c){var d,e;b.starts=function(a,b,c){return b===a.substr(c,b.length)},b.ends=function(a,b,c){var d;d=b.length;return b===a.substr(a.length-d-(c||0),d)},b.compact=function(a){var b,c,d,e;e=[];for(c=0,d=a.length;c<d;c++)b=a[c],b&&e.push(b);return e},b.count=function(a,b){var c,d;c=d=0;if(!b.length)return 1/0;while(d=1+a.indexOf(b,d))c++;return c},b.merge=function(a,b){return d(d({},a),b)},d=b.extend=function(a,b){var c,d;for(c in b)d=b[c],a[c]=d;return a},b.flatten=e=function(a){var b,c,d,f;c=[];for(d=0,f=a.length;d<f;d++)b=a[d],b instanceof Array?c=c.concat(e(b)):c.push(b);return c},b.del=function(a,b){var c;c=a[b],delete a[b];return c},b.last=function(a,b){return a[a.length-(b||0)-1]}}),define("ace/mode/coffee/parser",["require","exports","module"],function(a,b,c){var d={trace:function(){},yy:{},symbols_:{error:2,Root:3,Body:4,Block:5,TERMINATOR:6,Line:7,Expression:8,Statement:9,Return:10,Throw:11,Comment:12,STATEMENT:13,Value:14,Invocation:15,Code:16,Operation:17,Assign:18,If:19,Try:20,While:21,For:22,Switch:23,Class:24,INDENT:25,OUTDENT:26,Identifier:27,IDENTIFIER:28,AlphaNumeric:29,NUMBER:30,STRING:31,Literal:32,JS:33,REGEX:34,BOOL:35,Assignable:36,"=":37,AssignObj:38,ObjAssignable:39,":":40,ThisProperty:41,RETURN:42,HERECOMMENT:43,PARAM_START:44,ParamList:45,PARAM_END:46,FuncGlyph:47,"->":48,"=>":49,OptComma:50,",":51,Param:52,ParamVar:53,"...":54,Array:55,Object:56,Splat:57,SimpleAssignable:58,Accessor:59,Parenthetical:60,Range:61,This:62,".":63,"?.":64,"::":65,Index:66,INDEX_START:67,IndexValue:68,INDEX_END:69,INDEX_SOAK:70,INDEX_PROTO:71,Slice:72,"{":73,AssignList:74,"}":75,CLASS:76,EXTENDS:77,OptFuncExist:78,Arguments:79,SUPER:80,FUNC_EXIST:81,CALL_START:82,CALL_END:83,ArgList:84,THIS:85,"@":86,"[":87,"]":88,RangeDots:89,"..":90,Arg:91,SimpleArgs:92,TRY:93,Catch:94,FINALLY:95,CATCH:96,THROW:97,"(":98,")":99,WhileSource:100,WHILE:101,WHEN:102,UNTIL:103,Loop:104,LOOP:105,ForBody:106,FOR:107,ForStart:108,ForSource:109,ForVariables:110,OWN:111,ForValue:112,FORIN:113,FOROF:114,BY:115,SWITCH:116,Whens:117,ELSE:118,When:119,LEADING_WHEN:120,IfBlock:121,IF:122,POST_IF:123,UNARY:124,"-":125,"+":126,"--":127,"++":128,"?":129,MATH:130,SHIFT:131,COMPARE:132,LOGIC:133,RELATION:134,COMPOUND_ASSIGN:135,$accept:0,$end:1},terminals_:{2:"error",6:"TERMINATOR",13:"STATEMENT",25:"INDENT",26:"OUTDENT",28:"IDENTIFIER",30:"NUMBER",31:"STRING",33:"JS",34:"REGEX",35:"BOOL",37:"=",40:":",42:"RETURN",43:"HERECOMMENT",44:"PARAM_START",46:"PARAM_END",48:"->",49:"=>",51:",",54:"...",63:".",64:"?.",65:"::",67:"INDEX_START",69:"INDEX_END",70:"INDEX_SOAK",71:"INDEX_PROTO",73:"{",75:"}",76:"CLASS",77:"EXTENDS",80:"SUPER",81:"FUNC_EXIST",82:"CALL_START",83:"CALL_END",85:"THIS",86:"@",87:"[",88:"]",90:"..",93:"TRY",95:"FINALLY",96:"CATCH",97:"THROW",98:"(",99:")",101:"WHILE",102:"WHEN",103:"UNTIL",105:"LOOP",107:"FOR",111:"OWN",113:"FORIN",114:"FOROF",115:"BY",116:"SWITCH",118:"ELSE",120:"LEADING_WHEN",122:"IF",123:"POST_IF",124:"UNARY",125:"-",126:"+",127:"--",128:"++",129:"?",130:"MATH",131:"SHIFT",132:"COMPARE",133:"LOGIC",134:"RELATION",135:"COMPOUND_ASSIGN"},productions_:[0,[3,0],[3,1],[3,2],[4,1],[4,3],[4,2],[7,1],[7,1],[9,1],[9,1],[9,1],[9,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[8,1],[5,2],[5,3],[27,1],[29,1],[29,1],[32,1],[32,1],[32,1],[32,1],[18,3],[18,5],[38,1],[38,3],[38,5],[38,1],[39,1],[39,1],[39,1],[10,2],[10,1],[12,1],[16,5],[16,2],[47,1],[47,1],[50,0],[50,1],[45,0],[45,1],[45,3],[52,1],[52,2],[52,3],[53,1],[53,1],[53,1],[53,1],[57,2],[58,1],[58,2],[58,2],[58,1],[36,1],[36,1],[36,1],[14,1],[14,1],[14,1],[14,1],[14,1],[59,2],[59,2],[59,2],[59,1],[59,1],[66,3],[66,2],[66,2],[68,1],[68,1],[56,4],[74,0],[74,1],[74,3],[74,4],[74,6],[24,1],[24,2],[24,3],[24,4],[24,2],[24,3],[24,4],[24,5],[15,3],[15,3],[15,1],[15,2],[78,0],[78,1],[79,2],[79,4],[62,1],[62,1],[41,2],[55,2],[55,4],[89,1],[89,1],[61,5],[72,3],[72,2],[72,2],[84,1],[84,3],[84,4],[84,4],[84,6],[91,1],[91,1],[92,1],[92,3],[20,2],[20,3],[20,4],[20,5],[94,3],[11,2],[60,3],[60,5],[100,2],[100,4],[100,2],[100,4],[21,2],[21,2],[21,2],[21,1],[104,2],[104,2],[22,2],[22,2],[22,2],[106,2],[106,2],[108,2],[108,3],[112,1],[112,1],[112,1],[110,1],[110,3],[109,2],[109,2],[109,4],[109,4],[109,4],[109,6],[109,6],[23,5],[23,7],[23,4],[23,6],[117,1],[117,2],[119,3],[119,4],[121,3],[121,5],[19,1],[19,3],[19,3],[19,3],[17,2],[17,2],[17,2],[17,2],[17,2],[17,2],[17,2],[17,2],[17,3],[17,3],[17,3],[17,3],[17,3],[17,3],[17,3],[17,3],[17,5],[17,3]],performAction:function(b,c,d,e,f,g,h){var i=g.length-1;switch(f){case 1:return this.$=new e.Block;case 2:return this.$=g[i];case 3:return this.$=g[i-1];case 4:this.$=e.Block.wrap([g[i]]);break;case 5:this.$=g[i-2].push(g[i]);break;case 6:this.$=g[i-1];break;case 7:this.$=g[i];break;case 8:this.$=g[i];break;case 9:this.$=g[i];break;case 10:this.$=g[i];break;case 11:this.$=g[i];break;case 12:this.$=new e.Literal(g[i]);break;case 13:this.$=g[i];break;case 14:this.$=g[i];break;case 15:this.$=g[i];break;case 16:this.$=g[i];break;case 17:this.$=g[i];break;case 18:this.$=g[i];break;case 19:this.$=g[i];break;case 20:this.$=g[i];break;case 21:this.$=g[i];break;case 22:this.$=g[i];break;case 23:this.$=g[i];break;case 24:this.$=new e.Block;break;case 25:this.$=g[i-1];break;case 26:this.$=new e.Literal(g[i]);break;case 27:this.$=new e.Literal(g[i]);break;case 28:this.$=new e.Literal(g[i]);break;case 29:this.$=g[i];break;case 30:this.$=new e.Literal(g[i]);break;case 31:this.$=new e.Literal(g[i]);break;case 32:this.$=function(){var a;a=new e.Literal(g[i]),g[i]==="undefined"&&(a.isUndefined=!0);return a}();break;case 33:this.$=new e.Assign(g[i-2],g[i]);break;case 34:this.$=new e.Assign(g[i-4],g[i-1]);break;case 35:this.$=new e.Value(g[i]);break;case 36:this.$=new e.Assign(new e.Value(g[i-2]),g[i],"object");break;case 37:this.$=new e.Assign(new e.Value(g[i-4]),g[i-1],"object");break;case 38:this.$=g[i];break;case 39:this.$=g[i];break;case 40:this.$=g[i];break;case 41:this.$=g[i];break;case 42:this.$=new e.Return(g[i]);break;case 43:this.$=new e.Return;break;case 44:this.$=new e.Comment(g[i]);break;case 45:this.$=new e.Code(g[i-3],g[i],g[i-1]);break;case 46:this.$=new e.Code([],g[i],g[i-1]);break;case 47:this.$="func";break;case 48:this.$="boundfunc";break;case 49:this.$=g[i];break;case 50:this.$=g[i];break;case 51:this.$=[];break;case 52:this.$=[g[i]];break;case 53:this.$=g[i-2].concat(g[i]);break;case 54:this.$=new e.Param(g[i]);break;case 55:this.$=new e.Param(g[i-1],null,!0);break;case 56:this.$=new e.Param(g[i-2],g[i]);break;case 57:this.$=g[i];break;case 58:this.$=g[i];break;case 59:this.$=g[i];break;case 60:this.$=g[i];break;case 61:this.$=new e.Splat(g[i-1]);break;case 62:this.$=new e.Value(g[i]);break;case 63:this.$=g[i-1].push(g[i]);break;case 64:this.$=new e.Value(g[i-1],[g[i]]);break;case 65:this.$=g[i];break;case 66:this.$=g[i];break;case 67:this.$=new e.Value(g[i]);break;case 68:this.$=new e.Value(g[i]);break;case 69:this.$=g[i];break;case 70:this.$=new e.Value(g[i]);break;case 71:this.$=new e.Value(g[i]);break;case 72:this.$=new e.Value(g[i]);break;case 73:this.$=g[i];break;case 74:this.$=new e.Access(g[i]);break;case 75:this.$=new e.Access(g[i],"soak");break;case 76:this.$=new e.Access(g[i],"proto");break;case 77:this.$=new e.Access(new e.Literal("prototype"));break;case 78:this.$=g[i];break;case 79:this.$=g[i-1];break;case 80:this.$=e.extend(g[i],{soak:!0});break;case 81:this.$=e.extend(g[i],{proto:!0});break;case 82:this.$=new e.Index(g[i]);break;case 83:this.$=new e.Slice(g[i]);break;case 84:this.$=new e.Obj(g[i-2],g[i-3].generated);break;case 85:this.$=[];break;case 86:this.$=[g[i]];break;case 87:this.$=g[i-2].concat(g[i]);break;case 88:this.$=g[i-3].concat(g[i]);break;case 89:this.$=g[i-5].concat(g[i-2]);break;case 90:this.$=new e.Class;break;case 91:this.$=new e.Class(null,null,g[i]);break;case 92:this.$=new e.Class(null,g[i]);break;case 93:this.$=new e.Class(null,g[i-1],g[i]);break;case 94:this.$=new e.Class(g[i]);break;case 95:this.$=new e.Class(g[i-1],null,g[i]);break;case 96:this.$=new e.Class(g[i-2],g[i]);break;case 97:this.$=new e.Class(g[i-3],g[i-1],g[i]);break;case 98:this.$=new e.Call(g[i-2],g[i],g[i-1]);break;case 99:this.$=new e.Call(g[i-2],g[i],g[i-1]);break;case 100:this.$=new e.Call("super",[new e.Splat(new e.Literal("arguments"))]);break;case 101:this.$=new e.Call("super",g[i]);break;case 102:this.$=!1;break;case 103:this.$=!0;break;case 104:this.$=[];break;case 105:this.$=g[i-2];break;case 106:this.$=new e.Value(new e.Literal("this"));break;case 107:this.$=new e.Value(new e.Literal("this"));break;case 108:this.$=new e.Value(new e.Literal("this"),[new e.Access(g[i])],"this");break;case 109:this.$=new e.Arr([]);break;case 110:this.$=new e.Arr(g[i-2]);break;case 111:this.$="inclusive";break;case 112:this.$="exclusive";break;case 113:this.$=new e.Range(g[i-3],g[i-1],g[i-2]);break;case 114:this.$=new e.Range(g[i-2],g[i],g[i-1]);break;case 115:this.$=new e.Range(g[i-1],null,g[i]);break;case 116:this.$=new e.Range(null,g[i],g[i-1]);break;case 117:this.$=[g[i]];break;case 118:this.$=g[i-2].concat(g[i]);break;case 119:this.$=g[i-3].concat(g[i]);break;case 120:this.$=g[i-2];break;case 121:this.$=g[i-5].concat(g[i-2]);break;case 122:this.$=g[i];break;case 123:this.$=g[i];break;case 124:this.$=g[i];break;case 125:this.$=[].concat(g[i-2],g[i]);break;case 126:this.$=new e.Try(g[i]);break;case 127:this.$=new e.Try(g[i-1],g[i][0],g[i][1]);break;case 128:this.$=new e.Try(g[i-2],null,null,g[i]);break;case 129:this.$=new e.Try(g[i-3],g[i-2][0],g[i-2][1],g[i]);break;case 130:this.$=[g[i-1],g[i]];break;case 131:this.$=new e.Throw(g[i]);break;case 132:this.$=new e.Parens(g[i-1]);break;case 133:this.$=new e.Parens(g[i-2]);break;case 134:this.$=new e.While(g[i]);break;case 135:this.$=new e.While(g[i-2],{guard:g[i]});break;case 136:this.$=new e.While(g[i],{invert:!0});break;case 137:this.$=new e.While(g[i-2],{invert:!0,guard:g[i]});break;case 138:this.$=g[i-1].addBody(g[i]);break;case 139:this.$=g[i].addBody(e.Block.wrap([g[i-1]]));break;case 140:this.$=g[i].addBody(e.Block.wrap([g[i-1]]));break;case 141:this.$=g[i];break;case 142:this.$=(new e.While(new e.Literal("true"))).addBody(g[i]);break;case 143:this.$=(new e.While(new e.Literal("true"))).addBody(e.Block.wrap([g[i]]));break;case 144:this.$=new e.For(g[i-1],g[i]);break;case 145:this.$=new e.For(g[i-1],g[i]);break;case 146:this.$=new e.For(g[i],g[i-1]);break;case 147:this.$={source:new e.Value(g[i])};break;case 148:this.$=function(){g[i].own=g[i-1].own,g[i].name=g[i-1][0],g[i].index=g[i-1][1];return g[i]}();break;case 149:this.$=g[i];break;case 150:this.$=function(){g[i].own=!0;return g[i]}();break;case 151:this.$=g[i];break;case 152:this.$=new e.Value(g[i]);break;case 153:this.$=new e.Value(g[i]);break;case 154:this.$=[g[i]];break;case 155:this.$=[g[i-2],g[i]];break;case 156:this.$={source:g[i]};break;case 157:this.$={source:g[i],object:!0};break;case 158:this.$={source:g[i-2],guard:g[i]};break;case 159:this.$={source:g[i-2],guard:g[i],object:!0};break;case 160:this.$={source:g[i-2],step:g[i]};break;case 161:this.$={source:g[i-4],guard:g[i-2],step:g[i]};break;case 162:this.$={source:g[i-4],step:g[i-2],guard:g[i]};break;case 163:this.$=new e.Switch(g[i-3],g[i-1]);break;case 164:this.$=new e.Switch(g[i-5],g[i-3],g[i-1]);break;case 165:this.$=new e.Switch(null,g[i-1]);break;case 166:this.$=new e.Switch(null,g[i-3],g[i-1]);break;case 167:this.$=g[i];break;case 168:this.$=g[i-1].concat(g[i]);break;case 169:this.$=[[g[i-1],g[i]]];break;case 170:this.$=[[g[i-2],g[i-1]]];break;case 171:this.$=new e.If(g[i-1],g[i],{type:g[i-2]});break;case 172:this.$=g[i-4].addElse(new e.If(g[i-1],g[i],{type:g[i-2]}));break;case 173:this.$=g[i];break;case 174:this.$=g[i-2].addElse(g[i]);break;case 175:this.$=new e.If(g[i],e.Block.wrap([g[i-2]]),{type:g[i-1],statement:!0});break;case 176:this.$=new e.If(g[i],e.Block.wrap([g[i-2]]),{type:g[i-1],statement:!0});break;case 177:this.$=new e.Op(g[i-1],g[i]);break;case 178:this.$=new e.Op("-",g[i]);break;case 179:this.$=new e.Op("+",g[i]);break;case 180:this.$=new e.Op("--",g[i]);break;case 181:this.$=new e.Op("++",g[i]);break;case 182:this.$=new e.Op("--",g[i-1],null,!0);break;case 183:this.$=new e.Op("++",g[i-1],null,!0);break;case 184:this.$=new e.Existence(g[i-1]);break;case 185:this.$=new e.Op("+",g[i-2],g[i]);break;case 186:this.$=new e.Op("-",g[i-2],g[i]);break;case 187:this.$=new e.Op(g[i-1],g[i-2],g[i]);break;case 188:this.$=new e.Op(g[i-1],g[i-2],g[i]);break;case 189:this.$=new e.Op(g[i-1],g[i-2],g[i]);break;case 190:this.$=new e.Op(g[i-1],g[i-2],g[i]);break;case 191:this.$=function(){return g[i-1].charAt(0)==="!"?(new e.Op(g[i-1].slice(1),g[i-2],g[i])).invert():new e.Op(g[i-1],g[i-2],g[i])}();break;case 192:this.$=new e.Assign(g[i-2],g[i],g[i-1]);break;case 193:this.$=new e.Assign(g[i-4],g[i-1],g[i-3]);break;case 194:this.$=new e.Extends(g[i-2],g[i])}},table:[{1:[2,1],3:1,4:2,5:3,7:4,8:6,9:7,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,25:[1,5],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[3]},{1:[2,2],6:[1,71]},{6:[1,72]},{1:[2,4],6:[2,4],26:[2,4],99:[2,4]},{4:74,7:4,8:6,9:7,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,26:[1,73],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,7],6:[2,7],26:[2,7],99:[2,7],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,8],6:[2,8],26:[2,8],99:[2,8],100:87,101:[1,62],103:[1,63],106:88,107:[1,65],108:66,123:[1,86]},{1:[2,13],6:[2,13],25:[2,13],26:[2,13],46:[2,13],51:[2,13],54:[2,13],59:90,63:[1,92],64:[1,93],65:[1,94],66:95,67:[1,96],69:[2,13],70:[1,97],71:[1,98],75:[2,13],78:89,81:[1,91],82:[2,102],83:[2,13],88:[2,13],90:[2,13],99:[2,13],101:[2,13],102:[2,13],103:[2,13],107:[2,13],115:[2,13],123:[2,13],125:[2,13],126:[2,13],129:[2,13],130:[2,13],131:[2,13],132:[2,13],133:[2,13],134:[2,13]},{1:[2,14],6:[2,14],25:[2,14],26:[2,14],46:[2,14],51:[2,14],54:[2,14],59:100,63:[1,92],64:[1,93],65:[1,94],66:95,67:[1,96],69:[2,14],70:[1,97],71:[1,98],75:[2,14],78:99,81:[1,91],82:[2,102],83:[2,14],88:[2,14],90:[2,14],99:[2,14],101:[2,14],102:[2,14],103:[2,14],107:[2,14],115:[2,14],123:[2,14],125:[2,14],126:[2,14],129:[2,14],130:[2,14],131:[2,14],132:[2,14],133:[2,14],134:[2,14]},{1:[2,15],6:[2,15],25:[2,15],26:[2,15],46:[2,15],51:[2,15],54:[2,15],69:[2,15],75:[2,15],83:[2,15],88:[2,15],90:[2,15],99:[2,15],101:[2,15],102:[2,15],103:[2,15],107:[2,15],115:[2,15],123:[2,15],125:[2,15],126:[2,15],129:[2,15],130:[2,15],131:[2,15],132:[2,15],133:[2,15],134:[2,15]},{1:[2,16],6:[2,16],25:[2,16],26:[2,16],46:[2,16],51:[2,16],54:[2,16],69:[2,16],75:[2,16],83:[2,16],88:[2,16],90:[2,16],99:[2,16],101:[2,16],102:[2,16],103:[2,16],107:[2,16],115:[2,16],123:[2,16],125:[2,16],126:[2,16],129:[2,16],130:[2,16],131:[2,16],132:[2,16],133:[2,16],134:[2,16]},{1:[2,17],6:[2,17],25:[2,17],26:[2,17],46:[2,17],51:[2,17],54:[2,17],69:[2,17],75:[2,17],83:[2,17],88:[2,17],90:[2,17],99:[2,17],101:[2,17],102:[2,17],103:[2,17],107:[2,17],115:[2,17],123:[2,17],125:[2,17],126:[2,17],129:[2,17],130:[2,17],131:[2,17],132:[2,17],133:[2,17],134:[2,17]},{1:[2,18],6:[2,18],25:[2,18],26:[2,18],46:[2,18],51:[2,18],54:[2,18],69:[2,18],75:[2,18],83:[2,18],88:[2,18],90:[2,18],99:[2,18],101:[2,18],102:[2,18],103:[2,18],107:[2,18],115:[2,18],123:[2,18],125:[2,18],126:[2,18],129:[2,18],130:[2,18],131:[2,18],132:[2,18],133:[2,18],134:[2,18]},{1:[2,19],6:[2,19],25:[2,19],26:[2,19],46:[2,19],51:[2,19],54:[2,19],69:[2,19],75:[2,19],83:[2,19],88:[2,19],90:[2,19],99:[2,19],101:[2,19],102:[2,19],103:[2,19],107:[2,19],115:[2,19],123:[2,19],125:[2,19],126:[2,19],129:[2,19],130:[2,19],131:[2,19],132:[2,19],133:[2,19],134:[2,19]},{1:[2,20],6:[2,20],25:[2,20],26:[2,20],46:[2,20],51:[2,20],54:[2,20],69:[2,20],75:[2,20],83:[2,20],88:[2,20],90:[2,20],99:[2,20],101:[2,20],102:[2,20],103:[2,20],107:[2,20],115:[2,20],123:[2,20],125:[2,20],126:[2,20],129:[2,20],130:[2,20],131:[2,20],132:[2,20],133:[2,20],134:[2,20]},{1:[2,21],6:[2,21],25:[2,21],26:[2,21],46:[2,21],51:[2,21],54:[2,21],69:[2,21],75:[2,21],83:[2,21],88:[2,21],90:[2,21],99:[2,21],101:[2,21],102:[2,21],103:[2,21],107:[2,21],115:[2,21],123:[2,21],125:[2,21],126:[2,21],129:[2,21],130:[2,21],131:[2,21],132:[2,21],133:[2,21],134:[2,21]},{1:[2,22],6:[2,22],25:[2,22],26:[2,22],46:[2,22],51:[2,22],54:[2,22],69:[2,22],75:[2,22],83:[2,22],88:[2,22],90:[2,22],99:[2,22],101:[2,22],102:[2,22],103:[2,22],107:[2,22],115:[2,22],123:[2,22],125:[2,22],126:[2,22],129:[2,22],130:[2,22],131:[2,22],132:[2,22],133:[2,22],134:[2,22]},{1:[2,23],6:[2,23],25:[2,23],26:[2,23],46:[2,23],51:[2,23],54:[2,23],69:[2,23],75:[2,23],83:[2,23],88:[2,23],90:[2,23],99:[2,23],101:[2,23],102:[2,23],103:[2,23],107:[2,23],115:[2,23],123:[2,23],125:[2,23],126:[2,23],129:[2,23],130:[2,23],131:[2,23],132:[2,23],133:[2,23],134:[2,23]},{1:[2,9],6:[2,9],26:[2,9],99:[2,9],101:[2,9],103:[2,9],107:[2,9],123:[2,9]},{1:[2,10],6:[2,10],26:[2,10],99:[2,10],101:[2,10],103:[2,10],107:[2,10],123:[2,10]},{1:[2,11],6:[2,11],26:[2,11],99:[2,11],101:[2,11],103:[2,11],107:[2,11],123:[2,11]},{1:[2,12],6:[2,12],26:[2,12],99:[2,12],101:[2,12],103:[2,12],107:[2,12],123:[2,12]},{1:[2,69],6:[2,69],25:[2,69],26:[2,69],37:[1,101],46:[2,69],51:[2,69],54:[2,69],63:[2,69],64:[2,69],65:[2,69],67:[2,69],69:[2,69],70:[2,69],71:[2,69],75:[2,69],81:[2,69],82:[2,69],83:[2,69],88:[2,69],90:[2,69],99:[2,69],101:[2,69],102:[2,69],103:[2,69],107:[2,69],115:[2,69],123:[2,69],125:[2,69],126:[2,69],129:[2,69],130:[2,69],131:[2,69],132:[2,69],133:[2,69],134:[2,69]},{1:[2,70],6:[2,70],25:[2,70],26:[2,70],46:[2,70],51:[2,70],54:[2,70],63:[2,70],64:[2,70],65:[2,70],67:[2,70],69:[2,70],70:[2,70],71:[2,70],75:[2,70],81:[2,70],82:[2,70],83:[2,70],88:[2,70],90:[2,70],99:[2,70],101:[2,70],102:[2,70],103:[2,70],107:[2,70],115:[2,70],123:[2,70],125:[2,70],126:[2,70],129:[2,70],130:[2,70],131:[2,70],132:[2,70],133:[2,70],134:[2,70]},{1:[2,71],6:[2,71],25:[2,71],26:[2,71],46:[2,71],51:[2,71],54:[2,71],63:[2,71],64:[2,71],65:[2,71],67:[2,71],69:[2,71],70:[2,71],71:[2,71],75:[2,71],81:[2,71],82:[2,71],83:[2,71],88:[2,71],90:[2,71],99:[2,71],101:[2,71],102:[2,71],103:[2,71],107:[2,71],115:[2,71],123:[2,71],125:[2,71],126:[2,71],129:[2,71],130:[2,71],131:[2,71],132:[2,71],133:[2,71],134:[2,71]},{1:[2,72],6:[2,72],25:[2,72],26:[2,72],46:[2,72],51:[2,72],54:[2,72],63:[2,72],64:[2,72],65:[2,72],67:[2,72],69:[2,72],70:[2,72],71:[2,72],75:[2,72],81:[2,72],82:[2,72],83:[2,72],88:[2,72],90:[2,72],99:[2,72],101:[2,72],102:[2,72],103:[2,72],107:[2,72],115:[2,72],123:[2,72],125:[2,72],126:[2,72],129:[2,72],130:[2,72],131:[2,72],132:[2,72],133:[2,72],134:[2,72]},{1:[2,73],6:[2,73],25:[2,73],26:[2,73],46:[2,73],51:[2,73],54:[2,73],63:[2,73],64:[2,73],65:[2,73],67:[2,73],69:[2,73],70:[2,73],71:[2,73],75:[2,73],81:[2,73],82:[2,73],83:[2,73],88:[2,73],90:[2,73],99:[2,73],101:[2,73],102:[2,73],103:[2,73],107:[2,73],115:[2,73],123:[2,73],125:[2,73],126:[2,73],129:[2,73],130:[2,73],131:[2,73],132:[2,73],133:[2,73],134:[2,73]},{1:[2,100],6:[2,100],25:[2,100],26:[2,100],46:[2,100],51:[2,100],54:[2,100],63:[2,100],64:[2,100],65:[2,100],67:[2,100],69:[2,100],70:[2,100],71:[2,100],75:[2,100],79:102,81:[2,100],82:[1,103],83:[2,100],88:[2,100],90:[2,100],99:[2,100],101:[2,100],102:[2,100],103:[2,100],107:[2,100],115:[2,100],123:[2,100],125:[2,100],126:[2,100],129:[2,100],130:[2,100],131:[2,100],132:[2,100],133:[2,100],134:[2,100]},{27:107,28:[1,70],41:108,45:104,46:[2,51],51:[2,51],52:105,53:106,55:109,56:110,73:[1,67],86:[1,111],87:[1,112]},{5:113,25:[1,5]},{8:114,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:116,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:117,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{14:119,15:120,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:121,41:60,55:47,56:48,58:118,60:25,61:26,62:27,73:[1,67],80:[1,28],85:[1,55],86:[1,56],87:[1,54],98:[1,53]},{14:119,15:120,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:121,41:60,55:47,56:48,58:122,60:25,61:26,62:27,73:[1,67],80:[1,28],85:[1,55],86:[1,56],87:[1,54],98:[1,53]},{1:[2,66],6:[2,66],25:[2,66],26:[2,66],37:[2,66],46:[2,66],51:[2,66],54:[2,66],63:[2,66],64:[2,66],65:[2,66],67:[2,66],69:[2,66],70:[2,66],71:[2,66],75:[2,66],77:[1,126],81:[2,66],82:[2,66],83:[2,66],88:[2,66],90:[2,66],99:[2,66],101:[2,66],102:[2,66],103:[2,66],107:[2,66],115:[2,66],123:[2,66],125:[2,66],126:[2,66],127:[1,123],128:[1,124],129:[2,66],130:[2,66],131:[2,66],132:[2,66],133:[2,66],134:[2,66],135:[1,125]},{1:[2,173],6:[2,173],25:[2,173],26:[2,173],46:[2,173],51:[2,173],54:[2,173],69:[2,173],75:[2,173],83:[2,173],88:[2,173],90:[2,173],99:[2,173],101:[2,173],102:[2,173],103:[2,173],107:[2,173],115:[2,173],118:[1,127],123:[2,173],125:[2,173],126:[2,173],129:[2,173],130:[2,173],131:[2,173],132:[2,173],133:[2,173],134:[2,173]},{5:128,25:[1,5]},{5:129,25:[1,5]},{1:[2,141],6:[2,141],25:[2,141],26:[2,141],46:[2,141],51:[2,141],54:[2,141],69:[2,141],75:[2,141],83:[2,141],88:[2,141],90:[2,141],99:[2,141],101:[2,141],102:[2,141],103:[2,141],107:[2,141],115:[2,141],123:[2,141],125:[2,141],126:[2,141],129:[2,141],130:[2,141],131:[2,141],132:[2,141],133:[2,141],134:[2,141]},{5:130,25:[1,5]},{8:131,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,25:[1,132],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,90],5:133,6:[2,90],14:119,15:120,25:[1,5],26:[2,90],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:121,41:60,46:[2,90],51:[2,90],54:[2,90],55:47,56:48,58:135,60:25,61:26,62:27,69:[2,90],73:[1,67],75:[2,90],77:[1,134],80:[1,28],83:[2,90],85:[1,55],86:[1,56],87:[1,54],88:[2,90],90:[2,90],98:[1,53],99:[2,90],101:[2,90],102:[2,90],103:[2,90],107:[2,90],115:[2,90],123:[2,90],125:[2,90],126:[2,90],129:[2,90],130:[2,90],131:[2,90],132:[2,90],133:[2,90],134:[2,90]},{1:[2,43],6:[2,43],8:136,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,26:[2,43],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],99:[2,43],100:39,101:[2,43],103:[2,43],104:40,105:[1,64],106:41,107:[2,43],108:66,116:[1,42],121:37,122:[1,61],123:[2,43],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:137,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,44],6:[2,44],25:[2,44],26:[2,44],51:[2,44],75:[2,44],99:[2,44],101:[2,44],103:[2,44],107:[2,44],123:[2,44]},{1:[2,67],6:[2,67],25:[2,67],26:[2,67],37:[2,67],46:[2,67],51:[2,67],54:[2,67],63:[2,67],64:[2,67],65:[2,67],67:[2,67],69:[2,67],70:[2,67],71:[2,67],75:[2,67],81:[2,67],82:[2,67],83:[2,67],88:[2,67],90:[2,67],99:[2,67],101:[2,67],102:[2,67],103:[2,67],107:[2,67],115:[2,67],123:[2,67],125:[2,67],126:[2,67],129:[2,67],130:[2,67],131:[2,67],132:[2,67],133:[2,67],134:[2,67]},{1:[2,68],6:[2,68],25:[2,68],26:[2,68],37:[2,68],46:[2,68],51:[2,68],54:[2,68],63:[2,68],64:[2,68],65:[2,68],67:[2,68],69:[2,68],70:[2,68],71:[2,68],75:[2,68],81:[2,68],82:[2,68],83:[2,68],88:[2,68],90:[2,68],99:[2,68],101:[2,68],102:[2,68],103:[2,68],107:[2,68],115:[2,68],123:[2,68],125:[2,68],126:[2,68],129:[2,68],130:[2,68],131:[2,68],132:[2,68],133:[2,68],134:[2,68]},{1:[2,29],6:[2,29],25:[2,29],26:[2,29],46:[2,29],51:[2,29],54:[2,29],63:[2,29],64:[2,29],65:[2,29],67:[2,29],69:[2,29],70:[2,29],71:[2,29],75:[2,29],81:[2,29],82:[2,29],83:[2,29],88:[2,29],90:[2,29],99:[2,29],101:[2,29],102:[2,29],103:[2,29],107:[2,29],115:[2,29],123:[2,29],125:[2,29],126:[2,29],129:[2,29],130:[2,29],131:[2,29],132:[2,29],133:[2,29],134:[2,29]},{1:[2,30],6:[2,30],25:[2,30],26:[2,30],46:[2,30],51:[2,30],54:[2,30],63:[2,30],64:[2,30],65:[2,30],67:[2,30],69:[2,30],70:[2,30],71:[2,30],75:[2,30],81:[2,30],82:[2,30],83:[2,30],88:[2,30],90:[2,30],99:[2,30],101:[2,30],102:[2,30],103:[2,30],107:[2,30],115:[2,30],123:[2,30],125:[2,30],126:[2,30],129:[2,30],130:[2,30],131:[2,30],132:[2,30],133:[2,30],134:[2,30]},{1:[2,31],6:[2,31],25:[2,31],26:[2,31],46:[2,31],51:[2,31],54:[2,31],63:[2,31],64:[2,31],65:[2,31],67:[2,31],69:[2,31],70:[2,31],71:[2,31],75:[2,31],81:[2,31],82:[2,31],83:[2,31],88:[2,31],90:[2,31],99:[2,31],101:[2,31],102:[2,31],103:[2,31],107:[2,31],115:[2,31],123:[2,31],125:[2,31],126:[2,31],129:[2,31],130:[2,31],131:[2,31],132:[2,31],133:[2,31],134:[2,31]},{1:[2,32],6:[2,32],25:[2,32],26:[2,32],46:[2,32],51:[2,32],54:[2,32],63:[2,32],64:[2,32],65:[2,32],67:[2,32],69:[2,32],70:[2,32],71:[2,32],75:[2,32],81:[2,32],82:[2,32],83:[2,32],88:[2,32],90:[2,32],99:[2,32],101:[2,32],102:[2,32],103:[2,32],107:[2,32],115:[2,32],123:[2,32],125:[2,32],126:[2,32],129:[2,32],130:[2,32],131:[2,32],132:[2,32],133:[2,32],134:[2,32]},{4:138,7:4,8:6,9:7,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,25:[1,139],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:140,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,25:[1,144],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,57:145,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],84:142,85:[1,55],86:[1,56],87:[1,54],88:[1,141],91:143,93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,106],6:[2,106],25:[2,106],26:[2,106],46:[2,106],51:[2,106],54:[2,106],63:[2,106],64:[2,106],65:[2,106],67:[2,106],69:[2,106],70:[2,106],71:[2,106],75:[2,106],81:[2,106],82:[2,106],83:[2,106],88:[2,106],90:[2,106],99:[2,106],101:[2,106],102:[2,106],103:[2,106],107:[2,106],115:[2,106],123:[2,106],125:[2,106],126:[2,106],129:[2,106],130:[2,106],131:[2,106],132:[2,106],133:[2,106],134:[2,106]},{1:[2,107],6:[2,107],25:[2,107],26:[2,107],27:146,28:[1,70],46:[2,107],51:[2,107],54:[2,107],63:[2,107],64:[2,107],65:[2,107],67:[2,107],69:[2,107],70:[2,107],71:[2,107],75:[2,107],81:[2,107],82:[2,107],83:[2,107],88:[2,107],90:[2,107],99:[2,107],101:[2,107],102:[2,107],103:[2,107],107:[2,107],115:[2,107],123:[2,107],125:[2,107],126:[2,107],129:[2,107],130:[2,107],131:[2,107],132:[2,107],133:[2,107],134:[2,107]},{25:[2,47]},{25:[2,48]},{1:[2,62],6:[2,62],25:[2,62],26:[2,62],37:[2,62],46:[2,62],51:[2,62],54:[2,62],63:[2,62],64:[2,62],65:[2,62],67:[2,62],69:[2,62],70:[2,62],71:[2,62],75:[2,62],77:[2,62],81:[2,62],82:[2,62],83:[2,62],88:[2,62],90:[2,62],99:[2,62],101:[2,62],102:[2,62],103:[2,62],107:[2,62],115:[2,62],123:[2,62],125:[2,62],126:[2,62],127:[2,62],128:[2,62],129:[2,62],130:[2,62],131:[2,62],132:[2,62],133:[2,62],134:[2,62],135:[2,62]},{1:[2,65],6:[2,65],25:[2,65],26:[2,65],37:[2,65],46:[2,65],51:[2,65],54:[2,65],63:[2,65],64:[2,65],65:[2,65],67:[2,65],69:[2,65],70:[2,65],71:[2,65],75:[2,65],77:[2,65],81:[2,65],82:[2,65],83:[2,65],88:[2,65],90:[2,65],99:[2,65],101:[2,65],102:[2,65],103:[2,65],107:[2,65],115:[2,65],123:[2,65],125:[2,65],126:[2,65],127:[2,65],128:[2,65],129:[2,65],130:[2,65],131:[2,65],132:[2,65],133:[2,65],134:[2,65],135:[2,65]},{8:147,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:148,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:149,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{5:150,8:151,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,25:[1,5],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{27:156,28:[1,70],55:157,56:158,61:152,73:[1,67],87:[1,54],110:153,111:[1,154],112:155},{109:159,113:[1,160],114:[1,161]},{6:[2,85],12:165,25:[2,85],27:166,28:[1,70],29:167,30:[1,68],31:[1,69],38:163,39:164,41:168,43:[1,46],51:[2,85],74:162,75:[2,85],86:[1,111]},{1:[2,27],6:[2,27],25:[2,27],26:[2,27],40:[2,27],46:[2,27],51:[2,27],54:[2,27],63:[2,27],64:[2,27],65:[2,27],67:[2,27],69:[2,27],70:[2,27],71:[2,27],75:[2,27],81:[2,27],82:[2,27],83:[2,27],88:[2,27],90:[2,27],99:[2,27],101:[2,27],102:[2,27],103:[2,27],107:[2,27],115:[2,27],123:[2,27],125:[2,27],126:[2,27],129:[2,27],130:[2,27],131:[2,27],132:[2,27],133:[2,27],134:[2,27]},{1:[2,28],6:[2,28],25:[2,28],26:[2,28],40:[2,28],46:[2,28],51:[2,28],54:[2,28],63:[2,28],64:[2,28],65:[2,28],67:[2,28],69:[2,28],70:[2,28],71:[2,28],75:[2,28],81:[2,28],82:[2,28],83:[2,28],88:[2,28],90:[2,28],99:[2,28],101:[2,28],102:[2,28],103:[2,28],107:[2,28],115:[2,28],123:[2,28],125:[2,28],126:[2,28],129:[2,28],130:[2,28],131:[2,28],132:[2,28],133:[2,28],134:[2,28]},{1:[2,26],6:[2,26],25:[2,26],26:[2,26],37:[2,26],40:[2,26],46:[2,26],51:[2,26],54:[2,26],63:[2,26],64:[2,26],65:[2,26],67:[2,26],69:[2,26],70:[2,26],71:[2,26],75:[2,26],77:[2,26],81:[2,26],82:[2,26],83:[2,26],88:[2,26],90:[2,26],99:[2,26],101:[2,26],102:[2,26],103:[2,26],107:[2,26],113:[2,26],114:[2,26],115:[2,26],123:[2,26],125:[2,26],126:[2,26],127:[2,26],128:[2,26],129:[2,26],130:[2,26],131:[2,26],132:[2,26],133:[2,26],134:[2,26],135:[2,26]},{1:[2,6],6:[2,6],7:169,8:6,9:7,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,26:[2,6],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],99:[2,6],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,3]},{1:[2,24],6:[2,24],25:[2,24],26:[2,24],46:[2,24],51:[2,24],54:[2,24],69:[2,24],75:[2,24],83:[2,24],88:[2,24],90:[2,24],95:[2,24],96:[2,24],99:[2,24],101:[2,24],102:[2,24],103:[2,24],107:[2,24],115:[2,24],118:[2,24],120:[2,24],123:[2,24],125:[2,24],126:[2,24],129:[2,24],130:[2,24],131:[2,24],132:[2,24],133:[2,24],134:[2,24]},{6:[1,71],26:[1,170]},{1:[2,184],6:[2,184],25:[2,184],26:[2,184],46:[2,184],51:[2,184],54:[2,184],69:[2,184],75:[2,184],83:[2,184],88:[2,184],90:[2,184],99:[2,184],101:[2,184],102:[2,184],103:[2,184],107:[2,184],115:[2,184],123:[2,184],125:[2,184],126:[2,184],129:[2,184],130:[2,184],131:[2,184],132:[2,184],133:[2,184],134:[2,184]},{8:171,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:172,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:173,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:174,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:175,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:176,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:177,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:178,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,140],6:[2,140],25:[2,140],26:[2,140],46:[2,140],51:[2,140],54:[2,140],69:[2,140],75:[2,140],83:[2,140],88:[2,140],90:[2,140],99:[2,140],101:[2,140],102:[2,140],103:[2,140],107:[2,140],115:[2,140],123:[2,140],125:[2,140],126:[2,140],129:[2,140],130:[2,140],131:[2,140],132:[2,140],133:[2,140],134:[2,140]},{1:[2,145],6:[2,145],25:[2,145],26:[2,145],46:[2,145],51:[2,145],54:[2,145],69:[2,145],75:[2,145],83:[2,145],88:[2,145],90:[2,145],99:[2,145],101:[2,145],102:[2,145],103:[2,145],107:[2,145],115:[2,145],123:[2,145],125:[2,145],126:[2,145],129:[2,145],130:[2,145],131:[2,145],132:[2,145],133:[2,145],134:[2,145]},{8:179,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,139],6:[2,139],25:[2,139],26:[2,139],46:[2,139],51:[2,139],54:[2,139],69:[2,139],75:[2,139],83:[2,139],88:[2,139],90:[2,139],99:[2,139],101:[2,139],102:[2,139],103:[2,139],107:[2,139],115:[2,139],123:[2,139],125:[2,139],126:[2,139],129:[2,139],130:[2,139],131:[2,139],132:[2,139],133:[2,139],134:[2,139]},{1:[2,144],6:[2,144],25:[2,144],26:[2,144],46:[2,144],51:[2,144],54:[2,144],69:[2,144],75:[2,144],83:[2,144],88:[2,144],90:[2,144],99:[2,144],101:[2,144],102:[2,144],103:[2,144],107:[2,144],115:[2,144],123:[2,144],125:[2,144],126:[2,144],129:[2,144],130:[2,144],131:[2,144],132:[2,144],133:[2,144],134:[2,144]},{79:180,82:[1,103]},{1:[2,63],6:[2,63],25:[2,63],26:[2,63],37:[2,63],46:[2,63],51:[2,63],54:[2,63],63:[2,63],64:[2,63],65:[2,63],67:[2,63],69:[2,63],70:[2,63],71:[2,63],75:[2,63],77:[2,63],81:[2,63],82:[2,63],83:[2,63],88:[2,63],90:[2,63],99:[2,63],101:[2,63],102:[2,63],103:[2,63],107:[2,63],115:[2,63],123:[2,63],125:[2,63],126:[2,63],127:[2,63],128:[2,63],129:[2,63],130:[2,63],131:[2,63],132:[2,63],133:[2,63],134:[2,63],135:[2,63]},{82:[2,103]},{27:181,28:[1,70]},{27:182,28:[1,70]},{1:[2,77],6:[2,77],25:[2,77],26:[2,77],27:183,28:[1,70],37:[2,77],46:[2,77],51:[2,77],54:[2,77],63:[2,77],64:[2,77],65:[2,77],67:[2,77],69:[2,77],70:[2,77],71:[2,77],75:[2,77],77:[2,77],81:[2,77],82:[2,77],83:[2,77],88:[2,77],90:[2,77],99:[2,77],101:[2,77],102:[2,77],103:[2,77],107:[2,77],115:[2,77],123:[2,77],125:[2,77],126:[2,77],127:[2,77],128:[2,77],129:[2,77],130:[2,77],131:[2,77],132:[2,77],133:[2,77],134:[2,77],135:[2,77]},{1:[2,78],6:[2,78],25:[2,78],26:[2,78],37:[2,78],46:[2,78],51:[2,78],54:[2,78],63:[2,78],64:[2,78],65:[2,78],67:[2,78],69:[2,78],70:[2,78],71:[2,78],75:[2,78],77:[2,78],81:[2,78],82:[2,78],83:[2,78],88:[2,78],90:[2,78],99:[2,78],101:[2,78],102:[2,78],103:[2,78],107:[2,78],115:[2,78],123:[2,78],125:[2,78],126:[2,78],127:[2,78],128:[2,78],129:[2,78],130:[2,78],131:[2,78],132:[2,78],133:[2,78],134:[2,78],135:[2,78]},{8:185,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],54:[1,189],55:47,56:48,58:36,60:25,61:26,62:27,68:184,72:186,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],89:187,90:[1,188],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{66:190,67:[1,96],70:[1,97],71:[1,98]},{66:191,67:[1,96],70:[1,97],71:[1,98]},{79:192,82:[1,103]},{1:[2,64],6:[2,64],25:[2,64],26:[2,64],37:[2,64],46:[2,64],51:[2,64],54:[2,64],63:[2,64],64:[2,64],65:[2,64],67:[2,64],69:[2,64],70:[2,64],71:[2,64],75:[2,64],77:[2,64],81:[2,64],82:[2,64],83:[2,64],88:[2,64],90:[2,64],99:[2,64],101:[2,64],102:[2,64],103:[2,64],107:[2,64],115:[2,64],123:[2,64],125:[2,64],126:[2,64],127:[2,64],128:[2,64],129:[2,64],130:[2,64],131:[2,64],132:[2,64],133:[2,64],134:[2,64],135:[2,64]},{8:193,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,25:[1,194],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,101],6:[2,101],25:[2,101],26:[2,101],46:[2,101],51:[2,101],54:[2,101],63:[2,101],64:[2,101],65:[2,101],67:[2,101],69:[2,101],70:[2,101],71:[2,101],75:[2,101],81:[2,101],82:[2,101],83:[2,101],88:[2,101],90:[2,101],99:[2,101],101:[2,101],102:[2,101],103:[2,101],107:[2,101],115:[2,101],123:[2,101],125:[2,101],126:[2,101],129:[2,101],130:[2,101],131:[2,101],132:[2,101],133:[2,101],134:[2,101]},{8:197,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,25:[1,144],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,57:145,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],83:[1,195],84:196,85:[1,55],86:[1,56],87:[1,54],91:143,93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{46:[1,198],51:[1,199]},{46:[2,52],51:[2,52]},{37:[1,201],46:[2,54],51:[2,54],54:[1,200]},{37:[2,57],46:[2,57],51:[2,57],54:[2,57]},{37:[2,58],46:[2,58],51:[2,58],54:[2,58]},{37:[2,59],46:[2,59],51:[2,59],54:[2,59]},{37:[2,60],46:[2,60],51:[2,60],54:[2,60]},{27:146,28:[1,70]},{8:197,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,25:[1,144],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,57:145,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],84:142,85:[1,55],86:[1,56],87:[1,54],88:[1,141],91:143,93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,46],6:[2,46],25:[2,46],26:[2,46],46:[2,46],51:[2,46],54:[2,46],69:[2,46],75:[2,46],83:[2,46],88:[2,46],90:[2,46],99:[2,46],101:[2,46],102:[2,46],103:[2,46],107:[2,46],115:[2,46],123:[2,46],125:[2,46],126:[2,46],129:[2,46],130:[2,46],131:[2,46],132:[2,46],133:[2,46],134:[2,46]},{1:[2,177],6:[2,177],25:[2,177],26:[2,177],46:[2,177],51:[2,177],54:[2,177],69:[2,177],75:[2,177],83:[2,177],88:[2,177],90:[2,177],99:[2,177],100:84,101:[2,177],102:[2,177],103:[2,177],106:85,107:[2,177],108:66,115:[2,177],123:[2,177],125:[2,177],126:[2,177],129:[1,75],130:[2,177],131:[2,177],132:[2,177],133:[2,177],134:[2,177]},{100:87,101:[1,62],103:[1,63],106:88,107:[1,65],108:66,123:[1,86]},{1:[2,178],6:[2,178],25:[2,178],26:[2,178],46:[2,178],51:[2,178],54:[2,178],69:[2,178],75:[2,178],83:[2,178],88:[2,178],90:[2,178],99:[2,178],100:84,101:[2,178],102:[2,178],103:[2,178],106:85,107:[2,178],108:66,115:[2,178],123:[2,178],125:[2,178],126:[2,178],129:[1,75],130:[2,178],131:[2,178],132:[2,178],133:[2,178],134:[2,178]},{1:[2,179],6:[2,179],25:[2,179],26:[2,179],46:[2,179],51:[2,179],54:[2,179],69:[2,179],75:[2,179],83:[2,179],88:[2,179],90:[2,179],99:[2,179],100:84,101:[2,179],102:[2,179],103:[2,179],106:85,107:[2,179],108:66,115:[2,179],123:[2,179],125:[2,179],126:[2,179],129:[1,75],130:[2,179],131:[2,179],132:[2,179],133:[2,179],134:[2,179]},{1:[2,180],6:[2,180],25:[2,180],26:[2,180],46:[2,180],51:[2,180],54:[2,180],63:[2,66],64:[2,66],65:[2,66],67:[2,66],69:[2,180],70:[2,66],71:[2,66],75:[2,180],81:[2,66],82:[2,66],83:[2,180],88:[2,180],90:[2,180],99:[2,180],101:[2,180],102:[2,180],103:[2,180],107:[2,180],115:[2,180],123:[2,180],125:[2,180],126:[2,180],129:[2,180],130:[2,180],131:[2,180],132:[2,180],133:[2,180],134:[2,180]},{59:90,63:[1,92],64:[1,93],65:[1,94],66:95,67:[1,96],70:[1,97],71:[1,98],78:89,81:[1,91],82:[2,102]},{59:100,63:[1,92],64:[1,93],65:[1,94],66:95,67:[1,96],70:[1,97],71:[1,98],78:99,81:[1,91],82:[2,102]},{1:[2,69],6:[2,69],25:[2,69],26:[2,69],46:[2,69],51:[2,69],54:[2,69],63:[2,69],64:[2,69],65:[2,69],67:[2,69],69:[2,69],70:[2,69],71:[2,69],75:[2,69],81:[2,69],82:[2,69],83:[2,69],88:[2,69],90:[2,69],99:[2,69],101:[2,69],102:[2,69],103:[2,69],107:[2,69],115:[2,69],123:[2,69],125:[2,69],126:[2,69],129:[2,69],130:[2,69],131:[2,69],132:[2,69],133:[2,69],134:[2,69]},{1:[2,181],6:[2,181],25:[2,181],26:[2,181],46:[2,181],51:[2,181],54:[2,181],63:[2,66],64:[2,66],65:[2,66],67:[2,66],69:[2,181],70:[2,66],71:[2,66],75:[2,181],81:[2,66],82:[2,66],83:[2,181],88:[2,181],90:[2,181],99:[2,181],101:[2,181],102:[2,181],103:[2,181],107:[2,181],115:[2,181],123:[2,181],125:[2,181],126:[2,181],129:[2,181],130:[2,181],131:[2,181],132:[2,181],133:[2,181],134:[2,181]},{1:[2,182],6:[2,182],25:[2,182],26:[2,182],46:[2,182],51:[2,182],54:[2,182],69:[2,182],75:[2,182],83:[2,182],88:[2,182],90:[2,182],99:[2,182],101:[2,182],102:[2,182],103:[2,182],107:[2,182],115:[2,182],123:[2,182],125:[2,182],126:[2,182],129:[2,182],130:[2,182],131:[2,182],132:[2,182],133:[2,182],134:[2,182]},{1:[2,183],6:[2,183],25:[2,183],26:[2,183],46:[2,183],51:[2,183],54:[2,183],69:[2,183],75:[2,183],83:[2,183],88:[2,183],90:[2,183],99:[2,183],101:[2,183],102:[2,183],103:[2,183],107:[2,183],115:[2,183],123:[2,183],125:[2,183],126:[2,183],129:[2,183],130:[2,183],131:[2,183],132:[2,183],133:[2,183],134:[2,183]},{8:202,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,25:[1,203],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:204,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{5:205,25:[1,5],122:[1,206]},{1:[2,126],6:[2,126],25:[2,126],26:[2,126],46:[2,126],51:[2,126],54:[2,126],69:[2,126],75:[2,126],83:[2,126],88:[2,126],90:[2,126],94:207,95:[1,208],96:[1,209],99:[2,126],101:[2,126],102:[2,126],103:[2,126],107:[2,126],115:[2,126],123:[2,126],125:[2,126],126:[2,126],129:[2,126],130:[2,126],131:[2,126],132:[2,126],133:[2,126],134:[2,126]},{1:[2,138],6:[2,138],25:[2,138],26:[2,138],46:[2,138],51:[2,138],54:[2,138],69:[2,138],75:[2,138],83:[2,138],88:[2,138],90:[2,138],99:[2,138],101:[2,138],102:[2,138],103:[2,138],107:[2,138],115:[2,138],123:[2,138],125:[2,138],126:[2,138],129:[2,138],130:[2,138],131:[2,138],132:[2,138],133:[2,138],134:[2,138]},{1:[2,146],6:[2,146],25:[2,146],26:[2,146],46:[2,146],51:[2,146],54:[2,146],69:[2,146],75:[2,146],83:[2,146],88:[2,146],90:[2,146],99:[2,146],101:[2,146],102:[2,146],103:[2,146],107:[2,146],115:[2,146],123:[2,146],125:[2,146],126:[2,146],129:[2,146],130:[2,146],131:[2,146],132:[2,146],133:[2,146],134:[2,146]},{25:[1,210],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{117:211,119:212,120:[1,213]},{1:[2,91],6:[2,91],25:[2,91],26:[2,91],46:[2,91],51:[2,91],54:[2,91],69:[2,91],75:[2,91],83:[2,91],88:[2,91],90:[2,91],99:[2,91],101:[2,91],102:[2,91],103:[2,91],107:[2,91],115:[2,91],123:[2,91],125:[2,91],126:[2,91],129:[2,91],130:[2,91],131:[2,91],132:[2,91],133:[2,91],134:[2,91]},{14:214,15:120,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:121,41:60,55:47,56:48,58:215,60:25,61:26,62:27,73:[1,67],80:[1,28],85:[1,55],86:[1,56],87:[1,54],98:[1,53]},{1:[2,94],5:216,6:[2,94],25:[1,5],26:[2,94],46:[2,94],51:[2,94],54:[2,94],63:[2,66],64:[2,66],65:[2,66],67:[2,66],69:[2,94],70:[2,66],71:[2,66],75:[2,94],77:[1,217],81:[2,66],82:[2,66],83:[2,94],88:[2,94],90:[2,94],99:[2,94],101:[2,94],102:[2,94],103:[2,94],107:[2,94],115:[2,94],123:[2,94],125:[2,94],126:[2,94],129:[2,94],130:[2,94],131:[2,94],132:[2,94],133:[2,94],134:[2,94]},{1:[2,42],6:[2,42],26:[2,42],99:[2,42],100:84,101:[2,42],103:[2,42],106:85,107:[2,42],108:66,123:[2,42],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,131],6:[2,131],26:[2,131],99:[2,131],100:84,101:[2,131],103:[2,131],106:85,107:[2,131],108:66,123:[2,131],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{6:[1,71],99:[1,218]},{4:219,7:4,8:6,9:7,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{6:[2,122],25:[2,122],51:[2,122],54:[1,221],88:[2,122],89:220,90:[1,188],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,109],6:[2,109],25:[2,109],26:[2,109],37:[2,109],46:[2,109],51:[2,109],54:[2,109],63:[2,109],64:[2,109],65:[2,109],67:[2,109],69:[2,109],70:[2,109],71:[2,109],75:[2,109],81:[2,109],82:[2,109],83:[2,109],88:[2,109],90:[2,109],99:[2,109],101:[2,109],102:[2,109],103:[2,109],107:[2,109],113:[2,109],114:[2,109],115:[2,109],123:[2,109],125:[2,109],126:[2,109],129:[2,109],130:[2,109],131:[2,109],132:[2,109],133:[2,109],134:[2,109]},{6:[2,49],25:[2,49],50:222,51:[1,223],88:[2,49]},{6:[2,117],25:[2,117],26:[2,117],51:[2,117],83:[2,117],88:[2,117]},{8:197,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,25:[1,144],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,57:145,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],84:224,85:[1,55],86:[1,56],87:[1,54],91:143,93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{6:[2,123],25:[2,123],26:[2,123],51:[2,123],83:[2,123],88:[2,123]},{1:[2,108],6:[2,108],25:[2,108],26:[2,108],37:[2,108],40:[2,108],46:[2,108],51:[2,108],54:[2,108],63:[2,108],64:[2,108],65:[2,108],67:[2,108],69:[2,108],70:[2,108],71:[2,108],75:[2,108],77:[2,108],81:[2,108],82:[2,108],83:[2,108],88:[2,108],90:[2,108],99:[2,108],101:[2,108],102:[2,108],103:[2,108],107:[2,108],115:[2,108],123:[2,108],125:[2,108],126:[2,108],127:[2,108],128:[2,108],129:[2,108],130:[2,108],131:[2,108],132:[2,108],133:[2,108],134:[2,108],135:[2,108]},{5:225,25:[1,5],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,134],6:[2,134],25:[2,134],26:[2,134],46:[2,134],51:[2,134],54:[2,134],69:[2,134],75:[2,134],83:[2,134],88:[2,134],90:[2,134],99:[2,134],100:84,101:[1,62],102:[1,226],103:[1,63],106:85,107:[1,65],108:66,115:[2,134],123:[2,134],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,136],6:[2,136],25:[2,136],26:[2,136],46:[2,136],51:[2,136],54:[2,136],69:[2,136],75:[2,136],83:[2,136],88:[2,136],90:[2,136],99:[2,136],100:84,101:[1,62],102:[1,227],103:[1,63],106:85,107:[1,65],108:66,115:[2,136],123:[2,136],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,142],6:[2,142],25:[2,142],26:[2,142],46:[2,142],51:[2,142],54:[2,142],69:[2,142],75:[2,142],83:[2,142],88:[2,142],90:[2,142],99:[2,142],101:[2,142],102:[2,142],103:[2,142],107:[2,142],115:[2,142],123:[2,142],125:[2,142],126:[2,142],129:[2,142],130:[2,142],131:[2,142],132:[2,142],133:[2,142],134:[2,142]},{1:[2,143],6:[2,143],25:[2,143],26:[2,143],46:[2,143],51:[2,143],54:[2,143],69:[2,143],75:[2,143],83:[2,143],88:[2,143],90:[2,143],99:[2,143],100:84,101:[1,62],102:[2,143],103:[1,63],106:85,107:[1,65],108:66,115:[2,143],123:[2,143],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,147],6:[2,147],25:[2,147],26:[2,147],46:[2,147],51:[2,147],54:[2,147],69:[2,147],75:[2,147],83:[2,147],88:[2,147],90:[2,147],99:[2,147],101:[2,147],102:[2,147],103:[2,147],107:[2,147],115:[2,147],123:[2,147],125:[2,147],126:[2,147],129:[2,147],130:[2,147],131:[2,147],132:[2,147],133:[2,147],134:[2,147]},{113:[2,149],114:[2,149]},{27:156,28:[1,70],55:157,56:158,73:[1,67],87:[1,112],110:228,112:155},{51:[1,229],113:[2,154],114:[2,154]},{51:[2,151],113:[2,151],114:[2,151]},{51:[2,152],113:[2,152],114:[2,152]},{51:[2,153],113:[2,153],114:[2,153]},{1:[2,148],6:[2,148],25:[2,148],26:[2,148],46:[2,148],51:[2,148],54:[2,148],69:[2,148],75:[2,148],83:[2,148],88:[2,148],90:[2,148],99:[2,148],101:[2,148],102:[2,148],103:[2,148],107:[2,148],115:[2,148],123:[2,148],125:[2,148],126:[2,148],129:[2,148],130:[2,148],131:[2,148],132:[2,148],133:[2,148],134:[2,148]},{8:230,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:231,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{6:[2,49],25:[2,49],50:232,51:[1,233],75:[2,49]},{6:[2,86],25:[2,86],26:[2,86],51:[2,86],75:[2,86]},{6:[2,35],25:[2,35],26:[2,35],40:[1,234],51:[2,35],75:[2,35]},{6:[2,38],25:[2,38],26:[2,38],51:[2,38],75:[2,38]},{6:[2,39],25:[2,39],26:[2,39],40:[2,39],51:[2,39],75:[2,39]},{6:[2,40],25:[2,40],26:[2,40],40:[2,40],51:[2,40],75:[2,40]},{6:[2,41],25:[2,41],26:[2,41],40:[2,41],51:[2,41],75:[2,41]},{1:[2,5],6:[2,5],26:[2,5],99:[2,5]},{1:[2,25],6:[2,25],25:[2,25],26:[2,25],46:[2,25],51:[2,25],54:[2,25],69:[2,25],75:[2,25],83:[2,25],88:[2,25],90:[2,25],95:[2,25],96:[2,25],99:[2,25],101:[2,25],102:[2,25],103:[2,25],107:[2,25],115:[2,25],118:[2,25],120:[2,25],123:[2,25],125:[2,25],126:[2,25],129:[2,25],130:[2,25],131:[2,25],132:[2,25],133:[2,25],134:[2,25]},{1:[2,185],6:[2,185],25:[2,185],26:[2,185],46:[2,185],51:[2,185],54:[2,185],69:[2,185],75:[2,185],83:[2,185],88:[2,185],90:[2,185],99:[2,185],100:84,101:[2,185],102:[2,185],103:[2,185],106:85,107:[2,185],108:66,115:[2,185],123:[2,185],125:[2,185],126:[2,185],129:[1,75],130:[1,78],131:[2,185],132:[2,185],133:[2,185],134:[2,185]},{1:[2,186],6:[2,186],25:[2,186],26:[2,186],46:[2,186],51:[2,186],54:[2,186],69:[2,186],75:[2,186],83:[2,186],88:[2,186],90:[2,186],99:[2,186],100:84,101:[2,186],102:[2,186],103:[2,186],106:85,107:[2,186],108:66,115:[2,186],123:[2,186],125:[2,186],126:[2,186],129:[1,75],130:[1,78],131:[2,186],132:[2,186],133:[2,186],134:[2,186]},{1:[2,187],6:[2,187],25:[2,187],26:[2,187],46:[2,187],51:[2,187],54:[2,187],69:[2,187],75:[2,187],83:[2,187],88:[2,187],90:[2,187],99:[2,187],100:84,101:[2,187],102:[2,187],103:[2,187],106:85,107:[2,187],108:66,115:[2,187],123:[2,187],125:[2,187],126:[2,187],129:[1,75],130:[2,187],131:[2,187],132:[2,187],133:[2,187],134:[2,187]},{1:[2,188],6:[2,188],25:[2,188],26:[2,188],46:[2,188],51:[2,188],54:[2,188],69:[2,188],75:[2,188],83:[2,188],88:[2,188],90:[2,188],99:[2,188],100:84,101:[2,188],102:[2,188],103:[2,188],106:85,107:[2,188],108:66,115:[2,188],123:[2,188],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[2,188],132:[2,188],133:[2,188],134:[2,188]},{1:[2,189],6:[2,189],25:[2,189],26:[2,189],46:[2,189],51:[2,189],54:[2,189],69:[2,189],75:[2,189],83:[2,189],88:[2,189],90:[2,189],99:[2,189],100:84,101:[2,189],102:[2,189],103:[2,189],106:85,107:[2,189],108:66,115:[2,189],123:[2,189],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[2,189],133:[2,189],134:[1,82]},{1:[2,190],6:[2,190],25:[2,190],26:[2,190],46:[2,190],51:[2,190],54:[2,190],69:[2,190],75:[2,190],83:[2,190],88:[2,190],90:[2,190],99:[2,190],100:84,101:[2,190],102:[2,190],103:[2,190],106:85,107:[2,190],108:66,115:[2,190],123:[2,190],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[2,190],134:[1,82]},{1:[2,191],6:[2,191],25:[2,191],26:[2,191],46:[2,191],51:[2,191],54:[2,191],69:[2,191],75:[2,191],83:[2,191],88:[2,191],90:[2,191],99:[2,191],100:84,101:[2,191],102:[2,191],103:[2,191],106:85,107:[2,191],108:66,115:[2,191],123:[2,191],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[2,191],133:[2,191],134:[2,191]},{1:[2,176],6:[2,176],25:[2,176],26:[2,176],46:[2,176],51:[2,176],54:[2,176],69:[2,176],75:[2,176],83:[2,176],88:[2,176],90:[2,176],99:[2,176],100:84,101:[1,62],102:[2,176],103:[1,63],106:85,107:[1,65],108:66,115:[2,176],123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,175],6:[2,175],25:[2,175],26:[2,175],46:[2,175],51:[2,175],54:[2,175],69:[2,175],75:[2,175],83:[2,175],88:[2,175],90:[2,175],99:[2,175],100:84,101:[1,62],102:[2,175],103:[1,63],106:85,107:[1,65],108:66,115:[2,175],123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,98],6:[2,98],25:[2,98],26:[2,98],46:[2,98],51:[2,98],54:[2,98],63:[2,98],64:[2,98],65:[2,98],67:[2,98],69:[2,98],70:[2,98],71:[2,98],75:[2,98],81:[2,98],82:[2,98],83:[2,98],88:[2,98],90:[2,98],99:[2,98],101:[2,98],102:[2,98],103:[2,98],107:[2,98],115:[2,98],123:[2,98],125:[2,98],126:[2,98],129:[2,98],130:[2,98],131:[2,98],132:[2,98],133:[2,98],134:[2,98]},{1:[2,74],6:[2,74],25:[2,74],26:[2,74],37:[2,74],46:[2,74],51:[2,74],54:[2,74],63:[2,74],64:[2,74],65:[2,74],67:[2,74],69:[2,74],70:[2,74],71:[2,74],75:[2,74],77:[2,74],81:[2,74],82:[2,74],83:[2,74],88:[2,74],90:[2,74],99:[2,74],101:[2,74],102:[2,74],103:[2,74],107:[2,74],115:[2,74],123:[2,74],125:[2,74],126:[2,74],127:[2,74],128:[2,74],129:[2,74],130:[2,74],131:[2,74],132:[2,74],133:[2,74],134:[2,74],135:[2,74]},{1:[2,75],6:[2,75],25:[2,75],26:[2,75],37:[2,75],46:[2,75],51:[2,75],54:[2,75],63:[2,75],64:[2,75],65:[2,75],67:[2,75],69:[2,75],70:[2,75],71:[2,75],75:[2,75],77:[2,75],81:[2,75],82:[2,75],83:[2,75],88:[2,75],90:[2,75],99:[2,75],101:[2,75],102:[2,75],103:[2,75],107:[2,75],115:[2,75],123:[2,75],125:[2,75],126:[2,75],127:[2,75],128:[2,75],129:[2,75],130:[2,75],131:[2,75],132:[2,75],133:[2,75],134:[2,75],135:[2,75]},{1:[2,76],6:[2,76],25:[2,76],26:[2,76],37:[2,76],46:[2,76],51:[2,76],54:[2,76],63:[2,76],64:[2,76],65:[2,76],67:[2,76],69:[2,76],70:[2,76],71:[2,76],75:[2,76],77:[2,76],81:[2,76],82:[2,76],83:[2,76],88:[2,76],90:[2,76],99:[2,76],101:[2,76],102:[2,76],103:[2,76],107:[2,76],115:[2,76],123:[2,76],125:[2,76],126:[2,76],127:[2,76],128:[2,76],129:[2,76],130:[2,76],131:[2,76],132:[2,76],133:[2,76],134:[2,76],135:[2,76]},{69:[1,235]},{54:[1,189],69:[2,82],89:236,90:[1,188],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{69:[2,83]},{8:237,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{13:[2,111],28:[2,111],30:[2,111],31:[2,111],33:[2,111],34:[2,111],35:[2,111],42:[2,111],43:[2,111],44:[2,111],48:[2,111],49:[2,111],69:[2,111],73:[2,111],76:[2,111],80:[2,111],85:[2,111],86:[2,111],87:[2,111],93:[2,111],97:[2,111],98:[2,111],101:[2,111],103:[2,111],105:[2,111],107:[2,111],116:[2,111],122:[2,111],124:[2,111],125:[2,111],126:[2,111],127:[2,111],128:[2,111]},{13:[2,112],28:[2,112],30:[2,112],31:[2,112],33:[2,112],34:[2,112],35:[2,112],42:[2,112],43:[2,112],44:[2,112],48:[2,112],49:[2,112],69:[2,112],73:[2,112],76:[2,112],80:[2,112],85:[2,112],86:[2,112],87:[2,112],93:[2,112],97:[2,112],98:[2,112],101:[2,112],103:[2,112],105:[2,112],107:[2,112],116:[2,112],122:[2,112],124:[2,112],125:[2,112],126:[2,112],127:[2,112],128:[2,112]},{1:[2,80],6:[2,80],25:[2,80],26:[2,80],37:[2,80],46:[2,80],51:[2,80],54:[2,80],63:[2,80],64:[2,80],65:[2,80],67:[2,80],69:[2,80],70:[2,80],71:[2,80],75:[2,80],77:[2,80],81:[2,80],82:[2,80],83:[2,80],88:[2,80],90:[2,80],99:[2,80],101:[2,80],102:[2,80],103:[2,80],107:[2,80],115:[2,80],123:[2,80],125:[2,80],126:[2,80],127:[2,80],128:[2,80],129:[2,80],130:[2,80],131:[2,80],132:[2,80],133:[2,80],134:[2,80],135:[2,80]},{1:[2,81],6:[2,81],25:[2,81],26:[2,81],37:[2,81],46:[2,81],51:[2,81],54:[2,81],63:[2,81],64:[2,81],65:[2,81],67:[2,81],69:[2,81],70:[2,81],71:[2,81],75:[2,81],77:[2,81],81:[2,81],82:[2,81],83:[2,81],88:[2,81],90:[2,81],99:[2,81],101:[2,81],102:[2,81],103:[2,81],107:[2,81],115:[2,81],123:[2,81],125:[2,81],126:[2,81],127:[2,81],128:[2,81],129:[2,81],130:[2,81],131:[2,81],132:[2,81],133:[2,81],134:[2,81],135:[2,81]},{1:[2,99],6:[2,99],25:[2,99],26:[2,99],46:[2,99],51:[2,99],54:[2,99],63:[2,99],64:[2,99],65:[2,99],67:[2,99],69:[2,99],70:[2,99],71:[2,99],75:[2,99],81:[2,99],82:[2,99],83:[2,99],88:[2,99],90:[2,99],99:[2,99],101:[2,99],102:[2,99],103:[2,99],107:[2,99],115:[2,99],123:[2,99],125:[2,99],126:[2,99],129:[2,99],130:[2,99],131:[2,99],132:[2,99],133:[2,99],134:[2,99]},{1:[2,33],6:[2,33],25:[2,33],26:[2,33],46:[2,33],51:[2,33],54:[2,33],69:[2,33],75:[2,33],83:[2,33],88:[2,33],90:[2,33],99:[2,33],100:84,101:[2,33],102:[2,33],103:[2,33],106:85,107:[2,33],108:66,115:[2,33],123:[2,33],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{8:238,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,104],6:[2,104],25:[2,104],26:[2,104],46:[2,104],51:[2,104],54:[2,104],63:[2,104],64:[2,104],65:[2,104],67:[2,104],69:[2,104],70:[2,104],71:[2,104],75:[2,104],81:[2,104],82:[2,104],83:[2,104],88:[2,104],90:[2,104],99:[2,104],101:[2,104],102:[2,104],103:[2,104],107:[2,104],115:[2,104],123:[2,104],125:[2,104],126:[2,104],129:[2,104],130:[2,104],131:[2,104],132:[2,104],133:[2,104],134:[2,104]},{6:[2,49],25:[2,49],50:239,51:[1,223],83:[2,49]},{6:[2,122],25:[2,122],26:[2,122],51:[2,122],54:[1,240],83:[2,122],88:[2,122],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{47:241,48:[1,57],49:[1,58]},{27:107,28:[1,70],41:108,52:242,53:106,55:109,56:110,73:[1,67],86:[1,111],87:[1,112]},{46:[2,55],51:[2,55]},{8:243,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,192],6:[2,192],25:[2,192],26:[2,192],46:[2,192],51:[2,192],54:[2,192],69:[2,192],75:[2,192],83:[2,192],88:[2,192],90:[2,192],99:[2,192],100:84,101:[2,192],102:[2,192],103:[2,192],106:85,107:[2,192],108:66,115:[2,192],123:[2,192],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{8:244,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,194],6:[2,194],25:[2,194],26:[2,194],46:[2,194],51:[2,194],54:[2,194],69:[2,194],75:[2,194],83:[2,194],88:[2,194],90:[2,194],99:[2,194],100:84,101:[2,194],102:[2,194],103:[2,194],106:85,107:[2,194],108:66,115:[2,194],123:[2,194],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,174],6:[2,174],25:[2,174],26:[2,174],46:[2,174],51:[2,174],54:[2,174],69:[2,174],75:[2,174],83:[2,174],88:[2,174],90:[2,174],99:[2,174],101:[2,174],102:[2,174],103:[2,174],107:[2,174],115:[2,174],123:[2,174],125:[2,174],126:[2,174],129:[2,174],130:[2,174],131:[2,174],132:[2,174],133:[2,174],134:[2,174]},{8:245,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,127],6:[2,127],25:[2,127],26:[2,127],46:[2,127],51:[2,127],54:[2,127],69:[2,127],75:[2,127],83:[2,127],88:[2,127],90:[2,127],95:[1,246],99:[2,127],101:[2,127],102:[2,127],103:[2,127],107:[2,127],115:[2,127],123:[2,127],125:[2,127],126:[2,127],129:[2,127],130:[2,127],131:[2,127],132:[2,127],133:[2,127],134:[2,127]},{5:247,25:[1,5]},{27:248,28:[1,70]},{117:249,119:212,120:[1,213]},{26:[1,250],118:[1,251],119:252,120:[1,213]},{26:[2,167],118:[2,167],120:[2,167]},{8:254,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],92:253,93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,92],5:255,6:[2,92],25:[1,5],26:[2,92],46:[2,92],51:[2,92],54:[2,92],59:90,63:[1,92],64:[1,93],65:[1,94],66:95,67:[1,96],69:[2,92],70:[1,97],71:[1,98],75:[2,92],78:89,81:[1,91],82:[2,102],83:[2,92],88:[2,92],90:[2,92],99:[2,92],101:[2,92],102:[2,92],103:[2,92],107:[2,92],115:[2,92],123:[2,92],125:[2,92],126:[2,92],129:[2,92],130:[2,92],131:[2,92],132:[2,92],133:[2,92],134:[2,92]},{1:[2,66],6:[2,66],25:[2,66],26:[2,66],46:[2,66],51:[2,66],54:[2,66],63:[2,66],64:[2,66],65:[2,66],67:[2,66],69:[2,66],70:[2,66],71:[2,66],75:[2,66],81:[2,66],82:[2,66],83:[2,66],88:[2,66],90:[2,66],99:[2,66],101:[2,66],102:[2,66],103:[2,66],107:[2,66],115:[2,66],123:[2,66],125:[2,66],126:[2,66],129:[2,66],130:[2,66],131:[2,66],132:[2,66],133:[2,66],134:[2,66]},{1:[2,95],6:[2,95],25:[2,95],26:[2,95],46:[2,95],51:[2,95],54:[2,95],69:[2,95],75:[2,95],83:[2,95],88:[2,95],90:[2,95],99:[2,95],101:[2,95],102:[2,95],103:[2,95],107:[2,95],115:[2,95],123:[2,95],125:[2,95],126:[2,95],129:[2,95],130:[2,95],131:[2,95],132:[2,95],133:[2,95],134:[2,95]},{14:256,15:120,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:121,41:60,55:47,56:48,58:215,60:25,61:26,62:27,73:[1,67],80:[1,28],85:[1,55],86:[1,56],87:[1,54],98:[1,53]},{1:[2,132],6:[2,132],25:[2,132],26:[2,132],46:[2,132],51:[2,132],54:[2,132],63:[2,132],64:[2,132],65:[2,132],67:[2,132],69:[2,132],70:[2,132],71:[2,132],75:[2,132],81:[2,132],82:[2,132],83:[2,132],88:[2,132],90:[2,132],99:[2,132],101:[2,132],102:[2,132],103:[2,132],107:[2,132],115:[2,132],123:[2,132],125:[2,132],126:[2,132],129:[2,132],130:[2,132],131:[2,132],132:[2,132],133:[2,132],134:[2,132]},{6:[1,71],26:[1,257]},{8:258,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{6:[2,61],13:[2,112],25:[2,61],28:[2,112],30:[2,112],31:[2,112],33:[2,112],34:[2,112],35:[2,112],42:[2,112],43:[2,112],44:[2,112],48:[2,112],49:[2,112],51:[2,61],73:[2,112],76:[2,112],80:[2,112],85:[2,112],86:[2,112],87:[2,112],88:[2,61],93:[2,112],97:[2,112],98:[2,112],101:[2,112],103:[2,112],105:[2,112],107:[2,112],116:[2,112],122:[2,112],124:[2,112],125:[2,112],126:[2,112],127:[2,112],128:[2,112]},{6:[1,260],25:[1,261],88:[1,259]},{6:[2,50],8:197,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,25:[2,50],26:[2,50],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,57:145,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],83:[2,50],85:[1,55],86:[1,56],87:[1,54],88:[2,50],91:262,93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{6:[2,49],25:[2,49],26:[2,49],50:263,51:[1,223]},{1:[2,171],6:[2,171],25:[2,171],26:[2,171],46:[2,171],51:[2,171],54:[2,171],69:[2,171],75:[2,171],83:[2,171],88:[2,171],90:[2,171],99:[2,171],101:[2,171],102:[2,171],103:[2,171],107:[2,171],115:[2,171],118:[2,171],123:[2,171],125:[2,171],126:[2,171],129:[2,171],130:[2,171],131:[2,171],132:[2,171],133:[2,171],134:[2,171]},{8:264,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:265,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{113:[2,150],114:[2,150]},{27:156,28:[1,70],55:157,56:158,73:[1,67],87:[1,112],112:266},{1:[2,156],6:[2,156],25:[2,156],26:[2,156],46:[2,156],51:[2,156],54:[2,156],69:[2,156],75:[2,156],83:[2,156],88:[2,156],90:[2,156],99:[2,156],100:84,101:[2,156],102:[1,267],103:[2,156],106:85,107:[2,156],108:66,115:[1,268],123:[2,156],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,157],6:[2,157],25:[2,157],26:[2,157],46:[2,157],51:[2,157],54:[2,157],69:[2,157],75:[2,157],83:[2,157],88:[2,157],90:[2,157],99:[2,157],100:84,101:[2,157],102:[1,269],103:[2,157],106:85,107:[2,157],108:66,115:[2,157],123:[2,157],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{6:[1,271],25:[1,272],75:[1,270]},{6:[2,50],12:165,25:[2,50],26:[2,50],27:166,28:[1,70],29:167,30:[1,68],31:[1,69],38:273,39:164,41:168,43:[1,46],75:[2,50],86:[1,111]},{8:274,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,25:[1,275],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,79],6:[2,79],25:[2,79],26:[2,79],37:[2,79],46:[2,79],51:[2,79],54:[2,79],63:[2,79],64:[2,79],65:[2,79],67:[2,79],69:[2,79],70:[2,79],71:[2,79],75:[2,79],77:[2,79],81:[2,79],82:[2,79],83:[2,79],88:[2,79],90:[2,79],99:[2,79],101:[2,79],102:[2,79],103:[2,79],107:[2,79],115:[2,79],123:[2,79],125:[2,79],126:[2,79],127:[2,79],128:[2,79],129:[2,79],130:[2,79],131:[2,79],132:[2,79],133:[2,79],134:[2,79],135:[2,79]},{8:276,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,69:[2,115],73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{69:[2,116],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{26:[1,277],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{6:[1,260],25:[1,261],83:[1,278]},{6:[2,61],25:[2,61],26:[2,61],51:[2,61],83:[2,61],88:[2,61]},{5:279,25:[1,5]},{46:[2,53],51:[2,53]},{46:[2,56],51:[2,56],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{26:[1,280],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{5:281,25:[1,5],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{5:282,25:[1,5]},{1:[2,128],6:[2,128],25:[2,128],26:[2,128],46:[2,128],51:[2,128],54:[2,128],69:[2,128],75:[2,128],83:[2,128],88:[2,128],90:[2,128],99:[2,128],101:[2,128],102:[2,128],103:[2,128],107:[2,128],115:[2,128],123:[2,128],125:[2,128],126:[2,128],129:[2,128],130:[2,128],131:[2,128],132:[2,128],133:[2,128],134:[2,128]},{5:283,25:[1,5]},{26:[1,284],118:[1,285],119:252,120:[1,213]},{1:[2,165],6:[2,165],25:[2,165],26:[2,165],46:[2,165],51:[2,165],54:[2,165],69:[2,165],75:[2,165],83:[2,165],88:[2,165],90:[2,165],99:[2,165],101:[2,165],102:[2,165],103:[2,165],107:[2,165],115:[2,165],123:[2,165],125:[2,165],126:[2,165],129:[2,165],130:[2,165],131:[2,165],132:[2,165],133:[2,165],134:[2,165]},{5:286,25:[1,5]},{26:[2,168],118:[2,168],120:[2,168]},{5:287,25:[1,5],51:[1,288]},{25:[2,124],51:[2,124],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,93],6:[2,93],25:[2,93],26:[2,93],46:[2,93],51:[2,93],54:[2,93],69:[2,93],75:[2,93],83:[2,93],88:[2,93],90:[2,93],99:[2,93],101:[2,93],102:[2,93],103:[2,93],107:[2,93],115:[2,93],123:[2,93],125:[2,93],126:[2,93],129:[2,93],130:[2,93],131:[2,93],132:[2,93],133:[2,93],134:[2,93]},{1:[2,96],5:289,6:[2,96],25:[1,5],26:[2,96],46:[2,96],51:[2,96],54:[2,96],59:90,63:[1,92],64:[1,93],65:[1,94],66:95,67:[1,96],69:[2,96],70:[1,97],71:[1,98],75:[2,96],78:89,81:[1,91],82:[2,102],83:[2,96],88:[2,96],90:[2,96],99:[2,96],101:[2,96],102:[2,96],103:[2,96],107:[2,96],115:[2,96],123:[2,96],125:[2,96],126:[2,96],129:[2,96],130:[2,96],131:[2,96],132:[2,96],133:[2,96],134:[2,96]},{99:[1,290]},{88:[1,291],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,110],6:[2,110],25:[2,110],26:[2,110],37:[2,110],46:[2,110],51:[2,110],54:[2,110],63:[2,110],64:[2,110],65:[2,110],67:[2,110],69:[2,110],70:[2,110],71:[2,110],75:[2,110],81:[2,110],82:[2,110],83:[2,110],88:[2,110],90:[2,110],99:[2,110],101:[2,110],102:[2,110],103:[2,110],107:[2,110],113:[2,110],114:[2,110],115:[2,110],123:[2,110],125:[2,110],126:[2,110],129:[2,110],130:[2,110],131:[2,110],132:[2,110],133:[2,110],134:[2,110]},{8:197,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,57:145,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],91:292,93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:197,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,25:[1,144],27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,57:145,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],84:293,85:[1,55],86:[1,56],87:[1,54],91:143,93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{6:[2,118],25:[2,118],26:[2,118],51:[2,118],83:[2,118],88:[2,118]},{6:[1,260],25:[1,261],26:[1,294]},{1:[2,135],6:[2,135],25:[2,135],26:[2,135],46:[2,135],51:[2,135],54:[2,135],69:[2,135],75:[2,135],83:[2,135],88:[2,135],90:[2,135],99:[2,135],100:84,101:[1,62],102:[2,135],103:[1,63],106:85,107:[1,65],108:66,115:[2,135],123:[2,135],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,137],6:[2,137],25:[2,137],26:[2,137],46:[2,137],51:[2,137],54:[2,137],69:[2,137],75:[2,137],83:[2,137],88:[2,137],90:[2,137],99:[2,137],100:84,101:[1,62],102:[2,137],103:[1,63],106:85,107:[1,65],108:66,115:[2,137],123:[2,137],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{113:[2,155],114:[2,155]},{8:295,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:296,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:297,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,84],6:[2,84],25:[2,84],26:[2,84],37:[2,84],46:[2,84],51:[2,84],54:[2,84],63:[2,84],64:[2,84],65:[2,84],67:[2,84],69:[2,84],70:[2,84],71:[2,84],75:[2,84],81:[2,84],82:[2,84],83:[2,84],88:[2,84],90:[2,84],99:[2,84],101:[2,84],102:[2,84],103:[2,84],107:[2,84],113:[2,84],114:[2,84],115:[2,84],123:[2,84],125:[2,84],126:[2,84],129:[2,84],130:[2,84],131:[2,84],132:[2,84],133:[2,84],134:[2,84]},{12:165,27:166,28:[1,70],29:167,30:[1,68],31:[1,69],38:298,39:164,41:168,43:[1,46],86:[1,111]},{6:[2,85],12:165,25:[2,85],26:[2,85],27:166,28:[1,70],29:167,30:[1,68],31:[1,69],38:163,39:164,41:168,43:[1,46],51:[2,85],74:299,86:[1,111]},{6:[2,87],25:[2,87],26:[2,87],51:[2,87],75:[2,87]},{6:[2,36],25:[2,36],26:[2,36],51:[2,36],75:[2,36],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{8:300,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{69:[2,114],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,34],6:[2,34],25:[2,34],26:[2,34],46:[2,34],51:[2,34],54:[2,34],69:[2,34],75:[2,34],83:[2,34],88:[2,34],90:[2,34],99:[2,34],101:[2,34],102:[2,34],103:[2,34],107:[2,34],115:[2,34],123:[2,34],125:[2,34],126:[2,34],129:[2,34],130:[2,34],131:[2,34],132:[2,34],133:[2,34],134:[2,34]},{1:[2,105],6:[2,105],25:[2,105],26:[2,105],46:[2,105],51:[2,105],54:[2,105],63:[2,105],64:[2,105],65:[2,105],67:[2,105],69:[2,105],70:[2,105],71:[2,105],75:[2,105],81:[2,105],82:[2,105],83:[2,105],88:[2,105],90:[2,105],99:[2,105],101:[2,105],102:[2,105],103:[2,105],107:[2,105],115:[2,105],123:[2,105],125:[2,105],126:[2,105],129:[2,105],130:[2,105],131:[2,105],132:[2,105],133:[2,105],134:[2,105]},{1:[2,45],6:[2,45],25:[2,45],26:[2,45],46:[2,45],51:[2,45],54:[2,45],69:[2,45],75:[2,45],83:[2,45],88:[2,45],90:[2,45],99:[2,45],101:[2,45],102:[2,45],103:[2,45],107:[2,45],115:[2,45],123:[2,45],125:[2,45],126:[2,45],129:[2,45],130:[2,45],131:[2,45],132:[2,45],133:[2,45],134:[2,45]},{1:[2,193],6:[2,193],25:[2,193],26:[2,193],46:[2,193],51:[2,193],54:[2,193],69:[2,193],75:[2,193],83:[2,193],88:[2,193],90:[2,193],99:[2,193],101:[2,193],102:[2,193],103:[2,193],107:[2,193],115:[2,193],123:[2,193],125:[2,193],126:[2,193],129:[2,193],130:[2,193],131:[2,193],132:[2,193],133:[2,193],134:[2,193]},{1:[2,172],6:[2,172],25:[2,172],26:[2,172],46:[2,172],51:[2,172],54:[2,172],69:[2,172],75:[2,172],83:[2,172],88:[2,172],90:[2,172],99:[2,172],101:[2,172],102:[2,172],103:[2,172],107:[2,172],115:[2,172],118:[2,172],123:[2,172],125:[2,172],126:[2,172],129:[2,172],130:[2,172],131:[2,172],132:[2,172],133:[2,172],134:[2,172]},{1:[2,129],6:[2,129],25:[2,129],26:[2,129],46:[2,129],51:[2,129],54:[2,129],69:[2,129],75:[2,129],83:[2,129],88:[2,129],90:[2,129],99:[2,129],101:[2,129],102:[2,129],103:[2,129],107:[2,129],115:[2,129],123:[2,129],125:[2,129],126:[2,129],129:[2,129],130:[2,129],131:[2,129],132:[2,129],133:[2,129],134:[2,129]},{1:[2,130],6:[2,130],25:[2,130],26:[2,130],46:[2,130],51:[2,130],54:[2,130],69:[2,130],75:[2,130],83:[2,130],88:[2,130],90:[2,130],95:[2,130],99:[2,130],101:[2,130],102:[2,130],103:[2,130],107:[2,130],115:[2,130],123:[2,130],125:[2,130],126:[2,130],129:[2,130],130:[2,130],131:[2,130],132:[2,130],133:[2,130],134:[2,130]},{1:[2,163],6:[2,163],25:[2,163],26:[2,163],46:[2,163],51:[2,163],54:[2,163],69:[2,163],75:[2,163],83:[2,163],88:[2,163],90:[2,163],99:[2,163],101:[2,163],102:[2,163],103:[2,163],107:[2,163],115:[2,163],123:[2,163],125:[2,163],126:[2,163],129:[2,163],130:[2,163],131:[2,163],132:[2,163],133:[2,163],134:[2,163]},{5:301,25:[1,5]},{26:[1,302]},{6:[1,303],26:[2,169],118:[2,169],120:[2,169]},{8:304,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{1:[2,97],6:[2,97],25:[2,97],26:[2,97],46:[2,97],51:[2,97],54:[2,97],69:[2,97],75:[2,97],83:[2,97],88:[2,97],90:[2,97],99:[2,97],101:[2,97],102:[2,97],103:[2,97],107:[2,97],115:[2,97],123:[2,97],125:[2,97],126:[2,97],129:[2,97],130:[2,97],131:[2,97],132:[2,97],133:[2,97],134:[2,97]},{1:[2,133],6:[2,133],25:[2,133],26:[2,133],46:[2,133],51:[2,133],54:[2,133],63:[2,133],64:[2,133],65:[2,133],67:[2,133],69:[2,133],70:[2,133],71:[2,133],75:[2,133],81:[2,133],82:[2,133],83:[2,133],88:[2,133],90:[2,133],99:[2,133],101:[2,133],102:[2,133],103:[2,133],107:[2,133],115:[2,133],123:[2,133],125:[2,133],126:[2,133],129:[2,133],130:[2,133],131:[2,133],132:[2,133],133:[2,133],134:[2,133]},{1:[2,113],6:[2,113],25:[2,113],26:[2,113],46:[2,113],51:[2,113],54:[2,113],63:[2,113],64:[2,113],65:[2,113],67:[2,113],69:[2,113],70:[2,113],71:[2,113],75:[2,113],81:[2,113],82:[2,113],83:[2,113],88:[2,113],90:[2,113],99:[2,113],101:[2,113],102:[2,113],103:[2,113],107:[2,113],115:[2,113],123:[2,113],125:[2,113],126:[2,113],129:[2,113],130:[2,113],131:[2,113],132:[2,113],133:[2,113],134:[2,113]},{6:[2,119],25:[2,119],26:[2,119],51:[2,119],83:[2,119],88:[2,119]},{6:[2,49],25:[2,49],26:[2,49],50:305,51:[1,223]},{6:[2,120],25:[2,120],26:[2,120],51:[2,120],83:[2,120],88:[2,120]},{1:[2,158],6:[2,158],25:[2,158],26:[2,158],46:[2,158],51:[2,158],54:[2,158],69:[2,158],75:[2,158],83:[2,158],88:[2,158],90:[2,158],99:[2,158],100:84,101:[2,158],102:[2,158],103:[2,158],106:85,107:[2,158],108:66,115:[1,306],123:[2,158],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,160],6:[2,160],25:[2,160],26:[2,160],46:[2,160],51:[2,160],54:[2,160],69:[2,160],75:[2,160],83:[2,160],88:[2,160],90:[2,160],99:[2,160],100:84,101:[2,160],102:[1,307],103:[2,160],106:85,107:[2,160],108:66,115:[2,160],123:[2,160],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,159],6:[2,159],25:[2,159],26:[2,159],46:[2,159],51:[2,159],54:[2,159],69:[2,159],75:[2,159],83:[2,159],88:[2,159],90:[2,159],99:[2,159],100:84,101:[2,159],102:[2,159],103:[2,159],106:85,107:[2,159],108:66,115:[2,159],123:[2,159],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{6:[2,88],25:[2,88],26:[2,88],51:[2,88],75:[2,88]},{6:[2,49],25:[2,49],26:[2,49],50:308,51:[1,233]},{26:[1,309],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{26:[1,310]},{1:[2,166],6:[2,166],25:[2,166],26:[2,166],46:[2,166],51:[2,166],54:[2,166],69:[2,166],75:[2,166],83:[2,166],88:[2,166],90:[2,166],99:[2,166],101:[2,166],102:[2,166],103:[2,166],107:[2,166],115:[2,166],123:[2,166],125:[2,166],126:[2,166],129:[2,166],130:[2,166],131:[2,166],132:[2,166],133:[2,166],134:[2,166]},{26:[2,170],118:[2,170],120:[2,170]},{25:[2,125],51:[2,125],100:84,101:[1,62],103:[1,63],106:85,107:[1,65],108:66,123:[1,83],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{6:[1,260],25:[1,261],26:[1,311]},{8:312,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{8:313,9:115,10:19,11:20,12:21,13:[1,22],14:8,15:9,16:10,17:11,18:12,19:13,20:14,21:15,22:16,23:17,24:18,27:59,28:[1,70],29:49,30:[1,68],31:[1,69],32:24,33:[1,50],34:[1,51],35:[1,52],36:23,41:60,42:[1,44],43:[1,46],44:[1,29],47:30,48:[1,57],49:[1,58],55:47,56:48,58:36,60:25,61:26,62:27,73:[1,67],76:[1,43],80:[1,28],85:[1,55],86:[1,56],87:[1,54],93:[1,38],97:[1,45],98:[1,53],100:39,101:[1,62],103:[1,63],104:40,105:[1,64],106:41,107:[1,65],108:66,116:[1,42],121:37,122:[1,61],124:[1,31],125:[1,32],126:[1,33],127:[1,34],128:[1,35]},{6:[1,271],25:[1,272],26:[1,314]},{6:[2,37],25:[2,37],26:[2,37],51:[2,37],75:[2,37]},{1:[2,164],6:[2,164],25:[2,164],26:[2,164],46:[2,164],51:[2,164],54:[2,164],69:[2,164],75:[2,164],83:[2,164],88:[2,164],90:[2,164],99:[2,164],101:[2,164],102:[2,164],103:[2,164],107:[2,164],115:[2,164],123:[2,164],125:[2,164],126:[2,164],129:[2,164],130:[2,164],131:[2,164],132:[2,164],133:[2,164],134:[2,164]},{6:[2,121],25:[2,121],26:[2,121],51:[2,121],83:[2,121],88:[2,121]},{1:[2,161],6:[2,161],25:[2,161],26:[2,161],46:[2,161],51:[2,161],54:[2,161],69:[2,161],75:[2,161],83:[2,161],88:[2,161],90:[2,161],99:[2,161],100:84,101:[2,161],102:[2,161],103:[2,161],106:85,107:[2,161],108:66,115:[2,161],123:[2,161],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{1:[2,162],6:[2,162],25:[2,162],26:[2,162],46:[2,162],51:[2,162],54:[2,162],69:[2,162],75:[2,162],83:[2,162],88:[2,162],90:[2,162],99:[2,162],100:84,101:[2,162],102:[2,162],103:[2,162],106:85,107:[2,162],108:66,115:[2,162],123:[2,162],125:[1,77],126:[1,76],129:[1,75],130:[1,78],131:[1,79],132:[1,80],133:[1,81],134:[1,82]},{6:[2,89],25:[2,89],26:[2,89],51:[2,89],75:[2,89]}],defaultActions:{57:[2,47],58:[2,48],72:[2,3],91:[2,103],186:[2,83]},parseError:function(b,c){throw new Error(b)},parse:function(b){function p(){var a;a=c.lexer.lex()||1,typeof a!="number"&&(a=c.symbols_[a]||a);return a}function o(a){d.length=d.length-2*a,e.length=e.length-a,f.length=f.length-a}var c=this,d=[0],e=[null],f=[],g=this.table,h="",i=0,j=0,k=0,l=2,m=1;this.lexer.setInput(b),this.lexer.yy=this.yy,this.yy.lexer=this.lexer,typeof this.lexer.yylloc=="undefined"&&(this.lexer.yylloc={});var n=this.lexer.yylloc;f.push(n),typeof this.yy.parseError=="function"&&(this.parseError=this.yy.parseError);var q,r,s,t,u,v,w={},x,y,z,A;for(;;){s=d[d.length-1],this.defaultActions[s]?t=this.defaultActions[s]:(q==null&&(q=p()),t=g[s]&&g[s][q]);if(typeof t=="undefined"||!t.length||!t[0]){if(!k){A=[];for(x in g[s])this.terminals_[x]&&x>2&&A.push("'"+this.terminals_[x]+"'");var B="";this.lexer.showPosition?B="Parse error on line "+(i+1)+":\n"+this.lexer.showPosition()+"\nExpecting "+A.join(", "):B="Parse error on line "+(i+1)+": Unexpected "+(q==1?"end of input":"'"+(this.terminals_[q]||q)+"'"),this.parseError(B,{text:this.lexer.match,token:this.terminals_[q]||q,line:this.lexer.yylineno,loc:n,expected:A})}if(k==3){if(q==m)throw new Error(B||"Parsing halted.");j=this.lexer.yyleng,h=this.lexer.yytext,i=this.lexer.yylineno,n=this.lexer.yylloc,q=p()}for(;;){if(l.toString()in g[s])break;if(s==0)throw new Error(B||"Parsing halted.");o(1),s=d[d.length-1]}r=q,q=l,s=d[d.length-1],t=g[s]&&g[s][l],k=3}if(t[0]instanceof Array&&t.length>1)throw new Error("Parse Error: multiple actions possible at state: "+s+", token: "+q);switch(t[0]){case 1:d.push(q),e.push(this.lexer.yytext),f.push(this.lexer.yylloc),d.push(t[1]),q=null,r?(q=r,r=null):(j=this.lexer.yyleng,h=this.lexer.yytext,i=this.lexer.yylineno,n=this.lexer.yylloc,k>0&&k--);break;case 2:y=this.productions_[t[1]][1],w.$=e[e.length-y],w._$={first_line:f[f.length-(y||1)].first_line,last_line:f[f.length-1].last_line,first_column:f[f.length-(y||1)].first_column,last_column:f[f.length-1].last_column},v=this.performAction.call(w,h,j,i,this.yy,t[1],e,f);if(typeof v!="undefined")return v;y&&(d=d.slice(0,-1*y*2),e=e.slice(0,-1*y),f=f.slice(0,-1*y)),d.push(this.productions_[t[1]][0]),e.push(w.$),f.push(w._$),z=g[d[d.length-2]][d[d.length-1]],d.push(z);break;case 3:return!0}}return!0}};c.exports=d}),define("ace/mode/coffee/nodes",["require","exports","module","ace/mode/coffee/scope","ace/mode/coffee/helpers"],function(a,b,c){var d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,$,_,ba,bb,bc,bd,be,bf,bg,bh,bi,bj=Object.prototype.hasOwnProperty,bk=function(a,b){function d(){this.constructor=a}for(var c in b)bj.call(b,c)&&(a[c]=b[c]);d.prototype=b.prototype,a.prototype=new d,a.__super__=b.prototype;return a},bl=function(a,b){return function(){return a.apply(b,arguments)}},bm=Array.prototype.indexOf||function(a){for(var b=0,c=this.length;b<c;b++)if(this[b]===a)return b;return-1};N=a("ace/mode/coffee/scope").Scope,bi=a("ace/mode/coffee/helpers"),Z=bi.compact,bb=bi.flatten,ba=bi.extend,bd=bi.merge,$=bi.del,bf=bi.starts,_=bi.ends,bc=bi.last,b.extend=ba,Y=function(){return!0},E=function(){return!1},S=function(){return this},D=function(){this.negated=!this.negated;return this},b.Base=g=function(){function a(){}a.prototype.compile=function(a,b){var c;a=ba({},a),b&&(a.level=b),c=this.unfoldSoak(a)||this,c.tab=a.indent;return a.level===A||!c.isStatement(a)?c.compileNode(a):c.compileClosure(a)},a.prototype.compileClosure=function(a){if(this.jumps()||this instanceof T)throw SyntaxError("cannot use a pure statement in an expression.");a.sharedScope=!0;return k.wrap(this).compileNode(a)},a.prototype.cache=function(a,b,c){var d,e;if(!this.isComplex()){d=b?this.compile(a,b):this;return[d,d]}d=new B(c||a.scope.freeVariable("ref")),e=new f(d,this);return b?[e.compile(a,b),d.value]:[e,d]},a.prototype.compileLoopReference=function(a,b){var c,d;c=d=this.compile(a,x),-Infinity<+c&&+c<Infinity||q.test(c)&&a.scope.check(c,!0)||(c=""+(d=a.scope.freeVariable(b))+" = "+c);return[c,d]},a.prototype.makeReturn=function(){return new L(this)},a.prototype.contains=function(a){var b;b=!1,this.traverseChildren(!1,function(c){if(a(c)){b=!0;return!1}});return b},a.prototype.containsType=function(a){return this instanceof a||this.contains(function(b){return b instanceof a})},a.prototype.lastNonComment=function(a){var b;b=a.length;while(b--)if(!(a[b]instanceof m))return a[b];return null},a.prototype.toString=function(a,b){var c;a==null&&(a=""),b==null&&(b=this.constructor.name),c="\n"+a+b,this.soak&&(c+="?"),this.eachChild(function(b){return c+=b.toString(a+R)});return c},a.prototype.eachChild=function(a){var b,c,d,e,f,g,h,i;if(!this.children)return this;h=this.children;for(d=0,f=h.length;d<f;d++){b=h[d];if(this[b]){i=bb([this[b]]);for(e=0,g=i.length;e<g;e++){c=i[e];if(a(c)===!1)return this}}}return this},a.prototype.traverseChildren=function(a,b){return this.eachChild(function(c){if(b(c)===!1)return!1;return c.traverseChildren(a,b)})},a.prototype.invert=function(){return new G("!",this)},a.prototype.unwrapAll=function(){var a;a=this;while(a!==(a=a.unwrap()))continue;return a},a.prototype.children=[],a.prototype.isStatement=E,a.prototype.jumps=E,a.prototype.isComplex=Y,a.prototype.isChainable=E,a.prototype.isAssignable=E,a.prototype.unwrap=S,a.prototype.unfoldSoak=E,a.prototype.assigns=E;return a}(),b.Block=h=function(){function a(a){this.expressions=Z(bb(a||[]))}bk(a,g),a.prototype.children=["expressions"],a.prototype.push=function(a){this.expressions.push(a);return this},a.prototype.pop=function(){return this.expressions.pop()},a.prototype.unshift=function(a){this.expressions.unshift(a);return this},a.prototype.unwrap=function(){return this.expressions.length===1?this.expressions[0]:this},a.prototype.isEmpty=function(){return!this.expressions.length},a.prototype.isStatement=function(a){var b,c,d,e;e=this.expressions;for(c=0,d=e.length;c<d;c++){b=e[c];if(b.isStatement(a))return!0}return!1},a.prototype.jumps=function(a){var b,c,d,e;e=this.expressions;for(c=0,d=e.length;c<d;c++){b=e[c];if(b.jumps(a))return b}},a.prototype.makeReturn=function(){var a,b;b=this.expressions.length;while(b--){a=this.expressions[b];if(!(a instanceof m)){this.expressions[b]=a.makeReturn(),a instanceof L&&!a.expression&&this.expressions.splice(b,1);break}}return this},a.prototype.compile=function(b,c){b==null&&(b={});return b.scope?a.__super__.compile.call(this,b,c):this.compileRoot(b)},a.prototype.compileNode=function(a){var b,c,d,e,f,g,h;this.tab=a.indent,e=a.level===A,c=[],h=this.expressions;for(f=0,g=h.length;f<g;f++)d=h[f],d=d.unwrapAll(),d=d.unfoldSoak(a)||d,e?(d.front=!0,b=d.compile(a),c.push(d.isStatement(a)?b:this.tab+b+";")):c.push(d.compile(a,x));if(e)return c.join("\n");b=c.join(", ")||"void 0";return c.length>1&&a.level>=x?"("+b+")":b},a.prototype.compileRoot=function(a){var b;a.indent=this.tab=a.bare?"":R,a.scope=new N(null,this,null),a.level=A,b=this.compileWithDeclarations(a);return a.bare?b:"(function() {\n"+b+"\n}).call(this);\n"},a.prototype.compileWithDeclarations=function(a){var b,c,d,e,f,g,h,i,j,k;c=g="",k=this.expressions;for(f=0,j=k.length;f<j;f++){e=k[f],e=e.unwrap();if(!(e instanceof m||e instanceof B))break}a=bd(a,{level:A}),f&&(h=this.expressions.splice(f,this.expressions.length),c=this.compileNode(a),this.expressions=h),g=this.compileNode(a),i=a.scope,i.expressions===this&&(d=a.scope.hasDeclarations(),b=i.hasAssignments,(d||b)&&f&&(c+="\n"),d&&(c+=""+this.tab+"var "+i.declaredVariables().join(", ")+";\n"),b&&(c+=""+this.tab+"var "+be(i.assignedVariables().join(", "),this.tab)+";\n"));return c+g},a.wrap=function(b){if(b.length===1&&b[0]instanceof a)return b[0];return new a(b)};return a}(),b.Literal=B=function(){function a(a){this.value=a}bk(a,g),a.prototype.makeReturn=function(){return this.isStatement()?this:new L(this)},a.prototype.isAssignable=function(){return q.test(this.value)},a.prototype.isStatement=function(){var a;return(a=this.value)==="break"||a==="continue"||a==="debugger"},a.prototype.isComplex=E,a.prototype.assigns=function(a){return a===this.value},a.prototype.jumps=function(a){if(!this.isStatement())return!1;return!a||!(a.loop||a.block&&this.value!=="continue")?this:!1},a.prototype.compileNode=function(a){var b;b=this.isUndefined?a.level>=v?"(void 0)":"void 0":this.value.reserved?'"'+this.value+'"':this.value;return this.isStatement()?""+this.tab+b+";":b},a.prototype.toString=function(){return' "'+this.value+'"'};return a}(),b.Return=L=function(){function a(a){a&&!a.unwrap().isUndefined&&(this.expression=a)}bk(a,g),a.prototype.children=["expression"],a.prototype.isStatement=Y,a.prototype.makeReturn=S,a.prototype.jumps=S,a.prototype.compile=function(b,c){var d,e;d=(e=this.expression)!=null?e.makeReturn():void 0;return!d||d instanceof a?a.__super__.compile.call(this,b,c):d.compile(b,c)},a.prototype.compileNode=function(a){return this.tab+("return"+(this.expression?" "+this.expression.compile(a,z):"")+";")};return a}(),b.Value=W=function(){function a(b,c,d){if(!c&&b instanceof a)return b;this.base=b,this.properties=c||[],d&&(this[d]=!0);return this}bk(a,g),a.prototype.children=["base","properties"],a.prototype.push=function(a){this.properties.push(a);return this},a.prototype.hasProperties=function(){return!!this.properties.length},a.prototype.isArray=function(){return!this.properties.length&&this.base instanceof e},a.prototype.isComplex=function(){return this.hasProperties()||this.base.isComplex()},a.prototype.isAssignable=function(){return this.hasProperties()||this.base.isAssignable()},a.prototype.isSimpleNumber=function(){return this.base instanceof B&&M.test(this.base.value)},a.prototype.isAtomic=function(){var a,b,c,d;d=this.properties.concat(this.base);for(b=0,c=d.length;b<c;b++){a=d[b];if(a.soak||a instanceof i)return!1}return!0},a.prototype.isStatement=function(a){return!this.properties.length&&this.base.isStatement(a)},a.prototype.assigns=function(a){return!this.properties.length&&this.base.assigns(a)},a.prototype.jumps=function(a){return!this.properties.length&&this.base.jumps(a)},a.prototype.isObject=function(a){if(this.properties.length)return!1;return this.base instanceof F&&(!a||this.base.generated)},a.prototype.isSplice=function(){return bc(this.properties)instanceof O},a.prototype.makeReturn=function(){return this.properties.length?a.__super__.makeReturn.call(this):this.base.makeReturn()},a.prototype.unwrap=function(){return this.properties.length?this:this.base},a.prototype.cacheReference=function(b){var c,d,e,g;e=bc(this.properties);if(this.properties.length<2&&!this.base.isComplex()&&(e!=null?!e.isComplex():!void 0))return[this,this];c=new a(this.base,this.properties.slice(0,-1)),c.isComplex()&&(d=new B(b.scope.freeVariable("base")),c=new a(new I(new f(d,c))));if(!e)return[c,d];e.isComplex()&&(g=new B(b.scope.freeVariable("name")),e=new u(new f(g,e.index)),g=new u(g));return[c.push(e),new a(d||c.base,[g||e])]},a.prototype.compileNode=function(a){var b,c,e,f,g;this.base.front=this.front,e=this.properties,b=this.base.compile(a,e.length?v:null),e[0]instanceof d&&this.isSimpleNumber()&&(b="("+b+")");for(f=0,g=e.length;f<g;f++)c=e[f],b+=c.compile(a);return b},a.prototype.unfoldSoak=function(b){var c;if(this.unfoldedSoak!=null)return this.unfoldedSoak;c=bl(function(){var c,d,e,g,h,i,j,k;if(e=this.base.unfoldSoak(b)){Array.prototype.push.apply(e.body.properties,this.properties);return e}k=this.properties;for(d=0,j=k.length;d<j;d++){g=k[d];if(g.soak){g.soak=!1,c=new a(this.base,this.properties.slice(0,d)),i=new a(this.base,this.properties.slice(d)),c.isComplex()&&(h=new B(b.scope.freeVariable("ref")),c=new I(new f(h,c)),i.base=h);return new s(new n(c),i,{soak:!0})}}return null},this)();return this.unfoldedSoak=c||!1};return a}(),b.Comment=m=function(){function a(a){this.comment=a}bk(a,g),a.prototype.isStatement=Y,a.prototype.makeReturn=S,a.prototype.compileNode=function(a,b){var c;c="/*"+be(this.comment,this.tab)+"*/",(b||a.level)===A&&(c=a.indent+c);return c};return a}(),b.Call=i=function(){function a(a,b,c){this.args=b!=null?b:[],this.soak=c,this.isNew=!1,this.isSuper=a==="super",this.variable=this.isSuper?null:a}bk(a,g),a.prototype.children=["variable","args"],a.prototype.newInstance=function(){var b;b=this.variable.base||this.variable,b instanceof a?b.newInstance():this.isNew=!0;return this},a.prototype.superReference=function(a){var b,c;b=a.scope.method;if(!b)throw SyntaxError("cannot call super outside of a function.");c=b.name;if(!c)throw SyntaxError("cannot call super on an anonymous function.");return b.klass?""+b.klass+".__super__."+c:""+c+".__super__.constructor"},a.prototype.unfoldSoak=function(b){var c,d,e,f,g,h,i,j,k;if(this.soak){if(this.variable){if(d=bg(b,this,"variable"))return d;j=(new W(this.variable)).cacheReference(b),e=j[0],g=j[1]}else e=new B(this.superReference(b)),g=new W(e);g=new a(g,this.args),g.isNew=this.isNew,e=new B("typeof "+e.compile(b)+' === "function"');return new s(e,new W(g),{soak:!0})}c=this,f=[];for(;;){if(c.variable instanceof a){f.push(c),c=c.variable;continue}if(!(c.variable instanceof W))break;f.push(c);if(!((c=c.variable.base)instanceof a))break}k=f.reverse();for(h=0,i=k.length;h<i;h++)c=k[h],d&&(c.variable instanceof a?c.variable=d:c.variable.base=d),d=bg(b,c,"variable");return d},a.prototype.filterImplicitObjects=function(a){var b,c,d,e,g,h,i,j,k,l;c=[];for(h=0,j=a.length;h<j;h++){b=a[h];if(!((typeof b.isObject=="function"?b.isObject():void 0)&&b.base.generated)){c.push(b);continue}d=null,l=b.base.properties;for(i=0,k=l.length;i<k;i++)e=l[i],e instanceof f?(d||c.push(d=new F(g=[],!0)),g.push(e)):(c.push(e),d=null)}return c},a.prototype.compileNode=function(a){var b,c,d,e;(e=this.variable)!=null&&(e.front=this.front);if(d=P.compileSplattedArray(a,this.args,!0))return this.compileSplat(a,d);c=this.filterImplicitObjects(this.args),c=function(){var d,e,f;f=[];for(d=0,e=c.length;d<e;d++)b=c[d],f.push(b.compile(a,x));return f}().join(", ");return this.isSuper?this.superReference(a)+(".call(this"+(c&&", "+c)+")"):(this.isNew?"new ":"")+this.variable.compile(a,v)+("("+c+")")},a.prototype.compileSuper=function(a,b){return""+this.superReference(b)+".call(this"+(a.length?", ":"")+a+")"},a.prototype.compileSplat=function(a,b){var c,d,e,f,g;if(this.isSuper)return""+this.superReference(a)+".apply(this, "+b+")";if(this.isNew){e=this.tab+R;return"(function(func, args, ctor) {\n"+e+"ctor.prototype = func.prototype;\n"+e+"var child = new ctor, result = func.apply(child, args);\n"+e+'return typeof result === "object" ? result : child;\n'+this.tab+"})("+this.variable.compile(a,x)+", "+b+", function() {})"}c=new W(this.variable),(f=c.properties.pop())&&c.isComplex()?(g=a.scope.freeVariable("ref"),d="("+g+" = "+c.compile(a,x)+")"+f.compile(a)):(d=c.compile(a,v),M.test(d)&&(d="("+d+")"),f?(g=d,d+=f.compile(a)):g="null");return""+d+".apply("+g+", "+b+")"};return a}(),b.Extends=o=function(){function a(a,b){this.child=a,this.parent=b}bk(a,g),a.prototype.children=["child","parent"],a.prototype.compile=function(a){bh("hasProp");return(new i(new W(new B(bh("extends"))),[this.child,this.parent])).compile(a)};return a}(),b.Access=d=function(){function a(a,b){this.name=a,this.name.asKey=!0,this.proto=b==="proto"?".prototype":"",this.soak=b==="soak"}bk(a,g),a.prototype.children=["name"],a.prototype.compile=function(a){var b;b=this.name.compile(a);return this.proto+(r.test(b)?"["+b+"]":"."+b)},a.prototype.isComplex=E;return a}(),b.Index=u=function(){function a(a){this.index=a}bk(a,g),a.prototype.children=["index"],a.prototype.compile=function(a){return(this.proto?".prototype":"")+("["+this.index.compile(a,z)+"]")},a.prototype.isComplex=function(){return this.index.isComplex()};return a}(),b.Range=K=function(){function a(a,b,c){this.from=a,this.to=b,this.exclusive=c==="exclusive",this.equals=this.exclusive?"":"="}bk(a,g),a.prototype.children=["from","to"],a.prototype.compileVariables=function(a){var b,c,d,e,f;a=bd(a,{top:!0}),c=this.from.cache(a,x),this.from=c[0],this.fromVar=c[1],d=this.to.cache(a,x),this.to=d[0],this.toVar=d[1];if(b=$(a,"step"))e=b.cache(a,x),this.step=e[0],this.stepVar=e[1];f=[this.fromVar.match(M),this.toVar.match(M)],this.fromNum=f[0],this.toNum=f[1];if(this.stepVar)return this.stepNum=this.stepVar.match(M)},a.prototype.compileNode=function(a){var b,c,d,e,f,g,h,i,j,k,l,m;this.fromVar||this.compileVariables(a);if(!a.index)return this.compileArray(a);g=this.fromNum&&this.toNum,f=$(a,"index"),k=""+f+" = "+this.from,this.to!==this.toVar&&(k+=", "+this.to),this.step!==this.stepVar&&(k+=", "+this.step),l=[""+f+" <"+this.equals,""+f+" >"+this.equals],h=l[0],e=l[1],c=this.stepNum?c=+this.stepNum>0?""+h+" "+this.toVar:""+e+" "+this.toVar:g?(m=[+this.fromNum,+this.toNum],d=m[0],j=m[1],m,c=d<=j?""+h+" "+j:""+e+" "+j):(b=""+this.fromVar+" <= "+this.toVar,c=""+b+" ? "+h+" "+this.toVar+" : "+e+" "+this.toVar),i=this.stepVar?""+f+" += "+this.stepVar:g?d<=j?""+f+"++":""+f+"--":""+b+" ? "+f+"++ : "+f+"--";return""+k+"; "+c+"; "+i},a.prototype.compileArray=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n;if(this.fromNum&&this.toNum&&Math.abs(this.fromNum-this.toNum)<=20){h=function(){n=[];for(var a=l=+this.fromNum,b=+this.toNum;l<=b?a<=b:a>=b;l<=b?a++:a--)n.push(a);return n}.apply(this,arguments),this.exclusive&&h.pop();return"["+h.join(", ")+"]"}e=this.tab+R,d=a.scope.freeVariable("i"),i=a.scope.freeVariable("results"),g="\n"+e+i+" = [];",this.fromNum&&this.toNum?(a.index=d,b=this.compileNode(a)):(j=""+d+" = "+this.from+(this.to!==this.toVar?", "+this.to:""),c=""+this.fromVar+" <= "+this.toVar,b="var "+j+"; "+c+" ? "+d+" <"+this.equals+" "+this.toVar+" : "+d+" >"+this.equals+" "+this.toVar+"; "+c+" ? "+d+"++ : "+d+"--"),f="{ "+i+".push("+d+"); }\n"+e+"return "+i+";\n"+a.indent;return"(function() {"+g+"\n"+e+"for ("+b+")"+f+"}).apply(this, arguments)"};return a}(),b.Slice=O=function(){function a(b){this.range=b,a.__super__.constructor.call(this)}bk(a,g),a.prototype.children=["range"],a.prototype.compileNode=function(a){var b,c,d,e,f,g;g=this.range,e=g.to,c=g.from,d=c&&c.compile(a,z)||"0",b=e&&e.compile(a,z),e&&(!!this.range.exclusive||+b!==-1)&&(f=", "+(this.range.exclusive?b:M.test(b)?(+b+1).toString():"("+b+" + 1) || 9e9"));return".slice("+d+(f||"")+")"};return a}(),b.Obj=F=function(){function a(a,b){this.generated=b!=null?b:!1,this.objects=this.properties=a||[]}bk(a,g),a.prototype.children=["properties"],a.prototype.compileNode=function(a){var b,c,d,e,g,h,i,j,k,l,n;k=this.properties;if(!k.length)return this.front?"({})":"{}";if(this.generated)for(l=0,n=k.length;l<n;l++){h=k[l];if(h instanceof W)throw new Error("cannot have an implicit value in an implicit object")}c=a.indent+=R,g=this.lastNonComment(this.properties),k=function(){var h,i;i=[];for(b=0,h=k.length;b<h;b++)j=k[b],e=b===k.length-1?"":j===g||j instanceof m?"\n":",\n",d=j instanceof m?"":c,j instanceof W&&j["this"]&&(j=new f(j.properties[0].name,j,"object")),j instanceof m||(j instanceof f||(j=new f(j,j,"object")),(j.variable.base||j.variable).asKey=!0),i.push(d+j.compile(a,A)+e);return i}(),k=k.join(""),i="{"+(k&&"\n"+k+"\n"+this.tab)+"}";return this.front?"("+i+")":i},a.prototype.assigns=function(a){var b,c,d,e;e=this.properties;for(c=0,d=e.length;c<d;c++){b=e[c];if(b.assigns(a))return!0}return!1};return a}(),b.Arr=e=function(){function a(a){this.objects=a||[]}bk(a,g),a.prototype.children=["objects"],a.prototype.filterImplicitObjects=i.prototype.filterImplicitObjects,a.prototype.compileNode=function(a){var b,c,d;if(!this.objects.length)return"[]";a.indent+=R,d=this.filterImplicitObjects(this.objects);if(b=P.compileSplattedArray(a,d))return b;b=function(){var b,e,f;f=[];for(b=0,e=d.length;b<e;b++)c=d[b],f.push(c.compile(a,x));return f}().join(", ");return b.indexOf("\n")>=0?"[\n"+a.indent+b+"\n"+this.tab+"]":"["+b+"]"},a.prototype.assigns=function(a){var b,c,d,e;e=this.objects;for(c=0,d=e.length;c<d;c++){b=e[c];if(b.assigns(a))return!0}return!1};return a}(),b.Class=j=function(){function a(a,b,c){this.variable=a,this.parent=b,this.body=c!=null?c:new h,this.boundFuncs=[],this.body.classBody=!0}bk(a,g),a.prototype.children=["variable","parent","body"],a.prototype.determineName=function(){var a,b;if(!this.variable)return null;a=(b=bc(this.variable.properties))?b instanceof d&&b.name.value:this.variable.base.value;return a&&(a=q.test(a)&&a)},a.prototype.setContext=function(a){return this.body.traverseChildren(!1,function(b){if(b.classBody)return!1;if(b instanceof B&&b.value==="this")return b.value=a;if(b instanceof l){b.klass=a;if(b.bound)return b.context=a}})},a.prototype.addBoundFunctions=function(a){var b,c,d,e,f,g;if(this.boundFuncs.length){f=this.boundFuncs,g=[];for(d=0,e=f.length;d<e;d++)c=f[d],b=c.compile(a),g.push(this.ctor.body.unshift(new B("this."+b+" = "+bh("bind")+"(this."+b+", this)")));return g}},a.prototype.addProperties=function(a,b,c){var e,g,h,i,j;j=a.base.properties.slice(0),h=function(){var a;a=[];while(e=j.shift()){if(e instanceof f){g=e.variable.base,delete e.context,i=e.value;if(g.value==="constructor"){if(this.ctor)throw new Error("cannot define more than one constructor in a class");if(i.bound)throw new Error("cannot define a constructor as a bound function");i instanceof l?e=this.ctor=i:(this.externalCtor=c.scope.freeVariable("class"),e=new f(new B(this.externalCtor),i))}else e.variable["this"]||(e.variable=new W(new B(b),[new d(g,"proto")])),i instanceof l&&i.bound&&(this.boundFuncs.push(g),i.bound=!1)}a.push(e)}return a}.call(this);return Z(h)},a.prototype.walkBody=function(b,c){return this.traverseChildren(!1,bl(function(d){var e,f,g,i,j;if(d instanceof a)return!1;if(d instanceof h){j=e=d.expressions;for(f=0,i=j.length;f<i;f++)g=j[f],g instanceof W&&g.isObject(!0)&&(e[f]=this.addProperties(g,b,c));return d.expressions=e=bb(e)}},this))},a.prototype.ensureConstructor=function(a){this.ctor||(this.ctor=new l,this.parent&&this.ctor.body.push(new B(""+a+".__super__.constructor.apply(this, arguments)")),this.externalCtor&&this.ctor.body.push(new B(""+this.externalCtor+".apply(this, arguments)")),this.body.expressions.unshift(this.ctor)),this.ctor.ctor=this.ctor.name=a,this.ctor.klass=null;return this.ctor.noReturn=!0},a.prototype.compileNode=function(a){var b,c,d,e;b=this.determineName(),e=b||this.name||"_Class",d=new B(e),this.setContext(e),this.walkBody(e,a),this.ensureConstructor(e),this.parent&&this.body.expressions.unshift(new o(d,this.parent)),this.ctor instanceof l||this.body.expressions.unshift(this.ctor),this.body.expressions.push(d),this.addBoundFunctions(a),c=new I(k.wrap(this.body),!0),this.variable&&(c=new f(this.variable,c));return c.compile(a)};return a}(),b.Assign=f=function(){function a(a,b,c,d){this.variable=a,this.value=b,this.context=c,this.param=d&&d.param}bk(a,g),a.prototype.children=["variable","value"],a.prototype.assigns=function(a){return this[this.context==="object"?"value":"variable"].assigns(a)},a.prototype.unfoldSoak=function(a){return bg(a,this,"variable")},a.prototype.compileNode=function(a){var b,c,d,e,f;if(b=this.variable instanceof W){if(this.variable.isArray()||this.variable.isObject())return this.compilePatternMatch(a);if(this.variable.isSplice())return this.compileSplice(a);if((f=this.context)==="||="||f==="&&="||f==="?=")return this.compileConditional(a)}d=this.variable.compile(a,x);if(!this.context&&!this.variable.isAssignable())throw SyntaxError('"'+this.variable.compile(a)+'" cannot be assigned.');this.context||b&&(this.variable.namespaced||this.variable.hasProperties())||(this.param?a.scope.add(d,"var"):a.scope.find(d)),this.value instanceof l&&(c=C.exec(d))&&(this.value.name=c[2],c[1]&&(this.value.klass=c[1])),e=this.value.compile(a,x);if(this.context==="object")return""+d+": "+e;e=d+(" "+(this.context||"=")+" ")+e;return a.level<=x?e:"("+e+")"},a.prototype.compilePatternMatch=function(b){var c,e,f,g,h,i,j,k,l,m,n,o,p,r,s,t,v,w,z,C,D,E;r=b.level===A,t=this.value,l=this.variable.base.objects;if(!(m=l.length)){f=t.compile(b);return b.level>=y?"("+f+")":f}i=this.variable.isObject();if(r&&m===1&&!((k=l[0])instanceof P)){k instanceof a?(z=k,h=z.variable.base,k=z.value):k.base instanceof I?(C=(new W(k.unwrapAll())).cacheReference(b),k=C[0],h=C[1]):h=i?k["this"]?k.properties[0].name:k:new B(0),c=q.test(h.unwrap().value||0),t=new W(t),t.properties.push(new(c?d:u)(h));return(new a(k,t,null,{param:this.param})).compile(b,A)}v=t.compile(b,x),e=[],p=!1;if(!q.test(v)||this.variable.assigns(v))e.push(""+(n=b.scope.freeVariable("ref"))+" = "+v),v=n;for(g=0,w=l.length;g<w;g++){k=l[g],h=g,i&&(k instanceof a?(D=k,h=D.variable.base,k=D.value):k.base instanceof I?(E=(new W(k.unwrapAll())).cacheReference(b),k=E[0],h=E[1]):h=k["this"]?k.properties[0].name:k);if(!p&&k instanceof P)s=""+m+" <= "+v+".length ? "+bh("slice")+".call("+v+", "+g,(o=m-g-1)?(j=b.scope.freeVariable("i"),s+=", "+j+" = "+v+".length - "+o+") : ("+j+" = "+g+", [])"):s+=") : []",s=new B(s),p=""+j+"++";else{if(k instanceof P){k=k.name.compile(b);throw SyntaxError("multiple splats are disallowed in an assignment: "+k+" ...")}typeof h=="number"?(h=new B(p||h),c=!1):c=i&&q.test(h.unwrap().value||0),s=new W(new B(v),[new(c?d:u)(h)])}e.push((new a(k,s,null,{param:this.param})).compile(b,A))}r||e.push(v),f=e.join(", ");return b.level<x?f:"("+f+")"},a.prototype.compileConditional=function(b){var c,d,e;e=this.variable.cacheReference(b),c=e[0],d=e[1],bm.call(this.context,"?")>=0&&(b.isExistentialEquals=!0);return(new G(this.context.slice(0,-1),c,new a(d,this.value,"="))).compile(b)},a.prototype.compileSplice=function(a){var b,c,d,e,f,g,h,i,j,k,l,m;k=this.variable.properties.pop().range,d=k.from,h=k.to,c=k.exclusive,g=this.variable.compile(a),l=(d!=null?d.cache(a,y):void 0)||["0","0"],e=l[0],f=l[1],h?(d!=null?d.isSimpleNumber():void 0)&&h.isSimpleNumber()?(h=+h.compile(a)- +f,c||(h+=1)):(h=h.compile(a)+" - "+f,c||(h+=" + 1")):h="9e9",m=this.value.cache(a,x),i=m[0],j=m[1],b="[].splice.apply("+g+", ["+e+", "+h+"].concat("+i+")), "+j;return a.level>A?"("+b+")":b};return a}(),b.Code=l=function(){function a(a,b,c){this.params=a||[],this.body=b||new h,this.bound=c==="boundfunc",this.bound&&(this.context="this")}bk(a,g),a.prototype.children=["params","body"],a.prototype.isStatement=function(){return!!this.ctor},a.prototype.jumps=E,a.prototype.compileNode=function(a){var b,c,d,g,h,i,j,k,l,m,n,o,p,q,r,t,u,w,x,y,z,A,C,D;a.scope=new N(a.scope,this.body,this),a.scope.shared=$(a,"sharedScope"),a.indent+=R,delete a.bare,o=[],c=[],z=this.params;for(q=0,u=z.length;q<u;q++){j=z[q];if(j.splat){A=this.params;for(r=0,w=A.length;r<w;r++)i=A[r],i.name.value&&a.scope.add(i.name.value,"var",!0);l=new f(new W(new e(function(){var b,c,d,e;d=this.params,e=[];for(b=0,c=d.length;b<c;b++)i=d[b],e.push(i.asReference(a));return e}.call(this))),new W(new B("arguments")));break}}C=this.params;for(t=0,x=C.length;t<x;t++)j=C[t],j.isComplex()?(n=k=j.asReference(a),j.value&&(n=new G("?",k,j.value)),c.push(new f(new W(j.name),n,"=",{param:!0}))):(k=j,j.value&&(h=new B(k.name.value+" == null"),n=new f(new W(j.name),j.value,"="),c.push(new s(h,n)))),l||o.push(k);p=this.body.isEmpty(),l&&c.unshift(l),c.length&&(D=this.body.expressions).unshift.apply(D,c);if(!l)for(d=0,y=o.length;d<y;d++)m=o[d],a.scope.parameter(o[d]=m.compile(a));!p&&!this.noReturn&&this.body.makeReturn(),g=a.indent,b="function",this.ctor&&(b+=" "+this.name),b+="("+o.join(", ")+") {",this.body.isEmpty()||(b+="\n"+this.body.compileWithDeclarations(a)+"\n"+this.tab),b+="}";if(this.ctor)return this.tab+b;if(this.bound)return bh("bind")+("("+b+", "+this.context+")");return this.front||a.level>=v?"("+b+")":b},a.prototype.traverseChildren=function(b,c){if(b)return a.__super__.traverseChildren.call(this,b,c)};return a}(),b.Param=H=function(){function a(a,b,c){this.name=a,this.value=b,this.splat=c}bk(a,g),a.prototype.children=["name","value"],a.prototype.compile=function(a){return this.name.compile(a,x)},a.prototype.asReference=function(a){var b;if(this.reference)return this.reference;b=this.name,b["this"]?(b=b.properties[0].name,b.value.reserved&&(b=new B("_"+b.value))):b.isComplex()&&(b=new B(a.scope.freeVariable("arg"))),b=new W(b),this.splat&&(b=new P(b));return this.reference=b},a.prototype.isComplex=function(){return this.name.isComplex()};return a}(),b.Splat=P=function(){function a(a){this.name=a.compile?a:new B(a)}bk(a,g),a.prototype.children=["name"],a.prototype.isAssignable=Y,a.prototype.assigns=function(a){return this.name.assigns(a)},a.prototype.compile=function(a){return this.index!=null?this.compileParam(a):this.name.compile(a)},a.compileSplattedArray=function(b,c,d){var e,f,g,h,i,j,k;i=-1;while((j=c[++i])&&!(j instanceof a))continue;if(i>=c.length)return"";if(c.length===1){g=c[0].compile(b,x);if(d)return g;return""+bh("slice")+".call("+g+")"}e=c.slice(i);for(h=0,k=e.length;h<k;h++)j=e[h],g=j.compile(b,x),e[h]=j instanceof a?""+bh("slice")+".call("+g+")":"["+g+"]";if(i===0)return e[0]+(".concat("+e.slice(1).join(", ")+")");f=function(){var a,d,e,f;e=c.slice(0,i),f=[];for(a=0,d=e.length;a<d;a++)j=e[a],f.push(j.compile(b,x));return f}();return"["+f.join(", ")+"].concat("+e.join(", ")+")"};return a}(),b.While=X=function(){function a(a,b){this.condition=(b!=null?b.invert:void 0)?a.invert():a,this.guard=b!=null?b.guard:void 0}bk(a,g),a.prototype.children=["condition","guard","body"],a.prototype.isStatement=Y,a.prototype.makeReturn=function(){this.returns=!0;return this},a.prototype.addBody=function(a){this.body=a;return this},a.prototype.jumps=function(){var a,b,c,d;a=this.body.expressions;if(!a.length)return!1;for(c=0,d=a.length;c<d;c++){b=a[c];if(b.jumps({loop:!0}))return b}return!1},a.prototype.compileNode=function(a){var b,c,d,e;a.indent+=R,e="",b=this.body;if(b.isEmpty())b="";else{if(a.level>A||this.returns)d=a.scope.freeVariable("results"),e=""+this.tab+d+" = [];\n",b&&(b=J.wrap(d,b));this.guard&&(b=h.wrap([new s(this.guard,b)])),b="\n"+b.compile(a,A)+"\n"+this.tab}c=e+this.tab+("while ("+this.condition.compile(a,z)+") {"+b+"}"),this.returns&&(c+="\n"+this.tab+"return "+d+";");return c};return a}(),b.Op=G=function(){function c(b,c,d,e){var f;if(b==="in")return new t(c,d);if(b==="do"){f=new i(c,c.params||[]),f["do"]=!0;return f}if(b==="new"){if(c instanceof i&&!c["do"])return c.newInstance();if(c instanceof l&&c.bound||c["do"])c=new I(c)}this.operator=a[b]||b,this.first=c,this.second=d,this.flip=!!e;return this}var a,b;bk(c,g),a={"==":"===","!=":"!==",of:"in"},b={"!==":"===","===":"!=="},c.prototype.children=["first","second"],c.prototype.isSimpleNumber=E,c.prototype.isUnary=function(){return!this.second},c.prototype.isComplex=function(){var a;return!this.isUnary()||(a=this.operator)!=="+"&&a!=="-"||this.first.isComplex()},c.prototype.isChainable=function(){var a;return(a=this.operator)==="<"||a===">"||a===">="||a==="<="||a==="==="||a==="!=="},c.prototype.invert=function(){var a,d,e,f,g;if(this.isChainable()&&this.first.isChainable()){a=!0,d=this;while(d&&d.operator)a&&(a=d.operator in b),d=d.first;if(!a)return(new I(this)).invert();d=this;while(d&&d.operator)d.invert=!d.invert,d.operator=b[d.operator],d=d.first;return this}if(f=b[this.operator]){this.operator=f,this.first.unwrap()instanceof c&&this.first.invert();return this}return this.second?(new I(this)).invert():this.operator==="!"&&(e=this.first.unwrap())instanceof c&&((g=e.operator)==="!"||g==="in"||g==="instanceof")?e:new c("!",this)},c.prototype.unfoldSoak=function(a){var b;return((b=this.operator)==="++"||b==="--"||b==="delete")&&bg(a,this,"first")},c.prototype.compileNode=function(a){var b;if(this.isUnary())return this.compileUnary(a);if(this.isChainable()&&this.first.isChainable())return this.compileChain(a);if(this.operator==="?")return this.compileExistence(a);this.first.front=this.front,b=this.first.compile(a,y)+" "+this.operator+" "+this.second.compile(a,y);return a.level<=y?b:"("+b+")"},c.prototype.compileChain=function(a){var b,c,d,e;e=this.first.second.cache(a),this.first.second=e[0],d=e[1],c=this.first.compile(a,y),b=""+c+" "+(this.invert?"&&":"||")+" "+d.compile(a)+" "+this.operator+" "+this.second.compile(a,y);return"("+b+")"},c.prototype.compileExistence=function(a){var b,c;this.first.isComplex()?(c=new B(a.scope.freeVariable("ref")),b=new I(new f(c,this.first))):(b=this.first,c=b);return(new s(new n(b),c,{type:"if"})).addElse(this.second).compile(a)},c.prototype.compileUnary=function(a){var b,d;d=[b=this.operator],(b==="new"||b==="typeof"||b==="delete"||(b==="+"||b==="-")&&this.first instanceof c&&this.first.operator===b)&&d.push(" "),b==="new"&&this.first.isStatement(a)&&(this.first=new I(this.first)),d.push(this.first.compile(a,y)),this.flip&&d.reverse();return d.join("")},c.prototype.toString=function(a){return c.__super__.toString.call(this,a,this.constructor.name+" "+this.operator)};return c}(),b.In=t=function(){function a(a,b){this.object=a,this.array=b}bk(a,g),a.prototype.children=["object","array"],a.prototype.invert=D,a.prototype.compileNode=function(a){var b,c,d,e,f;if(this.array instanceof W&&this.array.isArray()){f=this.array.base.objects;for(d=0,e=f.length;d<e;d++){c=f[d];if(c instanceof P){b=!0;break}}if(!b)return this.compileOrTest(a)}return this.compileLoopTest(a)},a.prototype.compileOrTest=function(a){var b,c,d,e,f,g,h,i,j;i=this.object.cache(a,y),g=i[0],f=i[1],j=this.negated?[" !== "," && "]:[" === "," || "],b=j[0],c=j[1],h=function(){var c,h,i;h=this.array.base.objects,i=[];for(d=0,c=h.length;d<c;d++)e=h[d],i.push((d?f:g)+b+e.compile(a,y));return i}.call(this);if(h.length===0)return"false";h=h.join(c);return a.level<y?h:"("+h+")"},a.prototype.compileLoopTest=function(a){var b,c,d,e;e=this.object.cache(a,x),d=e[0],c=e[1],b=bh("indexOf")+(".call("+this.array.compile(a,x)+", "+c+") ")+(this.negated?"< 0":">= 0");if(d===c)return b;b=d+", "+b;return a.level<x?b:"("+b+")"},a.prototype.toString=function(b){return a.__super__.toString.call(this,b,this.constructor.name+(this.negated?"!":""))};return a}(),b.Try=U=function(){function a(a,b,c,d){this.attempt=a,this.error=b,this.recovery=c,this.ensure=d}bk(a,g),a.prototype.children=["attempt","recovery","ensure"],a.prototype.isStatement=Y,a.prototype.jumps=function(a){var b;return this.attempt.jumps(a)||((b=this.recovery)!=null?b.jumps(a):void 0)},a.prototype.makeReturn=function(){this.attempt&&(this.attempt=this.attempt.makeReturn()),this.recovery&&(this.recovery=this.recovery.makeReturn());return this},a.prototype.compileNode=function(a){var b,c;a.indent+=R,c=this.error?" ("+this.error.compile(a)+") ":" ",b=this.recovery?" catch"+c+"{\n"+this.recovery.compile(a,A)+"\n"+this.tab+"}":!this.ensure&&!this.recovery?" catch (_e) {}":void 0;return""+this.tab+"try {\n"+this.attempt.compile(a,A)+"\n"+this.tab+"}"+(b||"")+(this.ensure?" finally {\n"+this.ensure.compile(a,A)+"\n"+this.tab+"}":"")};return a}(),b.Throw=T=function(){function a(a){this.expression=a}bk(a,g),a.prototype.children=["expression"],a.prototype.isStatement=Y,a.prototype.jumps=E,a.prototype.makeReturn=S,a.prototype.compileNode=function(a){return this.tab+("throw "+this.expression.compile(a)+";")};return a}(),b.Existence=n=function(){function a(a){this.expression=a}bk(a,g),a.prototype.children=["expression"],a.prototype.invert=D,a.prototype.compileNode=function(a){var b,c,d,e;d=this.expression.compile(a,y),d=q.test(d)&&!a.scope.check(d)?(e=this.negated?["===","||"]:["!==","&&"],b=e[0],c=e[1],e,"typeof "+d+" "+b+' "undefined" '+c+" "+d+" "+b+" null"):""+d+" "+(this.negated?"==":"!=")+" null";return a.level<=w?d:"("+d+")"};return a}(),b.Parens=I=function(){function a(a){this.body=a}bk(a,g),a.prototype.children=["body"],a.prototype.unwrap=function(){return this.body},a.prototype.isComplex=function(){return this.body.isComplex()},a.prototype.makeReturn=function(){return this.body.makeReturn()},a.prototype.compileNode=function(a){var b,c,d;d=this.body.unwrap();if(d instanceof W&&d.isAtomic()){d.front=this.front;return d.compile(a)}c=d.compile(a,z),b=a.level<y&&(d instanceof G||d instanceof i||d instanceof p&&d.returns);return b?c:"("+c+")"};return a}(),b.For=p=function(){function a(a,b){var c;this.source=b.source,this.guard=b.guard,this.step=b.step,this.name=b.name,this.index=b.index,this.body=h.wrap([a]),this.own=!!b.own,this.object=!!b.object,this.object&&(c=[this.index,this.name],this.name=c[0],this.index=c[1]);if(this.index instanceof W)throw SyntaxError("index cannot be a pattern matching expression");this.range=this.source instanceof W&&this.source.base instanceof K&&!this.source.properties.length,this.pattern=this.name instanceof W;if(this.range&&this.index)throw SyntaxError("indexes do not apply to range loops");if(this.range&&this.pattern)throw SyntaxError("cannot pattern match over range loops");this.returns=!1}bk(a,g),a.prototype.children=["body","source","guard","step"],a.prototype.isStatement=Y,a.prototype.jumps=X.prototype.jumps,a.prototype.makeReturn=function(){this.returns=!0;return this},a.prototype.compileNode=function(a){var b,c,d,e,g,i,j,k,l,m,n,o,p,r,t,u,v,w,z,C,D,E,F;b=h.wrap([this.body]),l=(F=bc(b.expressions))!=null?F.jumps():void 0,l&&l instanceof L&&(this.returns=!1),w=this.range?this.source.base:this.source,v=a.scope,n=this.name&&this.name.compile(a,x),j=this.index&&this.index.compile(a,x),n&&!this.pattern&&v.find(n,{immediate:!0}),j&&v.find(j,{immediate:!0}),this.returns&&(u=v.freeVariable("results")),k=(this.range?n:j)||v.freeVariable("i"),this.step&&!this.range&&(C=v.freeVariable("step")),this.pattern&&(n=k),E="",g="",c="",i=this.tab+R,this.range?d=w.compile(bd(a,{index:k,step:this.step})):(D=this.source.compile(a,x),(n||this.own)&&!q.test(D)&&(c=""+this.tab+(p=v.freeVariable("ref"))+" = "+D+";\n",D=p),n&&!this.pattern&&(o=""+n+" = "+D+"["+k+"]"),this.object||(m=v.freeVariable("len"),e=""+k+" = 0, "+m+" = "+D+".length"+(this.step?", "+C+" = "+this.step.compile(a,y):""),z=this.step?""+k+" += "+C:""+k+"++",d=""+e+"; "+k+" < "+m+"; "+z)),this.returns&&(r=""+this.tab+u+" = [];\n",t="\n"+this.tab+"return "+u+";",b=J.wrap(u,b)),this.guard&&(b=h.wrap([new s(this.guard,b)])),this.pattern&&b.expressions.unshift(new f(this.name,new B(""+D+"["+k+"]"))),c+=this.pluckDirectCall(a,b),o&&(E="\n"+i+o+";"),this.object&&(d=""+k+" in "+D,this.own&&(g="\n"+i+"if (!"+bh("hasProp")+".call("+D+", "+k+")) continue;")),b=b.compile(bd(a,{indent:i}),A),b&&(b="\n"+b+"\n");return""+c+(r||"")+this.tab+"for ("+d+") {"+g+E+b+this.tab+"}"+(t||"")},a.prototype.pluckDirectCall=function(a,b){var c,d,e,g,h,j,k,m,n,o,p,q,r,s;d="",n=b.expressions;for(h=0,m=n.length;h<m;h++){e=n[h],e=e.unwrapAll();if(!(e instanceof i))continue;k=e.variable.unwrapAll();if(!(k instanceof l||k instanceof W&&((o=k.base)!=null?o.unwrapAll():void 0)instanceof l&&k.properties.length===1&&((p=(q=k.properties[0].name)!=null?q.value:void 0)==="call"||p==="apply")))continue;g=((r=k.base)!=null?r.unwrapAll():void 0)||k,j=new B(a.scope.freeVariable("fn")),c=new W(j),k.base&&(s=[c,k],k.base=s[0],c=s[1],args.unshift(new B("this"))),b.expressions[h]=new i(c,e.args),d+=this.tab+(new f(j,g)).compile(a,A)+";\n"}return d};return a}(),b.Switch=Q=function(){function a(a,b,c){this.subject=a,this.cases=b,this.otherwise=c}bk(a,g),a.prototype.children=["subject","cases","otherwise"],a.prototype.isStatement=Y,a.prototype.jumps=function(a){var b,c,d,e,f,g,h;a==null&&(a={block:!0}),f=this.cases;for(d=0,e=f.length;d<e;d++){g=f[d],c=g[0],b=g[1];if(b.jumps(a))return b}return(h=this.otherwise)!=null?h.jumps(a):void 0},a.prototype.makeReturn=function(){var a,b,c,d,e;d=this.cases;for(b=0,c=d.length;b<c;b++)a=d[b],a[1].makeReturn();(e=this.otherwise)!=null&&e.makeReturn();return this},a.prototype.compileNode=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q;i=a.indent+R,j=a.indent=i+R,d=this.tab+("switch ("+(((n=this.subject)!=null?n.compile(a,z):void 0)||!1)+") {\n"),o=this.cases;for(h=0,l=o.length;h<l;h++){p=o[h],f=p[0],b=p[1],q=bb([f]);for(k=0,m=q.length;k<m;k++)e=q[k],this.subject||(e=e.invert()),d+=i+("case "+e.compile(a,z)+":\n");if(c=b.compile(a,A))d+=c+"\n";if(h===this.cases.length-1&&!this.otherwise)break;g=this.lastNonComment(b.expressions);if(g instanceof L||g instanceof B&&g.jumps()&&g.value!=="debugger")continue;d+=j+"break;\n"}this.otherwise&&this.otherwise.expressions.length&&(d+=i+("default:\n"+this.otherwise.compile(a,A)+"\n"));return d+this.tab+"}"};return a}(),b.If=s=function(){function a(a,b,c){this.body=b,c==null&&(c={}),this.condition=c.type==="unless"?a.invert():a,this.elseBody=null,this.isChain=!1,this.soak=c.soak}bk(a,g),a.prototype.children=["condition","body","elseBody"],a.prototype.bodyNode=function(){var a;return(a=this.body)!=null?a.unwrap():void 0},a.prototype.elseBodyNode=function(){var a;return(a=this.elseBody)!=null?a.unwrap():void 0},a.prototype.addElse=function(b){this.isChain?this.elseBodyNode().addElse(b):(this.isChain=b instanceof a,this.elseBody=this.ensureBlock(b));return this},a.prototype.isStatement=function(a){var b;return(a!=null?a.level:void 0)===A||this.bodyNode().isStatement(a)||((b=this.elseBodyNode())!=null?b.isStatement(a):void 0)},a.prototype.jumps=function(a){var b;return this.body.jumps(a)||((b=this.elseBody)!=null?b.jumps(a):void 0)},a.prototype.compileNode=function(a){return this.isStatement(a)?this.compileStatement(a):this.compileExpression(a)},a.prototype.makeReturn=function(){this.body&&(this.body=new h([this.body.makeReturn()])),this.elseBody&&(this.elseBody=new h([this.elseBody.makeReturn()]));return this},a.prototype.ensureBlock=function(a){return a instanceof h?a:new h([a])},a.prototype.compileStatement=function(b){var c,d,e,f,g;d=$(b,"chainChild"),f=$(b,"isExistentialEquals");if(f)return(new a(this.condition.invert(),this.elseBodyNode(),{type:"if"})).compile(b);e=this.condition.compile(b,z),b.indent+=R,c=this.ensureBlock(this.body).compile(b),c&&(c="\n"+c+"\n"+this.tab),g="if ("+e+") {"+c+"}",d||(g=this.tab+g);if(!this.elseBody)return g;return g+" else "+(this.isChain?(b.indent=this.tab,b.chainChild=!0,this.elseBody.unwrap().compile(b,A)):"{\n"+this.elseBody.compile(b,A)+"\n"+this.tab+"}")},a.prototype.compileExpression=function(a){var b,c,d,e;e=this.condition.compile(a,w),c=this.bodyNode().compile(a,x),b=this.elseBodyNode()?this.elseBodyNode().compile(a,x):"void 0",d=""+e+" ? "+c+" : "+b;return a.level>=w?"("+d+")":d},a.prototype.unfoldSoak=function(){return this.soak&&this};return a}(),J={wrap:function(a,b){if(b.isEmpty()||bc(b.expressions).jumps())return b;return b.push(new i(new W(new B(a),[new d(new B("push"))]),[b.pop()]))}},k={wrap:function(a,b,c){var e,f,g,j,k;if(a.jumps())return a;g=new l([],h.wrap([a])),e=[];if((j=a.contains(this.literalArgs))||a.contains(this.literalThis))k=new B(j?"apply":"call"),e=[new B("this")],j&&e.push(new B("arguments")),g=new W(g,[new d(k)]);g.noReturn=c,f=new i(g,e);return b?h.wrap([f]):f},literalArgs:function(a){return a instanceof B&&a.value==="arguments"&&!a.asKey},literalThis:function(a){return a instanceof B&&a.value==="this"&&!a.asKey||a instanceof l&&a.bound}},bg=function(a,b,c){var d;if(!!(d=b[c].unfoldSoak(a))){b[c]=d.body,d.body=new W(b);return d}},V={"extends":"function(child, parent) {\n for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }\n function ctor() { this.constructor = child; }\n ctor.prototype = parent.prototype;\n child.prototype = new ctor;\n child.__super__ = parent.prototype;\n return child;\n}",bind:"function(fn, me){ return function(){ return fn.apply(me, arguments); }; }",indexOf:"Array.prototype.indexOf || function(item) {\n for (var i = 0, l = this.length; i < l; i++) {\n if (this[i] === item) return i;\n }\n return -1;\n}",hasProp:"Object.prototype.hasOwnProperty",slice:"Array.prototype.slice"},A=1,z=2,x=3,w=4,y=5,v=6,R=" ",q=/^[$A-Za-z_\x7f-\uffff][$\w\x7f-\uffff]*$/,M=/^[+-]?\d+$/,C=/^(?:([$A-Za-z_][$\w\x7f-\uffff]*)\.prototype\.)?([$A-Za-z_][$\w\x7f-\uffff]*)$/,r=/^['"]/,bh=function(a){var b;b="__"+a,N.root.assign(b,V[a]);return b},be=function(a,b){return a.replace(/\n/g,"$&"+b)}}),define("ace/mode/coffee/scope",["require","exports","module","ace/mode/coffee/helpers"],function(a,b,c){var d,e,f,g;g=a("ace/mode/coffee/helpers"),e=g.extend,f=g.last,b.Scope=d=function(){function a(b,c,d){this.parent=b,this.expressions=c,this.method=d,this.variables=[{name:"arguments",type:"arguments"}],this.positions={},this.parent||(a.root=this)}a.root=null,a.prototype.add=function(a,b,c){var d;if(this.shared&&!c)return this.parent.add(a,b,c);return typeof (d=this.positions[a])=="number"?this.variables[d].type=b:this.positions[a]=this.variables.push({name:a,type:b})-1},a.prototype.find=function(a,b){if(this.check(a,b))return!0;this.add(a,"var");return!1},a.prototype.parameter=function(a){if(!this.shared||!this.parent.check(a,!0))return this.add(a,"param")},a.prototype.check=function(a,b){var c,d;c=!!this.type(a);if(c||b)return c;return(d=this.parent)!=null?!!d.check(a):!!void 0},a.prototype.temporary=function(a,b){return a.length>1?"_"+a+(b>1?b:""):"_"+(b+parseInt(a,36)).toString(36).replace(/\d/g,"a")},a.prototype.type=function(a){var b,c,d,e;e=this.variables;for(c=0,d=e.length;c<d;c++){b=e[c];if(b.name===a)return b.type}return null},a.prototype.freeVariable=function(a){var b,c;b=0;while(this.check(c=this.temporary(a,b)))b++;this.add(c,"var",!0);return c},a.prototype.assign=function(a,b){this.add(a,{value:b,assigned:!0});return this.hasAssignments=!0},a.prototype.hasDeclarations=function(){return!!this.declaredVariables().length},a.prototype.declaredVariables=function(){var a,b,c,d,e,f;a=[],b=[],f=this.variables;for(d=0,e=f.length;d<e;d++)c=f[d],c.type==="var"&&(c.name.charAt(0)==="_"?b:a).push(c.name);return a.sort().concat(b.sort())},a.prototype.assignedVariables=function(){var a,b,c,d,e;d=this.variables,e=[];for(b=0,c=d.length;b<c;b++)a=d[b],a.type.assigned&&e.push(""+a.name+" = "+a.type.value);return e};return a}()}) \ No newline at end of file
diff --git a/www/js/ace/worker-css.js b/www/js/ace/worker-css.js
new file mode 100644
index 0000000..f76ed43
--- /dev/null
+++ b/www/js/ace/worker-css.js
@@ -0,0 +1 @@
+function initSender(){var a=require("pilot/event_emitter").EventEmitter,b=require("pilot/oop"),c=function(){};(function(){b.implement(this,a),this.callback=function(a,b){postMessage({type:"call",id:b,data:a})},this.emit=function(a,b){postMessage({type:"event",name:a,data:b})}}).call(c.prototype);return new c}function initBaseUrls(a){require.tlns=a}var console={log:function(a){postMessage({type:"log",data:a})}},window={console:console},require=function(a){var b=require.modules[a];if(b){b.initialized||(b.exports=b.factory().exports,b.initialized=!0);return b.exports}var c=a.split("/");c[0]=require.tlns[c[0]]||c[0],path=c.join("/")+".js",require.id=a,importScripts(path);return require(a)};require.modules={},require.tlns={};var define=function(a,b,c){arguments.length==2?c=b:arguments.length==1&&(c=a,a=require.id);a.indexOf("text/")!==0&&(require.modules[a]={factory:function(){var a={exports:{}},b=c(require,a.exports,a);b&&(a.exports=b);return a}})},main,sender;onmessage=function(a){var b=a.data;if(b.command)main[b.command].apply(main,b.args);else if(b.init){initBaseUrls(b.tlns),require("pilot/fixoldbrowsers"),sender=initSender();var c=require(b.module)[b.classname];main=new c(sender)}else b.event&&sender&&sender._dispatchEvent(b.event,b.data)},define("pilot/fixoldbrowsers",["require","exports","module"],function(a,b,c){if(!Function.prototype.bind){var d=Array.prototype.slice;Function.prototype.bind=function(b){var c=this;if(typeof c.apply!="function"||typeof c.call!="function")return new TypeError;var e=d.call(arguments),f=function a(){if(this instanceof a){var b=Object.create(c.prototype);c.apply(b,e.concat(d.call(arguments)));return b}return c.call.apply(c,e.concat(d.call(arguments)))};f.length=typeof c=="function"?Math.max(c.length-e.length,0):0;return f}}var e=Function.prototype.call,f=Array.prototype,g=Object.prototype,h=e.bind(g.hasOwnProperty),i,j,k,l,m;if(m=h(g,"__defineGetter__"))i=e.bind(g.__defineGetter__),j=e.bind(g.__defineSetter__),k=e.bind(g.__lookupGetter__),l=e.bind(g.__lookupSetter__);Array.isArray||(Array.isArray=function(b){return Object.prototype.toString.call(b)==="[object Array]"}),Array.prototype.forEach||(Array.prototype.forEach=function(b,c){var d=+this.length;for(var e=0;e<d;e++)e in this&&b.call(c,this[e],e,this)}),Array.prototype.map||(Array.prototype.map=function(b){var c=+this.length;if(typeof b!="function")throw new TypeError;var d=Array(c),e=arguments[1];for(var f=0;f<c;f++)f in this&&(d[f]=b.call(e,this[f],f,this));return d}),Array.prototype.filter||(Array.prototype.filter=function(b){var c=[],d=arguments[1];for(var e=0;e<this.length;e++)b.call(d,this[e])&&c.push(this[e]);return c}),Array.prototype.every||(Array.prototype.every=function(b){var c=arguments[1];for(var d=0;d<this.length;d++)if(!b.call(c,this[d]))return!1;return!0}),Array.prototype.some||(Array.prototype.some=function(b){var c=arguments[1];for(var d=0;d<this.length;d++)if(b.call(c,this[d]))return!0;return!1}),Array.prototype.reduce||(Array.prototype.reduce=function(b){var c=+this.length;if(typeof b!="function")throw new TypeError;if(c===0&&arguments.length===1)throw new TypeError;var d=0;if(arguments.length>=2)var e=arguments[1];else do{if(d in this){e=this[d++];break}if(++d>=c)throw new TypeError}while(!0);for(;d<c;d++)d in this&&(e=b.call(null,e,this[d],d,this));return e}),Array.prototype.reduceRight||(Array.prototype.reduceRight=function(b){var c=+this.length;if(typeof b!="function")throw new TypeError;if(c===0&&arguments.length===1)throw new TypeError;var d=c-1;if(arguments.length>=2)var e=arguments[1];else do{if(d in this){e=this[d--];break}if(--d<0)throw new TypeError}while(!0);for(;d>=0;d--)d in this&&(e=b.call(null,e,this[d],d,this));return e}),Array.prototype.indexOf||(Array.prototype.indexOf=function(b){var c=this.length;if(!c)return-1;var d=arguments[1]||0;if(d>=c)return-1;d<0&&(d+=c);for(;d<c;d++){if(!h(this,d))continue;if(b===this[d])return d}return-1}),Array.prototype.lastIndexOf||(Array.prototype.lastIndexOf=function(b){var c=this.length;if(!c)return-1;var d=arguments[1]||c;d<0&&(d+=c),d=Math.min(d,c-1);for(;d>=0;d--){if(!h(this,d))continue;if(b===this[d])return d}return-1}),Object.getPrototypeOf||(Object.getPrototypeOf=function(b){return b.__proto__||b.constructor.prototype});if(!Object.getOwnPropertyDescriptor){var n="Object.getOwnPropertyDescriptor called on a non-object: ";Object.getOwnPropertyDescriptor=function(b,c){if(typeof b!="object"&&typeof b!="function"||b===null)throw new TypeError(n+b);if(!h(b,c))return undefined;var d,e,f;d={enumerable:!0,configurable:!0};if(m){var i=b.__proto__;b.__proto__=g;var e=k(b,c),f=l(b,c);b.__proto__=i;if(e||f){e&&(descriptor.get=e),f&&(descriptor.set=f);return descriptor}}descriptor.value=b[c];return descriptor}}Object.getOwnPropertyNames||(Object.getOwnPropertyNames=function(b){return Object.keys(b)}),Object.create||(Object.create=function(b,c){var d;if(b===null)d={"__proto__":null};else{if(typeof b!="object")throw new TypeError("typeof prototype["+typeof b+"] != 'object'");var e=function(){};e.prototype=b,d=new e,d.__proto__=b}typeof c!="undefined"&&Object.defineProperties(d,c);return d});if(!Object.defineProperty){var o="Property description must be an object: ",p="Object.defineProperty called on non-object: ",q="getters & setters can not be defined on this javascript engine";Object.defineProperty=function(b,c,d){if(typeof b!="object"&&typeof b!="function")throw new TypeError(p+b);if(typeof b!="object"||b===null)throw new TypeError(o+d);if(h(d,"value"))if(m&&(k(b,c)||l(b,c))){var e=b.__proto__;b.__proto__=g,delete b[c],b[c]=d.value,b.prototype}else b[c]=d.value;else{if(!m)throw new TypeError(q);h(d,"get")&&i(b,c,d.get),h(d,"set")&&j(b,c,d.set)}return b}}Object.defineProperties||(Object.defineProperties=function(b,c){for(var d in c)h(c,d)&&Object.defineProperty(b,d,c[d]);return b}),Object.seal||(Object.seal=function(b){return b}),Object.freeze||(Object.freeze=function(b){return b});try{Object.freeze(function(){})}catch(r){Object.freeze=function(b){return function(c){return typeof c=="function"?c:b(c)}}(Object.freeze)}Object.preventExtensions||(Object.preventExtensions=function(b){return b}),Object.isSealed||(Object.isSealed=function(b){return!1}),Object.isFrozen||(Object.isFrozen=function(b){return!1}),Object.isExtensible||(Object.isExtensible=function(b){return!0});if(!Object.keys){var s=!0,t=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],u=t.length;for(var v in{toString:null})s=!1;Object.keys=function a(b){if(typeof b!="object"&&typeof b!="function"||b===null)throw new TypeError("Object.keys called on a non-object");var a=[];for(var c in b)h(b,c)&&a.push(c);if(s)for(var d=0,e=u;d<e;d++){var f=t[d];h(b,f)&&a.push(f)}return a}}Date.prototype.toISOString||(Date.prototype.toISOString=function(){return this.getUTCFullYear()+"-"+(this.getUTCMonth()+1)+"-"+this.getUTCDate()+"T"+this.getUTCHours()+":"+this.getUTCMinutes()+":"+this.getUTCSeconds()+"Z"}),Date.now||(Date.now=function(){return(new Date).getTime()}),Date.prototype.toJSON||(Date.prototype.toJSON=function(b){if(typeof this.toISOString!="function")throw new TypeError;return this.toISOString()}),isNaN(Date.parse("T00:00"))&&(Date=function(a){var b=function(c,d,e,f,g,h,i){var j=arguments.length;if(this instanceof a){var k=j===1&&String(c)===c?new a(b.parse(c)):j>=7?new a(c,d,e,f,g,h,i):j>=6?new a(c,d,e,f,g,h):j>=5?new a(c,d,e,f,g):j>=4?new a(c,d,e,f):j>=3?new a(c,d,e):j>=2?new a(c,d):j>=1?new a(c):new a;k.constructor=b;return k}return a.apply(this,arguments)},c=new RegExp("^(?:((?:[+-]\\d\\d)?\\d\\d\\d\\d)(?:-(\\d\\d)(?:-(\\d\\d))?)?)?(?:T(\\d\\d):(\\d\\d)(?::(\\d\\d)(?:\\.(\\d\\d\\d))?)?)?(?:Z|([+-])(\\d\\d):(\\d\\d))?$");for(var d in a)b[d]=a[d];b.now=a.now,b.UTC=a.UTC,b.prototype=a.prototype,b.prototype.constructor=b,b.parse=function(d){var e=c.exec(d);if(e){e.shift();var f=e[0]===undefined;for(var g=0;g<10;g++){if(g===7)continue;e[g]=+(e[g]||(g<3?1:0)),g===1&&e[g]--}if(f)return((e[3]*60+e[4])*60+e[5])*1e3+e[6];var h=(e[8]*60+e[9])*60*1e3;e[6]==="-"&&(h=-h);return a.UTC.apply(this,e.slice(0,7))+h}return a.parse.apply(this,arguments)};return b}(Date));if(!String.prototype.trim){var w=/^\s\s*/,x=/\s\s*$/;String.prototype.trim=function(){return String(this).replace(w,"").replace(x,"")}}}),define("pilot/event_emitter",["require","exports","module"],function(a,b,c){var d={};d._emit=d._dispatchEvent=function(a,b){this._eventRegistry=this._eventRegistry||{};var c=this._eventRegistry[a];if(!!c&&!!c.length){var b=b||{};b.type=a;for(var d=0;d<c.length;d++)c[d](b)}},d.on=d.addEventListener=function(a,b){this._eventRegistry=this._eventRegistry||{};var c=this._eventRegistry[a];if(!c)var c=this._eventRegistry[a]=[];c.indexOf(b)==-1&&c.push(b)},d.removeListener=d.removeEventListener=function(a,b){this._eventRegistry=this._eventRegistry||{};var c=this._eventRegistry[a];if(!!c){var d=c.indexOf(b);d!==-1&&c.splice(d,1)}},d.removeAllListeners=function(a){this._eventRegistry&&(this._eventRegistry[a]=[])},b.EventEmitter=d}),define("pilot/oop",["require","exports","module"],function(a,b,c){b.inherits=function(){var a=function(){};return function(b,c){a.prototype=c.prototype,b.super_=c.prototype,b.prototype=new a,b.prototype.constructor=b}}(),b.mixin=function(a,b){for(var c in b)a[c]=b[c]},b.implement=function(a,c){b.mixin(a,c)}}),define("ace/mode/css_worker",["require","exports","module","pilot/oop","ace/worker/mirror","ace/mode/css/csslint"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/worker/mirror").Mirror,f=a("ace/mode/css/csslint").CSSLint,g=b.Worker=function(a){e.call(this,a),this.setTimeout(200)};d.inherits(g,e),function(){this.onUpdate=function(){var a=this.doc.getValue();result=f.verify(a),this.sender.emit("csslint",result.messages)}}.call(g.prototype)}),define("ace/worker/mirror",["require","exports","module","ace/document","pilot/lang"],function(a,b,c){var d=a("ace/document").Document,e=a("pilot/lang"),f=b.Mirror=function(a){this.sender=a;var b=this.doc=new d(""),c=this.deferredUpdate=e.deferredCall(this.onUpdate.bind(this)),f=this;a.on("change",function(a){b.applyDeltas([a.data]),c.schedule(f.$timeout)})};(function(){this.$timeout=500,this.setTimeout=function(a){this.$timeout=a},this.setValue=function(a){this.doc.setValue(a),this.deferredUpdate.schedule(this.$timeout)},this.getValue=function(a){this.sender.callback(this.doc.getValue(),a)},this.onUpdate=function(){}}).call(f.prototype)}),define("ace/document",["require","exports","module","pilot/oop","pilot/event_emitter","ace/range","ace/anchor"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=a("ace/range").Range,g=a("ace/anchor").Anchor,h=function(a){this.$lines=[],Array.isArray(a)?this.insertLines(0,a):a.length==0?this.$lines=[""]:this.insert({row:0,column:0},a)};(function(){d.implement(this,e),this.setValue=function(a){var b=this.getLength();this.remove(new f(0,0,b,this.getLine(b-1).length)),this.insert({row:0,column:0},a)},this.getValue=function(){return this.getAllLines().join(this.getNewLineCharacter())},this.createAnchor=function(a,b){return new g(this,a,b)},"aaa".split(/a/).length==0?this.$split=function(a){return a.replace(/\r\n|\r/g,"\n").split("\n")}:this.$split=function(a){return a.split(/\r\n|\r|\n/)},this.$detectNewLine=function(a){var b=a.match(/^.*?(\r?\n)/m);b?this.$autoNewLine=b[1]:this.$autoNewLine="\n"},this.getNewLineCharacter=function(){switch(this.$newLineMode){case"windows":return"\r\n";case"unix":return"\n";case"auto":return this.$autoNewLine}},this.$autoNewLine="\n",this.$newLineMode="auto",this.setNewLineMode=function(a){this.$newLineMode!==a&&(this.$newLineMode=a)},this.getNewLineMode=function(){return this.$newLineMode},this.isNewLine=function(a){return a=="\r\n"||a=="\r"||a=="\n"},this.getLine=function(a){return this.$lines[a]||""},this.getLines=function(a,b){return this.$lines.slice(a,b+1)},this.getAllLines=function(){return this.getLines(0,this.getLength())},this.getLength=function(){return this.$lines.length},this.getTextRange=function(a){if(a.start.row==a.end.row)return this.$lines[a.start.row].substring(a.start.column,a.end.column);var b=[];b.push(this.$lines[a.start.row].substring(a.start.column)),b.push.apply(b,this.getLines(a.start.row+1,a.end.row-1)),b.push(this.$lines[a.end.row].substring(0,a.end.column));return b.join(this.getNewLineCharacter())},this.$clipPosition=function(a){var b=this.getLength();a.row>=b&&(a.row=Math.max(0,b-1),a.column=this.getLine(b-1).length);return a},this.insert=function(a,b){if(b.length==0)return a;a=this.$clipPosition(a),this.getLength()<=1&&this.$detectNewLine(b);var c=this.$split(b),d=c.splice(0,1)[0],e=c.length==0?null:c.splice(c.length-1,1)[0];this._dispatchEvent("changeStart"),a=this.insertInLine(a,d),e!==null&&(a=this.insertNewLine(a),a=this.insertLines(a.row,c),a=this.insertInLine(a,e||"")),this._dispatchEvent("changeEnd");return a},this.insertLines=function(a,b){if(b.length==0)return{row:a,column:0};var c=[a,0];c.push.apply(c,b),this.$lines.splice.apply(this.$lines,c),this._dispatchEvent("changeStart");var d=new f(a,0,a+b.length,0),e={action:"insertLines",range:d,lines:b};this._dispatchEvent("change",{data:e}),this._dispatchEvent("changeEnd");return d.end},this.insertNewLine=function(a){a=this.$clipPosition(a);var b=this.$lines[a.row]||"";this._dispatchEvent("changeStart"),this.$lines[a.row]=b.substring(0,a.column),this.$lines.splice(a.row+1,0,b.substring(a.column,b.length));var c={row:a.row+1,column:0},d={action:"insertText",range:f.fromPoints(a,c),text:this.getNewLineCharacter()};this._dispatchEvent("change",{data:d}),this._dispatchEvent("changeEnd");return c},this.insertInLine=function(a,b){if(b.length==0)return a;var c=this.$lines[a.row]||"";this._dispatchEvent("changeStart"),this.$lines[a.row]=c.substring(0,a.column)+b+c.substring(a.column);var d={row:a.row,column:a.column+b.length},e={action:"insertText",range:f.fromPoints(a,d),text:b};this._dispatchEvent("change",{data:e}),this._dispatchEvent("changeEnd");return d},this.remove=function(a){a.start=this.$clipPosition(a.start),a.end=this.$clipPosition(a.end);if(a.isEmpty())return a.start;var b=a.start.row,c=a.end.row;this._dispatchEvent("changeStart");if(a.isMultiLine()){var d=a.start.column==0?b:b+1,e=c-1;a.end.column>0&&this.removeInLine(c,0,a.end.column),e>=d&&this.removeLines(d,e),d!=b&&(this.removeInLine(b,a.start.column,this.getLine(b).length),this.removeNewLine(a.start.row))}else this.removeInLine(b,a.start.column,a.end.column);this._dispatchEvent("changeEnd");return a.start},this.removeInLine=function(a,b,c){if(b!=c){var d=new f(a,b,a,c),e=this.getLine(a),g=e.substring(b,c),h=e.substring(0,b)+e.substring(c,e.length);this._dispatchEvent("changeStart"),this.$lines.splice(a,1,h);var i={action:"removeText",range:d,text:g};this._dispatchEvent("change",{data:i}),this._dispatchEvent("changeEnd");return d.start}},this.removeLines=function(a,b){this._dispatchEvent("changeStart");var c=new f(a,0,b+1,0),d=this.$lines.splice(a,b-a+1),e={action:"removeLines",range:c,nl:this.getNewLineCharacter(),lines:d};this._dispatchEvent("change",{data:e}),this._dispatchEvent("changeEnd");return d},this.removeNewLine=function(a){var b=this.getLine(a),c=this.getLine(a+1),d=new f(a,b.length,a+1,0),e=b+c;this._dispatchEvent("changeStart"),this.$lines.splice(a,2,e);var g={action:"removeText",range:d,text:this.getNewLineCharacter()};this._dispatchEvent("change",{data:g}),this._dispatchEvent("changeEnd")},this.replace=function(a,b){if(b.length==0&&a.isEmpty())return a.start;if(b==this.getTextRange(a))return a.end;this._dispatchEvent("changeStart"),this.remove(a);if(b)var c=this.insert(a.start,b);else c=a.start;this._dispatchEvent("changeEnd");return c},this.applyDeltas=function(a){for(var b=0;b<a.length;b++){var c=a[b],d=f.fromPoints(c.range.start,c.range.end);c.action=="insertLines"?this.insertLines(d.start.row,c.lines):c.action=="insertText"?this.insert(d.start,c.text):c.action=="removeLines"?this.removeLines(d.start.row,d.end.row-1):c.action=="removeText"&&this.remove(d)}},this.revertDeltas=function(a){for(var b=a.length-1;b>=0;b--){var c=a[b],d=f.fromPoints(c.range.start,c.range.end);c.action=="insertLines"?this.removeLines(d.start.row,d.end.row-1):c.action=="insertText"?this.remove(d):c.action=="removeLines"?this.insertLines(d.start.row,c.lines):c.action=="removeText"&&this.insert(d.start,c.text)}}}).call(h.prototype),b.Document=h}),define("ace/range",["require","exports","module"],function(a,b,c){var d=function(a,b,c,d){this.start={row:a,column:b},this.end={row:c,column:d}};(function(){this.toString=function(){return"Range: ["+this.start.row+"/"+this.start.column+"] -> ["+this.end.row+"/"+this.end.column+"]"},this.contains=function(a,b){return this.compare(a,b)==0},this.compareRange=function(a){var b,c=a.end,d=a.start;b=this.compare(c.row,c.column);if(b==1){b=this.compare(d.row,d.column);return b==1?2:b==0?1:0}if(b==-1)return-2;b=this.compare(d.row,d.column);return b==-1?-1:b==1?42:0},this.containsRange=function(a){var b=this.compareRange(a);return b==-1||b==0||b==1},this.isEnd=function(a,b){return this.end.row==a&&this.end.column==b},this.isStart=function(a,b){return this.start.row==a&&this.start.column==b},this.setStart=function(a,b){typeof a=="object"?(this.start.column=a.column,this.start.row=a.row):(this.start.row=a,this.start.column=b)},this.setEnd=function(a,b){typeof a=="object"?(this.end.column=a.column,this.end.row=a.row):(this.end.row=a,this.end.column=b)},this.inside=function(a,b){if(this.compare(a,b)==0)return this.isEnd(a,b)||this.isStart(a,b)?!1:!0;return!1},this.insideStart=function(a,b){if(this.compare(a,b)==0)return this.isEnd(a,b)?!1:!0;return!1},this.insideEnd=function(a,b){if(this.compare(a,b)==0)return this.isStart(a,b)?!1:!0;return!1},this.compare=function(a,b){if(!this.isMultiLine()&&a===this.start.row)return b<this.start.column?-1:b>this.end.column?1:0;if(a<this.start.row)return-1;if(a>this.end.row)return 1;if(this.start.row===a)return b>=this.start.column?0:-1;if(this.end.row===a)return b<=this.end.column?0:1;return 0},this.compareStart=function(a,b){return this.start.row==a&&this.start.column==b?-1:this.compare(a,b)},this.compareEnd=function(a,b){return this.end.row==a&&this.end.column==b?1:this.compare(a,b)},this.compareInside=function(a,b){return this.end.row==a&&this.end.column==b?1:this.start.row==a&&this.start.column==b?-1:this.compare(a,b)},this.clipRows=function(a,b){if(this.end.row>b)var c={row:b+1,column:0};if(this.start.row>b)var e={row:b+1,column:0};if(this.start.row<a)var e={row:a,column:0};if(this.end.row<a)var c={row:a,column:0};return d.fromPoints(e||this.start,c||this.end)},this.extend=function(a,b){var c=this.compare(a,b);if(c==0)return this;if(c==-1)var e={row:a,column:b};else var f={row:a,column:b};return d.fromPoints(e||this.start,f||this.end)},this.isEmpty=function(){return this.start.row==this.end.row&&this.start.column==this.end.column},this.isMultiLine=function(){return this.start.row!==this.end.row},this.clone=function(){return d.fromPoints(this.start,this.end)},this.collapseRows=function(){return this.end.column==0?new d(this.start.row,0,Math.max(this.start.row,this.end.row-1),0):new d(this.start.row,0,this.end.row,0)},this.toScreenRange=function(a){var b=a.documentToScreenPosition(this.start),c=a.documentToScreenPosition(this.end);return new d(b.row,b.column,c.row,c.column)}}).call(d.prototype),d.fromPoints=function(a,b){return new d(a.row,a.column,b.row,b.column)},b.Range=d}),define("ace/anchor",["require","exports","module","pilot/oop","pilot/event_emitter"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=b.Anchor=function(a,b,c){this.document=a,typeof c=="undefined"?this.setPosition(b.row,b.column):this.setPosition(b,c),this.$onChange=this.onChange.bind(this),a.on("change",this.$onChange)};(function(){d.implement(this,e),this.getPosition=function(){return this.$clipPositionToDocument(this.row,this.column)},this.getDocument=function(){return this.document},this.onChange=function(a){var b=a.data,c=b.range;if(c.start.row!=c.end.row||c.start.row==this.row){if(c.start.row>this.row)return;if(c.start.row==this.row&&c.start.column>this.column)return;var d=this.row,e=this.column;b.action==="insertText"?c.start.row===d&&c.start.column<=e?c.start.row===c.end.row?e+=c.end.column-c.start.column:(e-=c.start.column,d+=c.end.row-c.start.row):c.start.row!==c.end.row&&c.start.row<d&&(d+=c.end.row-c.start.row):b.action==="insertLines"?c.start.row<=d&&(d+=c.end.row-c.start.row):b.action=="removeText"?c.start.row==d&&c.start.column<e?c.end.column>=e?e=c.start.column:e=Math.max(0,e-(c.end.column-c.start.column)):c.start.row!==c.end.row&&c.start.row<d?(c.end.row==d&&(e=Math.max(0,e-c.end.column)+c.start.column),d-=c.end.row-c.start.row):c.end.row==d&&(d-=c.end.row-c.start.row,e=Math.max(0,e-c.end.column)+c.start.column):b.action=="removeLines"&&c.start.row<=d&&(c.end.row<=d?d-=c.end.row-c.start.row:(d=c.start.row,e=0)),this.setPosition(d,e,!0)}},this.setPosition=function(a,b,c){c?pos={row:a,column:b}:pos=this.$clipPositionToDocument(a,b);if(this.row!=pos.row||this.column!=pos.column){var d={row:this.row,column:this.column};this.row=pos.row,this.column=pos.column,this._dispatchEvent("change",{old:d,value:pos})}},this.detach=function(){this.document.removeEventListener("change",this.$onChange)},this.$clipPositionToDocument=function(a,b){var c={};a>=this.document.getLength()?(c.row=Math.max(0,this.document.getLength()-1),c.column=this.document.getLine(c.row).length):a<0?(c.row=0,c.column=0):(c.row=a,c.column=Math.min(this.document.getLine(c.row).length,Math.max(0,b))),b<0&&(c.column=0);return c}}).call(f.prototype)}),define("pilot/lang",["require","exports","module"],function(a,b,c){b.stringReverse=function(a){return a.split("").reverse().join("")},b.stringRepeat=function(a,b){return Array(b+1).join(a)};var d=/^\s\s*/,e=/\s\s*$/;b.stringTrimLeft=function(a){return a.replace(d,"")},b.stringTrimRight=function(a){return a.replace(e,"")},b.copyObject=function(a){var b={};for(var c in a)b[c]=a[c];return b},b.copyArray=function(a){var b=[];for(i=0,l=a.length;i<l;i++)a[i]&&typeof a[i]=="object"?b[i]=this.copyObject(a[i]):b[i]=a[i];return b},b.deepCopy=function(a){if(typeof a!="object")return a;var b=a.constructor();for(var c in a)typeof a[c]=="object"?b[c]=this.deepCopy(a[c]):b[c]=a[c];return b},b.arrayToMap=function(a){var b={};for(var c=0;c<a.length;c++)b[a[c]]=1;return b},b.arrayRemove=function(a,b){for(var c=0;c<=a.length;c++)b===a[c]&&a.splice(c,1)},b.escapeRegExp=function(a){return a.replace(/([.*+?^${}()|[\]\/\\])/g,"\\$1")},b.deferredCall=function(a){var b=null,c=function(){b=null,a()},d=function(a){b||(b=setTimeout(c,a||0));return d};d.schedule=d,d.call=function(){this.cancel(),a();return d},d.cancel=function(){clearTimeout(b),b=null;return d};return d}}),define("ace/mode/css/csslint",["require","exports","module"],function(require,exports,module){function indexOf(a,b){if(a.indexOf)return a.indexOf(b);for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1}function mix(a,b){var c;for(c in b)b.hasOwnProperty(c)&&(receiver[c]=b[c]);return c}function Reporter(a){this.messages=[],this.stats=[],this.lines=a}var parserlib={};(function(){function e(a,c){this._reader=a?new b(a.toString()):null,this._token=null,this._tokenData=c,this._lt=[],this._ltIndex=0,this._ltIndexCache=[]}function d(a,b,c){this.col=c,this.line=b,this.text=a}function c(a,b,c){this.col=c,this.line=b,this.message=a}function b(a){this._input=a.replace(/\n\r?/g,"\n"),this._line=1,this._col=1,this._cursor=0}function a(){this._listeners={}}a.prototype={constructor:a,addListener:function(a,b){this._listeners[a]||(this._listeners[a]=[]),this._listeners[a].push(b)},fire:function(a){typeof a=="string"&&(a={type:a}),a.target||(a.target=this);if(!a.type)throw new Error("Event object missing 'type' property.");if(this._listeners[a.type]){var b=this._listeners[a.type].concat();for(var c=0,d=b.length;c<d;c++)b[c].call(this,a)}},removeListener:function(a,b){if(this._listeners[a]){var c=this._listeners[a];for(var d=0,e=c.length;d<e;d++)if(c[d]===b){c.splice(d,1);break}}}},b.prototype={constructor:b,getCol:function(){return this._col},getLine:function(){return this._line},eof:function(){return this._cursor==this._input.length},peek:function(a){var b=null;a=typeof a=="undefined"?1:a,this._cursor<this._input.length&&(b=this._input.charAt(this._cursor+a-1));return b},read:function(){var a=null;this._cursor<this._input.length&&(this._input.charAt(this._cursor)=="\n"?(this._line++,this._col=1):this._col++,a=this._input.charAt(this._cursor++));return a},mark:function(){this._bookmark={cursor:this._cursor,line:this._line,col:this._col}},reset:function(){this._bookmark&&(this._cursor=this._bookmark.cursor,this._line=this._bookmark.line,this._col=this._bookmark.col,delete this._bookmark)},readTo:function(a){var b="",c;while(b.length<a.length||b.lastIndexOf(a)!=b.length-a.length){c=this.read();if(c)b+=c;else throw new Error('Expected "'+a+'" at line '+this._line+", col "+this._col+".")}return b},readWhile:function(a){var b="",c=this.read();while(c!==null&&a(c))b+=c,c=this.read();return b},readMatch:function(a){var b=this._input.substring(this._cursor),c=null;typeof a=="string"?b.indexOf(a)===0&&(c=this.readCount(a.length)):a instanceof RegExp&&a.test(b)&&(c=this.readCount(RegExp.lastMatch.length));return c},readCount:function(a){var b="";while(a--)b+=this.read();return b}},c.prototype=new Error,d.fromToken=function(a){return new d(a.value,a.startLine,a.startCol)},d.prototype={constructor:d,valueOf:function(){return this.toString()},toString:function(){return this.text}},e.createTokenData=function(a){var b=[],c={},d=a.concat([]),e=0,f=d.length+1;d.UNKNOWN=-1,d.unshift({name:"EOF"});for(;e<f;e++)b.push(d[e].name),d[d[e].name]=e,d[e].text&&(c[d[e].text]=e);d.name=function(a){return b[a]},d.type=function(a){return c[a]};return d},e.prototype={constructor:e,match:function(a,b){a instanceof Array||(a=[a]);var c=this.get(b),d=0,e=a.length;while(d<e)if(c==a[d++])return!0;this.unget();return!1},mustMatch:function(a,b){a instanceof Array||(a=[a]);if(!this.match.apply(this,arguments)){token=this.LT(1);throw new c("Expected "+this._tokenData[a[0]].name+" at line "+token.startLine+", character "+token.startCol+".",token.startLine,token.startCol)}},advance:function(a,b){while(this.LA(0)!=0&&!this.match(a,b))this.get();return this.LA(0)},get:function(a){var b=this._tokenData,c=this._reader,d,e=0,f=b.length,g=!1,h,i;if(this._lt.length&&this._ltIndex>=0&&this._ltIndex<this._lt.length){e++,this._token=this._lt[this._ltIndex++],i=b[this._token.type];while(i.channel!==undefined&&a!==i.channel&&this._ltIndex<this._lt.length)this._token=this._lt[this._ltIndex++],i=b[this._token.type],e++;if((i.channel===undefined||a===i.channel)&&this._ltIndex<=this._lt.length){this._ltIndexCache.push(e);return this._token.type}}h=this._getToken(),h.type>-1&&!b[h.type].hide&&(h.channel=b[h.type].channel,this._token=h,this._lt.push(h),this._ltIndexCache.push(this._lt.length-this._ltIndex+e),this._lt.length>5&&this._lt.shift(),this._ltIndexCache.length>5&&this._ltIndexCache.shift(),this._ltIndex=this._lt.length),i=b[h.type];return i&&(i.hide||i.channel!==undefined&&a!==i.channel)?this.get(a):h.type},LA:function(a){var b=a,c;if(a>0){if(a>5)throw new Error("Too much lookahead.");while(b)c=this.get(),b--;while(b<a)this.unget(),b++}else if(a<0)if(this._lt[this._ltIndex+a])c=this._lt[this._ltIndex+a].type;else throw new Error("Too much lookbehind.");else c=this._token.type;return c},LT:function(a){this.LA(a);return this._lt[this._ltIndex+a-1]},peek:function(){return this.LA(1)},token:function(){return this._token},tokenName:function(a){return a<0||a>this._tokenData.length?"UNKNOWN_TOKEN":this._tokenData[a].name},tokenType:function(a){return this._tokenData[a]||-1},unget:function(){if(this._ltIndexCache.length)this._ltIndex-=this._ltIndexCache.pop(),this._token=this._lt[this._ltIndex-1];else throw new Error("Too much lookahead.")}},parserlib.util={StringReader:b,SyntaxError:c,SyntaxUnit:d,EventTarget:a,TokenStreamBase:e}})(),function(){function TokenStream(a){TokenStreamBase.call(this,a,Tokens)}function mix(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return a}function isIdentStart(a){return a!=null&&(isNameStart(a)||a=="-")}function isNameChar(a){return a!=null&&(isNameStart(a)||/[0-9\-]/.test(a))}function isNameStart(a){return a!=null&&/[a-z_\u0080-\uFFFF\\]/i.test(a)}function isNewLine(a){return a!=null&&nl.test(a)}function isWhitespace(a){return a!=null&&/\s/.test(a)}function isDigit(a){return a!=null&&/\d/.test(a)}function isHexDigit(a){return a!=null&&h.test(a)}function SelectorSubPart(a,b,c,d){SyntaxUnit.call(this,a,c,d),this.type=b,this.args=[]}function SelectorPart(a,b,c,d,e){SyntaxUnit.call(this,c,d,e),this.elementName=a,this.modifiers=b}function Selector(a,b,c){SyntaxUnit.call(this,a.join(" "),b,c),this.parts=a}function PropertyValuePart(text,line,col){SyntaxUnit.apply(this,arguments),this.type="unknown";var temp;if(/^([+\-]?[\d\.]+)([a-z]+)$/i.test(text)){this.type="dimension",this.value=+RegExp.$1,this.units=RegExp.$2;switch(this.units.toLowerCase()){case"em":case"rem":case"ex":case"px":case"cm":case"mm":case"in":case"pt":case"pc":this.type="length";break;case"deg":case"rad":case"grad":this.type="angle";break;case"ms":case"s":this.type="time";break;case"hz":case"khz":this.type="frequency";break;case"dpi":case"dpcm":this.type="resolution"}}else/^([+\-]?[\d\.]+)%$/i.test(text)?(this.type="percentage",this.value=+RegExp.$1):/^([+\-]?[\d\.]+)%$/i.test(text)?(this.type="percentage",this.value=+RegExp.$1):/^([+\-]?\d+)$/i.test(text)?(this.type="integer",this.value=+RegExp.$1):/^([+\-]?[\d\.]+)$/i.test(text)?(this.type="number",this.value=+RegExp.$1):/^#([a-f0-9]{3,6})/i.test(text)?(this.type="color",temp=RegExp.$1,temp.length==3?(this.red=parseInt(temp.charAt(0)+temp.charAt(0),16),this.green=parseInt(temp.charAt(1)+temp.charAt(1),16),this.blue=parseInt(temp.charAt(2)+temp.charAt(2),16)):(this.red=parseInt(temp.substring(0,2),16),this.green=parseInt(temp.substring(2,4),16),this.blue=parseInt(temp.substring(4,6),16))):/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/i.test(text)?(this.type="color",this.red=+RegExp.$1,this.green=+RegExp.$2,this.blue=+RegExp.$3):/^rgb\(\s*(\d+)%\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)/i.test(text)?(this.type="color",this.red=+RegExp.$1*255/100,this.green=+RegExp.$2*255/100,this.blue=+RegExp.$3*255/100):/^url\(["']?([^\)"']+)["']?\)/i.test(text)?(this.type="uri",this.uri=RegExp.$1):/^["'][^"']*["']/.test(text)?(this.type="string",this.value=eval(text)):Colors[text.toLowerCase()]?(this.type="color",temp=Colors[text.toLowerCase()].substring(1),this.red=parseInt(temp.substring(0,2),16),this.green=parseInt(temp.substring(2,4),16),this.blue=parseInt(temp.substring(4,6),16)):/^[\,\/]$/.test(text)?(this.type="operator",this.value=text):/^[a-z\-\u0080-\uFFFF][a-z0-9\-\u0080-\uFFFF]*$/i.test(text)&&(this.type="identifier",this.value=text)}function PropertyValue(a,b,c){SyntaxUnit.call(this,a.join(" "),b,c),this.parts=a}function PropertyName(a,b,c,d){SyntaxUnit.call(this,(b||"")+a,c,d),this.hack=b}function Parser(a){EventTarget.call(this),this.options=a||{},this._tokenStream=null}function MediaQuery(a,b,c,d,e){SyntaxUnit.call(this,(a?a+" ":"")+(b?b+" ":"")+c.join(" and "),d,e),this.modifier=a,this.mediaType=b,this.features=c}function MediaFeature(a,b){SyntaxUnit.call(this,"("+a+(b!==null?":"+b:"")+")",a.startLine,a.startCol),this.name=a,this.value=b}function Combinator(a,b,c){SyntaxUnit.call(this,a,b,c),this.type="unknown",/^\s+$/.test(a)?this.type="descendant":a==">"?this.type="child":a=="+"?this.type="adjacent-sibling":a=="~"&&(this.type="sibling")}var EventTarget=parserlib.util.EventTarget,TokenStreamBase=parserlib.util.TokenStreamBase,StringReader=parserlib.util.StringReader,SyntaxError=parserlib.util.SyntaxError,SyntaxUnit=parserlib.util.SyntaxUnit,Colors={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgreen:"#006400",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",green:"#008000",greenyellow:"#adff2f",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgrey:"#d3d3d3",lightgreen:"#90ee90",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370d8",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#d87093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"};Combinator.prototype=new SyntaxUnit,Combinator.prototype.constructor=Combinator;var Level1Properties={background:1,"background-attachment":1,"background-color":1,"background-image":1,"background-position":1,"background-repeat":1,border:1,"border-bottom":1,"border-bottom-width":1,"border-color":1,"border-left":1,"border-left-width":1,"border-right":1,"border-right-width":1,"border-style":1,"border-top":1,"border-top-width":1,"border-width":1,clear:1,color:1,display:1,"float":1,font:1,"font-family":1,"font-size":1,"font-style":1,"font-variant":1,"font-weight":1,height:1,"letter-spacing":1,"line-height":1,"list-style":1,"list-style-image":1,"list-style-position":1,"list-style-type":1,margin:1,"margin-bottom":1,"margin-left":1,"margin-right":1,"margin-top":1,padding:1,"padding-bottom":1,"padding-left":1,"padding-right":1,"padding-top":1,"text-align":1,"text-decoration":1,"text-indent":1,"text-transform":1,"vertical-align":1,"white-space":1,width:1,"word-spacing":1},Level2Properties={azimuth:1,"cue-after":1,"cue-before":1,cue:1,elevation:1,"pause-after":1,"pause-before":1,pause:1,"pitch-range":1,pitch:1,"play-during":1,richness:1,"speak-header":1,"speak-numeral":1,"speak-punctuation":1,speak:1,"speech-rate":1,stress:1,"voice-family":1,volume:1,orphans:1,"page-break-after":1,"page-break-before":1,"page-break-inside":1,widows:1,cursor:1,"outline-color":1,"outline-style":1,"outline-width":1,outline:1,"background-attachment":1,"background-color":1,"background-image":1,"background-position":1,"background-repeat":1,background:1,"border-collapse":1,"border-color":1,"border-spacing":1,"border-style":1,"border-top":1,"border-top-color":1,"border-top-style":1,"border-top-width":1,"border-width":1,border:1,bottom:1,"caption-side":1,clear:1,clip:1,color:1,content:1,"counter-increment":1,"counter-reset":1,direction:1,display:1,"empty-cells":1,"float":1,"font-family":1,"font-size":1,"font-style":1,"font-variant":1,"font-weight":1,font:1,height:1,left:1,"letter-spacing":1,"line-height":1,"list-style-image":1,"list-style-position":1,"list-style-type":1,"list-style":1,"margin-right":1,"margin-top":1,margin:1,"max-height":1,"max-width":1,"min-height":1,"min-width":1,overflow:1,"padding-top":1,padding:1,position:1,quotes:1,right:1,"table-layout":1,"text-align":1,"text-decoration":1,"text-indent":1,"text-transform":1,top:1,"unicode-bidi":1,"vertical-align":1,visibility:1,"white-space":1,width:1,"word-spacing":1,"z-index":1};MediaFeature.prototype=new SyntaxUnit,MediaFeature.prototype.constructor=MediaFeature,MediaQuery.prototype=new SyntaxUnit,MediaQuery.prototype.constructor=MediaQuery,Parser.prototype=function(){var a=new EventTarget,b,c={constructor:Parser,_stylesheet:function(){var a=this._tokenStream,b=null,c,d;this.fire("startstylesheet"),this._charset(),this._skipCruft();while(a.peek()==Tokens.IMPORT_SYM)this._import(),this._skipCruft();while(a.peek()==Tokens.NAMESPACE_SYM)this._namespace(),this._skipCruft();d=a.peek();while(d>Tokens.EOF){try{switch(d){case Tokens.MEDIA_SYM:this._media(),this._skipCruft();break;case Tokens.PAGE_SYM:this._page(),this._skipCruft();break;case Tokens.FONT_FACE_SYM:this._font_face(),this._skipCruft();break;case Tokens.S:this._readWhitespace();break;default:if(!this._ruleset())switch(d){case Tokens.CHARSET_SYM:c=a.LT(1),this._charset(!1);throw new SyntaxError("@charset not allowed here.",c.startLine,c.startCol);case Tokens.IMPORT_SYM:c=a.LT(1),this._import(!1);throw new SyntaxError("@import not allowed here.",c.startLine,c.startCol);case Tokens.NAMESPACE_SYM:c=a.LT(1),this._namespace(!1);throw new SyntaxError("@namespace not allowed here.",c.startLine,c.startCol);default:a.get(),this._unexpectedToken(a.token())}}}catch(e){if(e instanceof SyntaxError&&!this.options.strict)this.fire({type:"error",error:e,message:e.message,line:e.line,col:e.col});else throw e}d=a.peek()}d!=Tokens.EOF&&this._unexpectedToken(a.token()),this.fire("endstylesheet")},_charset:function(a){var b=this._tokenStream;b.match(Tokens.CHARSET_SYM)&&(this._readWhitespace(),b.mustMatch(Tokens.STRING),token=b.token(),charset=token.value,this._readWhitespace(),b.mustMatch(Tokens.SEMICOLON),a!==!1&&this.fire({type:"charset",charset:charset}))},_import:function(a){var b=this._tokenStream,c,d,e=[];b.mustMatch(Tokens.IMPORT_SYM),this._readWhitespace(),b.mustMatch([Tokens.STRING,Tokens.URI]),d=b.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/,"$1"),this._readWhitespace(),e=this._media_query_list(),b.mustMatch(Tokens.SEMICOLON),this._readWhitespace(),a!==!1&&this.fire({type:"import",uri:d,media:e})},_namespace:function(a){var b=this._tokenStream,c,d;b.mustMatch(Tokens.NAMESPACE_SYM),this._readWhitespace(),b.match(Tokens.IDENT)&&(c=b.token().value,this._readWhitespace()),b.mustMatch([Tokens.STRING,Tokens.URI]),d=b.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/,"$1"),this._readWhitespace(),b.mustMatch(Tokens.SEMICOLON),this._readWhitespace(),a!==!1&&this.fire({type:"namespace",prefix:c,uri:d})},_media:function(){var a=this._tokenStream,b;a.mustMatch(Tokens.MEDIA_SYM),this._readWhitespace(),b=this._media_query_list(),a.mustMatch(Tokens.LBRACE),this._readWhitespace(),this.fire({type:"startmedia",media:b});while(this._ruleset());a.mustMatch(Tokens.RBRACE),this._readWhitespace(),this.fire({type:"endmedia",media:b})},_media_query_list:function(){var a=this._tokenStream,b=[];this._readWhitespace(),a.peek()==Tokens.IDENT&&b.push(this._media_query());while(a.match(Tokens.COMMA))this._readWhitespace(),b.push(this._media_query());return b},_media_query:function(){var a=this._tokenStream,b=null,c=null,d=null,e=[];a.match(Tokens.IDENT)&&(c=a.token().value.toLowerCase(),c!="only"&&c!="not"?(a.unget(),c=null):d=a.token()),this._readWhitespace(),a.peek()==Tokens.IDENT?(b=this._media_type(),d===null&&(d=a.token())):a.peek()==Tokens.LPAREN&&(d===null&&(d=a.LT(1)),e.push(this._media_expression()));if(b===null&&e.length===0)return null;this._readWhitespace();while(a.match(Tokens.IDENT))a.token().value.toLowerCase()!="and"&&this._unexpectedToken(a.token()),this._readWhitespace(),e.push(this._media_expression());return new MediaQuery(c,b,e,d.startLine,d.startCol)},_media_type:function(){return this._media_feature()},_media_expression:function(){var a=this._tokenStream,b=null,c,d=null;a.mustMatch(Tokens.LPAREN),b=this._media_feature(),this._readWhitespace(),a.match(Tokens.COLON)&&(this._readWhitespace(),c=a.LT(1),d=this._expression()),a.mustMatch(Tokens.RPAREN),this._readWhitespace();return new MediaFeature(b,d?new SyntaxUnit(d,c.startLine,c.startCol):null)},_media_feature:function(){var a=this._tokenStream;a.mustMatch(Tokens.IDENT);return SyntaxUnit.fromToken(a.token())},_page:function(){var a=this._tokenStream,b=null,c=null;a.mustMatch(Tokens.PAGE_SYM),this._readWhitespace(),a.match(Tokens.IDENT)&&(b=a.token().value,b.toLowerCase()==="auto"&&this._unexpectedToken(a.token())),a.peek()==Tokens.COLON&&(c=this._pseudo_page()),this._readWhitespace(),this.fire({type:"startpage",id:b,pseudo:c}),this._readDeclarations(!0,!0),this.fire({type:"endpage",id:b,pseudo:c})},_margin:function(){var a=this._tokenStream,b=this._margin_sym();if(b){this.fire({type:"startpagemargin",margin:b}),this._readDeclarations(!0),this.fire({type:"endpagemargin",margin:b});return!0}return!1},_margin_sym:function(){var a=this._tokenStream;return a.match([Tokens.TOPLEFTCORNER_SYM,Tokens.TOPLEFT_SYM,Tokens.TOPCENTER_SYM,Tokens.TOPRIGHT_SYM,Tokens.TOPRIGHTCORNER_SYM,Tokens.BOTTOMLEFTCORNER_SYM,Tokens.BOTTOMLEFT_SYM,Tokens.BOTTOMCENTER_SYM,Tokens.BOTTOMRIGHT_SYM,Tokens.BOTTOMRIGHTCORNER_SYM,Tokens.LEFTTOP_SYM,Tokens.LEFTMIDDLE_SYM,Tokens.LEFTBOTTOM_SYM,Tokens.RIGHTTOP_SYM,Tokens.RIGHTMIDDLE_SYM,Tokens.RIGHTBOTTOM_SYM])?SyntaxUnit.fromToken(a.token()):null},_pseudo_page:function(){var a=this._tokenStream;a.mustMatch(Tokens.COLON),a.mustMatch(Tokens.IDENT);return a.token().value},_font_face:function(){var a=this._tokenStream;a.mustMatch(Tokens.FONT_FACE_SYM),this._readWhitespace(),this.fire({type:"startfontface"}),this._readDeclarations(!0),this.fire({type:"endfontface"})},_operator:function(){var a=this._tokenStream,b=null;a.match([Tokens.SLASH,Tokens.COMMA])&&(b=a.token(),this._readWhitespace());return b?PropertyValuePart.fromToken(b):null},_combinator:function(){var a=this._tokenStream,b=null,c;a.match([Tokens.PLUS,Tokens.GREATER,Tokens.TILDE])&&(c=a.token(),b=new Combinator(c.value,c.startLine,c.startCol),this._readWhitespace());return b},_unary_operator:function(){var a=this._tokenStream;return a.match([Tokens.MINUS,Tokens.PLUS])?a.token().value:null},_property:function(){var a=this._tokenStream,b=null,c=null,d,e,f,g;a.peek()==Tokens.STAR&&this.options.starHack&&(a.get(),e=a.token(),c=e.value,f=e.startLine,g=e.startCol),a.match(Tokens.IDENT)&&(e=a.token(),d=e.value,d.charAt(0)=="_"&&this.options.underscoreHack&&(c="_",d=d.substring(1)),b=new PropertyName(d,c,f||e.startLine,g||e.startCol),this._readWhitespace());return b},_ruleset:function(){var a=this._tokenStream,b;try{b=this._selectors_group()}catch(c){if(!(c instanceof SyntaxError&&!this.options.strict))throw c;this.fire({type:"error",error:c,message:c.message,line:c.line,col:c.col}),tt=a.advance([Tokens.RBRACE]);if(tt!=Tokens.RBRACE)throw c;return!0}b&&(this.fire({type:"startrule",selectors:b}),this._readDeclarations(!0),this.fire({type:"endrule",selectors:b}));return b},_selectors_group:function(){var a=this._tokenStream,b=[],c;c=this._selector();if(c!==null){b.push(c);while(a.match(Tokens.COMMA))this._readWhitespace(),c=this._selector(),c!==null?b.push(c):this._unexpectedToken(a.LT(1))}return b.length?b:null},_selector:function(){var a=this._tokenStream,b=[],c=null,d=null,e=null;c=this._simple_selector_sequence();if(c===null)return null;b.push(c);do{d=this._combinator();if(d!==null)b.push(d),c=this._simple_selector_sequence(),c===null?this._unexpectedToken(this.LT(1)):b.push(c);else if(this._readWhitespace())e=new Combinator(a.token().value,a.token().startLine,a.token().startCol),d=this._combinator(),c=this._simple_selector_sequence(),c===null?d!==null&&this._unexpectedToken(a.LT(1)):(d!==null?b.push(d):b.push(e),b.push(c));else break}while(!0);return new Selector(b,b[0].line,b[0].col)},_simple_selector_sequence:function(){var a=this._tokenStream,b=null,c=[],d="",e=[function(){return a.match(Tokens.HASH)?new SelectorSubPart(a.token().value,"id",a.token().startLine,a.token().startCol):null},this._class,this._attrib,this._pseudo,this._negation],f=0,g=e.length,h=null,i=!1,j,k;j=a.LT(1).startLine,k=a.LT(1).startCol,b=this._type_selector(),b||(b=this._universal()),b!==null&&(d+=b);for(;;){if(a.peek()===Tokens.S)break;while(f<g&&h===null)h=e[f++].call(this);if(h===null){if(d==="")return null;break}f=0,c.push(h),d+=h.toString(),h=null}return d!==""?new SelectorPart(b,c,d,j,k):null},_type_selector:function(){var a=this._tokenStream,b=this._namespace_prefix(),c=this._element_name();if(!c){b&&(a.unget(),b.length>1&&a.unget());return null}b&&(c.text=b+c.text,c.col-=b.length);return c},_class:function(){var a=this._tokenStream,b;if(a.match(Tokens.DOT)){a.mustMatch(Tokens.IDENT),b=a.token();return new SelectorSubPart("."+b.value,"class",b.startLine,b.startCol-1)}return null},_element_name:function(){var a=this._tokenStream,b;if(a.match(Tokens.IDENT)){b=a.token();return new SelectorSubPart(b.value,"elementName",b.startLine,b.startCol)}return null},_namespace_prefix:function(){var a=this._tokenStream,b="";if(a.LA(1)===Tokens.PIPE||a.LA(2)===Tokens.PIPE)a.match([Tokens.IDENT,Tokens.STAR])&&(b+=a.token().value),a.mustMatch(Tokens.PIPE),b+="|";return b.length?b:null},_universal:function(){var a=this._tokenStream,b="",c;c=this._namespace_prefix(),c&&(b+=c),a.match(Tokens.STAR)&&(b+="*");return b.length?b:null},_attrib:function(){var a=this._tokenStream,b=null,c,d;if(a.match(Tokens.LBRACKET)){d=a.token(),b=d.value,b+=this._readWhitespace(),c=this._namespace_prefix(),c&&(b+=c),a.mustMatch(Tokens.IDENT),b+=a.token().value,b+=this._readWhitespace(),a.match([Tokens.PREFIXMATCH,Tokens.SUFFIXMATCH,Tokens.SUBSTRINGMATCH,Tokens.EQUALS,Tokens.INCLUDES,Tokens.DASHMATCH])&&(b+=a.token().value,b+=this._readWhitespace(),a.mustMatch([Tokens.IDENT,Tokens.STRING]),b+=a.token().value,b+=this._readWhitespace()),a.mustMatch(Tokens.RBRACKET);return new SelectorSubPart(b+"]","attribute",d.startLine,d.startCol)}return null},_pseudo:function(){var a=this._tokenStream,b=null,c=":",d,e;a.match(Tokens.COLON)&&(a.match(Tokens.COLON)&&(c+=":"),a.match(Tokens.IDENT)?(b=a.token().value,d=a.token().startLine,e=a.token().startCol-c.length):a.peek()==Tokens.FUNCTION&&(d=a.LT(1).startLine,e=a.LT(1).startCol-c.length,b=this._functional_pseudo()),b&&(b=new SelectorSubPart(c+b,"pseudo",d,e)));return b},_functional_pseudo:function(){var a=this._tokenStream,b=null;a.match(Tokens.FUNCTION)&&(b=a.token().value,b+=this._readWhitespace(),b+=this._expression(),a.mustMatch(Tokens.RPAREN),b+=")");return b},_expression:function(){var a=this._tokenStream,b="";while(a.match([Tokens.PLUS,Tokens.MINUS,Tokens.DIMENSION,Tokens.NUMBER,Tokens.STRING,Tokens.IDENT,Tokens.LENGTH,Tokens.FREQ,Tokens.EMS,Tokens.EXS,Tokens.ANGLE,Tokens.TIME,Tokens.RESOLUTION]))b+=a.token().value,b+=this._readWhitespace();return b.length?b:null},_negation:function(){var a=this._tokenStream,b,c,d="",e,f=null;a.match(Tokens.NOT)&&(d=a.token().value,b=a.token().startLine,c=a.token().startCol,d+=this._readWhitespace(),e=this._negation_arg(),d+=e,d+=this._readWhitespace(),a.match(Tokens.RPAREN),d+=a.token().value,f=new SelectorSubPart(d,"not",b,c),f.args.push(e));return f},_negation_arg:function(){var a=this._tokenStream,b=[this._type_selector,this._universal,function(){return a.match(Tokens.HASH)?new SelectorSubPart(a.token().value,"id",a.token().startLine,a.token().startCol):null},this._class,this._attrib,this._pseudo],c=null,d=0,e=b.length,f,g,h,i;g=a.LT(1).startLine,h=a.LT(1).startCol;while(d<e&&c===null)c=b[d].call(this),d++;c===null&&this._unexpectedToken(a.LT(1)),c.type=="elementName"?i=new SelectorPart(c,[],c.toString(),g,h):i=new SelectorPart(null,[c],c.toString(),g,h);return i},_declaration:function(){var a=this._tokenStream,b=null,c=null,d=null;b=this._property();if(b!==null){a.mustMatch(Tokens.COLON),this._readWhitespace(),c=this._expr(),(!c||c.length===0)&&this._unexpectedToken(a.LT(1)),d=this._prio(),this.fire({type:"property",property:b,value:c,important:d});return!0}return!1},_prio:function(){var a=this._tokenStream,b=a.match(Tokens.IMPORTANT_SYM);this._readWhitespace();return b},_expr:function(){var a=this._tokenStream,b=[],c=null,d=null;c=this._term();if(c!==null){b.push(c);do{d=this._operator(),d&&b.push(d),c=this._term();if(c===null)break;b.push(c)}while(!0)}return b.length>0?new PropertyValue(b,b[0].startLine,b[0].startCol):null},_term:function(){var a=this._tokenStream,b=null,c=null,d,e;b=this._unary_operator(),b!==null&&(d=a.token().startLine,e=a.token().startCol),a.peek()==Tokens.IE_FUNCTION&&this.options.ieFilters?(c=this._ie_function(),b===null&&(d=a.token().startLine,e=a.token().startCol)):a.match([Tokens.NUMBER,Tokens.PERCENTAGE,Tokens.LENGTH,Tokens.EMS,Tokens.EXS,Tokens.ANGLE,Tokens.TIME,Tokens.FREQ,Tokens.STRING,Tokens.IDENT,Tokens.URI,Tokens.UNICODE_RANGE])?(c=a.token().value,b===null&&(d=a.token().startLine,e=a.token().startCol),this._readWhitespace()):(c=this._hexcolor(),c===null?(b===null&&(d=a.LT(1).startLine,e=a.LT(1).startCol),c===null&&(a.LA(3)==Tokens.EQUALS&&this.options.ieFilters?c=this._ie_function():c=this._function())):b===null&&(d=a.token().startLine,e=a.token().startCol));return c!==null?new PropertyValuePart(b!==null?b+c:c,d,e):null},_function:function(){var a=this._tokenStream,b=null,c=null;a.match(Tokens.FUNCTION)&&(b=a.token().value,this._readWhitespace(),c=this._expr(),a.match(Tokens.RPAREN),b+=c+")",this._readWhitespace());return b},_ie_function:function(){var a=this._tokenStream,b=null,c=null,d;if(a.match([Tokens.IE_FUNCTION,Tokens.FUNCTION])){b=a.token().value;do{this._readWhitespace()&&(b+=a.token().value),a.LA(0)==Tokens.COMMA&&(b+=a.token().value),a.match(Tokens.IDENT),b+=a.token().value,a.match(Tokens.EQUALS),b+=a.token().value,d=a.peek();while(d!=Tokens.COMMA&&d!=Tokens.S&&d!=Tokens.RPAREN)a.get(),b+=a.token().value,d=a.peek()}while(a.match([Tokens.COMMA,Tokens.S]));a.match(Tokens.RPAREN),b+=")",this._readWhitespace()}return b},_hexcolor:function(){var a=this._tokenStream,b,c=null;if(a.match(Tokens.HASH)){b=a.token(),c=b.value;if(!/#[a-f0-9]{3,6}/i.test(c))throw new SyntaxError("Expected a hex color but found '"+c+"' at line "+b.startLine+", character "+b.startCol+".",b.startLine,b.startCol);this._readWhitespace()}return c},_skipCruft:function(){while(this._tokenStream.match([Tokens.S,Tokens.CDO,Tokens.CDC]));},_readDeclarations:function(a,b){var c=this._tokenStream,d;this._readWhitespace(),a&&c.mustMatch(Tokens.LBRACE),this._readWhitespace();try{for(;;){if(!b||!this._margin()){if(!this._declaration())break;if(!c.match(Tokens.SEMICOLON))break}this._readWhitespace()}c.mustMatch(Tokens.RBRACE),this._readWhitespace()}catch(e){if(!(e instanceof SyntaxError&&!this.options.strict))throw e;this.fire({type:"error",error:e,message:e.message,line:e.line,col:e.col}),d=c.advance([Tokens.SEMICOLON,Tokens.RBRACE]);if(d==Tokens.SEMICOLON)this._readDeclarations(!1,b);else if(d!=Tokens.RBRACE)throw e}},_readWhitespace:function(){var a=this._tokenStream,b="";while(a.match(Tokens.S))b+=a.token().value;return b},_unexpectedToken:function(a){throw new SyntaxError("Unexpected token '"+a.value+"' at line "+a.startLine+", char "+a.startCol+".",a.startLine,a.startCol)},_verifyEnd:function(){this._tokenStream.LA(1)!=Tokens.EOF&&this._unexpectedToken(this._tokenStream.LT(1))},parse:function(a){this._tokenStream=new TokenStream(a,Tokens),this._stylesheet()},parseStyleSheet:function(a){return this.parse(a)},parseMediaQuery:function(a){this._tokenStream=new TokenStream(a,Tokens);var b=this._media_query();this._verifyEnd();return b},parsePropertyValue:function(a){this._tokenStream=new TokenStream(a,Tokens),this._readWhitespace();var b=this._expr();this._readWhitespace(),this._verifyEnd();return b},parseRule:function(a){this._tokenStream=new TokenStream(a,Tokens),this._readWhitespace();var b=this._ruleset();this._readWhitespace(),this._verifyEnd();return b},parseSelector:function(a){this._tokenStream=new TokenStream(a,Tokens),this._readWhitespace();var b=this._selector();this._readWhitespace(),this._verifyEnd();return b}};for(b in c)a[b]=c[b];return a}(),PropertyName.prototype=new SyntaxUnit,PropertyName.prototype.constructor=PropertyName,PropertyValue.prototype=new SyntaxUnit,PropertyValue.prototype.constructor=PropertyValue,PropertyValuePart.prototype=new SyntaxUnit,PropertyValuePart.prototype.constructor=PropertyValue,PropertyValuePart.fromToken=function(a){return new PropertyValuePart(a.value,a.startLine,a.startCol)},Selector.prototype=new SyntaxUnit,Selector.prototype.constructor=Selector,SelectorPart.prototype=new SyntaxUnit,SelectorPart.prototype.constructor=SelectorPart,SelectorSubPart.prototype=new SyntaxUnit,SelectorSubPart.prototype.constructor=SelectorSubPart;var h=/^[0-9a-fA-F]$/,nonascii=/^[\u0080-\uFFFF]$/,nl=/\n|\r\n|\r|\f/;TokenStream.prototype=mix(new TokenStreamBase,{_getToken:function(a){var b,c=this._reader,d=null,e=c.getLine(),f=c.getCol();b=c.read();while(b){switch(b){case"/":c.peek()=="*"?d=this.commentToken(b,e,f):d=this.charToken(b,e,f);break;case"|":case"~":case"^":case"$":case"*":c.peek()=="="?d=this.comparisonToken(b,e,f):d=this.charToken(b,e,f);break;case'"':case"'":d=this.stringToken(b,e,f);break;case"#":isNameChar(c.peek())?d=this.hashToken(b,e,f):d=this.charToken(b,e,f);break;case".":isDigit(c.peek())?d=this.numberToken(b,e,f):d=this.charToken(b,e,f);break;case"-":c.peek()=="-"?d=this.htmlCommentEndToken(b,e,f):isNameStart(c.peek())?d=this.identOrFunctionToken(b,e,f):d=this.charToken(b,e,f);break;case"!":d=this.importantToken(b,e,f);break;case"@":d=this.atRuleToken(b,e,f);break;case":":d=this.notToken(b,e,f);break;case"<":d=this.htmlCommentStartToken(b,e,f);break;case"U":case"u":if(c.peek()=="+"){d=this.unicodeRangeToken(b,e,f);break};default:isDigit(b)?d=this.numberToken(b,e,f):isWhitespace(b)?d=this.whitespaceToken(b,e,f):isIdentStart(b)?d=this.identOrFunctionToken(b,e,f):d=this.charToken(b,e,f)}break}!d&&b==null&&(d=this.createToken(Tokens.EOF,null,e,f));return d},createToken:function(a,b,c,d,e){var f=this._reader;e=e||{};return{value:b,type:a,channel:e.channel,hide:e.hide||!1,startLine:c,startCol:d,endLine:f.getLine(),endCol:f.getCol()}},atRuleToken:function(a,b,c){var d=a,e=this._reader,f=Tokens.CHAR,g=!1,h,i;e.mark(),h=this.readName(),d=a+h,f=Tokens.type(d.toLowerCase());if(f==Tokens.CHAR||f==Tokens.UNKNOWN)f=Tokens.CHAR,d=a,e.reset();return this.createToken(f,d,b,c)},charToken:function(a,b,c){var d=Tokens.type(a);d==-1&&(d=Tokens.CHAR);return this.createToken(d,a,b,c)},commentToken:function(a,b,c){var d=this._reader,e=this.readComment(a);return this.createToken(Tokens.COMMENT,e,b,c)},comparisonToken:function(a,b,c){var d=this._reader,e=a+d.read(),f=Tokens.type(e)||Tokens.CHAR;return this.createToken(f,e,b,c)},hashToken:function(a,b,c){var d=this._reader,e=this.readName(a);return this.createToken(Tokens.HASH,e,b,c)},htmlCommentStartToken:function(a,b,c){var d=this._reader,e=a;d.mark(),e+=d.readCount(3);if(e=="<!--")return this.createToken(Tokens.CDO,e,b,c);d.reset();return this.charToken(a,b,c)},htmlCommentEndToken:function(a,b,c){var d=this._reader,e=a;d.mark(),e+=d.readCount(2);if(e=="-->")return this.createToken(Tokens.CDC,e,b,c);d.reset();return this.charToken(a,b,c)},identOrFunctionToken:function(a,b,c){var d=this._reader,e=this.readName(a),f=Tokens.IDENT;d.peek()=="("?(e+=d.read(),e.toLowerCase()=="url("?(f=Tokens.URI,e=this.readURI(e),e.toLowerCase()=="url("&&(f=Tokens.FUNCTION)):f=Tokens.FUNCTION):d.peek()==":"&&e.toLowerCase()=="progid"&&(e+=d.readTo("("),f=Tokens.IE_FUNCTION);return this.createToken(f,e,b,c)},importantToken:function(a,b,c){var d=this._reader,e=a,f=Tokens.CHAR,g,h;d.mark(),h=d.read();while(h){if(h=="/"){if(d.peek()!="*")break;g=this.readComment(h);if(g=="")break}else if(isWhitespace(h))e+=h+this.readWhitespace();else{if(/i/i.test(h)){g=d.readCount(8),/mportant/i.test(g)&&(e+=h+g,f=Tokens.IMPORTANT_SYM);break}break}h=d.read()}if(f==Tokens.CHAR){d.reset();return this.charToken(a,b,c)}return this.createToken(f,e,b,c)},notToken:function(a,b,c){var d=this._reader,e=a;d.mark(),e+=d.readCount(4);if(e.toLowerCase()==":not(")return this.createToken(Tokens.NOT,e,b,c);d.reset();return this.charToken(a,b,c)},numberToken:function(a,b,c){var d=this._reader,e=this.readNumber(a),f,g=Tokens.NUMBER,h=d.peek();isIdentStart(h)?(f=this.readName(d.read()),e+=f,/em/i.test(f)?g=Tokens.EMS:/ex/i.test(f)?g=Tokens.EXS:/px|cm|mm|in|pt|pc/i.test(f)?g=Tokens.LENGTH:/deg|rad|grad/i.test(f)?g=Tokens.ANGLE:/ms|s/i.test(f)?g=Tokens.TIME:/hz|khz/i.test(f)?g=Tokens.FREQ:/dpi|dpcm/i.test(f)?g=Tokens.RESOLUTION:g=Tokens.DIMENSION):h=="%"&&(e+=d.read(),g=Tokens.PERCENTAGE);return this.createToken(g,e,b,c)},stringToken:function(a,b,c){var d=a,e=a,f=this._reader,g=a,h=Tokens.STRING,i=f.read();while(i){e+=i;if(i==d&&g!="\\")break;if(isNewLine(f.peek())&&i!="\\"){h=Tokens.INVALID;break}g=i,i=f.read()}i==null&&(h=Tokens.INVALID);return this.createToken(h,e,b,c)},unicodeRangeToken:function(a,b,c){var d=this._reader,e=a,f,g=Tokens.CHAR;d.peek()=="+"&&(d.mark(),e+=d.read(),e+=this.readUnicodeRangePart(!0),e.length==2?d.reset():(g=Tokens.UNICODE_RANGE,e.indexOf("?")==-1&&d.peek()=="-"&&(d.mark(),f=d.read(),f+=this.readUnicodeRangePart(!1),f.length==1?d.reset():e+=f)));return this.createToken(g,e,b,c)},whitespaceToken:function(a,b,c){var d=this._reader,e=a+this.readWhitespace();return this.createToken(Tokens.S,e,b,c)},readUnicodeRangePart:function(a){var b=this._reader,c="",d=b.peek();while(isHexDigit(d)&&c.length<6)b.read(),c+=d,d=b.peek();if(a)while(d=="?"&&c.length<6)b.read(),c+=d,d=b.peek();return c},readWhitespace:function(){var a=this._reader,b="",c=a.peek();while(isWhitespace(c))a.read(),b+=c,c=a.peek();return b},readNumber:function(a){var b=this._reader,c=a,d=a==".",e=b.peek();while(e){if(isDigit(e))c+=b.read();else{if(e!=".")break;if(d)break;d=!0,c+=b.read()}e=b.peek()}return c},readString:function(){var a=this._reader,b=a.read(),c=b,d=b,e=a.peek();while(e){e=a.read(),c+=e;if(e==b&&d!="\\")break;if(isNewLine(a.peek())&&e!="\\"){c="";break}d=e,e=a.peek()}e==null&&(c="");return c},readURI:function(a){var b=this._reader,c=a,d="",e=b.peek();b.mark(),e=="'"||e=='"'?d=this.readString():d=this.readURL(),e=b.peek(),d==""||e!=")"?(c=a,b.reset()):c+=d+b.read();return c},readURL:function(){var a=this._reader,b="",c=a.peek();while(/^[!#$%&\\*-~]$/.test(c))b+=a.read(),c=a.peek();return b},readName:function(a){var b=this._reader,c=a||"",d=b.peek();while(d&&isNameChar(d))c+=b.read(),d=b.peek();return c},readComment:function(a){var b=this._reader,c=a||"",d=b.read();if(d=="*"){while(d){c+=d;if(d=="*"&&b.peek()=="/"){c+=b.read();break}d=b.read()}return c}return""}});var Tokens=[{name:"CDO"},{name:"CDC"},{name:"S",whitespace:!0},{name:"COMMENT",comment:!0,hide:!0,channel:"comment"},{name:"INCLUDES",text:"~="},{name:"DASHMATCH",text:"|="},{name:"PREFIXMATCH",text:"^="},{name:"SUFFIXMATCH",text:"$="},{name:"SUBSTRINGMATCH",text:"*="},{name:"STRING"},{name:"IDENT"},{name:"HASH"},{name:"IMPORT_SYM",text:"@import"},{name:"PAGE_SYM",text:"@page"},{name:"MEDIA_SYM",text:"@media"},{name:"FONT_FACE_SYM",text:"@font-face"},{name:"CHARSET_SYM",text:"@charset"},{name:"NAMESPACE_SYM",text:"@namespace"},{name:"IMPORTANT_SYM"},{name:"EMS"},{name:"EXS"},{name:"LENGTH"},{name:"ANGLE"},{name:"TIME"},{name:"FREQ"},{name:"DIMENSION"},{name:"PERCENTAGE"},{name:"NUMBER"},{name:"URI"},{name:"FUNCTION"},{name:"UNICODE_RANGE"},{name:"INVALID"},{name:"PLUS",text:"+"},{name:"GREATER",text:">"},{name:"COMMA",text:","},{name:"TILDE",text:"~"},{name:"NOT"},{name:"TOPLEFTCORNER_SYM",text:"@top-left-corner"},{name:"TOPLEFT_SYM",text:"@top-left"},{name:"TOPCENTER_SYM",text:"@top-center"},{name:"TOPRIGHT_SYM",text:"@top-right"},{name:"TOPRIGHTCORNER_SYM",text:"@top-right-corner"},{name:"BOTTOMLEFTCORNER_SYM",text:"@bottom-left-corner"},{name:"BOTTOMLEFT_SYM",text:"@bottom-left"},{name:"BOTTOMCENTER_SYM",text:"@bottom-center"},{name:"BOTTOMRIGHT_SYM",text:"@bottom-right"},{name:"BOTTOMRIGHTCORNER_SYM",text:"@bottom-right-corner"},{name:"LEFTTOP_SYM",text:"@left-top"},{name:"LEFTMIDDLE_SYM",text:"@left-middle"},{name:"LEFTBOTTOM_SYM",text:"@left-bottom"},{name:"RIGHTTOP_SYM",text:"@right-top"},{name:"RIGHTMIDDLE_SYM",text:"@right-middle"},{name:"RIGHTBOTTOM_SYM",text:"@right-bottom"},{name:"RESOLUTION",state:"media"},{name:"IE_FUNCTION"},{name:"CHAR"},{name:"PIPE",text:"|"},{name:"SLASH",text:"/"},{name:"MINUS",text:"-"},{name:"STAR",text:"*"},{name:"LBRACE",text:"{"},{name:"RBRACE",text:"}"},{name:"LBRACKET",text:"["},{name:"RBRACKET",text:"]"},{name:"EQUALS",text:"="},{name:"COLON",text:":"},{name:"SEMICOLON",text:";"},{name:"LPAREN",text:"("},{name:"RPAREN",text:")"},{name:"DOT",text:"."}];(function(){var a=[],b={};Tokens.UNKNOWN=-1,Tokens.unshift({name:"EOF"});for(var c=0,d=Tokens.length;c<d;c++)a.push(Tokens[c].name),Tokens[Tokens[c].name]=c,Tokens[c].text&&(b[Tokens[c].text]=c);Tokens.name=function(b){return a[b]},Tokens.type=function(a){return b[a]||-1}})(),parserlib.css={Colors:Colors,Combinator:Combinator,Parser:Parser,PropertyName:PropertyName,PropertyValue:PropertyValue,PropertyValuePart:PropertyValuePart,MediaFeature:MediaFeature,MediaQuery:MediaQuery,Selector:Selector,SelectorPart:SelectorPart,SelectorSubPart:SelectorSubPart,TokenStream:TokenStream,Tokens:Tokens}}();var CSSLint=function(){var a=[],b=new parserlib.util.EventTarget;b.addRule=function(b){a.push(b),a[b.id]=b},b.clearRules=function(){a=[]},b.verify=function(b,c){var d=0,e=a.length,f,g,h=new parserlib.css.Parser({starHack:!0,ieFilters:!0,underscoreHack:!0,strict:!1});g=b.split(/\n\r?/g),f=new Reporter(g);if(!c)while(d<e)a[d++].init(h,f);else for(d in c)c.hasOwnProperty(d)&&a[d]&&a[d].init(h,f);try{h.parse(b)}catch(i){f.error("Fatal error, cannot continue: "+i.message,i.line,i.col)}return{messages:f.messages,stats:f.stats}};return b}();Reporter.prototype={constructor:Reporter,error:function(a,b,c,d){this.messages.push({type:"error",line:b,col:c,message:a,evidence:this.lines[b-1],rule:d})},warn:function(a,b,c,d){this.messages.push({type:"warning",line:b,col:c,message:a,evidence:this.lines[b-1],rule:d})},info:function(a,b,c,d){this.messages.push({type:"info",line:b,col:c,message:a,evidence:this.lines[b-1],rule:d})},rollupError:function(a,b){this.messages.push({type:"error",rollup:!0,message:a,rule:b})},rollupWarn:function(a,b){this.messages.push({type:"warning",rollup:!0,message:a,rule:b})},stat:function(a,b){this.stats[a]=b}},CSSLint.addRule({id:"adjoining-classes",name:"Adjoining Classes",desc:"Don't use adjoining classes.",browsers:"IE6, IE7",init:function(a,b){var c=this;a.addListener("startrule",function(a){var d=a.selectors,e,f,g,h,i,j,k;for(i=0;i<d.length;i++){e=d[i];for(j=0;j<e.parts.length;j++){f=e.parts[j];if(f instanceof parserlib.css.SelectorPart){h=0;for(k=0;k<f.modifiers.length;k++)g=f.modifiers[k],g.type=="class"&&h++,h>1&&b.warn("Don't use adjoining classes.",f.line,f.col,c)}}}})}}),CSSLint.addRule({id:"box-model",name:"Box Model",desc:"Don't use width or height when using padding or border.",browsers:"All",init:function(a,b){var c=this,d={border:1,"border-left":1,"border-right":1,"border-bottom":1,"border-top":1,padding:1,"padding-left":1,"padding-right":1,"padding-bottom":1,"padding-top":1},e;a.addListener("startrule",function(a){e={}}),a.addListener("property",function(a){var b=a.property;if(d[b])e[b]={line:b.line,col:b.col};else if(b=="width"||b=="height")e._flagProperty=b.text}),a.addListener("endrule",function(a){var f;if(e._flagProperty)for(f in d)d.hasOwnProperty(f)&&e[f]&&b.warn("Broken box model: using "+e._flagProperty+" with "+f+".",e[f].line,e[f].col,c)})}}),CSSLint.addRule({id:"display-property-grouping",name:"Display Property Grouping",desc:"Certain properties shouldn't be used with certain display property values.",browsers:"All",init:function(a,b){function f(a,d){e[a]&&b.warn(a+" can't be used with display: "+d+".",e[a].line,e[a].col,c)}var c=this,d={display:1,"float":1,height:1,width:1,margin:1,"margin-left":1,"margin-right":1,"margin-bottom":1,"margin-top":1,padding:1,"padding-left":1,"padding-right":1,"padding-bottom":1,"padding-top":1,"vertical-align":1},e;a.addListener("startrule",function(a){e={}}),a.addListener("property",function(a){var b=a.property;d[b]&&(e[b]={value:a.value.text,line:b.line,col:b.col})}),a.addListener("endrule",function(a){var b=e.display?e.display.value:null;if(b)switch(b){case"inline":f("height",b),f("width",b),f("margin",b),f("margin-left",b),f("margin-right",b),f("margin-top",b),f("margin-bottom",b),f("padding",b),f("padding-left",b),f("padding-right",b),f("padding-top",b),f("padding-bottom",b),f("float",b);break;case"block":f("vertical-align",b);break;case"inline-block":f("float",b);break;default:b.indexOf("table-")==0&&(f("margin",b),f("margin-left",b),f("margin-right",b),f("margin-top",b),f("margin-bottom",b),f("float",b))}})}}),CSSLint.addRule({id:"empty-rules",name:"Empty Rules",desc:"Rules without any properties specified should be removed.",browsers:"All",init:function(a,b){var c=this,d=0;a.addListener("startrule",function(a){d=0}),a.addListener("property",function(a){d++}),a.addListener("endrule",function(a){var e=a.selectors;d==0&&b.warn("Rule is empty.",e[0].line,e[0].col,c)})}}),CSSLint.addRule({id:"errors",name:"Parsing Errors",desc:"This rule looks for recoverable syntax errors.",browsers:"All",init:function(a,b){var c=this;a.addListener("error",function(a){b.error(a.message,a.line,a.col,c)})}}),CSSLint.addRule({id:"floats",name:"Floats",desc:"This rule tests if the float property is used too many times",browsers:"All",init:function(a,b){var c=this,d=0;a.addListener("property",function(a){a.property=="float"&&d++}),a.addListener("endstylesheet",function(a){b.stat("floats",d),d>=10&&b.rollupWarn("Too many floats ("+d+"), abstraction needed.",c)})}}),CSSLint.addRule({id:"font-faces",name:"Font Faces",desc:"Too many different web fonts in the same stylesheet.",browsers:"All",init:function(a,b){var c=this,d=0;a.addListener("startfontface",function(a){d++}),a.addListener("endstylesheet",function(a){d>5&&b.rollupWarn("Too many @font-face declarations ("+d+").",c)})}}),CSSLint.addRule({id:"font-sizes",name:"Font Sizes",desc:"Checks the number of font-size declarations.",browsers:"All",init:function(a,b){var c=this,d=0;a.addListener("property",function(a){var b=a.value.parts[0];a.property=="font-size"&&d++}),a.addListener("endstylesheet",function(a){b.stat("font-sizes",d),d>=10&&b.rollupWarn("Too many font-size declarations ("+d+"), abstraction needed.",c)})}}),CSSLint.addRule({id:"gradients",name:"Gradients",desc:"When using a vendor-prefixed gradient, make sure to use them all.",browsers:"All",init:function(a,b){var c=this,d;a.addListener("startrule",function(a){d={moz:0,webkit:0,ms:0,o:0}}),a.addListener("property",function(a){/\-(moz|ms|o|webkit)(?:\-(?:linear|radial))\-gradient/.test(a.value)&&(d[RegExp.$1]=1)}),a.addListener("endrule",function(a){var e=[];d.moz||e.push("Firefox 3.6+"),d.webkit||e.push("Webkit (Safari, Chrome)"),d.ms||e.push("Internet Explorer 10+"),d.o||e.push("Opera 11.1+"),e.length&&e.length<4&&b.warn("Missing vendor-prefixed CSS gradients for "+e.join(", ")+".",a.selectors[0].line,a.selectors[0].col,c)})}}),CSSLint.addRule({id:"ids",name:"IDs",desc:"Selectors should not contain IDs.",browsers:"All",init:function(a,b){var c=this;a.addListener("startrule",function(a){var d=a.selectors,e,f,g,h,i,j,k;for(i=0;i<d.length;i++){e=d[i],h=0;for(j=0;j<e.parts.length;j++){f=e.parts[j];if(f instanceof parserlib.css.SelectorPart)for(k=0;k<f.modifiers.length;k++)g=f.modifiers[k],g.type=="id"&&h++}h==1?b.warn("Don't use IDs in selectors.",e.line,e.col,c):h>1&&b.warn(h+" IDs in the selector, really?",e.line,e.col,c)}})}}),CSSLint.addRule({id:"overqualified-elements",name:"Overqualified Elements",desc:"Don't use classes or IDs with elements (a.foo or a#foo).",browsers:"All",init:function(a,b){var c=this;a.addListener("startrule",function(a){var d=a.selectors,e,f,g,h,i,j;for(h=0;h<d.length;h++){e=d[h];for(i=0;i<e.parts.length;i++){f=e.parts[i];if(f instanceof parserlib.css.SelectorPart&&f.elementName)for(j=0;j<f.modifiers.length;j++)g=f.modifiers[j],(g.type=="class"||g.type=="id")&&b.warn("Element ("+f+") is overqualified, just use "+g+" without element name.",f.line,f.col,c)}}})}}),CSSLint.addRule({id:"qualified-headings",name:"Qualified Headings",desc:"Headings should not be qualified (namespaced).",browsers:"All",init:function(a,b){var c=this;a.addListener("startrule",function(a){var d=a.selectors,e,f,g,h,i,j;for(h=0;h<d.length;h++){e=d[h];for(i=0;i<e.parts.length;i++)f=e.parts[i],f instanceof parserlib.css.SelectorPart&&f.elementName&&/h[1-6]/.test(f.elementName.toString())&&i>0&&b.warn("Heading ("+f.elementName+") should not be qualified.",f.line,f.col,c)}})}}),CSSLint.addRule({id:"regex-selectors",name:"Regex Selectors",desc:"Selectors that look like regular expressions are slow and should be avoided.",browsers:"All",init:function(a,b){var c=this;a.addListener("startrule",function(a){var d=a.selectors,e,f,g,h,i,j;for(h=0;h<d.length;h++){e=d[h];for(i=0;i<e.parts.length;i++){f=e.parts[i];if(f instanceof parserlib.css.SelectorPart)for(j=0;j<f.modifiers.length;j++)g=f.modifiers[j],g.type=="attribute"&&/([\~\|\^\$\*]=)/.test(g)&&b.warn("Attribute selectors with "+RegExp.$1+" are slow!",g.line,g.col,c)}}})}}),CSSLint.addRule({id:"rules-count",name:"Rules Count",desc:"Track how many rules there are.",browsers:"All",init:function(a,b){var c=this,d=0;a.addListener("startrule",function(a){d++}),a.addListener("endstylesheet",function(a){b.stat("rule-count",d)})}}),CSSLint.addRule({id:"unique-headings",name:"Unique Headings",desc:"Headings should be defined only once.",browsers:"All",init:function(a,b){var c=this,d={h1:0,h2:0,h3:0,h4:0,h5:0,h6:0};a.addListener("startrule",function(a){var e=a.selectors,f,g,h,i,j,k;for(i=0;i<e.length;i++){f=e[i];for(j=0;j<f.parts.length;j++)g=f.parts[j],g instanceof parserlib.css.SelectorPart&&g.elementName&&/(h[1-6])/.test(g.elementName.toString())&&(d[RegExp.$1]++,d[RegExp.$1]>1&&b.warn("Heading ("+g.elementName+") has already been defined.",g.line,g.col,c))}})}}),CSSLint.addRule({id:"vendor-prefix",name:"Vendor Prefix",desc:"When using a vendor-prefixed property, make sure to include the standard one.",browsers:"All",init:function(a,b){var c=this,d,e;a.addListener("startrule",function(a){d={},e=1}),a.addListener("property",function(a){var b=a.property,c=a.value.parts,f=0,g=c.length,h;d[b]||(d[b]=[]),d[b].push({name:a.property,value:a.value,pos:e++})}),a.addListener("endrule",function(a){var e,f,g,h,i,j,k=[];for(e in d)/(\-(?:ms|moz|webkit|o)\-)/.test(e)&&k.push({actual:e,needed:e.substring(RegExp.$1.length)});for(f=0,g=k.length;f<g;f++)i=k[f].needed,j=k[f].actual,/\-moz\-border\-radius\-(.+)/.test(j)?h="border-"+RegExp.$1.replace(/(left|right)/,"-$1")+"-radius":h=i,d[i]?d[i][0].pos<d[j][0].pos&&b.warn("Standard property '"+h+"' should come after vendor-prefixed property '"+j+"'.",a.selectors[0].line,a.selectors[0].col,c):b.warn("Missing standard property '"+h+"' to go along with '"+j+"'.",a.selectors[0].line,a.selectors[0].col,c)})}}),CSSLint.addRule({id:"width-100",name:"Width 100%",desc:"Be careful when using width: 100% on elements.",browsers:"All",init:function(a,b){var c=this;a.addListener("property",function(a){var d=a.property,e=a.value;d=="width"&&e=="100%"&&b.warn("Elements with a width of 100% may not appear as you expect inside of other elements.",d.line,d.col,c)})}}),CSSLint.addRule({id:"zero-units",name:"Zero Units",desc:"You don't need to specify units when a value is 0.",browsers:"All",init:function(a,b){var c=this;a.addListener("property",function(a){var d=a.value.parts,e=0,f=d.length,g;while(e<f)(d[e].units||d[e].type=="percentage")&&d[e].value===0&&b.warn("Values of 0 shouldn't have units specified.",d[e].line,d[e].col,c),e++})}}),exports.CSSLint=CSSLint}) \ No newline at end of file
diff --git a/www/js/ace/worker-javascript.js b/www/js/ace/worker-javascript.js
new file mode 100644
index 0000000..1156c45
--- /dev/null
+++ b/www/js/ace/worker-javascript.js
@@ -0,0 +1 @@
+function initSender(){var a=require("pilot/event_emitter").EventEmitter,b=require("pilot/oop"),c=function(){};(function(){b.implement(this,a),this.callback=function(a,b){postMessage({type:"call",id:b,data:a})},this.emit=function(a,b){postMessage({type:"event",name:a,data:b})}}).call(c.prototype);return new c}function initBaseUrls(a){require.tlns=a}var console={log:function(a){postMessage({type:"log",data:a})}},window={console:console},require=function(a){var b=require.modules[a];if(b){b.initialized||(b.exports=b.factory().exports,b.initialized=!0);return b.exports}var c=a.split("/");c[0]=require.tlns[c[0]]||c[0],path=c.join("/")+".js",require.id=a,importScripts(path);return require(a)};require.modules={},require.tlns={};var define=function(a,b,c){arguments.length==2?c=b:arguments.length==1&&(c=a,a=require.id);a.indexOf("text/")!==0&&(require.modules[a]={factory:function(){var a={exports:{}},b=c(require,a.exports,a);b&&(a.exports=b);return a}})},main,sender;onmessage=function(a){var b=a.data;if(b.command)main[b.command].apply(main,b.args);else if(b.init){initBaseUrls(b.tlns),require("pilot/fixoldbrowsers"),sender=initSender();var c=require(b.module)[b.classname];main=new c(sender)}else b.event&&sender&&sender._dispatchEvent(b.event,b.data)},define("pilot/fixoldbrowsers",["require","exports","module"],function(a,b,c){if(!Function.prototype.bind){var d=Array.prototype.slice;Function.prototype.bind=function(b){var c=this;if(typeof c.apply!="function"||typeof c.call!="function")return new TypeError;var e=d.call(arguments),f=function a(){if(this instanceof a){var b=Object.create(c.prototype);c.apply(b,e.concat(d.call(arguments)));return b}return c.call.apply(c,e.concat(d.call(arguments)))};f.length=typeof c=="function"?Math.max(c.length-e.length,0):0;return f}}var e=Function.prototype.call,f=Array.prototype,g=Object.prototype,h=e.bind(g.hasOwnProperty),i,j,k,l,m;if(m=h(g,"__defineGetter__"))i=e.bind(g.__defineGetter__),j=e.bind(g.__defineSetter__),k=e.bind(g.__lookupGetter__),l=e.bind(g.__lookupSetter__);Array.isArray||(Array.isArray=function(b){return Object.prototype.toString.call(b)==="[object Array]"}),Array.prototype.forEach||(Array.prototype.forEach=function(b,c){var d=+this.length;for(var e=0;e<d;e++)e in this&&b.call(c,this[e],e,this)}),Array.prototype.map||(Array.prototype.map=function(b){var c=+this.length;if(typeof b!="function")throw new TypeError;var d=Array(c),e=arguments[1];for(var f=0;f<c;f++)f in this&&(d[f]=b.call(e,this[f],f,this));return d}),Array.prototype.filter||(Array.prototype.filter=function(b){var c=[],d=arguments[1];for(var e=0;e<this.length;e++)b.call(d,this[e])&&c.push(this[e]);return c}),Array.prototype.every||(Array.prototype.every=function(b){var c=arguments[1];for(var d=0;d<this.length;d++)if(!b.call(c,this[d]))return!1;return!0}),Array.prototype.some||(Array.prototype.some=function(b){var c=arguments[1];for(var d=0;d<this.length;d++)if(b.call(c,this[d]))return!0;return!1}),Array.prototype.reduce||(Array.prototype.reduce=function(b){var c=+this.length;if(typeof b!="function")throw new TypeError;if(c===0&&arguments.length===1)throw new TypeError;var d=0;if(arguments.length>=2)var e=arguments[1];else do{if(d in this){e=this[d++];break}if(++d>=c)throw new TypeError}while(!0);for(;d<c;d++)d in this&&(e=b.call(null,e,this[d],d,this));return e}),Array.prototype.reduceRight||(Array.prototype.reduceRight=function(b){var c=+this.length;if(typeof b!="function")throw new TypeError;if(c===0&&arguments.length===1)throw new TypeError;var d=c-1;if(arguments.length>=2)var e=arguments[1];else do{if(d in this){e=this[d--];break}if(--d<0)throw new TypeError}while(!0);for(;d>=0;d--)d in this&&(e=b.call(null,e,this[d],d,this));return e}),Array.prototype.indexOf||(Array.prototype.indexOf=function(b){var c=this.length;if(!c)return-1;var d=arguments[1]||0;if(d>=c)return-1;d<0&&(d+=c);for(;d<c;d++){if(!h(this,d))continue;if(b===this[d])return d}return-1}),Array.prototype.lastIndexOf||(Array.prototype.lastIndexOf=function(b){var c=this.length;if(!c)return-1;var d=arguments[1]||c;d<0&&(d+=c),d=Math.min(d,c-1);for(;d>=0;d--){if(!h(this,d))continue;if(b===this[d])return d}return-1}),Object.getPrototypeOf||(Object.getPrototypeOf=function(b){return b.__proto__||b.constructor.prototype});if(!Object.getOwnPropertyDescriptor){var n="Object.getOwnPropertyDescriptor called on a non-object: ";Object.getOwnPropertyDescriptor=function(b,c){if(typeof b!="object"&&typeof b!="function"||b===null)throw new TypeError(n+b);if(!h(b,c))return undefined;var d,e,f;d={enumerable:!0,configurable:!0};if(m){var i=b.__proto__;b.__proto__=g;var e=k(b,c),f=l(b,c);b.__proto__=i;if(e||f){e&&(descriptor.get=e),f&&(descriptor.set=f);return descriptor}}descriptor.value=b[c];return descriptor}}Object.getOwnPropertyNames||(Object.getOwnPropertyNames=function(b){return Object.keys(b)}),Object.create||(Object.create=function(b,c){var d;if(b===null)d={"__proto__":null};else{if(typeof b!="object")throw new TypeError("typeof prototype["+typeof b+"] != 'object'");var e=function(){};e.prototype=b,d=new e,d.__proto__=b}typeof c!="undefined"&&Object.defineProperties(d,c);return d});if(!Object.defineProperty){var o="Property description must be an object: ",p="Object.defineProperty called on non-object: ",q="getters & setters can not be defined on this javascript engine";Object.defineProperty=function(b,c,d){if(typeof b!="object"&&typeof b!="function")throw new TypeError(p+b);if(typeof b!="object"||b===null)throw new TypeError(o+d);if(h(d,"value"))if(m&&(k(b,c)||l(b,c))){var e=b.__proto__;b.__proto__=g,delete b[c],b[c]=d.value,b.prototype}else b[c]=d.value;else{if(!m)throw new TypeError(q);h(d,"get")&&i(b,c,d.get),h(d,"set")&&j(b,c,d.set)}return b}}Object.defineProperties||(Object.defineProperties=function(b,c){for(var d in c)h(c,d)&&Object.defineProperty(b,d,c[d]);return b}),Object.seal||(Object.seal=function(b){return b}),Object.freeze||(Object.freeze=function(b){return b});try{Object.freeze(function(){})}catch(r){Object.freeze=function(b){return function(c){return typeof c=="function"?c:b(c)}}(Object.freeze)}Object.preventExtensions||(Object.preventExtensions=function(b){return b}),Object.isSealed||(Object.isSealed=function(b){return!1}),Object.isFrozen||(Object.isFrozen=function(b){return!1}),Object.isExtensible||(Object.isExtensible=function(b){return!0});if(!Object.keys){var s=!0,t=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],u=t.length;for(var v in{toString:null})s=!1;Object.keys=function a(b){if(typeof b!="object"&&typeof b!="function"||b===null)throw new TypeError("Object.keys called on a non-object");var a=[];for(var c in b)h(b,c)&&a.push(c);if(s)for(var d=0,e=u;d<e;d++){var f=t[d];h(b,f)&&a.push(f)}return a}}Date.prototype.toISOString||(Date.prototype.toISOString=function(){return this.getUTCFullYear()+"-"+(this.getUTCMonth()+1)+"-"+this.getUTCDate()+"T"+this.getUTCHours()+":"+this.getUTCMinutes()+":"+this.getUTCSeconds()+"Z"}),Date.now||(Date.now=function(){return(new Date).getTime()}),Date.prototype.toJSON||(Date.prototype.toJSON=function(b){if(typeof this.toISOString!="function")throw new TypeError;return this.toISOString()}),isNaN(Date.parse("T00:00"))&&(Date=function(a){var b=function(c,d,e,f,g,h,i){var j=arguments.length;if(this instanceof a){var k=j===1&&String(c)===c?new a(b.parse(c)):j>=7?new a(c,d,e,f,g,h,i):j>=6?new a(c,d,e,f,g,h):j>=5?new a(c,d,e,f,g):j>=4?new a(c,d,e,f):j>=3?new a(c,d,e):j>=2?new a(c,d):j>=1?new a(c):new a;k.constructor=b;return k}return a.apply(this,arguments)},c=new RegExp("^(?:((?:[+-]\\d\\d)?\\d\\d\\d\\d)(?:-(\\d\\d)(?:-(\\d\\d))?)?)?(?:T(\\d\\d):(\\d\\d)(?::(\\d\\d)(?:\\.(\\d\\d\\d))?)?)?(?:Z|([+-])(\\d\\d):(\\d\\d))?$");for(var d in a)b[d]=a[d];b.now=a.now,b.UTC=a.UTC,b.prototype=a.prototype,b.prototype.constructor=b,b.parse=function(d){var e=c.exec(d);if(e){e.shift();var f=e[0]===undefined;for(var g=0;g<10;g++){if(g===7)continue;e[g]=+(e[g]||(g<3?1:0)),g===1&&e[g]--}if(f)return((e[3]*60+e[4])*60+e[5])*1e3+e[6];var h=(e[8]*60+e[9])*60*1e3;e[6]==="-"&&(h=-h);return a.UTC.apply(this,e.slice(0,7))+h}return a.parse.apply(this,arguments)};return b}(Date));if(!String.prototype.trim){var w=/^\s\s*/,x=/\s\s*$/;String.prototype.trim=function(){return String(this).replace(w,"").replace(x,"")}}}),define("pilot/event_emitter",["require","exports","module"],function(a,b,c){var d={};d._emit=d._dispatchEvent=function(a,b){this._eventRegistry=this._eventRegistry||{};var c=this._eventRegistry[a];if(!!c&&!!c.length){var b=b||{};b.type=a;for(var d=0;d<c.length;d++)c[d](b)}},d.on=d.addEventListener=function(a,b){this._eventRegistry=this._eventRegistry||{};var c=this._eventRegistry[a];if(!c)var c=this._eventRegistry[a]=[];c.indexOf(b)==-1&&c.push(b)},d.removeListener=d.removeEventListener=function(a,b){this._eventRegistry=this._eventRegistry||{};var c=this._eventRegistry[a];if(!!c){var d=c.indexOf(b);d!==-1&&c.splice(d,1)}},d.removeAllListeners=function(a){this._eventRegistry&&(this._eventRegistry[a]=[])},b.EventEmitter=d}),define("pilot/oop",["require","exports","module"],function(a,b,c){b.inherits=function(){var a=function(){};return function(b,c){a.prototype=c.prototype,b.super_=c.prototype,b.prototype=new a,b.prototype.constructor=b}}(),b.mixin=function(a,b){for(var c in b)a[c]=b[c]},b.implement=function(a,c){b.mixin(a,c)}}),define("ace/mode/javascript_worker",["require","exports","module","pilot/oop","ace/worker/mirror","ace/worker/jshint","ace/narcissus/jsparse"],function(a,b,c){var d=a("pilot/oop"),e=a("ace/worker/mirror").Mirror,f=a("ace/worker/jshint").JSHINT,g=b.JavaScriptWorker=function(a){e.call(this,a),this.setTimeout(500)};d.inherits(g,e),function(){this.onUpdate=function(){var b=this.doc.getValue();b=b.replace(/^#!.*\n/,"\n");var c=a("ace/narcissus/jsparse");try{c.parse(b)}catch(d){this.sender.emit("narcissus",{row:d.lineno-1,column:null,text:d.message,type:"error"});return}finally{}f(b,{undef:!1,onevar:!1,passfail:!1}),this.sender.emit("jslint",f.errors)}}.call(g.prototype)}),define("ace/worker/mirror",["require","exports","module","ace/document","pilot/lang"],function(a,b,c){var d=a("ace/document").Document,e=a("pilot/lang"),f=b.Mirror=function(a){this.sender=a;var b=this.doc=new d(""),c=this.deferredUpdate=e.deferredCall(this.onUpdate.bind(this)),f=this;a.on("change",function(a){b.applyDeltas([a.data]),c.schedule(f.$timeout)})};(function(){this.$timeout=500,this.setTimeout=function(a){this.$timeout=a},this.setValue=function(a){this.doc.setValue(a),this.deferredUpdate.schedule(this.$timeout)},this.getValue=function(a){this.sender.callback(this.doc.getValue(),a)},this.onUpdate=function(){}}).call(f.prototype)}),define("ace/document",["require","exports","module","pilot/oop","pilot/event_emitter","ace/range","ace/anchor"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=a("ace/range").Range,g=a("ace/anchor").Anchor,h=function(a){this.$lines=[],Array.isArray(a)?this.insertLines(0,a):a.length==0?this.$lines=[""]:this.insert({row:0,column:0},a)};(function(){d.implement(this,e),this.setValue=function(a){var b=this.getLength();this.remove(new f(0,0,b,this.getLine(b-1).length)),this.insert({row:0,column:0},a)},this.getValue=function(){return this.getAllLines().join(this.getNewLineCharacter())},this.createAnchor=function(a,b){return new g(this,a,b)},"aaa".split(/a/).length==0?this.$split=function(a){return a.replace(/\r\n|\r/g,"\n").split("\n")}:this.$split=function(a){return a.split(/\r\n|\r|\n/)},this.$detectNewLine=function(a){var b=a.match(/^.*?(\r?\n)/m);b?this.$autoNewLine=b[1]:this.$autoNewLine="\n"},this.getNewLineCharacter=function(){switch(this.$newLineMode){case"windows":return"\r\n";case"unix":return"\n";case"auto":return this.$autoNewLine}},this.$autoNewLine="\n",this.$newLineMode="auto",this.setNewLineMode=function(a){this.$newLineMode!==a&&(this.$newLineMode=a)},this.getNewLineMode=function(){return this.$newLineMode},this.isNewLine=function(a){return a=="\r\n"||a=="\r"||a=="\n"},this.getLine=function(a){return this.$lines[a]||""},this.getLines=function(a,b){return this.$lines.slice(a,b+1)},this.getAllLines=function(){return this.getLines(0,this.getLength())},this.getLength=function(){return this.$lines.length},this.getTextRange=function(a){if(a.start.row==a.end.row)return this.$lines[a.start.row].substring(a.start.column,a.end.column);var b=[];b.push(this.$lines[a.start.row].substring(a.start.column)),b.push.apply(b,this.getLines(a.start.row+1,a.end.row-1)),b.push(this.$lines[a.end.row].substring(0,a.end.column));return b.join(this.getNewLineCharacter())},this.$clipPosition=function(a){var b=this.getLength();a.row>=b&&(a.row=Math.max(0,b-1),a.column=this.getLine(b-1).length);return a},this.insert=function(a,b){if(b.length==0)return a;a=this.$clipPosition(a),this.getLength()<=1&&this.$detectNewLine(b);var c=this.$split(b),d=c.splice(0,1)[0],e=c.length==0?null:c.splice(c.length-1,1)[0];this._dispatchEvent("changeStart"),a=this.insertInLine(a,d),e!==null&&(a=this.insertNewLine(a),a=this.insertLines(a.row,c),a=this.insertInLine(a,e||"")),this._dispatchEvent("changeEnd");return a},this.insertLines=function(a,b){if(b.length==0)return{row:a,column:0};var c=[a,0];c.push.apply(c,b),this.$lines.splice.apply(this.$lines,c),this._dispatchEvent("changeStart");var d=new f(a,0,a+b.length,0),e={action:"insertLines",range:d,lines:b};this._dispatchEvent("change",{data:e}),this._dispatchEvent("changeEnd");return d.end},this.insertNewLine=function(a){a=this.$clipPosition(a);var b=this.$lines[a.row]||"";this._dispatchEvent("changeStart"),this.$lines[a.row]=b.substring(0,a.column),this.$lines.splice(a.row+1,0,b.substring(a.column,b.length));var c={row:a.row+1,column:0},d={action:"insertText",range:f.fromPoints(a,c),text:this.getNewLineCharacter()};this._dispatchEvent("change",{data:d}),this._dispatchEvent("changeEnd");return c},this.insertInLine=function(a,b){if(b.length==0)return a;var c=this.$lines[a.row]||"";this._dispatchEvent("changeStart"),this.$lines[a.row]=c.substring(0,a.column)+b+c.substring(a.column);var d={row:a.row,column:a.column+b.length},e={action:"insertText",range:f.fromPoints(a,d),text:b};this._dispatchEvent("change",{data:e}),this._dispatchEvent("changeEnd");return d},this.remove=function(a){a.start=this.$clipPosition(a.start),a.end=this.$clipPosition(a.end);if(a.isEmpty())return a.start;var b=a.start.row,c=a.end.row;this._dispatchEvent("changeStart");if(a.isMultiLine()){var d=a.start.column==0?b:b+1,e=c-1;a.end.column>0&&this.removeInLine(c,0,a.end.column),e>=d&&this.removeLines(d,e),d!=b&&(this.removeInLine(b,a.start.column,this.getLine(b).length),this.removeNewLine(a.start.row))}else this.removeInLine(b,a.start.column,a.end.column);this._dispatchEvent("changeEnd");return a.start},this.removeInLine=function(a,b,c){if(b!=c){var d=new f(a,b,a,c),e=this.getLine(a),g=e.substring(b,c),h=e.substring(0,b)+e.substring(c,e.length);this._dispatchEvent("changeStart"),this.$lines.splice(a,1,h);var i={action:"removeText",range:d,text:g};this._dispatchEvent("change",{data:i}),this._dispatchEvent("changeEnd");return d.start}},this.removeLines=function(a,b){this._dispatchEvent("changeStart");var c=new f(a,0,b+1,0),d=this.$lines.splice(a,b-a+1),e={action:"removeLines",range:c,nl:this.getNewLineCharacter(),lines:d};this._dispatchEvent("change",{data:e}),this._dispatchEvent("changeEnd");return d},this.removeNewLine=function(a){var b=this.getLine(a),c=this.getLine(a+1),d=new f(a,b.length,a+1,0),e=b+c;this._dispatchEvent("changeStart"),this.$lines.splice(a,2,e);var g={action:"removeText",range:d,text:this.getNewLineCharacter()};this._dispatchEvent("change",{data:g}),this._dispatchEvent("changeEnd")},this.replace=function(a,b){if(b.length==0&&a.isEmpty())return a.start;if(b==this.getTextRange(a))return a.end;this._dispatchEvent("changeStart"),this.remove(a);if(b)var c=this.insert(a.start,b);else c=a.start;this._dispatchEvent("changeEnd");return c},this.applyDeltas=function(a){for(var b=0;b<a.length;b++){var c=a[b],d=f.fromPoints(c.range.start,c.range.end);c.action=="insertLines"?this.insertLines(d.start.row,c.lines):c.action=="insertText"?this.insert(d.start,c.text):c.action=="removeLines"?this.removeLines(d.start.row,d.end.row-1):c.action=="removeText"&&this.remove(d)}},this.revertDeltas=function(a){for(var b=a.length-1;b>=0;b--){var c=a[b],d=f.fromPoints(c.range.start,c.range.end);c.action=="insertLines"?this.removeLines(d.start.row,d.end.row-1):c.action=="insertText"?this.remove(d):c.action=="removeLines"?this.insertLines(d.start.row,c.lines):c.action=="removeText"&&this.insert(d.start,c.text)}}}).call(h.prototype),b.Document=h}),define("ace/range",["require","exports","module"],function(a,b,c){var d=function(a,b,c,d){this.start={row:a,column:b},this.end={row:c,column:d}};(function(){this.toString=function(){return"Range: ["+this.start.row+"/"+this.start.column+"] -> ["+this.end.row+"/"+this.end.column+"]"},this.contains=function(a,b){return this.compare(a,b)==0},this.compareRange=function(a){var b,c=a.end,d=a.start;b=this.compare(c.row,c.column);if(b==1){b=this.compare(d.row,d.column);return b==1?2:b==0?1:0}if(b==-1)return-2;b=this.compare(d.row,d.column);return b==-1?-1:b==1?42:0},this.containsRange=function(a){var b=this.compareRange(a);return b==-1||b==0||b==1},this.isEnd=function(a,b){return this.end.row==a&&this.end.column==b},this.isStart=function(a,b){return this.start.row==a&&this.start.column==b},this.setStart=function(a,b){typeof a=="object"?(this.start.column=a.column,this.start.row=a.row):(this.start.row=a,this.start.column=b)},this.setEnd=function(a,b){typeof a=="object"?(this.end.column=a.column,this.end.row=a.row):(this.end.row=a,this.end.column=b)},this.inside=function(a,b){if(this.compare(a,b)==0)return this.isEnd(a,b)||this.isStart(a,b)?!1:!0;return!1},this.insideStart=function(a,b){if(this.compare(a,b)==0)return this.isEnd(a,b)?!1:!0;return!1},this.insideEnd=function(a,b){if(this.compare(a,b)==0)return this.isStart(a,b)?!1:!0;return!1},this.compare=function(a,b){if(!this.isMultiLine()&&a===this.start.row)return b<this.start.column?-1:b>this.end.column?1:0;if(a<this.start.row)return-1;if(a>this.end.row)return 1;if(this.start.row===a)return b>=this.start.column?0:-1;if(this.end.row===a)return b<=this.end.column?0:1;return 0},this.compareStart=function(a,b){return this.start.row==a&&this.start.column==b?-1:this.compare(a,b)},this.compareEnd=function(a,b){return this.end.row==a&&this.end.column==b?1:this.compare(a,b)},this.compareInside=function(a,b){return this.end.row==a&&this.end.column==b?1:this.start.row==a&&this.start.column==b?-1:this.compare(a,b)},this.clipRows=function(a,b){if(this.end.row>b)var c={row:b+1,column:0};if(this.start.row>b)var e={row:b+1,column:0};if(this.start.row<a)var e={row:a,column:0};if(this.end.row<a)var c={row:a,column:0};return d.fromPoints(e||this.start,c||this.end)},this.extend=function(a,b){var c=this.compare(a,b);if(c==0)return this;if(c==-1)var e={row:a,column:b};else var f={row:a,column:b};return d.fromPoints(e||this.start,f||this.end)},this.isEmpty=function(){return this.start.row==this.end.row&&this.start.column==this.end.column},this.isMultiLine=function(){return this.start.row!==this.end.row},this.clone=function(){return d.fromPoints(this.start,this.end)},this.collapseRows=function(){return this.end.column==0?new d(this.start.row,0,Math.max(this.start.row,this.end.row-1),0):new d(this.start.row,0,this.end.row,0)},this.toScreenRange=function(a){var b=a.documentToScreenPosition(this.start),c=a.documentToScreenPosition(this.end);return new d(b.row,b.column,c.row,c.column)}}).call(d.prototype),d.fromPoints=function(a,b){return new d(a.row,a.column,b.row,b.column)},b.Range=d}),define("ace/anchor",["require","exports","module","pilot/oop","pilot/event_emitter"],function(a,b,c){var d=a("pilot/oop"),e=a("pilot/event_emitter").EventEmitter,f=b.Anchor=function(a,b,c){this.document=a,typeof c=="undefined"?this.setPosition(b.row,b.column):this.setPosition(b,c),this.$onChange=this.onChange.bind(this),a.on("change",this.$onChange)};(function(){d.implement(this,e),this.getPosition=function(){return this.$clipPositionToDocument(this.row,this.column)},this.getDocument=function(){return this.document},this.onChange=function(a){var b=a.data,c=b.range;if(c.start.row!=c.end.row||c.start.row==this.row){if(c.start.row>this.row)return;if(c.start.row==this.row&&c.start.column>this.column)return;var d=this.row,e=this.column;b.action==="insertText"?c.start.row===d&&c.start.column<=e?c.start.row===c.end.row?e+=c.end.column-c.start.column:(e-=c.start.column,d+=c.end.row-c.start.row):c.start.row!==c.end.row&&c.start.row<d&&(d+=c.end.row-c.start.row):b.action==="insertLines"?c.start.row<=d&&(d+=c.end.row-c.start.row):b.action=="removeText"?c.start.row==d&&c.start.column<e?c.end.column>=e?e=c.start.column:e=Math.max(0,e-(c.end.column-c.start.column)):c.start.row!==c.end.row&&c.start.row<d?(c.end.row==d&&(e=Math.max(0,e-c.end.column)+c.start.column),d-=c.end.row-c.start.row):c.end.row==d&&(d-=c.end.row-c.start.row,e=Math.max(0,e-c.end.column)+c.start.column):b.action=="removeLines"&&c.start.row<=d&&(c.end.row<=d?d-=c.end.row-c.start.row:(d=c.start.row,e=0)),this.setPosition(d,e,!0)}},this.setPosition=function(a,b,c){c?pos={row:a,column:b}:pos=this.$clipPositionToDocument(a,b);if(this.row!=pos.row||this.column!=pos.column){var d={row:this.row,column:this.column};this.row=pos.row,this.column=pos.column,this._dispatchEvent("change",{old:d,value:pos})}},this.detach=function(){this.document.removeEventListener("change",this.$onChange)},this.$clipPositionToDocument=function(a,b){var c={};a>=this.document.getLength()?(c.row=Math.max(0,this.document.getLength()-1),c.column=this.document.getLine(c.row).length):a<0?(c.row=0,c.column=0):(c.row=a,c.column=Math.min(this.document.getLine(c.row).length,Math.max(0,b))),b<0&&(c.column=0);return c}}).call(f.prototype)}),define("pilot/lang",["require","exports","module"],function(a,b,c){b.stringReverse=function(a){return a.split("").reverse().join("")},b.stringRepeat=function(a,b){return Array(b+1).join(a)};var d=/^\s\s*/,e=/\s\s*$/;b.stringTrimLeft=function(a){return a.replace(d,"")},b.stringTrimRight=function(a){return a.replace(e,"")},b.copyObject=function(a){var b={};for(var c in a)b[c]=a[c];return b},b.copyArray=function(a){var b=[];for(i=0,l=a.length;i<l;i++)a[i]&&typeof a[i]=="object"?b[i]=this.copyObject(a[i]):b[i]=a[i];return b},b.deepCopy=function(a){if(typeof a!="object")return a;var b=a.constructor();for(var c in a)typeof a[c]=="object"?b[c]=this.deepCopy(a[c]):b[c]=a[c];return b},b.arrayToMap=function(a){var b={};for(var c=0;c<a.length;c++)b[a[c]]=1;return b},b.arrayRemove=function(a,b){for(var c=0;c<=a.length;c++)b===a[c]&&a.splice(c,1)},b.escapeRegExp=function(a){return a.replace(/([.*+?^${}()|[\]\/\\])/g,"\\$1")},b.deferredCall=function(a){var b=null,c=function(){b=null,a()},d=function(a){b||(b=setTimeout(c,a||0));return d};d.schedule=d,d.call=function(){this.cancel(),a();return d},d.cancel=function(){clearTimeout(b),b=null;return d};return d}}),define("ace/worker/jshint",["require","exports","module"],function(a,b,c){var d=function(){function bY(){function b(){var a=w;bl("[");if(w.id!=="]")for(;;){if(w.id==="(end)")bf("Missing ']' to match '[' from line {a}.",w,a.line);else{if(w.id==="]"){bd("Unexpected comma.",N);break}w.id===","&&bf("Unexpected comma.",w)}bY();if(w.id!==",")break;bl(",")}bl("]")}function a(){var a={},b=w;bl("{");if(w.id!=="}")for(;;){if(w.id==="(end)")bf("Missing '}' to match '{' from line {a}.",w,b.line);else{if(w.id==="}"){bd("Unexpected comma.",N);break}w.id===","?bf("Unexpected comma.",w):w.id!=="(string)"&&bd("Expected a string and instead saw {a}.",w,w.value)}a[w.value]===!0?bd("Duplicate key '{a}'.",w,w.value):w.value==="__proto__"?bd("Stupid key '{a}'.",w,w.value):a[w.value]=!0,bl(),bl(":"),bY();if(w.id!==",")break;bl(",")}bl("}")}switch(w.id){case"{":a();break;case"[":b();break;case"true":case"false":case"null":case"(number)":case"(string)":bl();break;case"-":bl("-"),N.character!==w.from&&bd("Unexpected space after '-'.",N),bn(N,w),bl("(number)");break;default:bf("Expected a JSON value.",w)}}function bW(b,c){var d,e=z,f=F;z=Object.create(z),F=Object.create(F),i={"(name)":b||'"'+a+'"',"(line)":w.line,"(context)":i,"(breakage)":0,"(loopage)":0,"(scope)":F,"(statement)":c},d=i,N.funct=i,k.push(i),b&&bi(b,"function"),i["(params)"]=bV(),bR(!1),F=f,z=e,i["(last)"]=N.line,i=i["(context)"];return d}function bV(){var a,b=w,c=[];bl("("),bp();if(w.id===")")bl(")"),bp(C,N);else for(;;){a=bM(!0),c.push(a),bi(a,"parameter");if(w.id===",")bu();else{bl(")",b),bp(C,N);return c}}}function bU(){var a=bL(!0);a||(w.id==="(string)"?(a=w.value,bl()):w.id==="(number)"&&(a=w.value.toString(),bl()));return a}function bT(a){var b=a.value,c=a.line,d=m[b];typeof d=="function"&&(d=!1),d?d[d.length-1]!==c&&d.push(c):(d=[c],m[b]=d)}function bS(a){u&&typeof u[a]!="boolean"&&bd("Unexpected /*member '{a}'.",N,a),typeof t[a]=="number"?t[a]+=1:t[a]=1}function bR(a,b){var c,d=n,e=o,f=K,g=F,h;n=a,F=Object.create(F),bq(N,w),h=w;if(w.id==="{"){bl("{");if(w.id!=="}"||N.line!==w.line){o+=z.indent;while(!a&&w.from>o)o+=z.indent;!a&&!bP()&&!f&&z.strict&&i["(context)"]["(global)"]&&bd('Missing "use strict" statement.'),c=bQ(),K=f,o-=z.indent,bs()}bl("}",h),o=e}else a?((!b||z.curly)&&bd("Expected '{a}' and instead saw '{b}'.",w,"{",w.value),y=!0,c=[bO()],y=!1):bf("Expected '{a}' and instead saw '{b}'.",w,"{",w.value);i["(verb)"]=null,F=g,n=d,a&&z.noempty&&(!c||c.length===0)&&bd("Empty block.");return c}function bQ(a){var b=[],c,d;while(!w.reach&&w.id!=="(end)")w.id===";"?(bd("Unnecessary semicolon."),bl(";")):b.push(bO());return b}function bP(){if(w.value==="use strict"){K&&bd('Unnecessary "use strict".'),bl(),bl(";"),K=!0,z.newcap=!0,z.undef=!0;return!0}return!1}function bO(a){var b=o,c,d=F,e=w;if(e.id===";")bd("Unnecessary semicolon.",e),bl(";");else{e.identifier&&!e.reserved&&bk().id===":"&&(bl(),bl(":"),F=Object.create(d),bi(e.value,"label"),w.labelled||bd("Label '{a}' on {b} statement.",w,e.value,w.value),Y.test(e.value+":")&&bd("Label '{a}' looks like a javascript url.",e,e.value),w.label=e.value,e=w),a||bs(),c=bm(0,!0),e.block||(!z.expr&&(!c||!c.exps)?bd("Expected an assignment or function call and instead saw an expression.",N):z.nonew&&c.id==="("&&c.left.id==="new"&&bd("Do not use 'new' for side effects."),w.id!==";"?!z.asi&&(!z.lastsemic||w.id!="}"||w.line!=N.line)&&be("Missing semicolon.",N.line,N.from+N.value.length):(bn(N,w),bl(";"),bq(N,w))),o=b,F=d;return c}}function bN(a){var b=0,c;if(w.id===";"&&!y)for(;;){c=bk(b);if(c.reach)return;if(c.id!=="(endline)"){if(c.id==="function"){bd("Inner functions should be listed at the top of the outer function.",c);break}bd("Unreachable '{a}' after '{b}'.",c,c.value,a);break}b+=1}}function bM(a){var b=bL(a);if(b)return b;N.id==="function"&&w.id==="("?bd("Missing name in function declaration."):bf("Expected an identifier and instead saw '{a}'.",w,w.value)}function bL(a){if(w.identifier){bl(),N.reserved&&!z.es5&&(!a||N.value!="undefined")&&bd("Expected an identifier and instead saw '{a}' (a reserved word).",N,N.id);return N.value}}function bK(a,b){var c=bv(a,150);c.led=function(a){z.plusplus?bd("Unexpected use of '{a}'.",this,this.id):(!a.identifier||a.reserved)&&a.id!=="."&&a.id!=="["&&bd("Bad operand.",this),this.left=a;return this};return c}function bJ(a){bv(a,20).exps=!0;return bE(a,function(a,b){z.bitwise&&bd("Unexpected use of '{a}'.",b,b.id),bq(C,N),bq(N,w);if(a){if(a.id==="."||a.id==="["||a.identifier&&!a.reserved){bm(19);return b}a===L["function"]&&bd("Expected an identifier in an assignment, and instead saw a function invocation.",N);return b}bf("Bad assignment.",b)},20)}function bI(a,b,c){var d=bv(a,c);bz(d),d.led=typeof b=="function"?b:function(a){z.bitwise&&bd("Unexpected use of '{a}'.",this,this.id),this.left=a,this.right=bm(c);return this};return d}function bH(a,b){bv(a,20).exps=!0;return bE(a,function(a,b){var c;b.left=a,A[a.value]===!1&&F[a.value]["(global)"]===!0?bd("Read only.",a):a["function"]&&bd("'{a}' is a function.",a,a.value);if(a){if(a.id==="."||a.id==="["){(!a.left||a.left.value==="arguments")&&bd("Bad assignment.",b),b.right=bm(19);return b}if(a.identifier&&!a.reserved){i[a.value]==="exception"&&bd("Do not assign to the exception parameter.",a),b.right=bm(19);return b}a===L["function"]&&bd("Expected an identifier in an assignment and instead saw a function invocation.",N)}bf("Bad assignment.",b)},20)}function bG(a){return a&&(a.type==="(number)"&&+a.value===0||a.type==="(string)"&&a.value===""||a.type==="null"&&!z.eqnull||a.type==="true"||a.type==="false"||a.type==="undefined")}function bF(a,b){var c=bv(a,100);c.led=function(a){br(C,N),bq(N,w);var c=bm(100);a&&a.id==="NaN"||c&&c.id==="NaN"?bd("Use the isNaN function to compare with NaN.",this):b&&b.apply(this,[a,c]),a.id==="!"&&bd("Confusing use of '{a}'.",a,"!"),c.id==="!"&&bd("Confusing use of '{a}'.",a,"!"),this.left=a,this.right=c;return this};return c}function bE(a,b,c,d){var e=bv(a,c);bz(e),e.led=function(a){d||(br(C,N),bq(N,w));if(typeof b=="function")return b(a,this);this.left=a,this.right=bm(c);return this};return e}function bD(a,b){return bC(a,function(){typeof b=="function"&&b(this);return this})}function bC(a,b){var c=bB(a,b);c.identifier=c.reserved=!0;return c}function bB(a,b){var c=bw(a);c.type=a,c.nud=b;return c}function bA(a,b){var c=bv(a,150);bz(c),c.nud=typeof b=="function"?b:function(){this.right=bm(150),this.arity="unary";if(this.id==="++"||this.id==="--")z.plusplus?bd("Unexpected use of '{a}'.",this,this.id):(!this.right.identifier||this.right.reserved)&&this.right.id!=="."&&this.right.id!=="["&&bd("Bad operand.",this);return this};return c}function bz(a){var b=a.id.charAt(0);if(b>="a"&&b<="z"||b>="A"&&b<="Z")a.identifier=a.reserved=!0;return a}function by(a,b){var c=bx(a,b);c.block=!0;return c}function bx(a,b){var c=bw(a);c.identifier=c.reserved=!0,c.fud=b;return c}function bw(a){return bv(a,0)}function bv(a,b){var c=L[a];if(!c||typeof c!="object")L[a]=c={id:a,lbp:b,value:a};return c}function bu(){N.line!==w.line?z.laxbreak||bd("Bad line breaking before '{a}'.",N,w.id):N.character!==w.from&&z.white&&bd("Unexpected space after '{a}'.",w,N.value),bl(","),bq(N,w)}function bt(a){a=a||N,a.line!==w.line&&bd("Line breaking error '{a}'.",a,a.value)}function bs(a){var b;z.white&&w.id!=="(end)"&&(b=o+(a||0),w.from!==b&&bd("Expected '{a}' to have an indentation at {b} instead at {c}.",w,w.value,b,w.from))}function br(a,b){a=a||N,b=b||w,!z.laxbreak&&a.line!==b.line?bd("Bad line breaking before '{a}'.",b,b.id):z.white&&(a=a||N,b=b||w,a.character===b.from&&bd("Missing space after '{a}'.",w,a.value))}function bq(a,b){z.white&&(a=a||N,b=b||w,a.line===b.line&&a.character===b.from&&bd("Missing space after '{a}'.",w,a.value))}function bp(a,b){a=a||N,b=b||w,z.white&&!a.comment&&a.line===b.line&&bn(a,b)}function bo(a,b){a=a||N,b=b||w,z.white&&(a.character!==b.from||a.line!==b.line)&&bd("Unexpected space before '{a}'.",b,b.value)}function bn(a,b){a=a||N,b=b||w,z.white&&a.character!==b.from&&a.line===b.line&&bd("Unexpected space after '{a}'.",b,a.value)}function bm(b,c){var d,e=!1;w.id==="(end)"&&bf("Unexpected early end of program.",N),bl(),c&&(a="anonymous",i["(verb)"]=N.value);if(c===!0&&N.fud)d=N.fud();else{if(N.nud)d=N.nud();else{if(w.type==="(number)"&&N.id==="."){bd("A leading decimal point can be confused with a dot: '.{a}'.",N,w.value),bl();return N}bf("Expected an identifier and instead saw '{a}'.",N,N.id)}while(b<w.lbp)e=N.value=="Array",bl(),e&&N.id=="("&&w.id==")"&&bd("Use the array literal notation [].",N),N.led?d=N.led(d):bf("Expected an operator and instead saw '{a}'.",N,N.id)}return d}function bl(b,c){switch(N.id){case"(number)":w.id==="."&&bd("A dot following a number can be confused with a decimal point.",N);break;case"-":(w.id==="-"||w.id==="--")&&bd("Confusing minusses.");break;case"+":(w.id==="+"||w.id==="++")&&bd("Confusing plusses.")}if(N.type==="(string)"||N.identifier)a=N.value;b&&w.id!==b&&(c?w.id==="(end)"?bd("Unmatched '{a}'.",c,c.id):bd("Expected '{a}' to match '{b}' from line {c} and instead saw '{d}'.",w,b,c.id,c.line,w.value):(w.type!=="(identifier)"||w.value!==b)&&bd("Expected '{a}' and instead saw '{b}'.",w,b,w.value)),C=N,N=w;for(;;){w=s.shift()||bh.token();if(w.id==="(end)"||w.id==="(error)")return;if(w.type==="special")bj();else if(w.id!=="(endline)")break}}function bk(a){var b=a||0,c=0,d;while(c<=b)d=s[c],d||(d=s[c]=bh.token()),c+=1;return d}function bj(){var a,b,d,e=w.value,f,g;switch(e){case"*/":bf("Unbegun comment.");break;case"/*members":case"/*member":e="/*members",u||(u={}),b=u;break;case"/*jshint":case"/*jslint":b=z,d=c;break;case"/*global":b=A;break;default:bf("What?")}f=bh.token();loop:for(;;){for(;;){if(f.type==="special"&&f.value==="*/")break loop;if(f.id!=="(endline)"&&f.id!==",")break;f=bh.token()}f.type!=="(string)"&&f.type!=="(identifier)"&&e!=="/*members"&&bf("Bad option.",f),g=bh.token(),g.id===":"?(g=bh.token(),b===u&&bf("Expected '{a}' and instead saw '{b}'.",f,"*/",":"),f.value!=="indent"||e!=="/*jshint"&&e!=="/*jslint"?f.value!=="maxerr"||e!=="/*jshint"&&e!=="/*jslint"?f.value!=="maxlen"||e!=="/*jshint"&&e!=="/*jslint"?g.value==="true"?b[f.value]=!0:g.value==="false"?b[f.value]=!1:bf("Bad option value.",g):(a=+g.value,(typeof a!="number"||!isFinite(a)||a<=0||Math.floor(a)!==a)&&bf("Expected a small integer and instead saw '{a}'.",g,g.value),b.maxlen=a):(a=+g.value,(typeof a!="number"||!isFinite(a)||a<=0||Math.floor(a)!==a)&&bf("Expected a small integer and instead saw '{a}'.",g,g.value),b.maxerr=a):(a=+g.value,(typeof a!="number"||!isFinite(a)||a<=0||Math.floor(a)!==a)&&bf("Expected a small integer and instead saw '{a}'.",g,g.value),b.white=!0,b.indent=a),f=bh.token()):((e==="/*jshint"||e==="/*jslint")&&bf("Missing option value.",f),b[f.value]=!1,f=g)}d&&bb()}function bi(a,b){a==="hasOwnProperty"&&bd("'hasOwnProperty' is a really bad name."),_(i,a)&&!i["(global)"]&&(i[a]===!0?z.latedef&&bd("'{a}' was used before it was defined.",w,a):z.shadow||bd("'{a}' is already defined.",w,a)),i[a]=b,i["(global)"]?(l[a]=i,_(m,a)&&(z.latedef&&bd("'{a}' was used before it was defined.",w,a),delete m[a])):F[a]=i}function bg(a,b,c,d,e,f,g){return bf(a,{line:b,from:c},d,e,f,g)}function bf(a,b,c,d,e,f){var g=bd(a,b,c,d,e,f);bc("Stopping, unable to continue.",g.line,g.character)}function be(a,b,c,d,e,f,g){return bd(a,{line:b,from:c},d,e,f,g)}function bd(a,b,c,e,f,g){var h,i,j;b=b||w,b.id==="(end)"&&(b=N),i=b.line||0,h=b.from||0,j={id:"(error)",raw:a,evidence:r[i-1]||"",line:i,character:h,a:c,b:e,c:f,d:g},j.reason=a.supplant(j),d.errors.push(j),z.passfail&&bc("Stopping. ",i,h),P+=1,P>=z.maxerr&&bc("Too many errors.",i,h);return j}function bc(a,b,c){var d=Math.floor(b/r.length*100);throw{name:"JSHintError",line:b,character:c,message:a+" ("+d+"% scanned)."}}function bb(){z.couch&&ba(A,f),z.rhino&&ba(A,E),z.prototypejs&&ba(A,D),z.node&&ba(A,x),z.devel&&ba(A,g),z.browser&&ba(A,e),z.jquery&&ba(A,q),z.mootools&&ba(A,v),z.wsh&&ba(A,Q),z.globalstrict&&z.strict!==!1&&(z.strict=!0)}function ba(a,b){var c;for(c in b)_(b,c)&&(a[c]=b[c])}function _(a,b){return Object.prototype.hasOwnProperty.call(a,b)}function $(){}"use strict";var a,b={"<":!0,"<=":!0,"==":!0,"===":!0,"!==":!0,"!=":!0,">":!0,">=":!0,"+":!0,"-":!0,"*":!0,"/":!0,"%":!0},c={asi:!0,bitwise:!0,boss:!0,browser:!0,couch:!0,curly:!0,debug:!0,devel:!0,eqeqeq:!0,eqnull:!0,es5:!0,evil:!0,expr:!0,forin:!0,globalstrict:!0,immed:!0,jquery:!0,latedef:!0,laxbreak:!0,loopfunc:!0,mootools:!0,newcap:!0,noarg:!0,node:!0,noempty:!0,nonew:!0,nomen:!0,onevar:!0,passfail:!0,plusplus:!0,prototypejs:!0,regexdash:!0,regexp:!0,rhino:!0,undef:!0,shadow:!0,strict:!0,sub:!0,supernew:!0,trailing:!0,white:!0,wsh:!0},e={ArrayBuffer:!1,ArrayBufferView:!1,addEventListener:!1,applicationCache:!1,blur:!1,clearInterval:!1,clearTimeout:!1,close:!1,closed:!1,DataView:!1,defaultStatus:!1,document:!1,event:!1,FileReader:!1,Float32Array:!1,Float64Array:!1,focus:!1,frames:!1,getComputedStyle:!1,HTMLElement:!1,history:!1,Int16Array:!1,Int32Array:!1,Int8Array:!1,Image:!1,length:!1,localStorage:!1,location:!1,moveBy:!1,moveTo:!1,name:!1,navigator:!1,onbeforeunload:!0,onblur:!0,onerror:!0,onfocus:!0,onload:!0,onresize:!0,onunload:!0,open:!1,openDatabase:!1,opener:!1,Option:!1,parent:!1,print:!1,removeEventListener:!1,resizeBy:!1,resizeTo:!1,screen:!1,scroll:!1,scrollBy:!1,scrollTo:!1,setInterval:!1,setTimeout:!1,status:!1,top:!1,Uint16Array:!1,Uint32Array:!1,Uint8Array:!1,WebSocket:!1,window:!1,Worker:!1,XMLHttpRequest:!1,XPathEvaluator:!1,XPathException:!1,XPathExpression:!1,XPathNamespace:!1,XPathNSResolver:!1,XPathResult:!1},f={require:!1,respond:!1,getRow:!1,emit:!1,send:!1,start:!1,sum:!1,log:!1,exports:!1,module:!1},g={alert:!1,confirm:!1,console:!1,Debug:!1,opera:!1,prompt:!1},h={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"/":"\\/","\\":"\\\\"},i,j=["closure","exception","global","label","outer","unused","var"],k,l,m,n,o,p,q={$:!1,jQuery:!1},r,s,t,u,v={$:!1,$$:!1,Assets:!1,Browser:!1,Chain:!1,Class:!1,Color:!1,Cookie:!1,Core:!1,Document:!1,DomReady:!1,DOMReady:!1,Drag:!1,Element:!1,Elements:!1,Event:!1,Events:!1,Fx:!1,Group:!1,Hash:!1,HtmlTable:!1,Iframe:!1,IframeShim:!1,InputValidator:!1,instanceOf:!1,Keyboard:!1,Locale:!1,Mask:!1,MooTools:!1,Native:!1,Options:!1,OverText:!1,Request:!1,Scroller:!1,Slick:!1,Slider:!1,Sortables:!1,Spinner:!1,Swiff:!1,Tips:!1,Type:!1,typeOf:!1,URI:!1,Window:!1},w,x={__filename:!1,__dirname:!1,exports:!1,Buffer:!1,GLOBAL:!1,global:!1,module:!1,process:!1,require:!1},y,z,A,B,C,D={$:!1,$$:!1,$A:!1,$F:!1,$H:!1,$R:!1,$break:!1,$continue:!1,$w:!1,Abstract:!1,Ajax:!1,Class:!1,Enumerable:!1,Element:!1,Event:!1,Field:!1,Form:!1,Hash:!1,Insertion:!1,ObjectRange:!1,PeriodicalExecuter:!1,Position:!1,Prototype:!1,Selector:!1,Template:!1,Toggle:!1,Try:!1,Autocompleter:!1,Builder:!1,Control:!1,Draggable:!1,Draggables:!1,Droppables:!1,Effect:!1,Sortable:!1,SortableObserver:!1,Sound:!1,Scriptaculous:!1},E={defineClass:!1,deserialize:!1,gc:!1,help:!1,load:!1,loadClass:!1,print:!1,quit:!1,readFile:!1,readUrl:!1,runCommand:!1,seal:!1,serialize:!1,spawn:!1,sync:!1,toint32:!1,version:!1},F,G,H,I={Array:!1,Boolean:!1,Date:!1,decodeURI:!1,decodeURIComponent:!1,encodeURI:!1,encodeURIComponent:!1,Error:!1,eval:!1,EvalError:!1,Function:!1,hasOwnProperty:!1,isFinite:!1,isNaN:!1,JSON:!1,Math:!1,Number:!1,Object:!1,parseInt:!1,parseFloat:!1,RangeError:!1,ReferenceError:!1,RegExp:!1,String:!1,SyntaxError:!1,TypeError:!1,URIError:!1},J={E:!0,LN2:!0,LN10:!0,LOG2E:!0,LOG10E:!0,MAX_VALUE:!0,MIN_VALUE:!0,NEGATIVE_INFINITY:!0,PI:!0,POSITIVE_INFINITY:!0,SQRT1_2:!0,SQRT2:!0},K,L={},M,N,O,P,Q={ActiveXObject:!0,Enumerator:!0,GetObject:!0,ScriptEngine:!0,ScriptEngineBuildVersion:!0,ScriptEngineMajorVersion:!0,ScriptEngineMinorVersion:!0,VBArray:!0,WSH:!0,WScript:!0},R=/@cc|<\/?|script|\]\s*\]|<\s*!|&lt/i,S=/[\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/,T=/^\s*([(){}\[.,:;'"~\?\]#@]|==?=?|\/(\*(jshint|jslint|members?|global)?|=|\/)?|\*[\/=]?|\+(?:=|\++)?|-(?:=|-+)?|%=?|&[&=]?|\|[|=]?|>>?>?=?|<([\/=!]|\!(\[|--)?|<=?)?|\^=?|\!=?=?|[a-zA-Z_$][a-zA-Z0-9_$]*|[0-9]+([xX][0-9a-fA-F]+|\.[0-9]*)?([eE][+\-]?[0-9]+)?)/,U=/[\u0000-\u001f&<"\/\\\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/,V=/[\u0000-\u001f&<"\/\\\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,W=/\*\/|\/\*/,X=/^([a-zA-Z_$][a-zA-Z0-9_$]*)$/,Y=/^(?:javascript|jscript|ecmascript|vbscript|mocha|livescript)\s*:/i,Z=/^\s*\/\*\s*falls\sthrough\s*\*\/\s*$/;typeof Array.isArray!="function"&&(Array.isArray=function(a){return Object.prototype.toString.apply(a)==="[object Array]"}),typeof Object.create!="function"&&(Object.create=function(a){$.prototype=a;return new $}),typeof Object.keys!="function"&&(Object.keys=function(a){var b=[],c;for(c in a)_(a,c)&&b.push(c);return b}),typeof String.prototype.entityify!="function"&&(String.prototype.entityify=function(){return this.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}),typeof String.prototype.isAlpha!="function"&&(String.prototype.isAlpha=function(){return this>="a"&&this<="z￿"||this>="A"&&this<="Z￿"}),typeof String.prototype.isDigit!="function"&&(String.prototype.isDigit=function(){return this>="0"&&this<="9"}),typeof String.prototype.supplant!="function"&&(String.prototype.supplant=function(a){return this.replace(/\{([^{}]*)\}/g,function(b,c){var d=a[c];return typeof d=="string"||typeof d=="number"?d:b})}),typeof String.prototype.name!="function"&&(String.prototype.name=function(){if(X.test(this))return this;if(U.test(this))return'"'+this.replace(V,function(a){var b=h[a];if(b)return b;return"\\u"+("0000"+a.charCodeAt().toString(16)).slice(-4)})+'"';return'"'+this+'"'});var bh=function(){function g(a,e){var f,g;a==="(color)"||a==="(range)"?g={type:a}:a==="(punctuator)"||a==="(identifier)"&&_(L,e)?g=L[e]||L["(error)"]:g=L[a],g=Object.create(g),(a==="(string)"||a==="(range)")&&Y.test(e)&&be("Script URL.",d,c),a==="(identifier)"&&(g.identifier=!0,e==="__iterator__"||e==="__proto__"?bg("Reserved name '{a}'.",d,c,e):z.nomen&&(e.charAt(0)==="_"||e.charAt(e.length-1)==="_")&&be("Unexpected {a} in '{b}'.",d,c,"dangling '_'",e)),g.value=e,g.line=d,g.character=b,g.from=c,f=g.id,f!=="(endline)"&&(B=f&&("(,=:[!&|?{};".indexOf(f.charAt(f.length-1))>=0||f==="return"));return g}function f(){var a,c;if(d>=r.length)return!1;b=1,e=r[d],d+=1,a=e.search(/ \t/),a>=0&&be("Mixed spaces and tabs.",d,a+1),e=e.replace(/\t/g,M),a=e.search(S),a>=0&&be("Unsafe character.",d,a),z.maxlen&&z.maxlen<e.length&&be("Line too long.",d,e.length),c=e.search(/\s+$/),z.trailing&&~c&&be("Trailing whitespace.",d,c);return!0}var b,c,d,e;return{init:function(a){typeof a=="string"?r=a.replace(/\r\n/g,"\n").replace(/\r/g,"\n").split("\n"):r=a,r[0]&&r[0].substr(0,2)=="#!"&&(r[0]=""),d=0,f(),c=1},range:function(a,f){var h,i="";c=b,e.charAt(0)!==a&&bg("Expected '{a}' and instead saw '{b}'.",d,b,a,e.charAt(0));for(;;){e=e.slice(1),b+=1,h=e.charAt(0);switch(h){case"":bg("Missing '{a}'.",d,b,h);break;case f:e=e.slice(1),b+=1;return g("(range)",i);case"\\":be("Unexpected '{a}'.",d,b,h)}i+=h}},token:function(){function t(a){function k(a){var c=parseInt(e.substr(i+1,a),16);i+=a,c>=32&&c<=126&&c!==34&&c!==92&&c!==39&&be("Unnecessary escapement.",d,b),b+=a,h=String.fromCharCode(c)}var h,i,j="";p&&a!=='"'&&be("Strings must use doublequote.",d,b),i=0;for(;;){while(i>=e.length)i=0,f()||bg("Unclosed string.",d,c);h=e.charAt(i);if(h===a){b+=1,e=e.substr(i+1);return g("(string)",j,a)}if(h<" "){if(h==="\n"||h==="\r")break;be("Control character in string: {a}.",d,b+i,e.slice(0,i))}else if(h==="\\"){i+=1,b+=1,h=e.charAt(i);switch(h){case"\\":case'"':case"/":break;case"'":p&&be("Avoid \\'.",d,b);break;case"b":h="\b";break;case"f":h="\f";break;case"n":h="\n";break;case"r":h="\r";break;case"t":h="\t";break;case"u":k(4);break;case"v":p&&be("Avoid \\v.",d,b),h=" ";break;case"x":p&&be("Avoid \\x-.",d,b),k(2);break;default:be("Bad escapement.",d,b)}}j+=h,b+=1,i+=1}}function s(a){var d=a.exec(e),f;if(d){n=d[0].length,f=d[1],h=f.charAt(0),e=e.substr(n),c=b+n-f.length,b+=n;return f}}var a,h,i,j,k,l,m,n,o,q,r;for(;;){if(!e)return g(f()?"(endline)":"(end)","");r=s(T);if(!r){r="",h="";while(e&&e<"!")e=e.substr(1);e&&bg("Unexpected '{a}'.",d,b,e.substr(0,1))}else{if(h.isAlpha()||h==="_"||h==="$")return g("(identifier)",r);if(h.isDigit()){isFinite(Number(r))||be("Bad number '{a}'.",d,b,r),e.substr(0,1).isAlpha()&&be("Missing space after '{a}'.",d,b,r),h==="0"&&(j=r.substr(1,1),j.isDigit()?N.id!=="."&&be("Don't use extra leading zeros '{a}'.",d,b,r):p&&(j==="x"||j==="X")&&be("Avoid 0x-. '{a}'.",d,b,r)),r.substr(r.length-1)==="."&&be("A trailing decimal point can be confused with a dot '{a}'.",d,b,r);return g("(number)",r)}switch(r){case'"':case"'":return t(r);case"//":G&&be("Unexpected comment.",d,b),e="",N.comment=!0;break;case"/*":G&&be("Unexpected comment.",d,b);for(;;){m=e.search(W);if(m>=0)break;f()||bg("Unclosed comment.",d,b)}b+=m+2,e.substr(m,1)==="/"&&bg("Nested comment.",d,b),e=e.substr(m+2),N.comment=!0;break;case"/*members":case"/*member":case"/*jshint":case"/*jslint":case"/*global":case"*/":return{value:r,type:"special",line:d,character:b,from:c};case"":break;case"/":N.id==="/="&&bg("A regular expression literal can be confused with '/='.",d,c);if(B){k=0,i=0,n=0;for(;;){a=!0,h=e.charAt(n),n+=1;switch(h){case"":bg("Unclosed regular expression.",d,c);return;case"/":k>0&&be("Unescaped '{a}'.",d,c+n,"/"),h=e.substr(0,n-1),q={g:!0,i:!0,m:!0};while(q[e.charAt(n)]===!0)q[e.charAt(n)]=!1,n+=1;b+=n,e=e.substr(n),q=e.charAt(0),(q==="/"||q==="*")&&bg("Confusing regular expression.",d,c);return g("(regexp)",h);case"\\":h=e.charAt(n),h<" "?be("Unexpected control character in regular expression.",d,c+n):h==="<"&&be("Unexpected escaped character '{a}' in regular expression.",d,c+n,h),n+=1;break;case"(":k+=1,a=!1;if(e.charAt(n)==="?"){n+=1;switch(e.charAt(n)){case":":case"=":case"!":n+=1;break;default:be("Expected '{a}' and instead saw '{b}'.",d,c+n,":",e.charAt(n))}}else i+=1;break;case"|":a=!1;break;case")":k===0?be("Unescaped '{a}'.",d,c+n,")"):k-=1;break;case" ":q=1;while(e.charAt(n)===" ")n+=1,q+=1;q>1&&be("Spaces are hard to count. Use {{a}}.",d,c+n,q);break;case"[":h=e.charAt(n),h==="^"&&(n+=1,z.regexp?be("Insecure '{a}'.",d,c+n,h):e.charAt(n)==="]"&&bg("Unescaped '{a}'.",d,c+n,"^")),q=!1,h==="]"&&(be("Empty class.",d,c+n-1),q=!0);klass:do{h=e.charAt(n),n+=1;switch(h){case"[":case"^":be("Unescaped '{a}'.",d,c+n,h),q=!0;break;case"-":q?q=!1:(be("Unescaped '{a}'.",d,c+n,"-"),q=!0);break;case"]":!q&&!z.regexdash&&be("Unescaped '{a}'.",d,c+n-1,"-");break klass;case"\\":h=e.charAt(n),h<" "?be("Unexpected control character in regular expression.",d,c+n):h==="<"&&be("Unexpected escaped character '{a}' in regular expression.",d,c+n,h),n+=1,q=!0;break;case"/":be("Unescaped '{a}'.",d,c+n-1,"/"),q=!0;break;case"<":q=!0;break;default:q=!0}}while(h);break;case".":z.regexp&&be("Insecure '{a}'.",d,c+n,h);break;case"]":case"?":case"{":case"}":case"+":case"*":be("Unescaped '{a}'.",d,c+n,h)}if(a)switch(e.charAt(n)){case"?":case"+":case"*":n+=1,e.charAt(n)==="?"&&(n+=1);break;case"{":n+=1,h=e.charAt(n),(h<"0"||h>"9")&&be("Expected a number and instead saw '{a}'.",d,c+n,h),n+=1,o=+h;for(;;){h=e.charAt(n);if(h<"0"||h>"9")break;n+=1,o=+h+o*10}l=o;if(h===","){n+=1,l=Infinity,h=e.charAt(n);if(h>="0"&&h<="9"){n+=1,l=+h;for(;;){h=e.charAt(n);if(h<"0"||h>"9")break;n+=1,l=+h+l*10}}}e.charAt(n)!=="}"?be("Expected '{a}' and instead saw '{b}'.",d,c+n,"}",h):n+=1,e.charAt(n)==="?"&&(n+=1),o>l&&be("'{a}' should not be greater than '{b}'.",d,c+n,o,l)}}h=e.substr(0,n-1),b+=n,e=e.substr(n);return g("(regexp)",h)}return g("(punctuator)",r);case"#":return g("(punctuator)",r);default:return g("(punctuator)",r)}}}}}}();bB("(number)",function(){return this}),bB("(string)",function(){return this}),L["(identifier)"]={type:"(identifier)",lbp:0,identifier:!0,nud:function(){var b=this.value,c=F[b],d;typeof c=="function"?c=undefined:typeof c=="boolean"&&(d=i,i=k[0],bi(b,"var"),c=i,i=d);if(i===c)switch(i[b]){case"unused":i[b]="var";break;case"unction":i[b]="function",this["function"]=!0;break;case"function":this["function"]=!0;break;case"label":bd("'{a}' is a statement label.",N,b)}else if(i["(global)"])a!="typeof"&&a!="delete"&&z.undef&&typeof A[b]!="boolean"&&bd("'{a}' is not defined.",N,b),bT(N);else switch(i[b]){case"closure":case"function":case"var":case"unused":bd("'{a}' used out of scope.",N,b);break;case"label":bd("'{a}' is a statement label.",N,b);break;case"outer":case"global":break;default:if(c===!0)i[b]=!0;else if(c===null)bd("'{a}' is not allowed.",N,b),bT(N);else if(typeof c!="object")a!="typeof"&&a!="delete"&&z.undef?bd("'{a}' is not defined.",N,b):i[b]=!0,bT(N);else switch(c[b]){case"function":case"unction":this["function"]=!0,c[b]="closure",i[b]=c["(global)"]?"global":"outer";break;case"var":case"unused":c[b]="closure",i[b]=c["(global)"]?"global":"outer";break;case"closure":case"parameter":i[b]=c["(global)"]?"global":"outer";break;case"label":bd("'{a}' is a statement label.",N,b)}}return this},led:function(){bf("Expected an operator and instead saw '{a}'.",w,w.value)}},bB("(regexp)",function(){return this}),bw("(endline)"),bw("(begin)"),bw("(end)").reach=!0,bw("</").reach=!0,bw("<!"),bw("<!--"),bw("-->"),bw("(error)").reach=!0,bw("}").reach=!0,bw(")"),bw("]"),bw('"').reach=!0,bw("'").reach=!0,bw(";"),bw(":").reach=!0,bw(","),bw("#"),bw("@"),bC("else"),bC("case").reach=!0,bC("catch"),bC("default").reach=!0,bC("finally"),bD("arguments",function(a){K&&i["(global)"]&&bd("Strict violation.",a)}),bD("eval"),bD("false"),bD("Infinity"),bD("NaN"),bD("null"),bD("this",function(a){K&&(i["(statement)"]&&i["(name)"].charAt(0)>"Z"||i["(global)"])&&bd("Strict violation.",a)}),bD("true"),bD("undefined"),bH("=","assign",20),bH("+=","assignadd",20),bH("-=","assignsub",20),bH("*=","assignmult",20),bH("/=","assigndiv",20).nud=function(){bf("A regular expression literal can be confused with '/='.")},bH("%=","assignmod",20),bJ("&=","assignbitand",20),bJ("|=","assignbitor",20),bJ("^=","assignbitxor",20),bJ("<<=","assignshiftleft",20),bJ(">>=","assignshiftright",20),bJ(">>>=","assignshiftrightunsigned",20),bE("?",function(a,b){b.left=a,b.right=bm(10),bl(":"),b["else"]=bm(10);return b},30),bE("||","or",40),bE("&&","and",50),bI("|","bitor",70),bI("^","bitxor",80),bI("&","bitand",90),bF("==",function(a,b){var c=z.eqnull&&(a.value=="null"||b.value=="null");!c&&z.eqeqeq?bd("Expected '{a}' and instead saw '{b}'.",this,"===","=="):bG(a)?bd("Use '{a}' to compare with '{b}'.",this,"===",a.value):bG(b)&&bd("Use '{a}' to compare with '{b}'.",this,"===",b.value);return this}),bF("==="),bF("!=",function(a,b){z.eqeqeq?bd("Expected '{a}' and instead saw '{b}'.",this,"!==","!="):bG(a)?bd("Use '{a}' to compare with '{b}'.",this,"!==",a.value):bG(b)&&bd("Use '{a}' to compare with '{b}'.",this,"!==",b.value);return this}),bF("!=="),bF("<"),bF(">"),bF("<="),bF(">="),bI("<<","shiftleft",120),bI(">>","shiftright",120),bI(">>>","shiftrightunsigned",120),bE("in","in",120),bE("instanceof","instanceof",120),bE("+",function(a,b){var c=bm(130);if(a&&c&&a.id==="(string)"&&c.id==="(string)"){a.value+=c.value,a.character=c.character,Y.test(a.value)&&bd("JavaScript URL.",a);return a}b.left=a,b.right=c;return b},130),bA("+","num"),bA("+++",function(){bd("Confusing pluses."),this.right=bm(150),this.arity="unary";return this}),bE("+++",function(a){bd("Confusing pluses."),this.left=a,this.right=bm(130);return this},130),bE("-","sub",130),bA("-","neg"),bA("---",function(){bd("Confusing minuses."),this.right=bm(150),this.arity="unary";return this}),bE("---",function(a){bd("Confusing minuses."),this.left=a,this.right=bm(130);return this},130),bE("*","mult",140),bE("/","div",140),bE("%","mod",140),bK("++","postinc"),bA("++","preinc"),L["++"].exps=!0,bK("--","postdec"),bA("--","predec"),L["--"].exps=!0,bA("delete",function(){var a=bm(0);(!a||a.id!=="."&&a.id!=="[")&&bd("Variables should not be deleted."),this.first=a;return this}).exps=!0,bA("~",function(){z.bitwise&&bd("Unexpected '{a}'.",this,"~"),bm(150);return this}),bA("!",function(){this.right=bm(150),this.arity="unary",b[this.right.id]===!0&&bd("Confusing use of '{a}'.",this,"!");return this}),bA("typeof","typeof"),bA("new",function(){var a=bm(155),b;if(a&&a.id!=="function")if(a.identifier){a["new"]=!0;switch(a.value){case"Object":bd("Use the object literal notation {}.",N);break;case"Number":case"String":case"Boolean":case"Math":case"JSON":bd("Do not use {a} as a constructor.",N,a.value);break;case"Function":z.evil||bd("The Function constructor is eval.");break;case"Date":case"RegExp":break;default:a.id!=="function"&&(b=a.value.substr(0,1),z.newcap&&(b<"A"||b>"Z")&&bd("A constructor name should start with an uppercase letter.",N))}}else a.id!=="."&&a.id!=="["&&a.id!=="("&&bd("Bad constructor.",N);else z.supernew||bd("Weird construction. Delete 'new'.",this);bn(N,w),w.id!=="("&&!z.supernew&&bd("Missing '()' invoking a constructor."),this.first=a;return this}),L["new"].exps=!0,bA("void").exps=!0,bE(".",function(a,b){bn(C,N),bo();var c=bM();typeof c=="string"&&bS(c),b.left=a,b.right=c,z.noarg&&a&&a.value==="arguments"&&(c==="callee"||c==="caller")?bd("Avoid arguments.{a}.",a,c):!z.evil&&a&&a.value==="document"&&(c==="write"||c==="writeln")&&bd("document.write can be a form of eval.",a),!z.evil&&(c==="eval"||c==="execScript")&&bd("eval is evil.");return b},160,!0),bE("(",function(a,b){C.id!=="}"&&C.id!==")"&&bo(C,N),bp(),z.immed&&!a.immed&&a.id==="function"&&bd("Wrap an immediate function invocation in parentheses to assist the reader in understanding that the expression is the result of a function, and not the function itself.");var c=0,d=[];a&&a.type==="(identifier)"&&a.value.match(/^[A-Z]([A-Z0-9_$]*[a-z][A-Za-z0-9_$]*)?$/)&&a.value!=="Number"&&a.value!=="String"&&a.value!=="Boolean"&&a.value!=="Date"&&(a.value==="Math"?bd("Math is not a function.",a):z.newcap&&bd("Missing 'new' prefix when invoking a constructor.",a));if(w.id!==")")for(;;){d[d.length]=bm(10),c+=1;if(w.id!==",")break;bu()}bl(")"),bp(C,N),typeof a=="object"&&(a.value==="parseInt"&&c===1&&bd("Missing radix parameter.",a),z.evil||(a.value==="eval"||a.value==="Function"||a.value==="execScript"?bd("eval is evil.",a):d[0]&&d[0].id==="(string)"&&(a.value==="setTimeout"||a.value==="setInterval")&&bd("Implied eval is evil. Pass a function instead of a string.",a)),!a.identifier&&a.id!=="."&&a.id!=="["&&a.id!=="("&&a.id!=="&&"&&a.id!=="||"&&a.id!=="?"&&bd("Bad invocation.",a)),b.left=a;return b},155,!0).exps=!0,bA("(",function(){bp(),w.id==="function"&&(w.immed=!0);var a=bm(0);bl(")",this),bp(C,N),z.immed&&a.id==="function"&&(w.id==="("?bd("Move the invocation into the parens that contain the function.",w):bd("Do not wrap function literals in parens unless they are to be immediately invoked.",this));return a}),bE("[",function(a,b){bo(C,N),bp();var c=bm(0),d;c&&c.type==="(string)"&&(!z.evil&&(c.value==="eval"||c.value==="execScript")&&bd("eval is evil.",b),bS(c.value),!z.sub&&X.test(c.value)&&(d=L[c.value],(!d||!d.reserved)&&bd("['{a}'] is better written in dot notation.",c,c.value))),bl("]",b),bp(C,N),b.left=a,b.right=c;return b},160,!0),bA("[",function(){var a=N.line!==w.line;this.first=[],a&&(o+=z.indent,w.from===o+z.indent&&(o+=z.indent));while(w.id!=="(end)"){while(w.id===",")bd("Extra comma."),bl(",");if(w.id==="]")break;a&&N.line!==w.line&&bs(),this.first.push(bm(10));if(w.id!==",")break;bu();if(w.id==="]"&&!z.es5){bd("Extra comma.",N);break}}a&&(o-=z.indent,bs()),bl("]",this);return this},160),function(a){a.nud=function(){var a,b,c,d,e,f={},g;a=N.line!==w.line,a&&(o+=z.indent,w.from===o+z.indent&&(o+=z.indent));for(;;){if(w.id==="}")break;a&&bs();if(w.value==="get"&&bk().id!==":")bl("get"),z.es5||bf("get/set are ES5 features."),c=bU(),c||bf("Missing property name."),g=w,bn(N,w),b=bW(),!z.loopfunc&&i["(loopage)"]&&bd("Don't make functions within a loop.",g),e=b["(params)"],e&&bd("Unexpected parameter '{a}' in get {b} function.",g,e[0],c),bn(N,w),bl(","),bs(),bl("set"),d=bU(),c!==d&&bf("Expected {a} and instead saw {b}.",N,c,d),g=w,bn(N,w),b=bW(),e=b["(params)"],(!e||e.length!==1||e[0]!=="value")&&bd("Expected (value) in set {a} function.",g,c);else{c=bU();if(typeof c!="string")break;bl(":"),bq(N,w),bm(10)}f[c]===!0&&bd("Duplicate member '{a}'.",w,c),f[c]=!0,bS(c);if(w.id===",")bu(),w.id===","?bd("Extra comma.",N):w.id==="}"&&!z.es5&&bd("Extra comma.",N);else break}a&&(o-=z.indent,bs()),bl("}",this);return this},a.fud=function(){bf("Expected to see a statement and instead saw a block.",N)}}(bw("{"));var bX=bx("var",function(a){var b,c,d;i["(onevar)"]&&z.onevar?bd("Too many var statements."):i["(global)"]||(i["(onevar)"]=!0),this.first=[];for(;;){bq(N,w),b=bM(),i["(global)"]&&A[b]===!1&&bd("Redefinition of '{a}'.",N,b),bi(b,"unused");if(a)break;c=N,this.first.push(N),w.id==="="&&(bq(N,w),bl("="),bq(N,w),w.id==="undefined"&&bd("It is not necessary to initialize '{a}' to 'undefined'.",N,b),bk(0).id==="="&&w.identifier&&bf("Variable {a} was not declared correctly.",w,w.value),d=bm(0),c.first=d);if(w.id!==",")break;bu()}return this});bX.exps=!0,by("function",function(){n&&bd("Function declarations should not be placed in blocks. Use a function expression or move the statement to the top of the outer function.",N);var a=bM();bn(N,w),bi(a,"unction"),bW(a,!0),w.id==="("&&w.line===N.line&&bf("Function declarations are not invocable. Wrap the whole function invocation in parens.");return this}),bA("function",function(){var a=bL();a?bn(N,w):bq(N,w),bW(a),!z.loopfunc&&i["(loopage)"]&&bd("Don't make functions within a loop.");return this}),by("if",function(){var a=w;bl("("),bq(this,a),bp(),bm(20),w.id==="="&&(z.boss||bd("Expected a conditional expression and instead saw an assignment."),bl("="),bm(20)),bl(")",a),bp(C,N),bR(!0,!0),w.id==="else"&&(bq(N,w),bl("else"),w.id==="if"||w.id==="switch"?bO(!0):bR(!0,!0));return this}),by("try",function(){var a,b,c;bR(!1),w.id==="catch"&&(bl("catch"),bq(N,w),bl("("),c=F,F=Object.create(c),b=w.value,w.type!=="(identifier)"?bd("Expected an identifier and instead saw '{a}'.",w,b):bi(b,"exception"),bl(),bl(")"),bR(!1),a=!0,F=c);if(w.id==="finally")bl("finally"),bR(!1);else{a||bf("Expected '{a}' and instead saw '{b}'.",w,"catch",w.value);return this}}),by("while",function(){var a=w;i["(breakage)"]+=1,i["(loopage)"]+=1,bl("("),bq(this,a),bp(),bm(20),w.id==="="&&(z.boss||bd("Expected a conditional expression and instead saw an assignment."),bl("="),bm(20)),bl(")",a),bp(C,N),bR(!0,!0),i["(breakage)"]-=1,i["(loopage)"]-=1;return this}).labelled=!0,bC("with"),by("switch",function(){var a=w,b=!1;i["(breakage)"]+=1,bl("("),bq(this,a),bp(),this.condition=bm(20),bl(")",a),bp(C,N),bq(N,w),a=w,bl("{"),bq(N,w),o+=z.indent,this.cases=[];for(;;)switch(w.id){case"case":switch(i["(verb)"]){case"break":case"case":case"continue":case"return":case"switch":case"throw":break;default:Z.test(r[w.line-2])||bd("Expected a 'break' statement before 'case'.",N)}bs(-z.indent),bl("case"),this.cases.push(bm(20)),b=!0,bl(":"),i["(verb)"]="case";break;case"default":switch(i["(verb)"]){case"break":case"continue":case"return":case"throw":break;default:Z.test(r[w.line-2])||bd("Expected a 'break' statement before 'default'.",N)}bs(-z.indent),bl("default"),b=!0,bl(":");break;case"}":o-=z.indent,bs(),bl("}",a),(this.cases.length===1||this.condition.id==="true"||this.condition.id==="false")&&bd("This 'switch' should be an 'if'.",this),i["(breakage)"]-=1,i["(verb)"]=undefined;return;case"(end)":bf("Missing '{a}'.",w,"}");return;default:if(b)switch(N.id){case",":bf("Each value should have its own case label.");return;case":":bQ();break;default:bf("Missing ':' on a case clause.",N)}else bf("Expected '{a}' and instead saw '{b}'.",w,"case",w.value)}}).labelled=!0,bx("debugger",function(){z.debug||bd("All 'debugger' statements should be removed.");return this}).exps=!0,function(){var a=bx("do",function(){i["(breakage)"]+=1,i["(loopage)"]+=1,this.first=bR(!0),bl("while");var a=w;bq(N,a),bl("("),bp(),bm(20),w.id==="="&&(z.boss||bd("Expected a conditional expression and instead saw an assignment."),bl("="),bm(20)),bl(")",a),bp(C,N),i["(breakage)"]-=1,i["(loopage)"]-=1;return this});a.labelled=!0,a.exps=!0}(),by("for",function(){var a,b=w;i["(breakage)"]+=1,i["(loopage)"]+=1,bl("("),bq(this,b),bp();if(bk(w.id==="var"?1:0).id==="in"){if(w.id==="var")bl("var"),bX.fud.call(bX,!0);else{switch(i[w.value]){case"unused":i[w.value]="var";break;case"var":break;default:bd("Bad for in variable '{a}'.",w,w.value)}bl()}bl("in"),bm(20),bl(")",b),a=bR(!0,!0),z.forin&&(a.length>1||typeof a[0]!="object"||a[0].value!=="if")&&bd("The body of a for in should be wrapped in an if statement to filter unwanted properties from the prototype.",this),i["(breakage)"]-=1,i["(loopage)"]-=1;return this}if(w.id!==";")if(w.id==="var")bl("var"),bX.fud.call(bX);else for(;;){bm(0,"for");if(w.id!==",")break;bu()}bt(N),bl(";"),w.id!==";"&&(bm(20),w.id==="="&&(z.boss||bd("Expected a conditional expression and instead saw an assignment."),bl("="),bm(20))),bt(N),bl(";"),w.id===";"&&bf("Expected '{a}' and instead saw '{b}'.",w,")",";");if(w.id!==")")for(;;){bm(0,"for");if(w.id!==",")break;bu()}bl(")",b),bp(C,N),bR(!0,!0),i["(breakage)"]-=1,i["(loopage)"]-=1;return this}).labelled=!0,bx("break",function(){var a=w.value;i["(breakage)"]===0&&bd("Unexpected '{a}'.",w,this.value),bt(this),w.id!==";"&&N.line===w.line&&(i[a]!=="label"?bd("'{a}' is not a statement label.",w,a):F[a]!==i&&bd("'{a}' is out of scope.",w,a),this.first=w,bl()),bN("break");return this}).exps=!0,bx("continue",function(){var a=w.value;i["(breakage)"]===0&&bd("Unexpected '{a}'.",w,this.value),bt(this),w.id!==";"?N.line===w.line&&(i[a]!=="label"?bd("'{a}' is not a statement label.",w,a):F[a]!==i&&bd("'{a}' is out of scope.",w,a),this.first=w,bl()):i["(loopage)"]||bd("Unexpected '{a}'.",w,this.value),bN("continue");return this}).exps=!0,bx("return",function(){bt(this),w.id==="(regexp)"&&bd("Wrap the /regexp/ literal in parens to disambiguate the slash operator."),w.id!==";"&&!w.reach&&(bq(N,w),this.first=bm(20)),bN("return");return this}).exps=!0,bx("throw",function(){bt(this),bq(N,w),this.first=bm(20),bN("throw");return this}).exps=!0,bC("class"),bC("const"),bC("enum"),bC("export"),bC("extends"),bC("import"),bC("super"),bC("let"),bC("yield"),bC("implements"),bC("interface"),bC("package"),bC("private"),bC("protected"),bC("public"),bC("static");var bZ=function(a,b,c){var e,f,g;d.errors=[],A=Object.create(I),ba(A,c||{});if(b){e=b.predef;if(e)if(Array.isArray(e))for(f=0;f<e.length;f+=1)A[e[f]]=!0;else if(typeof e=="object"){g=Object.keys(e);for(f=0;f<g.length;f+=1)A[g[f]]=!!e[g]}z=b}else z={};z.indent=z.indent||4,z.maxerr=z.maxerr||50,M="";for(f=0;f<z.indent;f+=1)M+=" ";o=1,l=Object.create(A),F=l,i={"(global)":!0,"(name)":"(global)","(scope)":F,"(breakage)":0,"(loopage)":0},k=[i],O=[],G=!1,H=null,t={},u=null,m={},n=!1,s=[],p=!1,P=0,bh.init(a),B=!0,K=!1,C=N=w=L["(begin)"],bb();try{bl();switch(w.id){case"{":case"[":z.laxbreak=!0,p=!0,bY();break;default:w.value==="use strict"&&(z.globalstrict||bd('Use the function form of "use strict".'),bP()),bQ("lib")}bl("(end)")}catch(h){h&&d.errors.push({reason:h.message,line:h.line||w.line,character:h.character||w.from},null)}return d.errors.length===0};bZ.data=function(){var a={functions:[]},b,c,d=[],e,f,g,h=[],i,l=[],n;bZ.errors.length&&(a.errors=bZ.errors),p&&(a.json=!0);for(i in m)_(m,i)&&d.push({name:i,line:m[i]});d.length>0&&(a.implieds=d),O.length>0&&(a.urls=O),c=Object.keys(F),c.length>0&&(a.globals=c);for(f=1;f<k.length;f+=1){e=k[f],b={};for(g=0;g<j.length;g+=1)b[j[g]]=[];for(i in e)_(e,i)&&i.charAt(0)!=="("&&(n=e[i],n==="unction"&&(n="unused"),Array.isArray(b[n])&&(b[n].push(i),n==="unused"&&l.push({name:i,line:e["(line)"],"function":e["(name)"]})));for(g=0;g<j.length;g+=1)b[j[g]].length===0&&delete b[j[g]];b.name=e["(name)"],b.param=e["(params)"],b.line=e["(line)"],b.last=e["(last)"],a.functions.push(b)}l.length>0&&(a.unused=l),h=[];for(i in t)if(typeof t[i]=="number"){a.member=t;break}return a},bZ.report=function(a){function o(a,b){var c,d,e;if(b){m.push("<div><i>"+a+"</i> "),b=b.sort();for(d=0;d<b.length;d+=1)b[d]!==e&&(e=b[d],m.push((c?", ":"")+e),c=!0);m.push("</div>")}}var b=bZ.data(),c=[],d,e,f,g,h,i,j,k="",l,m=[],n;if(b.errors||b.implieds||b.unused){f=!0,m.push("<div id=errors><i>Error:</i>");if(b.errors)for(h=0;h<b.errors.length;h+=1)d=b.errors[h],d&&(e=d.evidence||"",m.push("<p>Problem"+(isFinite(d.line)?" at line "+d.line+" character "+d.character:"")+": "+d.reason.entityify()+"</p><p class=evidence>"+(e&&(e.length>80?e.slice(0,77)+"...":e).entityify())+"</p>"));if(b.implieds){n=[];for(h=0;h<b.implieds.length;h+=1)n[h]="<code>"+b.implieds[h].name+"</code>&nbsp;<i>"+b.implieds[h].line+"</i>";m.push("<p><i>Implied global:</i> "+n.join(", ")+"</p>")}if(b.unused){n=[];for(h=0;h<b.unused.length;h+=1)n[h]="<code><u>"+b.unused[h].name+"</u></code>&nbsp;<i>"+b.unused[h].line+"</i> <code>"+b.unused[h]["function"]+"</code>";m.push("<p><i>Unused variable:</i> "+n.join(", ")+"</p>")}b.json&&m.push("<p>JSON: bad.</p>"),m.push("</div>")}if(!a){m.push("<br><div id=functions>"),b.urls&&o("URLs<br>",b.urls,"<br>"),b.json&&!f?m.push("<p>JSON: good.</p>"):b.globals?m.push("<div><i>Global</i> "+b.globals.sort().join(", ")+"</div>"):m.push("<div><i>No new global variables introduced.</i></div>");for(h=0;h<b.functions.length;h+=1)g=b.functions[h],m.push("<br><div class=function><i>"+g.line+"-"+g.last+"</i> "+(g.name||"")+"("+(g.param?g.param.join(", "):"")+")</div>"),o("<big><b>Unused</b></big>",g.unused),o("Closure",g.closure),o("Variable",g["var"]),o("Exception",g.exception),o("Outer",g.outer),o("Global",g.global),o("Label",g.label);if(b.member){c=Object.keys(b.member);if(c.length){c=c.sort(),k="<br><pre id=members>/*members ",j=10;for(h=0;h<c.length;h+=1)i=c[h],l=i.name(),j+l.length>72&&(m.push(k+"<br>"),k=" ",j=1),j+=l.length+2,b.member[i]===1&&(l="<i>"+l+"</i>"),h<c.length-1&&(l+=", "),k+=l;m.push(k+"<br>*/</pre>")}m.push("</div>")}}return m.join("")},bZ.jshint=bZ,bZ.edition="2011-04-16";return bZ}();typeof b=="object"&&b&&(b.JSHINT=d)}),define("ace/narcissus/jsparse",["require","exports","module","ace/narcissus/jslex","ace/narcissus/jsdefs"],function(require,exports,module){function parseStdin(a,b){for(;;)try{var c=new lexer.Tokenizer(a,"stdin",b.value),d=Script(c,!1);b.value=c.lineno;return d}catch(e){if(!c.unexpectedEOF)throw e;var f=readline();if(!f)throw e;a+="\n"+f}}function parse(a,b,c){var d=new lexer.Tokenizer(a,b,c),e=Script(d,!1);if(!d.done)throw d.newSyntaxError("Syntax error");return e}function PrimaryExpression(a,b){var c,d,e=a.get(!0);switch(e){case FUNCTION:c=FunctionDefinition(a,b,!1,EXPRESSED_FORM);break;case LEFT_BRACKET:c=new Node(a,{type:ARRAY_INIT});while((e=a.peek(!0))!==RIGHT_BRACKET){if(e===COMMA){a.get(),c.push(null);continue}c.push(AssignExpression(a,b));if(e!==COMMA&&!a.match(COMMA))break}c.children.length===1&&a.match(FOR)&&(d=new Node(a,{type:ARRAY_COMP,expression:c.children[0],tail:ComprehensionTail(a,b)}),c=d),a.mustMatch(RIGHT_BRACKET);break;case LEFT_CURLY:var f,g;c=new Node(a,{type:OBJECT_INIT});object_init:if(!a.match(RIGHT_CURLY)){do{e=a.get();if(a.token.value!=="get"&&a.token.value!=="set"||a.peek()!==IDENTIFIER){switch(e){case IDENTIFIER:case NUMBER:case STRING:f=new Node(a,{type:IDENTIFIER});break;case RIGHT_CURLY:if(b.ecma3OnlyMode)throw a.newSyntaxError("Illegal trailing ,");break object_init;default:if(a.token.value in definitions.keywords){f=new Node(a,{type:IDENTIFIER});break}throw a.newSyntaxError("Invalid property name")}if(a.match(COLON))d=new Node(a,{type:PROPERTY_INIT}),d.push(f),d.push(AssignExpression(a,b)),c.push(d);else{if(a.peek()!==COMMA&&a.peek()!==RIGHT_CURLY)throw a.newSyntaxError("missing : after property");c.push(f)}}else{if(b.ecma3OnlyMode)throw a.newSyntaxError("Illegal property accessor");c.push(FunctionDefinition(a,b,!0,EXPRESSED_FORM))}}while(a.match(COMMA));a.mustMatch(RIGHT_CURLY)}break;case LEFT_PAREN:c=ParenExpression(a,b),a.mustMatch(RIGHT_PAREN),c.parenthesized=!0;break;case LET:c=LetBlock(a,b,!1);break;case NULL:case THIS:case TRUE:case FALSE:case IDENTIFIER:case NUMBER:case STRING:case REGEXP:c=new Node(a);break;default:throw a.newSyntaxError("missing operand")}return c}function ArgumentList(a,b){var c,d;c=new Node(a,{type:LIST});if(a.match(RIGHT_PAREN,!0))return c;do{d=AssignExpression(a,b);if(d.type===YIELD&&!d.parenthesized&&a.peek()===COMMA)throw a.newSyntaxError("Yield expression must be parenthesized");if(a.match(FOR)){d=GeneratorExpression(a,b,d);if(c.children.length>1||a.peek(!0)===COMMA)throw a.newSyntaxError("Generator expression must be parenthesized")}c.push(d)}while(a.match(COMMA));a.mustMatch(RIGHT_PAREN);return c}function MemberExpression(a,b,c){var d,e,f,g;a.match(NEW)?(d=new Node(a),d.push(MemberExpression(a,b,!1)),a.match(LEFT_PAREN)&&(d.type=NEW_WITH_ARGS,d.push(ArgumentList(a,b)))):d=PrimaryExpression(a,b);while((g=a.get())!==END){switch(g){case DOT:e=new Node(a),e.push(d),a.mustMatch(IDENTIFIER),e.push(new Node(a));break;case LEFT_BRACKET:e=new Node(a,{type:INDEX}),e.push(d),e.push(Expression(a,b)),a.mustMatch(RIGHT_BRACKET);break;case LEFT_PAREN:if(c){e=new Node(a,{type:CALL}),e.push(d),e.push(ArgumentList(a,b));break};default:a.unget();return d}d=e}return d}function UnaryExpression(a,b){var c,d,e;switch(e=a.get(!0)){case DELETE:case VOID:case TYPEOF:case NOT:case BITWISE_NOT:case PLUS:case MINUS:e===PLUS?c=new Node(a,{type:UNARY_PLUS}):e===MINUS?c=new Node(a,{type:UNARY_MINUS}):c=new Node(a),c.push(UnaryExpression(a,b));break;case INCREMENT:case DECREMENT:c=new Node(a),c.push(MemberExpression(a,b,!0));break;default:a.unget(),c=MemberExpression(a,b,!0),a.tokens[a.tokenIndex+a.lookahead-1&3].lineno===a.lineno&&(a.match(INCREMENT)||a.match(DECREMENT))&&(d=new Node(a,{postfix:!0}),d.push(c),c=d)}return c}function MultiplyExpression(a,b){var c,d;c=UnaryExpression(a,b);while(a.match(MUL)||a.match(DIV)||a.match(MOD))d=new Node(a),d.push(c),d.push(UnaryExpression(a,b)),c=d;return c}function AddExpression(a,b){var c,d;c=MultiplyExpression(a,b);while(a.match(PLUS)||a.match(MINUS))d=new Node(a),d.push(c),d.push(MultiplyExpression(a,b)),c=d;return c}function ShiftExpression(a,b){var c,d;c=AddExpression(a,b);while(a.match(LSH)||a.match(RSH)||a.match(URSH))d=new Node(a),d.push(c),d.push(AddExpression(a,b)),c=d;return c}function RelationalExpression(a,b){var c,d,e=b.update({inForLoopInit:!1});c=ShiftExpression(a,e);while(a.match(LT)||a.match(LE)||a.match(GE)||a.match(GT)||!b.inForLoopInit&&a.match(IN)||a.match(INSTANCEOF))d=new Node(a),d.push(c),d.push(ShiftExpression(a,e)),c=d;return c}function EqualityExpression(a,b){var c,d;c=RelationalExpression(a,b);while(a.match(EQ)||a.match(NE)||a.match(STRICT_EQ)||a.match(STRICT_NE))d=new Node(a),d.push(c),d.push(RelationalExpression(a,b)),c=d;return c}function BitwiseAndExpression(a,b){var c,d;c=EqualityExpression(a,b);while(a.match(BITWISE_AND))d=new Node(a),d.push(c),d.push(EqualityExpression(a,b)),c=d;return c}function BitwiseXorExpression(a,b){var c,d;c=BitwiseAndExpression(a,b);while(a.match(BITWISE_XOR))d=new Node(a),d.push(c),d.push(BitwiseAndExpression(a,b)),c=d;return c}function BitwiseOrExpression(a,b){var c,d;c=BitwiseXorExpression(a,b);while(a.match(BITWISE_OR))d=new Node(a),d.push(c),d.push(BitwiseXorExpression(a,b)),c=d;return c}function AndExpression(a,b){var c,d;c=BitwiseOrExpression(a,b);while(a.match(AND))d=new Node(a),d.push(c),d.push(BitwiseOrExpression(a,b)),c=d;return c}function OrExpression(a,b){var c,d;c=AndExpression(a,b);while(a.match(OR))d=new Node(a),d.push(c),d.push(AndExpression(a,b)),c=d;return c}function ConditionalExpression(a,b){var c,d;c=OrExpression(a,b);if(a.match(HOOK)){d=c,c=new Node(a,{type:HOOK}),c.push(d),c.push(AssignExpression(a,b.update({inForLoopInit:!1})));if(!a.match(COLON))throw a.newSyntaxError("missing : after ?");c.push(AssignExpression(a,b))}return c}function AssignExpression(a,b){var c,d;if(a.match(YIELD,!0))return ReturnOrYield(a,b);c=new Node(a,{type:ASSIGN}),d=ConditionalExpression(a,b);if(!a.match(ASSIGN))return d;switch(d.type){case OBJECT_INIT:case ARRAY_INIT:d.destructuredNames=checkDestructuring(a,b,d);case IDENTIFIER:case DOT:case INDEX:case CALL:break;default:throw a.newSyntaxError("Bad left-hand side of assignment")}c.assignOp=a.token.assignOp,c.push(d),c.push(AssignExpression(a,b));return c}function Expression(a,b){var c,d;c=AssignExpression(a,b);if(a.match(COMMA)){d=new Node(a,{type:COMMA}),d.push(c),c=d;do{d=c.children[c.children.length-1];if(d.type===YIELD&&!d.parenthesized)throw a.newSyntaxError("Yield expression must be parenthesized");c.push(AssignExpression(a,b))}while(a.match(COMMA))}return c}function ParenExpression(a,b){var c=Expression(a,b.update({inForLoopInit:b.inForLoopInit&&a.token.type===LEFT_PAREN}));if(a.match(FOR)){if(c.type===YIELD&&!c.parenthesized)throw a.newSyntaxError("Yield expression must be parenthesized");if(c.type===COMMA&&!c.parenthesized)throw a.newSyntaxError("Generator expression must be parenthesized");c=GeneratorExpression(a,b,c)}return c}function HeadExpression(a,b){var c=MaybeLeftParen(a,b),d=ParenExpression(a,b);MaybeRightParen(a,c);if(c===END&&!d.parenthesized){var e=a.peek();if(e!==LEFT_CURLY&&!definitions.isStatementStartCode[e])throw a.newSyntaxError("Unparenthesized head followed by unbraced body")}return d}function ComprehensionTail(a,b){var c,d,e,f,g;c=new Node(a,{type:COMP_TAIL});do{d=new Node(a,{type:FOR_IN,isLoop:!0}),a.match(IDENTIFIER)&&(a.token.value==="each"?d.isEach=!0:a.unget()),g=MaybeLeftParen(a,b);switch(a.get()){case LEFT_BRACKET:case LEFT_CURLY:a.unget(),d.iterator=DestructuringExpression(a,b);break;case IDENTIFIER:d.iterator=f=new Node(a,{type:IDENTIFIER}),f.name=f.value,d.varDecl=e=new Node(a,{type:VAR}),e.push(f),b.parentScript.varDecls.push(f);break;default:throw a.newSyntaxError("missing identifier")}a.mustMatch(IN),d.object=Expression(a,b),MaybeRightParen(a,g),c.push(d)}while(a.match(FOR));a.match(IF)&&(c.guard=HeadExpression(a,b));return c}function GeneratorExpression(a,b,c){return new Node(a,{type:GENERATOR,expression:c,tail:ComprehensionTail(a,b)})}function DestructuringExpression(a,b,c){var d=PrimaryExpression(a,b);d.destructuredNames=checkDestructuring(a,b,d,c);return d}function checkDestructuring(a,b,c,d){if(c.type===ARRAY_COMP)throw a.newSyntaxError("Invalid array comprehension left-hand side");if(c.type===ARRAY_INIT||c.type===OBJECT_INIT){var e={},f,g,h,i,j,k=c.children;for(var l=0,m=k.length;l<m;l++){if(!(f=k[l]))continue;f.type===PROPERTY_INIT?(j=f.children,i=j[1],h=j[0].value):c.type===OBJECT_INIT?(i=f,h=f.value):(i=f,h=l);if(i.type===ARRAY_INIT||i.type===OBJECT_INIT)e[h]=checkDestructuring(a,b,i,d);else{if(d&&i.type!==IDENTIFIER)throw a.newSyntaxError("missing name in pattern");e[h]=i}}return e}}function LetBlock(a,b,c){var d,e;d=new Node(a,{type:LET_BLOCK,varDecls:[]}),a.mustMatch(LEFT_PAREN),d.variables=Variables(a,b,d),a.mustMatch(RIGHT_PAREN),c&&a.peek()!==LEFT_CURLY&&(e=new Node(a,{type:SEMICOLON,expression:d}),c=!1),c?d.block=Block(a,b):d.expression=AssignExpression(a,b);return d}function Variables(a,b,c){var d,e,f,g,h,i;i=a.token.type;switch(i){case VAR:case CONST:h=b.parentScript;break;case LET:h=b.parentBlock;break;case LEFT_PAREN:i=LET,h=c}d=new Node(a,{type:i,destructurings:[]});do{i=a.get();if(i===LEFT_BRACKET||i===LEFT_CURLY){a.unget();var j=DestructuringExpression(a,b,!0);e=new Node(a,{type:IDENTIFIER,name:j,readOnly:d.type===CONST}),d.push(e),pushDestructuringVarDecls(e.name.destructuredNames,h),d.destructurings.push({exp:j,decl:e});if(b.inForLoopInit&&a.peek()===IN)continue;a.mustMatch(ASSIGN);if(a.token.assignOp)throw a.newSyntaxError("Invalid variable initialization");e.initializer=AssignExpression(a,b);continue}if(i!==IDENTIFIER)throw a.newSyntaxError("missing variable name");e=new Node(a,{type:IDENTIFIER,name:a.token.value,readOnly:d.type===CONST}),d.push(e),h.varDecls.push(e);if(a.match(ASSIGN)){if(a.token.assignOp)throw a.newSyntaxError("Invalid variable initialization");e.initializer=AssignExpression(a,b)}}while(a.match(COMMA));return d}function FunctionDefinition(a,b,c,d){var e,f=new Node(a,{params:[]});f.type!==FUNCTION&&(f.type=f.value==="get"?GETTER:SETTER);if(a.match(IDENTIFIER))f.name=a.token.value;else if(c)throw a.newSyntaxError("missing function identifier");var g=new StaticContext(null,null,!0,!1,NESTING_TOP);a.mustMatch(LEFT_PAREN);if(!a.match(RIGHT_PAREN)){do switch(a.get()){case LEFT_BRACKET:case LEFT_CURLY:a.unget(),f.params.push(DestructuringExpression(a,g));break;case IDENTIFIER:f.params.push(a.token.value);break;default:throw a.newSyntaxError("missing formal parameter")}while(a.match(COMMA));a.mustMatch(RIGHT_PAREN)}e=a.get(),e!==LEFT_CURLY&&a.unget();if(e!==LEFT_CURLY){f.body=AssignExpression(a,g);if(f.body.isGenerator)throw a.newSyntaxError("Generator returns a value")}else f.body=Script(a,!0);e===LEFT_CURLY&&a.mustMatch(RIGHT_CURLY),f.end=a.token.end,f.functionForm=d,d===DECLARED_FORM&&b.parentScript.funDecls.push(f);return f}function ReturnOrYield(a,b){var c,d,e=a.token.type,f,g=b.parentScript;if(e===RETURN){if(!b.inFunction)throw a.newSyntaxError("Return not in function")}else{if(!b.inFunction)throw a.newSyntaxError("Yield not in function");g.isGenerator=!0}c=new Node(a,{value:undefined}),f=a.peek(!0),f!==END&&f!==NEWLINE&&f!==SEMICOLON&&f!==RIGHT_CURLY&&(e!==YIELD||f!==e&&f!==RIGHT_BRACKET&&f!==RIGHT_PAREN&&f!==COLON&&f!==COMMA)?e===RETURN?(c.value=Expression(a,b),g.hasReturnWithValue=!0):c.value=AssignExpression(a,b):e===RETURN&&(g.hasEmptyReturn=!0);if(g.hasReturnWithValue&&g.isGenerator)throw a.newSyntaxError("Generator returns a value");return c}function MagicalSemicolon(a){var b;if(a.lineno===a.token.lineno){b=a.peekOnSameLine();if(b!==END&&b!==NEWLINE&&b!==SEMICOLON&&b!==RIGHT_CURLY)throw a.newSyntaxError("missing ; before statement")}a.match(SEMICOLON)}function Statement(a,b){var c,d,e,f,g,h,i,j=a.get(!0),k,l,m;switch(j){case FUNCTION:return FunctionDefinition(a,b,!0,b.nesting!==NESTING_TOP?STATEMENT_FORM:DECLARED_FORM);case LEFT_CURLY:e=new Node(a,blockInit()),Statements(a,b.update({parentBlock:e}).pushTarget(e).nest(NESTING_SHALLOW),e),a.mustMatch(RIGHT_CURLY);return e;case IF:e=new Node(a),e.condition=HeadExpression(a,b),l=b.pushTarget(e).nest(NESTING_DEEP),e.thenPart=Statement(a,l),e.elsePart=a.match(ELSE)?Statement(a,l):null;return e;case SWITCH:e=new Node(a,{cases:[],defaultIndex:-1}),e.discriminant=HeadExpression(a,b),l=b.pushTarget(e).nest(NESTING_DEEP),a.mustMatch(LEFT_CURLY);while((j=a.get())!==RIGHT_CURLY){switch(j){case DEFAULT:if(e.defaultIndex>=0)throw a.newSyntaxError("More than one switch default");case CASE:f=new Node(a),j===DEFAULT?e.defaultIndex=e.cases.length:f.caseLabel=Expression(a,l,COLON);break;default:throw a.newSyntaxError("Invalid switch case")}a.mustMatch(COLON),f.statements=new Node(a,blockInit());while((j=a.peek(!0))!==CASE&&j!==DEFAULT&&j!==RIGHT_CURLY)f.statements.push(Statement(a,l));e.cases.push(f)}return e;case FOR:e=new Node(a,LOOP_INIT),a.match(IDENTIFIER)&&(a.token.value==="each"?e.isEach=!0:a.unget()),b.parenFreeMode||a.mustMatch(LEFT_PAREN),l=b.pushTarget(e).nest(NESTING_DEEP),m=b.update({inForLoopInit:!0}),(j=a.peek())!==SEMICOLON&&(j===VAR||j===CONST?(a.get(),f=Variables(a,m)):j===LET?(a.get(),a.peek()===LEFT_PAREN?f=LetBlock(a,m,!1):(m.parentBlock=e,e.varDecls=[],f=Variables(a,m))):f=Expression(a,m));if(f&&a.match(IN)){e.type=FOR_IN,e.object=Expression(a,m);if(f.type===VAR||f.type===LET){h=f.children;if(h.length!==1&&f.destructurings.length!==1)throw new SyntaxError("Invalid for..in left-hand side",a.filename,f.lineno);f.destructurings.length>0?e.iterator=f.destructurings[0]:e.iterator=h[0],e.varDecl=f}else{if(f.type===ARRAY_INIT||f.type===OBJECT_INIT)f.destructuredNames=checkDestructuring(a,m,f);e.iterator=f}}else{e.setup=f,a.mustMatch(SEMICOLON);if(e.isEach)throw a.newSyntaxError("Invalid for each..in loop");e.condition=a.peek()===SEMICOLON?null:Expression(a,m),a.mustMatch(SEMICOLON),k=a.peek(),e.update=(b.parenFreeMode?k===LEFT_CURLY||definitions.isStatementStartCode[k]:k===RIGHT_PAREN)?null:Expression(a,m)}b.parenFreeMode||a.mustMatch(RIGHT_PAREN),e.body=Statement(a,l);return e;case WHILE:e=new Node(a,{isLoop:!0}),e.condition=HeadExpression(a,b),e.body=Statement(a,b.pushTarget(e).nest(NESTING_DEEP));return e;case DO:e=new Node(a,{isLoop:!0}),e.body=Statement(a,b.pushTarget(e).nest(NESTING_DEEP)),a.mustMatch(WHILE),e.condition=HeadExpression(a,b);if(!b.ecmaStrictMode){a.match(SEMICOLON);return e}break;case BREAK:case CONTINUE:e=new Node(a),l=b.pushTarget(e),a.peekOnSameLine()===IDENTIFIER&&(a.get(),e.label=a.token.value),e.target=e.label?l.labeledTargets.find(function(a){return a.labels.has(e.label)}):l.defaultTarget;if(!e.target)throw a.newSyntaxError("Invalid "+(j===BREAK?"break":"continue"));if(!e.target.isLoop&&j===CONTINUE)throw a.newSyntaxError("Invalid continue");break;case TRY:e=new Node(a,{catchClauses:[]}),e.tryBlock=Block(a,b);while(a.match(CATCH)){f=new Node(a),g=MaybeLeftParen(a,b);switch(a.get()){case LEFT_BRACKET:case LEFT_CURLY:a.unget(),f.varName=DestructuringExpression(a,b,!0);break;case IDENTIFIER:f.varName=a.token.value;break;default:throw a.newSyntaxError("missing identifier in catch")}if(a.match(IF)){if(b.ecma3OnlyMode)throw a.newSyntaxError("Illegal catch guard");if(e.catchClauses.length&&!e.catchClauses.top().guard)throw a.newSyntaxError("Guarded catch after unguarded");f.guard=Expression(a,b)}MaybeRightParen(a,g),f.block=Block(a,b),e.catchClauses.push(f)}a.match(FINALLY)&&(e.finallyBlock=Block(a,b));if(!e.catchClauses.length&&!e.finallyBlock)throw a.newSyntaxError("Invalid try statement");return e;case CATCH:case FINALLY:throw a.newSyntaxError(definitions.tokens[j]+" without preceding try");case THROW:e=new Node(a),e.exception=Expression(a,b);break;case RETURN:e=ReturnOrYield(a,b);break;case WITH:e=new Node(a),e.object=HeadExpression(a,b),e.body=Statement(a,b.pushTarget(e).nest(NESTING_DEEP));return e;case VAR:case CONST:e=Variables(a,b);break;case LET:a.peek()===LEFT_PAREN?e=LetBlock(a,b,!0):e=Variables(a,b);break;case DEBUGGER:e=new Node(a);break;case NEWLINE:case SEMICOLON:e=new Node(a,{type:SEMICOLON}),e.expression=null;return e;default:if(j===IDENTIFIER){j=a.peek();if(j===COLON){d=a.token.value;if(b.allLabels.has(d))throw a.newSyntaxError("Duplicate label");a.get(),e=new Node(a,{type:LABEL,label:d}),e.statement=Statement(a,b.pushLabel(d).nest(NESTING_SHALLOW)),e.target=e.statement.type===LABEL?e.statement.target:e.statement;return e}}e=new Node(a,{type:SEMICOLON}),a.unget(),e.expression=Expression(a,b),e.end=e.expression.end}MagicalSemicolon(a);return e}function Block(a,b){a.mustMatch(LEFT_CURLY);var c=new Node(a,blockInit());Statements(a,b.update({parentBlock:c}).pushTarget(c),c),a.mustMatch(RIGHT_CURLY);return c}function Statements(a,b,c){try{while(!a.done&&a.peek(!0)!==RIGHT_CURLY)c.push(Statement(a,b))}catch(d){a.done&&(a.unexpectedEOF=!0);throw d}}function MaybeRightParen(a,b){b===LEFT_PAREN&&a.mustMatch(RIGHT_PAREN)}function MaybeLeftParen(a,b){if(b.parenFreeMode)return a.match(LEFT_PAREN)?LEFT_PAREN:END;return a.mustMatch(LEFT_PAREN).type}function scriptInit(){return{type:SCRIPT,funDecls:[],varDecls:[],modDecls:[],impDecls:[],expDecls:[],loadDeps:[],hasEmptyReturn:!1,hasReturnWithValue:!1,isGenerator:!1}}function blockInit(){return{type:BLOCK,varDecls:[]}}function tokenString(a){var b=definitions.tokens[a];return/^\W/.test(b)?definitions.opTypeNames[b]:b.toUpperCase()}function Node(a,b){var c=a.token;c?(this.type=c.type,this.value=c.value,this.lineno=c.lineno,this.start=c.start,this.end=c.end):this.lineno=a.lineno,this.tokenizer=a,this.children=[];for(var d in b)this[d]=b[d]}function Script(a,b){var c=new Node(a,scriptInit()),d=new StaticContext(c,c,b,!1,NESTING_TOP);Statements(a,d,c);return c}function StaticContext(a,b,c,d,e){this.parentScript=a,this.parentBlock=b,this.inFunction=c,this.inForLoopInit=d,this.nesting=e,this.allLabels=new Stack,this.currentLabels=new Stack,this.labeledTargets=new Stack,this.defaultTarget=null,definitions.options.ecma3OnlyMode&&(this.ecma3OnlyMode=!0),definitions.options.parenFreeMode&&(this.parenFreeMode=!0)}function pushDestructuringVarDecls(a,b){for(var c in a){var d=a[c];d.type===IDENTIFIER?b.varDecls.push(d):pushDestructuringVarDecls(d,b)}}var lexer=require("ace/narcissus/jslex"),definitions=require("ace/narcissus/jsdefs");const StringMap=definitions.StringMap,Stack=definitions.Stack;eval(definitions.consts);const NESTING_TOP=0,NESTING_SHALLOW=1,NESTING_DEEP=2;StaticContext.prototype={ecma3OnlyMode:!1,parenFreeMode:!1,update:function(a){var b={};for(var c in a)b[c]={value:a[c],writable:!0,enumerable:!0,configurable:!0};return Object.create(this,b)},pushLabel:function(a){return this.update({currentLabels:this.currentLabels.push(a),allLabels:this.allLabels.push(a)})},pushTarget:function(a){var b=a.isLoop||a.type===SWITCH;if(this.currentLabels.isEmpty())return b?this.update({defaultTarget:a}):this;a.labels=new StringMap,this.currentLabels.forEach(function(b){a.labels.set(b,!0)});return this.update({currentLabels:new Stack,labeledTargets:this.labeledTargets.push(a),defaultTarget:b?a:this.defaultTarget})},nest:function(a){var b=Math.max(this.nesting,a);return b!==this.nesting?this.update({nesting:b}):this}},definitions.defineProperty(Array.prototype,"top",function(){return this.length&&this[this.length-1]},!1,!1,!0);var Np=Node.prototype={};Np.constructor=Node,Np.toSource=Object.prototype.toSource,Np.push=function(a){a!==null&&(a.start<this.start&&(this.start=a.start),this.end<a.end&&(this.end=a.end));return this.children.push(a)},Node.indentLevel=0,Np.toString=function(){var a=[];for(var b in this)this.hasOwnProperty(b)&&b!=="type"&&b!=="target"&&a.push({id:b,value:this[b]});a.sort(function(a,b){return a.id<b.id?-1:1});const c=" ";var d=++Node.indentLevel,e="{\n"+c.repeat(d)+"type: "+tokenString(this.type);for(b=0;b<a.length;b++)e+=",\n"+c.repeat(d)+a[b].id+": "+a[b].value;d=--Node.indentLevel,e+="\n"+c.repeat(d)+"}";return e},Np.getSource=function(){return this.tokenizer.source.slice(this.start,this.end)};const LOOP_INIT={isLoop:!0};definitions.defineGetter(Np,"filename",function(){return this.tokenizer.filename}),definitions.defineProperty(String.prototype,"repeat",function(a){var b="",c=this+b;while(--a>=0)b+=c;return b},!1,!1,!0);const DECLARED_FORM=0,EXPRESSED_FORM=1,STATEMENT_FORM=2;exports.parse=parse,exports.parseStdin=parseStdin,exports.Node=Node,exports.DECLARED_FORM=DECLARED_FORM,exports.EXPRESSED_FORM=EXPRESSED_FORM,exports.STATEMENT_FORM=STATEMENT_FORM,exports.Tokenizer=lexer.Tokenizer,exports.FunctionDefinition=FunctionDefinition}),define("ace/narcissus/jslex",["require","exports","module","ace/narcissus/jsdefs"],function(require,exports,module){function Tokenizer(a,b,c){this.cursor=0,this.source=String(a),this.tokens=[],this.tokenIndex=0,this.lookahead=0,this.scanNewlines=!1,this.unexpectedEOF=!1,this.filename=b||"",this.lineno=c||1}var definitions=require("ace/narcissus/jsdefs");eval(definitions.consts);var opTokens={};for(var op in definitions.opTypeNames){if(op==="\n"||op===".")continue;var node=opTokens;for(var i=0;i<op.length;i++){var ch=op[i];ch in node||(node[ch]={}),node=node[ch],node.op=op}}Tokenizer.prototype={get done(){return this.peek(!0)===END},get token(){return this.tokens[this.tokenIndex]},match:function(a,b){return this.get(b)===a||this.unget()},mustMatch:function(a){if(!this.match(a))throw this.newSyntaxError("Missing "+definitions.tokens[a].toLowerCase());return this.token},peek:function(a){var b,c;this.lookahead?(c=this.tokens[this.tokenIndex+this.lookahead&3],b=this.scanNewlines&&c.lineno!==this.lineno?NEWLINE:c.type):(b=this.get(a),this.unget());return b},peekOnSameLine:function(a){this.scanNewlines=!0;var b=this.peek(a);this.scanNewlines=!1;return b},skip:function(){var a=this.source;for(;;){var b=a[this.cursor++],c=a[this.cursor];if(b==="\n"&&!this.scanNewlines)this.lineno++;else if(b==="/"&&c==="*"){this.cursor++;for(;;){b=a[this.cursor++];if(b===undefined)throw this.newSyntaxError("Unterminated comment");if(b==="*"){c=a[this.cursor];if(c==="/"){this.cursor++;break}}else b==="\n"&&this.lineno++}}else if(b==="/"&&c==="/"){this.cursor++;for(;;){b=a[this.cursor++];if(b===undefined)return;if(b==="\n"){this.lineno++;break}}}else if(b!==" "&&b!=="\t"){this.cursor--;return}}},lexExponent:function(){var a=this.source,b=a[this.cursor];if(b==="e"||b==="E"){this.cursor++,ch=a[this.cursor++];if(ch==="+"||ch==="-")ch=a[this.cursor++];if(ch<"0"||ch>"9")throw this.newSyntaxError("Missing exponent");do ch=a[this.cursor++];while(ch>="0"&&ch<="9");this.cursor--;return!0}return!1},lexZeroNumber:function(a){var b=this.token,c=this.source;b.type=NUMBER,a=c[this.cursor++];if(a==="."){do a=c[this.cursor++];while(a>="0"&&a<="9");this.cursor--,this.lexExponent(),b.value=parseFloat(b.start,this.cursor)}else if(a==="x"||a==="X"){do a=c[this.cursor++];while(a>="0"&&a<="9"||a>="a"&&a<="f"||a>="A"&&a<="F");this.cursor--,b.value=parseInt(c.substring(b.start,this.cursor))}else if(a>="0"&&a<="7"){do a=c[this.cursor++];while(a>="0"&&a<="7");this.cursor--,b.value=parseInt(c.substring(b.start,this.cursor))}else this.cursor--,this.lexExponent(),b.value=0},lexNumber:function(a){var b=this.token,c=this.source;b.type=NUMBER;var d=!1;do a=c[this.cursor++],a==="."&&!d&&(d=!0,a=c[this.cursor++]);while(a>="0"&&a<="9");this.cursor--;var e=this.lexExponent();d=d||e;var f=c.substring(b.start,this.cursor);b.value=d?parseFloat(f):parseInt(f)},lexDot:function(a){var b=this.token,c=this.source,d=c[this.cursor];if(d>="0"&&d<="9"){do a=c[this.cursor++];while(a>="0"&&a<="9");this.cursor--,this.lexExponent(),b.type=NUMBER,b.value=parseFloat(b.start,this.cursor)}else b.type=DOT,b.assignOp=null,b.value="."},lexString:function(ch){var token=this.token,input=this.source;token.type=STRING;var hasEscapes=!1,delim=ch;if(input.length<=this.cursor)throw this.newSyntaxError("Unterminated string literal");while((ch=input[this.cursor++])!==delim){if(this.cursor==input.length)throw this.newSyntaxError("Unterminated string literal");if(ch==="\\"){hasEscapes=!0;if(++this.cursor==input.length)throw this.newSyntaxError("Unterminated string literal")}}token.value=hasEscapes?eval(input.substring(token.start,this.cursor)):input.substring(token.start+1,this.cursor-1)},lexRegExp:function(ch){var token=this.token,input=this.source;token.type=REGEXP;do{ch=input[this.cursor++];if(ch==="\\")this.cursor++;else if(ch==="["){do{if(ch===undefined)throw this.newSyntaxError("Unterminated character class");ch==="\\"&&this.cursor++,ch=input[this.cursor++]}while(ch!=="]")}else if(ch===undefined)throw this.newSyntaxError("Unterminated regex")}while(ch!=="/");do ch=input[this.cursor++];while(ch>="a"&&ch<="z");this.cursor--,token.value=eval(input.substring(token.start,this.cursor))},lexOp:function(a){var b=this.token,c=this.source,d=opTokens[a],e=c[this.cursor];e in d&&(d=d[e],this.cursor++,e=c[this.cursor],e in d&&(d=d[e],this.cursor++,e=c[this.cursor]));var f=d.op;definitions.assignOps[f]&&c[this.cursor]==="="?(this.cursor++,b.type=ASSIGN,b.assignOp=definitions.tokenIds[definitions.opTypeNames[f]],f+="="):(b.type=definitions.tokenIds[definitions.opTypeNames[f]],b.assignOp=null),b.value=f},lexIdent:function(a){var b=this.token,c=this.source;do a=c[this.cursor++];while(a>="a"&&a<="z"||a>="A"&&a<="Z"||a>="0"&&a<="9"||a==="$"||a==="_");this.cursor--;var d=c.substring(b.start,this.cursor);b.type=definitions.keywords[d]||IDENTIFIER,b.value=d},get:function(a){var b;while(this.lookahead){--this.lookahead,this.tokenIndex=this.tokenIndex+1&3,b=this.tokens[this.tokenIndex];if(b.type!==NEWLINE||this.scanNewlines)return b.type}this.skip(),this.tokenIndex=this.tokenIndex+1&3,b=this.tokens[this.tokenIndex],b||(this.tokens[this.tokenIndex]=b={});var c=this.source;if(this.cursor===c.length)return b.type=END;b.start=this.cursor,b.lineno=this.lineno;var d=c[this.cursor++];if(d>="a"&&d<="z"||d>="A"&&d<="Z"||d==="$"||d==="_")this.lexIdent(d);else if(a&&d==="/")this.lexRegExp(d);else if(d in opTokens)this.lexOp(d);else if(d===".")this.lexDot(d);else if(d>="1"&&d<="9")this.lexNumber(d);else if(d==="0")this.lexZeroNumber(d);else if(d==='"'||d==="'")this.lexString(d);else if(this.scanNewlines&&d==="\n")b.type=NEWLINE,b.value="\n",this.lineno++;else throw this.newSyntaxError("Illegal token");b.end=this.cursor;return b.type},unget:function(){if(++this.lookahead===4)throw"PANIC: too much lookahead!";this.tokenIndex=this.tokenIndex-1&3},newSyntaxError:function(a){var b=new SyntaxError(a,this.filename,this.lineno);b.source=this.source,b.lineno=this.lineno,b.cursor=this.lookahead?this.tokens[this.tokenIndex+this.lookahead&3].start:this.cursor;return b}},exports.Tokenizer=Tokenizer}),define("ace/narcissus/jsdefs",["require","exports","module"],function(a,b,c){function y(a){this.elts=a||null}function x(){this.table=Object.create(null,{}),this.size=0}function v(){return undefined}function u(a){return{getOwnPropertyDescriptor:function(b){var c=Object.getOwnPropertyDescriptor(a,b);c.configurable=!0;return c},getPropertyDescriptor:function(b){var c=s(a,b);c.configurable=!0;return c},getOwnPropertyNames:function(){return Object.getOwnPropertyNames(a)},defineProperty:function(b,c){Object.defineProperty(a,b,c)},"delete":function(b){return delete a[b]},fix:function(){if(Object.isFrozen(a))return t(a);return undefined},has:function(b){return b in a},hasOwn:function(b){return{}.hasOwnProperty.call(a,b)},get:function(b,c){return a[c]},set:function(b,c,d){a[c]=d;return!0},enumerate:function(){var b=[];for(m in a)b.push(m);return b},keys:function(){return Object.keys(a)}}}function t(a){var b={};for(var c in Object.getOwnPropertyNames(a))b[c]=Object.getOwnPropertyDescriptor(a,c);return b}function s(a,b){while(a){if({}.hasOwnProperty.call(a,b))return Object.getOwnPropertyDescriptor(a,b);a=Object.getPrototypeOf(a)}}function r(a){return typeof a=="function"&&a.toString().match(/\[native code\]/)}function q(a,b,c,d,e,f){Object.defineProperty(a,b,{value:c,writable:!e,configurable:!d,enumerable:!f})}function p(a,b,c,d,e){Object.defineProperty(a,b,{get:c,configurable:!d,enumerable:!e})}b.options={version:185},function(){b.hostGlobal=this}();var d=["END","\n",";",",","=","?",":","CONDITIONAL","||","&&","|","^","&","==","!=","===","!==","<","<=",">=",">","<<",">>",">>>","+","-","*","/","%","!","~","UNARY_PLUS","UNARY_MINUS","++","--",".","[","]","{","}","(",")","SCRIPT","BLOCK","LABEL","FOR_IN","CALL","NEW_WITH_ARGS","INDEX","ARRAY_INIT","OBJECT_INIT","PROPERTY_INIT","GETTER","SETTER","GROUP","LIST","LET_BLOCK","ARRAY_COMP","GENERATOR","COMP_TAIL","IDENTIFIER","NUMBER","STRING","REGEXP","break","case","catch","const","continue","debugger","default","delete","do","else","false","finally","for","function","if","in","instanceof","let","new","null","return","switch","this","throw","true","try","typeof","var","void","yield","while","with"],e=["break","const","continue","debugger","do","for","if","return","switch","throw","try","var","yield","while","with"],f={"\n":"NEWLINE",";":"SEMICOLON",",":"COMMA","?":"HOOK",":":"COLON","||":"OR","&&":"AND","|":"BITWISE_OR","^":"BITWISE_XOR","&":"BITWISE_AND","===":"STRICT_EQ","==":"EQ","=":"ASSIGN","!==":"STRICT_NE","!=":"NE","<<":"LSH","<=":"LE","<":"LT",">>>":"URSH",">>":"RSH",">=":"GE",">":"GT","++":"INCREMENT","--":"DECREMENT","+":"PLUS","-":"MINUS","*":"MUL","/":"DIV","%":"MOD","!":"NOT","~":"BITWISE_NOT",".":"DOT","[":"LEFT_BRACKET","]":"RIGHT_BRACKET","{":"LEFT_CURLY","}":"RIGHT_CURLY","(":"LEFT_PAREN",")":"RIGHT_PAREN"},g={"__proto__":null},h={},i="const ";for(var j=0,k=d.length;j<k;j++){j>0&&(i+=", ");var l=d[j],m;/^[a-z]/.test(l)?(m=l.toUpperCase(),g[l]=j):m=/^\W/.test(l)?f[l]:l,i+=m+" = "+j,h[m]=j,d[l]=j}i+=";";var n={"__proto__":null};for(j=0,k=e.length;j<k;j++)n[g[e[j]]]=!0;var o=["|","^","&","<<",">>",">>>","+","-","*","/","%"];for(j=0,k=o.length;j<k;j++)l=o[j],o[l]=d[l];var w={}.hasOwnProperty;x.prototype={has:function(a){return w.call(this.table,a)},set:function(a,b){w.call(this.table,a)||this.size++,this.table[a]=b},get:function(a){return this.table[a]},getDef:function(a,b){w.call(this.table,a)||(this.size++,this.table[a]=b());return this.table[a]},forEach:function(a){var b=this.table;for(var c in b)a.call(this,c,b[c])},toString:function(){return"[object StringMap]"}},y.prototype={push:function(a){return new y({top:a,rest:this.elts})},top:function(){if(!this.elts)throw new Error("empty stack");return this.elts.top},isEmpty:function(){return this.top===null},find:function(a){for(var b=this.elts;b;b=b.rest)if(a(b.top))return b.top;return null},has:function(a){return Boolean(this.find(function(b){return b===a}))},forEach:function(a){for(var b=this.elts;b;b=b.rest)a(b.top)}},b.tokens=d,b.opTypeNames=f,b.keywords=g,b.isStatementStartCode=n,b.tokenIds=h,b.consts=i,b.assignOps=o,b.defineGetter=p,b.defineProperty=q,b.isNativeCode=r,b.makePassthruHandler=u,b.noPropFound=v,b.StringMap=x,b.Stack=y}) \ No newline at end of file
diff --git a/www/js/jquery-1.6.2.js b/www/js/jquery-1.6.2.js
new file mode 100644
index 0000000..f3201aa
--- /dev/null
+++ b/www/js/jquery-1.6.2.js
@@ -0,0 +1,8981 @@
+/*!
+ * jQuery JavaScript Library v1.6.2
+ * http://jquery.com/
+ *
+ * Copyright 2011, John Resig
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ *
+ * Includes Sizzle.js
+ * http://sizzlejs.com/
+ * Copyright 2011, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ *
+ * Date: Thu Jun 30 14:16:56 2011 -0400
+ */
+(function( window, undefined ) {
+
+// Use the correct document accordingly with window argument (sandbox)
+var document = window.document,
+ navigator = window.navigator,
+ location = window.location;
+var jQuery = (function() {
+
+// Define a local copy of jQuery
+var jQuery = function( selector, context ) {
+ // The jQuery object is actually just the init constructor 'enhanced'
+ return new jQuery.fn.init( selector, context, rootjQuery );
+ },
+
+ // Map over jQuery in case of overwrite
+ _jQuery = window.jQuery,
+
+ // Map over the $ in case of overwrite
+ _$ = window.$,
+
+ // A central reference to the root jQuery(document)
+ rootjQuery,
+
+ // A simple way to check for HTML strings or ID strings
+ // (both of which we optimize for)
+ quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,
+
+ // Check if a string has a non-whitespace character in it
+ rnotwhite = /\S/,
+
+ // Used for trimming whitespace
+ trimLeft = /^\s+/,
+ trimRight = /\s+$/,
+
+ // Check for digits
+ rdigit = /\d/,
+
+ // Match a standalone tag
+ rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/,
+
+ // JSON RegExp
+ rvalidchars = /^[\],:{}\s]*$/,
+ rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,
+ rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,
+ rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g,
+
+ // Useragent RegExp
+ rwebkit = /(webkit)[ \/]([\w.]+)/,
+ ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/,
+ rmsie = /(msie) ([\w.]+)/,
+ rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/,
+
+ // Matches dashed string for camelizing
+ rdashAlpha = /-([a-z])/ig,
+
+ // Used by jQuery.camelCase as callback to replace()
+ fcamelCase = function( all, letter ) {
+ return letter.toUpperCase();
+ },
+
+ // Keep a UserAgent string for use with jQuery.browser
+ userAgent = navigator.userAgent,
+
+ // For matching the engine and version of the browser
+ browserMatch,
+
+ // The deferred used on DOM ready
+ readyList,
+
+ // The ready event handler
+ DOMContentLoaded,
+
+ // Save a reference to some core methods
+ toString = Object.prototype.toString,
+ hasOwn = Object.prototype.hasOwnProperty,
+ push = Array.prototype.push,
+ slice = Array.prototype.slice,
+ trim = String.prototype.trim,
+ indexOf = Array.prototype.indexOf,
+
+ // [[Class]] -> type pairs
+ class2type = {};
+
+jQuery.fn = jQuery.prototype = {
+ constructor: jQuery,
+ init: function( selector, context, rootjQuery ) {
+ var match, elem, ret, doc;
+
+ // Handle $(""), $(null), or $(undefined)
+ if ( !selector ) {
+ return this;
+ }
+
+ // Handle $(DOMElement)
+ if ( selector.nodeType ) {
+ this.context = this[0] = selector;
+ this.length = 1;
+ return this;
+ }
+
+ // The body element only exists once, optimize finding it
+ if ( selector === "body" && !context && document.body ) {
+ this.context = document;
+ this[0] = document.body;
+ this.selector = selector;
+ this.length = 1;
+ return this;
+ }
+
+ // Handle HTML strings
+ if ( typeof selector === "string" ) {
+ // Are we dealing with HTML string or an ID?
+ if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
+ // Assume that strings that start and end with <> are HTML and skip the regex check
+ match = [ null, selector, null ];
+
+ } else {
+ match = quickExpr.exec( selector );
+ }
+
+ // Verify a match, and that no context was specified for #id
+ if ( match && (match[1] || !context) ) {
+
+ // HANDLE: $(html) -> $(array)
+ if ( match[1] ) {
+ context = context instanceof jQuery ? context[0] : context;
+ doc = (context ? context.ownerDocument || context : document);
+
+ // If a single string is passed in and it's a single tag
+ // just do a createElement and skip the rest
+ ret = rsingleTag.exec( selector );
+
+ if ( ret ) {
+ if ( jQuery.isPlainObject( context ) ) {
+ selector = [ document.createElement( ret[1] ) ];
+ jQuery.fn.attr.call( selector, context, true );
+
+ } else {
+ selector = [ doc.createElement( ret[1] ) ];
+ }
+
+ } else {
+ ret = jQuery.buildFragment( [ match[1] ], [ doc ] );
+ selector = (ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment).childNodes;
+ }
+
+ return jQuery.merge( this, selector );
+
+ // HANDLE: $("#id")
+ } else {
+ elem = document.getElementById( match[2] );
+
+ // Check parentNode to catch when Blackberry 4.6 returns
+ // nodes that are no longer in the document #6963
+ if ( elem && elem.parentNode ) {
+ // Handle the case where IE and Opera return items
+ // by name instead of ID
+ if ( elem.id !== match[2] ) {
+ return rootjQuery.find( selector );
+ }
+
+ // Otherwise, we inject the element directly into the jQuery object
+ this.length = 1;
+ this[0] = elem;
+ }
+
+ this.context = document;
+ this.selector = selector;
+ return this;
+ }
+
+ // HANDLE: $(expr, $(...))
+ } else if ( !context || context.jquery ) {
+ return (context || rootjQuery).find( selector );
+
+ // HANDLE: $(expr, context)
+ // (which is just equivalent to: $(context).find(expr)
+ } else {
+ return this.constructor( context ).find( selector );
+ }
+
+ // HANDLE: $(function)
+ // Shortcut for document ready
+ } else if ( jQuery.isFunction( selector ) ) {
+ return rootjQuery.ready( selector );
+ }
+
+ if (selector.selector !== undefined) {
+ this.selector = selector.selector;
+ this.context = selector.context;
+ }
+
+ return jQuery.makeArray( selector, this );
+ },
+
+ // Start with an empty selector
+ selector: "",
+
+ // The current version of jQuery being used
+ jquery: "1.6.2",
+
+ // The default length of a jQuery object is 0
+ length: 0,
+
+ // The number of elements contained in the matched element set
+ size: function() {
+ return this.length;
+ },
+
+ toArray: function() {
+ return slice.call( this, 0 );
+ },
+
+ // Get the Nth element in the matched element set OR
+ // Get the whole matched element set as a clean array
+ get: function( num ) {
+ return num == null ?
+
+ // Return a 'clean' array
+ this.toArray() :
+
+ // Return just the object
+ ( num < 0 ? this[ this.length + num ] : this[ num ] );
+ },
+
+ // Take an array of elements and push it onto the stack
+ // (returning the new matched element set)
+ pushStack: function( elems, name, selector ) {
+ // Build a new jQuery matched element set
+ var ret = this.constructor();
+
+ if ( jQuery.isArray( elems ) ) {
+ push.apply( ret, elems );
+
+ } else {
+ jQuery.merge( ret, elems );
+ }
+
+ // Add the old object onto the stack (as a reference)
+ ret.prevObject = this;
+
+ ret.context = this.context;
+
+ if ( name === "find" ) {
+ ret.selector = this.selector + (this.selector ? " " : "") + selector;
+ } else if ( name ) {
+ ret.selector = this.selector + "." + name + "(" + selector + ")";
+ }
+
+ // Return the newly-formed element set
+ return ret;
+ },
+
+ // Execute a callback for every element in the matched set.
+ // (You can seed the arguments with an array of args, but this is
+ // only used internally.)
+ each: function( callback, args ) {
+ return jQuery.each( this, callback, args );
+ },
+
+ ready: function( fn ) {
+ // Attach the listeners
+ jQuery.bindReady();
+
+ // Add the callback
+ readyList.done( fn );
+
+ return this;
+ },
+
+ eq: function( i ) {
+ return i === -1 ?
+ this.slice( i ) :
+ this.slice( i, +i + 1 );
+ },
+
+ first: function() {
+ return this.eq( 0 );
+ },
+
+ last: function() {
+ return this.eq( -1 );
+ },
+
+ slice: function() {
+ return this.pushStack( slice.apply( this, arguments ),
+ "slice", slice.call(arguments).join(",") );
+ },
+
+ map: function( callback ) {
+ return this.pushStack( jQuery.map(this, function( elem, i ) {
+ return callback.call( elem, i, elem );
+ }));
+ },
+
+ end: function() {
+ return this.prevObject || this.constructor(null);
+ },
+
+ // For internal use only.
+ // Behaves like an Array's method, not like a jQuery method.
+ push: push,
+ sort: [].sort,
+ splice: [].splice
+};
+
+// Give the init function the jQuery prototype for later instantiation
+jQuery.fn.init.prototype = jQuery.fn;
+
+jQuery.extend = jQuery.fn.extend = function() {
+ var options, name, src, copy, copyIsArray, clone,
+ target = arguments[0] || {},
+ i = 1,
+ length = arguments.length,
+ deep = false;
+
+ // Handle a deep copy situation
+ if ( typeof target === "boolean" ) {
+ deep = target;
+ target = arguments[1] || {};
+ // skip the boolean and the target
+ i = 2;
+ }
+
+ // Handle case when target is a string or something (possible in deep copy)
+ if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
+ target = {};
+ }
+
+ // extend jQuery itself if only one argument is passed
+ if ( length === i ) {
+ target = this;
+ --i;
+ }
+
+ for ( ; i < length; i++ ) {
+ // Only deal with non-null/undefined values
+ if ( (options = arguments[ i ]) != null ) {
+ // Extend the base object
+ for ( name in options ) {
+ src = target[ name ];
+ copy = options[ name ];
+
+ // Prevent never-ending loop
+ if ( target === copy ) {
+ continue;
+ }
+
+ // Recurse if we're merging plain objects or arrays
+ if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
+ if ( copyIsArray ) {
+ copyIsArray = false;
+ clone = src && jQuery.isArray(src) ? src : [];
+
+ } else {
+ clone = src && jQuery.isPlainObject(src) ? src : {};
+ }
+
+ // Never move original objects, clone them
+ target[ name ] = jQuery.extend( deep, clone, copy );
+
+ // Don't bring in undefined values
+ } else if ( copy !== undefined ) {
+ target[ name ] = copy;
+ }
+ }
+ }
+ }
+
+ // Return the modified object
+ return target;
+};
+
+jQuery.extend({
+ noConflict: function( deep ) {
+ if ( window.$ === jQuery ) {
+ window.$ = _$;
+ }
+
+ if ( deep && window.jQuery === jQuery ) {
+ window.jQuery = _jQuery;
+ }
+
+ return jQuery;
+ },
+
+ // Is the DOM ready to be used? Set to true once it occurs.
+ isReady: false,
+
+ // A counter to track how many items to wait for before
+ // the ready event fires. See #6781
+ readyWait: 1,
+
+ // Hold (or release) the ready event
+ holdReady: function( hold ) {
+ if ( hold ) {
+ jQuery.readyWait++;
+ } else {
+ jQuery.ready( true );
+ }
+ },
+
+ // Handle when the DOM is ready
+ ready: function( wait ) {
+ // Either a released hold or an DOMready/load event and not yet ready
+ if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) {
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+ if ( !document.body ) {
+ return setTimeout( jQuery.ready, 1 );
+ }
+
+ // Remember that the DOM is ready
+ jQuery.isReady = true;
+
+ // If a normal DOM Ready event fired, decrement, and wait if need be
+ if ( wait !== true && --jQuery.readyWait > 0 ) {
+ return;
+ }
+
+ // If there are functions bound, to execute
+ readyList.resolveWith( document, [ jQuery ] );
+
+ // Trigger any bound ready events
+ if ( jQuery.fn.trigger ) {
+ jQuery( document ).trigger( "ready" ).unbind( "ready" );
+ }
+ }
+ },
+
+ bindReady: function() {
+ if ( readyList ) {
+ return;
+ }
+
+ readyList = jQuery._Deferred();
+
+ // Catch cases where $(document).ready() is called after the
+ // browser event has already occurred.
+ if ( document.readyState === "complete" ) {
+ // Handle it asynchronously to allow scripts the opportunity to delay ready
+ return setTimeout( jQuery.ready, 1 );
+ }
+
+ // Mozilla, Opera and webkit nightlies currently support this event
+ if ( document.addEventListener ) {
+ // Use the handy event callback
+ document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+
+ // A fallback to window.onload, that will always work
+ window.addEventListener( "load", jQuery.ready, false );
+
+ // If IE event model is used
+ } else if ( document.attachEvent ) {
+ // ensure firing before onload,
+ // maybe late but safe also for iframes
+ document.attachEvent( "onreadystatechange", DOMContentLoaded );
+
+ // A fallback to window.onload, that will always work
+ window.attachEvent( "onload", jQuery.ready );
+
+ // If IE and not a frame
+ // continually check to see if the document is ready
+ var toplevel = false;
+
+ try {
+ toplevel = window.frameElement == null;
+ } catch(e) {}
+
+ if ( document.documentElement.doScroll && toplevel ) {
+ doScrollCheck();
+ }
+ }
+ },
+
+ // See test/unit/core.js for details concerning isFunction.
+ // Since version 1.3, DOM methods and functions like alert
+ // aren't supported. They return false on IE (#2968).
+ isFunction: function( obj ) {
+ return jQuery.type(obj) === "function";
+ },
+
+ isArray: Array.isArray || function( obj ) {
+ return jQuery.type(obj) === "array";
+ },
+
+ // A crude way of determining if an object is a window
+ isWindow: function( obj ) {
+ return obj && typeof obj === "object" && "setInterval" in obj;
+ },
+
+ isNaN: function( obj ) {
+ return obj == null || !rdigit.test( obj ) || isNaN( obj );
+ },
+
+ type: function( obj ) {
+ return obj == null ?
+ String( obj ) :
+ class2type[ toString.call(obj) ] || "object";
+ },
+
+ isPlainObject: function( obj ) {
+ // Must be an Object.
+ // Because of IE, we also have to check the presence of the constructor property.
+ // Make sure that DOM nodes and window objects don't pass through, as well
+ if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
+ return false;
+ }
+
+ // Not own constructor property must be Object
+ if ( obj.constructor &&
+ !hasOwn.call(obj, "constructor") &&
+ !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
+ return false;
+ }
+
+ // Own properties are enumerated firstly, so to speed up,
+ // if last one is own, then all properties are own.
+
+ var key;
+ for ( key in obj ) {}
+
+ return key === undefined || hasOwn.call( obj, key );
+ },
+
+ isEmptyObject: function( obj ) {
+ for ( var name in obj ) {
+ return false;
+ }
+ return true;
+ },
+
+ error: function( msg ) {
+ throw msg;
+ },
+
+ parseJSON: function( data ) {
+ if ( typeof data !== "string" || !data ) {
+ return null;
+ }
+
+ // Make sure leading/trailing whitespace is removed (IE can't handle it)
+ data = jQuery.trim( data );
+
+ // Attempt to parse using the native JSON parser first
+ if ( window.JSON && window.JSON.parse ) {
+ return window.JSON.parse( data );
+ }
+
+ // Make sure the incoming data is actual JSON
+ // Logic borrowed from http://json.org/json2.js
+ if ( rvalidchars.test( data.replace( rvalidescape, "@" )
+ .replace( rvalidtokens, "]" )
+ .replace( rvalidbraces, "")) ) {
+
+ return (new Function( "return " + data ))();
+
+ }
+ jQuery.error( "Invalid JSON: " + data );
+ },
+
+ // Cross-browser xml parsing
+ // (xml & tmp used internally)
+ parseXML: function( data , xml , tmp ) {
+
+ if ( window.DOMParser ) { // Standard
+ tmp = new DOMParser();
+ xml = tmp.parseFromString( data , "text/xml" );
+ } else { // IE
+ xml = new ActiveXObject( "Microsoft.XMLDOM" );
+ xml.async = "false";
+ xml.loadXML( data );
+ }
+
+ tmp = xml.documentElement;
+
+ if ( ! tmp || ! tmp.nodeName || tmp.nodeName === "parsererror" ) {
+ jQuery.error( "Invalid XML: " + data );
+ }
+
+ return xml;
+ },
+
+ noop: function() {},
+
+ // Evaluates a script in a global context
+ // Workarounds based on findings by Jim Driscoll
+ // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context
+ globalEval: function( data ) {
+ if ( data && rnotwhite.test( data ) ) {
+ // We use execScript on Internet Explorer
+ // We use an anonymous function so that context is window
+ // rather than jQuery in Firefox
+ ( window.execScript || function( data ) {
+ window[ "eval" ].call( window, data );
+ } )( data );
+ }
+ },
+
+ // Converts a dashed string to camelCased string;
+ // Used by both the css and data modules
+ camelCase: function( string ) {
+ return string.replace( rdashAlpha, fcamelCase );
+ },
+
+ nodeName: function( elem, name ) {
+ return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();
+ },
+
+ // args is for internal usage only
+ each: function( object, callback, args ) {
+ var name, i = 0,
+ length = object.length,
+ isObj = length === undefined || jQuery.isFunction( object );
+
+ if ( args ) {
+ if ( isObj ) {
+ for ( name in object ) {
+ if ( callback.apply( object[ name ], args ) === false ) {
+ break;
+ }
+ }
+ } else {
+ for ( ; i < length; ) {
+ if ( callback.apply( object[ i++ ], args ) === false ) {
+ break;
+ }
+ }
+ }
+
+ // A special, fast, case for the most common use of each
+ } else {
+ if ( isObj ) {
+ for ( name in object ) {
+ if ( callback.call( object[ name ], name, object[ name ] ) === false ) {
+ break;
+ }
+ }
+ } else {
+ for ( ; i < length; ) {
+ if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) {
+ break;
+ }
+ }
+ }
+ }
+
+ return object;
+ },
+
+ // Use native String.trim function wherever possible
+ trim: trim ?
+ function( text ) {
+ return text == null ?
+ "" :
+ trim.call( text );
+ } :
+
+ // Otherwise use our own trimming functionality
+ function( text ) {
+ return text == null ?
+ "" :
+ text.toString().replace( trimLeft, "" ).replace( trimRight, "" );
+ },
+
+ // results is for internal usage only
+ makeArray: function( array, results ) {
+ var ret = results || [];
+
+ if ( array != null ) {
+ // The window, strings (and functions) also have 'length'
+ // The extra typeof function check is to prevent crashes
+ // in Safari 2 (See: #3039)
+ // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930
+ var type = jQuery.type( array );
+
+ if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) {
+ push.call( ret, array );
+ } else {
+ jQuery.merge( ret, array );
+ }
+ }
+
+ return ret;
+ },
+
+ inArray: function( elem, array ) {
+
+ if ( indexOf ) {
+ return indexOf.call( array, elem );
+ }
+
+ for ( var i = 0, length = array.length; i < length; i++ ) {
+ if ( array[ i ] === elem ) {
+ return i;
+ }
+ }
+
+ return -1;
+ },
+
+ merge: function( first, second ) {
+ var i = first.length,
+ j = 0;
+
+ if ( typeof second.length === "number" ) {
+ for ( var l = second.length; j < l; j++ ) {
+ first[ i++ ] = second[ j ];
+ }
+
+ } else {
+ while ( second[j] !== undefined ) {
+ first[ i++ ] = second[ j++ ];
+ }
+ }
+
+ first.length = i;
+
+ return first;
+ },
+
+ grep: function( elems, callback, inv ) {
+ var ret = [], retVal;
+ inv = !!inv;
+
+ // Go through the array, only saving the items
+ // that pass the validator function
+ for ( var i = 0, length = elems.length; i < length; i++ ) {
+ retVal = !!callback( elems[ i ], i );
+ if ( inv !== retVal ) {
+ ret.push( elems[ i ] );
+ }
+ }
+
+ return ret;
+ },
+
+ // arg is for internal usage only
+ map: function( elems, callback, arg ) {
+ var value, key, ret = [],
+ i = 0,
+ length = elems.length,
+ // jquery objects are treated as arrays
+ isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ;
+
+ // Go through the array, translating each of the items to their
+ if ( isArray ) {
+ for ( ; i < length; i++ ) {
+ value = callback( elems[ i ], i, arg );
+
+ if ( value != null ) {
+ ret[ ret.length ] = value;
+ }
+ }
+
+ // Go through every key on the object,
+ } else {
+ for ( key in elems ) {
+ value = callback( elems[ key ], key, arg );
+
+ if ( value != null ) {
+ ret[ ret.length ] = value;
+ }
+ }
+ }
+
+ // Flatten any nested arrays
+ return ret.concat.apply( [], ret );
+ },
+
+ // A global GUID counter for objects
+ guid: 1,
+
+ // Bind a function to a context, optionally partially applying any
+ // arguments.
+ proxy: function( fn, context ) {
+ if ( typeof context === "string" ) {
+ var tmp = fn[ context ];
+ context = fn;
+ fn = tmp;
+ }
+
+ // Quick check to determine if target is callable, in the spec
+ // this throws a TypeError, but we will just return undefined.
+ if ( !jQuery.isFunction( fn ) ) {
+ return undefined;
+ }
+
+ // Simulated bind
+ var args = slice.call( arguments, 2 ),
+ proxy = function() {
+ return fn.apply( context, args.concat( slice.call( arguments ) ) );
+ };
+
+ // Set the guid of unique handler to the same of original handler, so it can be removed
+ proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;
+
+ return proxy;
+ },
+
+ // Mutifunctional method to get and set values to a collection
+ // The value/s can optionally be executed if it's a function
+ access: function( elems, key, value, exec, fn, pass ) {
+ var length = elems.length;
+
+ // Setting many attributes
+ if ( typeof key === "object" ) {
+ for ( var k in key ) {
+ jQuery.access( elems, k, key[k], exec, fn, value );
+ }
+ return elems;
+ }
+
+ // Setting one attribute
+ if ( value !== undefined ) {
+ // Optionally, function values get executed if exec is true
+ exec = !pass && exec && jQuery.isFunction(value);
+
+ for ( var i = 0; i < length; i++ ) {
+ fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass );
+ }
+
+ return elems;
+ }
+
+ // Getting an attribute
+ return length ? fn( elems[0], key ) : undefined;
+ },
+
+ now: function() {
+ return (new Date()).getTime();
+ },
+
+ // Use of jQuery.browser is frowned upon.
+ // More details: http://docs.jquery.com/Utilities/jQuery.browser
+ uaMatch: function( ua ) {
+ ua = ua.toLowerCase();
+
+ var match = rwebkit.exec( ua ) ||
+ ropera.exec( ua ) ||
+ rmsie.exec( ua ) ||
+ ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) ||
+ [];
+
+ return { browser: match[1] || "", version: match[2] || "0" };
+ },
+
+ sub: function() {
+ function jQuerySub( selector, context ) {
+ return new jQuerySub.fn.init( selector, context );
+ }
+ jQuery.extend( true, jQuerySub, this );
+ jQuerySub.superclass = this;
+ jQuerySub.fn = jQuerySub.prototype = this();
+ jQuerySub.fn.constructor = jQuerySub;
+ jQuerySub.sub = this.sub;
+ jQuerySub.fn.init = function init( selector, context ) {
+ if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) {
+ context = jQuerySub( context );
+ }
+
+ return jQuery.fn.init.call( this, selector, context, rootjQuerySub );
+ };
+ jQuerySub.fn.init.prototype = jQuerySub.fn;
+ var rootjQuerySub = jQuerySub(document);
+ return jQuerySub;
+ },
+
+ browser: {}
+});
+
+// Populate the class2type map
+jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) {
+ class2type[ "[object " + name + "]" ] = name.toLowerCase();
+});
+
+browserMatch = jQuery.uaMatch( userAgent );
+if ( browserMatch.browser ) {
+ jQuery.browser[ browserMatch.browser ] = true;
+ jQuery.browser.version = browserMatch.version;
+}
+
+// Deprecated, use jQuery.browser.webkit instead
+if ( jQuery.browser.webkit ) {
+ jQuery.browser.safari = true;
+}
+
+// IE doesn't match non-breaking spaces with \s
+if ( rnotwhite.test( "\xA0" ) ) {
+ trimLeft = /^[\s\xA0]+/;
+ trimRight = /[\s\xA0]+$/;
+}
+
+// All jQuery objects should point back to these
+rootjQuery = jQuery(document);
+
+// Cleanup functions for the document ready method
+if ( document.addEventListener ) {
+ DOMContentLoaded = function() {
+ document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
+ jQuery.ready();
+ };
+
+} else if ( document.attachEvent ) {
+ DOMContentLoaded = function() {
+ // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
+ if ( document.readyState === "complete" ) {
+ document.detachEvent( "onreadystatechange", DOMContentLoaded );
+ jQuery.ready();
+ }
+ };
+}
+
+// The DOM ready check for Internet Explorer
+function doScrollCheck() {
+ if ( jQuery.isReady ) {
+ return;
+ }
+
+ try {
+ // If IE is used, use the trick by Diego Perini
+ // http://javascript.nwbox.com/IEContentLoaded/
+ document.documentElement.doScroll("left");
+ } catch(e) {
+ setTimeout( doScrollCheck, 1 );
+ return;
+ }
+
+ // and execute any waiting functions
+ jQuery.ready();
+}
+
+return jQuery;
+
+})();
+
+
+var // Promise methods
+ promiseMethods = "done fail isResolved isRejected promise then always pipe".split( " " ),
+ // Static reference to slice
+ sliceDeferred = [].slice;
+
+jQuery.extend({
+ // Create a simple deferred (one callbacks list)
+ _Deferred: function() {
+ var // callbacks list
+ callbacks = [],
+ // stored [ context , args ]
+ fired,
+ // to avoid firing when already doing so
+ firing,
+ // flag to know if the deferred has been cancelled
+ cancelled,
+ // the deferred itself
+ deferred = {
+
+ // done( f1, f2, ...)
+ done: function() {
+ if ( !cancelled ) {
+ var args = arguments,
+ i,
+ length,
+ elem,
+ type,
+ _fired;
+ if ( fired ) {
+ _fired = fired;
+ fired = 0;
+ }
+ for ( i = 0, length = args.length; i < length; i++ ) {
+ elem = args[ i ];
+ type = jQuery.type( elem );
+ if ( type === "array" ) {
+ deferred.done.apply( deferred, elem );
+ } else if ( type === "function" ) {
+ callbacks.push( elem );
+ }
+ }
+ if ( _fired ) {
+ deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
+ }
+ }
+ return this;
+ },
+
+ // resolve with given context and args
+ resolveWith: function( context, args ) {
+ if ( !cancelled && !fired && !firing ) {
+ // make sure args are available (#8421)
+ args = args || [];
+ firing = 1;
+ try {
+ while( callbacks[ 0 ] ) {
+ callbacks.shift().apply( context, args );
+ }
+ }
+ finally {
+ fired = [ context, args ];
+ firing = 0;
+ }
+ }
+ return this;
+ },
+
+ // resolve with this as context and given arguments
+ resolve: function() {
+ deferred.resolveWith( this, arguments );
+ return this;
+ },
+
+ // Has this deferred been resolved?
+ isResolved: function() {
+ return !!( firing || fired );
+ },
+
+ // Cancel
+ cancel: function() {
+ cancelled = 1;
+ callbacks = [];
+ return this;
+ }
+ };
+
+ return deferred;
+ },
+
+ // Full fledged deferred (two callbacks list)
+ Deferred: function( func ) {
+ var deferred = jQuery._Deferred(),
+ failDeferred = jQuery._Deferred(),
+ promise;
+ // Add errorDeferred methods, then and promise
+ jQuery.extend( deferred, {
+ then: function( doneCallbacks, failCallbacks ) {
+ deferred.done( doneCallbacks ).fail( failCallbacks );
+ return this;
+ },
+ always: function() {
+ return deferred.done.apply( deferred, arguments ).fail.apply( this, arguments );
+ },
+ fail: failDeferred.done,
+ rejectWith: failDeferred.resolveWith,
+ reject: failDeferred.resolve,
+ isRejected: failDeferred.isResolved,
+ pipe: function( fnDone, fnFail ) {
+ return jQuery.Deferred(function( newDefer ) {
+ jQuery.each( {
+ done: [ fnDone, "resolve" ],
+ fail: [ fnFail, "reject" ]
+ }, function( handler, data ) {
+ var fn = data[ 0 ],
+ action = data[ 1 ],
+ returned;
+ if ( jQuery.isFunction( fn ) ) {
+ deferred[ handler ](function() {
+ returned = fn.apply( this, arguments );
+ if ( returned && jQuery.isFunction( returned.promise ) ) {
+ returned.promise().then( newDefer.resolve, newDefer.reject );
+ } else {
+ newDefer[ action ]( returned );
+ }
+ });
+ } else {
+ deferred[ handler ]( newDefer[ action ] );
+ }
+ });
+ }).promise();
+ },
+ // Get a promise for this deferred
+ // If obj is provided, the promise aspect is added to the object
+ promise: function( obj ) {
+ if ( obj == null ) {
+ if ( promise ) {
+ return promise;
+ }
+ promise = obj = {};
+ }
+ var i = promiseMethods.length;
+ while( i-- ) {
+ obj[ promiseMethods[i] ] = deferred[ promiseMethods[i] ];
+ }
+ return obj;
+ }
+ });
+ // Make sure only one callback list will be used
+ deferred.done( failDeferred.cancel ).fail( deferred.cancel );
+ // Unexpose cancel
+ delete deferred.cancel;
+ // Call given func if any
+ if ( func ) {
+ func.call( deferred, deferred );
+ }
+ return deferred;
+ },
+
+ // Deferred helper
+ when: function( firstParam ) {
+ var args = arguments,
+ i = 0,
+ length = args.length,
+ count = length,
+ deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ?
+ firstParam :
+ jQuery.Deferred();
+ function resolveFunc( i ) {
+ return function( value ) {
+ args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;
+ if ( !( --count ) ) {
+ // Strange bug in FF4:
+ // Values changed onto the arguments object sometimes end up as undefined values
+ // outside the $.when method. Cloning the object into a fresh array solves the issue
+ deferred.resolveWith( deferred, sliceDeferred.call( args, 0 ) );
+ }
+ };
+ }
+ if ( length > 1 ) {
+ for( ; i < length; i++ ) {
+ if ( args[ i ] && jQuery.isFunction( args[ i ].promise ) ) {
+ args[ i ].promise().then( resolveFunc(i), deferred.reject );
+ } else {
+ --count;
+ }
+ }
+ if ( !count ) {
+ deferred.resolveWith( deferred, args );
+ }
+ } else if ( deferred !== firstParam ) {
+ deferred.resolveWith( deferred, length ? [ firstParam ] : [] );
+ }
+ return deferred.promise();
+ }
+});
+
+
+
+jQuery.support = (function() {
+
+ var div = document.createElement( "div" ),
+ documentElement = document.documentElement,
+ all,
+ a,
+ select,
+ opt,
+ input,
+ marginDiv,
+ support,
+ fragment,
+ body,
+ testElementParent,
+ testElement,
+ testElementStyle,
+ tds,
+ events,
+ eventName,
+ i,
+ isSupported;
+
+ // Preliminary tests
+ div.setAttribute("className", "t");
+ div.innerHTML = " <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>";
+
+ all = div.getElementsByTagName( "*" );
+ a = div.getElementsByTagName( "a" )[ 0 ];
+
+ // Can't get basic test support
+ if ( !all || !all.length || !a ) {
+ return {};
+ }
+
+ // First batch of supports tests
+ select = document.createElement( "select" );
+ opt = select.appendChild( document.createElement("option") );
+ input = div.getElementsByTagName( "input" )[ 0 ];
+
+ support = {
+ // IE strips leading whitespace when .innerHTML is used
+ leadingWhitespace: ( div.firstChild.nodeType === 3 ),
+
+ // Make sure that tbody elements aren't automatically inserted
+ // IE will insert them into empty tables
+ tbody: !div.getElementsByTagName( "tbody" ).length,
+
+ // Make sure that link elements get serialized correctly by innerHTML
+ // This requires a wrapper element in IE
+ htmlSerialize: !!div.getElementsByTagName( "link" ).length,
+
+ // Get the style information from getAttribute
+ // (IE uses .cssText instead)
+ style: /top/.test( a.getAttribute("style") ),
+
+ // Make sure that URLs aren't manipulated
+ // (IE normalizes it by default)
+ hrefNormalized: ( a.getAttribute( "href" ) === "/a" ),
+
+ // Make sure that element opacity exists
+ // (IE uses filter instead)
+ // Use a regex to work around a WebKit issue. See #5145
+ opacity: /^0.55$/.test( a.style.opacity ),
+
+ // Verify style float existence
+ // (IE uses styleFloat instead of cssFloat)
+ cssFloat: !!a.style.cssFloat,
+
+ // Make sure that if no value is specified for a checkbox
+ // that it defaults to "on".
+ // (WebKit defaults to "" instead)
+ checkOn: ( input.value === "on" ),
+
+ // Make sure that a selected-by-default option has a working selected property.
+ // (WebKit defaults to false instead of true, IE too, if it's in an optgroup)
+ optSelected: opt.selected,
+
+ // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)
+ getSetAttribute: div.className !== "t",
+
+ // Will be defined later
+ submitBubbles: true,
+ changeBubbles: true,
+ focusinBubbles: false,
+ deleteExpando: true,
+ noCloneEvent: true,
+ inlineBlockNeedsLayout: false,
+ shrinkWrapBlocks: false,
+ reliableMarginRight: true
+ };
+
+ // Make sure checked status is properly cloned
+ input.checked = true;
+ support.noCloneChecked = input.cloneNode( true ).checked;
+
+ // Make sure that the options inside disabled selects aren't marked as disabled
+ // (WebKit marks them as disabled)
+ select.disabled = true;
+ support.optDisabled = !opt.disabled;
+
+ // Test to see if it's possible to delete an expando from an element
+ // Fails in Internet Explorer
+ try {
+ delete div.test;
+ } catch( e ) {
+ support.deleteExpando = false;
+ }
+
+ if ( !div.addEventListener && div.attachEvent && div.fireEvent ) {
+ div.attachEvent( "onclick", function() {
+ // Cloning a node shouldn't copy over any
+ // bound event handlers (IE does this)
+ support.noCloneEvent = false;
+ });
+ div.cloneNode( true ).fireEvent( "onclick" );
+ }
+
+ // Check if a radio maintains it's value
+ // after being appended to the DOM
+ input = document.createElement("input");
+ input.value = "t";
+ input.setAttribute("type", "radio");
+ support.radioValue = input.value === "t";
+
+ input.setAttribute("checked", "checked");
+ div.appendChild( input );
+ fragment = document.createDocumentFragment();
+ fragment.appendChild( div.firstChild );
+
+ // WebKit doesn't clone checked state correctly in fragments
+ support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;
+
+ div.innerHTML = "";
+
+ // Figure out if the W3C box model works as expected
+ div.style.width = div.style.paddingLeft = "1px";
+
+ body = document.getElementsByTagName( "body" )[ 0 ];
+ // We use our own, invisible, body unless the body is already present
+ // in which case we use a div (#9239)
+ testElement = document.createElement( body ? "div" : "body" );
+ testElementStyle = {
+ visibility: "hidden",
+ width: 0,
+ height: 0,
+ border: 0,
+ margin: 0
+ };
+ if ( body ) {
+ jQuery.extend( testElementStyle, {
+ position: "absolute",
+ left: -1000,
+ top: -1000
+ });
+ }
+ for ( i in testElementStyle ) {
+ testElement.style[ i ] = testElementStyle[ i ];
+ }
+ testElement.appendChild( div );
+ testElementParent = body || documentElement;
+ testElementParent.insertBefore( testElement, testElementParent.firstChild );
+
+ // Check if a disconnected checkbox will retain its checked
+ // value of true after appended to the DOM (IE6/7)
+ support.appendChecked = input.checked;
+
+ support.boxModel = div.offsetWidth === 2;
+
+ if ( "zoom" in div.style ) {
+ // Check if natively block-level elements act like inline-block
+ // elements when setting their display to 'inline' and giving
+ // them layout
+ // (IE < 8 does this)
+ div.style.display = "inline";
+ div.style.zoom = 1;
+ support.inlineBlockNeedsLayout = ( div.offsetWidth === 2 );
+
+ // Check if elements with layout shrink-wrap their children
+ // (IE 6 does this)
+ div.style.display = "";
+ div.innerHTML = "<div style='width:4px;'></div>";
+ support.shrinkWrapBlocks = ( div.offsetWidth !== 2 );
+ }
+
+ div.innerHTML = "<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>";
+ tds = div.getElementsByTagName( "td" );
+
+ // Check if table cells still have offsetWidth/Height when they are set
+ // to display:none and there are still other visible table cells in a
+ // table row; if so, offsetWidth/Height are not reliable for use when
+ // determining if an element has been hidden directly using
+ // display:none (it is still safe to use offsets if a parent element is
+ // hidden; don safety goggles and see bug #4512 for more information).
+ // (only IE 8 fails this test)
+ isSupported = ( tds[ 0 ].offsetHeight === 0 );
+
+ tds[ 0 ].style.display = "";
+ tds[ 1 ].style.display = "none";
+
+ // Check if empty table cells still have offsetWidth/Height
+ // (IE < 8 fail this test)
+ support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );
+ div.innerHTML = "";
+
+ // Check if div with explicit width and no margin-right incorrectly
+ // gets computed margin-right based on width of container. For more
+ // info see bug #3333
+ // Fails in WebKit before Feb 2011 nightlies
+ // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+ if ( document.defaultView && document.defaultView.getComputedStyle ) {
+ marginDiv = document.createElement( "div" );
+ marginDiv.style.width = "0";
+ marginDiv.style.marginRight = "0";
+ div.appendChild( marginDiv );
+ support.reliableMarginRight =
+ ( parseInt( ( document.defaultView.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0;
+ }
+
+ // Remove the body element we added
+ testElement.innerHTML = "";
+ testElementParent.removeChild( testElement );
+
+ // Technique from Juriy Zaytsev
+ // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/
+ // We only care about the case where non-standard event systems
+ // are used, namely in IE. Short-circuiting here helps us to
+ // avoid an eval call (in setAttribute) which can cause CSP
+ // to go haywire. See: https://developer.mozilla.org/en/Security/CSP
+ if ( div.attachEvent ) {
+ for( i in {
+ submit: 1,
+ change: 1,
+ focusin: 1
+ } ) {
+ eventName = "on" + i;
+ isSupported = ( eventName in div );
+ if ( !isSupported ) {
+ div.setAttribute( eventName, "return;" );
+ isSupported = ( typeof div[ eventName ] === "function" );
+ }
+ support[ i + "Bubbles" ] = isSupported;
+ }
+ }
+
+ // Null connected elements to avoid leaks in IE
+ testElement = fragment = select = opt = body = marginDiv = div = input = null;
+
+ return support;
+})();
+
+// Keep track of boxModel
+jQuery.boxModel = jQuery.support.boxModel;
+
+
+
+
+var rbrace = /^(?:\{.*\}|\[.*\])$/,
+ rmultiDash = /([a-z])([A-Z])/g;
+
+jQuery.extend({
+ cache: {},
+
+ // Please use with caution
+ uuid: 0,
+
+ // Unique for each copy of jQuery on the page
+ // Non-digits removed to match rinlinejQuery
+ expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),
+
+ // The following elements throw uncatchable exceptions if you
+ // attempt to add expando properties to them.
+ noData: {
+ "embed": true,
+ // Ban all objects except for Flash (which handle expandos)
+ "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
+ "applet": true
+ },
+
+ hasData: function( elem ) {
+ elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];
+
+ return !!elem && !isEmptyDataObject( elem );
+ },
+
+ data: function( elem, name, data, pvt /* Internal Use Only */ ) {
+ if ( !jQuery.acceptData( elem ) ) {
+ return;
+ }
+
+ var internalKey = jQuery.expando, getByName = typeof name === "string", thisCache,
+
+ // We have to handle DOM nodes and JS objects differently because IE6-7
+ // can't GC object references properly across the DOM-JS boundary
+ isNode = elem.nodeType,
+
+ // Only DOM nodes need the global jQuery cache; JS object data is
+ // attached directly to the object so GC can occur automatically
+ cache = isNode ? jQuery.cache : elem,
+
+ // Only defining an ID for JS objects if its cache already exists allows
+ // the code to shortcut on the same path as a DOM node with no cache
+ id = isNode ? elem[ jQuery.expando ] : elem[ jQuery.expando ] && jQuery.expando;
+
+ // Avoid doing any more work than we need to when trying to get data on an
+ // object that has no data at all
+ if ( (!id || (pvt && id && !cache[ id ][ internalKey ])) && getByName && data === undefined ) {
+ return;
+ }
+
+ if ( !id ) {
+ // Only DOM nodes need a new unique ID for each element since their data
+ // ends up in the global cache
+ if ( isNode ) {
+ elem[ jQuery.expando ] = id = ++jQuery.uuid;
+ } else {
+ id = jQuery.expando;
+ }
+ }
+
+ if ( !cache[ id ] ) {
+ cache[ id ] = {};
+
+ // TODO: This is a hack for 1.5 ONLY. Avoids exposing jQuery
+ // metadata on plain JS objects when the object is serialized using
+ // JSON.stringify
+ if ( !isNode ) {
+ cache[ id ].toJSON = jQuery.noop;
+ }
+ }
+
+ // An object can be passed to jQuery.data instead of a key/value pair; this gets
+ // shallow copied over onto the existing cache
+ if ( typeof name === "object" || typeof name === "function" ) {
+ if ( pvt ) {
+ cache[ id ][ internalKey ] = jQuery.extend(cache[ id ][ internalKey ], name);
+ } else {
+ cache[ id ] = jQuery.extend(cache[ id ], name);
+ }
+ }
+
+ thisCache = cache[ id ];
+
+ // Internal jQuery data is stored in a separate object inside the object's data
+ // cache in order to avoid key collisions between internal data and user-defined
+ // data
+ if ( pvt ) {
+ if ( !thisCache[ internalKey ] ) {
+ thisCache[ internalKey ] = {};
+ }
+
+ thisCache = thisCache[ internalKey ];
+ }
+
+ if ( data !== undefined ) {
+ thisCache[ jQuery.camelCase( name ) ] = data;
+ }
+
+ // TODO: This is a hack for 1.5 ONLY. It will be removed in 1.6. Users should
+ // not attempt to inspect the internal events object using jQuery.data, as this
+ // internal data object is undocumented and subject to change.
+ if ( name === "events" && !thisCache[name] ) {
+ return thisCache[ internalKey ] && thisCache[ internalKey ].events;
+ }
+
+ return getByName ?
+ // Check for both converted-to-camel and non-converted data property names
+ thisCache[ jQuery.camelCase( name ) ] || thisCache[ name ] :
+ thisCache;
+ },
+
+ removeData: function( elem, name, pvt /* Internal Use Only */ ) {
+ if ( !jQuery.acceptData( elem ) ) {
+ return;
+ }
+
+ var internalKey = jQuery.expando, isNode = elem.nodeType,
+
+ // See jQuery.data for more information
+ cache = isNode ? jQuery.cache : elem,
+
+ // See jQuery.data for more information
+ id = isNode ? elem[ jQuery.expando ] : jQuery.expando;
+
+ // If there is already no cache entry for this object, there is no
+ // purpose in continuing
+ if ( !cache[ id ] ) {
+ return;
+ }
+
+ if ( name ) {
+ var thisCache = pvt ? cache[ id ][ internalKey ] : cache[ id ];
+
+ if ( thisCache ) {
+ delete thisCache[ name ];
+
+ // If there is no data left in the cache, we want to continue
+ // and let the cache object itself get destroyed
+ if ( !isEmptyDataObject(thisCache) ) {
+ return;
+ }
+ }
+ }
+
+ // See jQuery.data for more information
+ if ( pvt ) {
+ delete cache[ id ][ internalKey ];
+
+ // Don't destroy the parent cache unless the internal data object
+ // had been the only thing left in it
+ if ( !isEmptyDataObject(cache[ id ]) ) {
+ return;
+ }
+ }
+
+ var internalCache = cache[ id ][ internalKey ];
+
+ // Browsers that fail expando deletion also refuse to delete expandos on
+ // the window, but it will allow it on all other JS objects; other browsers
+ // don't care
+ if ( jQuery.support.deleteExpando || cache != window ) {
+ delete cache[ id ];
+ } else {
+ cache[ id ] = null;
+ }
+
+ // We destroyed the entire user cache at once because it's faster than
+ // iterating through each key, but we need to continue to persist internal
+ // data if it existed
+ if ( internalCache ) {
+ cache[ id ] = {};
+ // TODO: This is a hack for 1.5 ONLY. Avoids exposing jQuery
+ // metadata on plain JS objects when the object is serialized using
+ // JSON.stringify
+ if ( !isNode ) {
+ cache[ id ].toJSON = jQuery.noop;
+ }
+
+ cache[ id ][ internalKey ] = internalCache;
+
+ // Otherwise, we need to eliminate the expando on the node to avoid
+ // false lookups in the cache for entries that no longer exist
+ } else if ( isNode ) {
+ // IE does not allow us to delete expando properties from nodes,
+ // nor does it have a removeAttribute function on Document nodes;
+ // we must handle all of these cases
+ if ( jQuery.support.deleteExpando ) {
+ delete elem[ jQuery.expando ];
+ } else if ( elem.removeAttribute ) {
+ elem.removeAttribute( jQuery.expando );
+ } else {
+ elem[ jQuery.expando ] = null;
+ }
+ }
+ },
+
+ // For internal use only.
+ _data: function( elem, name, data ) {
+ return jQuery.data( elem, name, data, true );
+ },
+
+ // A method for determining if a DOM node can handle the data expando
+ acceptData: function( elem ) {
+ if ( elem.nodeName ) {
+ var match = jQuery.noData[ elem.nodeName.toLowerCase() ];
+
+ if ( match ) {
+ return !(match === true || elem.getAttribute("classid") !== match);
+ }
+ }
+
+ return true;
+ }
+});
+
+jQuery.fn.extend({
+ data: function( key, value ) {
+ var data = null;
+
+ if ( typeof key === "undefined" ) {
+ if ( this.length ) {
+ data = jQuery.data( this[0] );
+
+ if ( this[0].nodeType === 1 ) {
+ var attr = this[0].attributes, name;
+ for ( var i = 0, l = attr.length; i < l; i++ ) {
+ name = attr[i].name;
+
+ if ( name.indexOf( "data-" ) === 0 ) {
+ name = jQuery.camelCase( name.substring(5) );
+
+ dataAttr( this[0], name, data[ name ] );
+ }
+ }
+ }
+ }
+
+ return data;
+
+ } else if ( typeof key === "object" ) {
+ return this.each(function() {
+ jQuery.data( this, key );
+ });
+ }
+
+ var parts = key.split(".");
+ parts[1] = parts[1] ? "." + parts[1] : "";
+
+ if ( value === undefined ) {
+ data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);
+
+ // Try to fetch any internally stored data first
+ if ( data === undefined && this.length ) {
+ data = jQuery.data( this[0], key );
+ data = dataAttr( this[0], key, data );
+ }
+
+ return data === undefined && parts[1] ?
+ this.data( parts[0] ) :
+ data;
+
+ } else {
+ return this.each(function() {
+ var $this = jQuery( this ),
+ args = [ parts[0], value ];
+
+ $this.triggerHandler( "setData" + parts[1] + "!", args );
+ jQuery.data( this, key, value );
+ $this.triggerHandler( "changeData" + parts[1] + "!", args );
+ });
+ }
+ },
+
+ removeData: function( key ) {
+ return this.each(function() {
+ jQuery.removeData( this, key );
+ });
+ }
+});
+
+function dataAttr( elem, key, data ) {
+ // If nothing was found internally, try to fetch any
+ // data from the HTML5 data-* attribute
+ if ( data === undefined && elem.nodeType === 1 ) {
+ var name = "data-" + key.replace( rmultiDash, "$1-$2" ).toLowerCase();
+
+ data = elem.getAttribute( name );
+
+ if ( typeof data === "string" ) {
+ try {
+ data = data === "true" ? true :
+ data === "false" ? false :
+ data === "null" ? null :
+ !jQuery.isNaN( data ) ? parseFloat( data ) :
+ rbrace.test( data ) ? jQuery.parseJSON( data ) :
+ data;
+ } catch( e ) {}
+
+ // Make sure we set the data so it isn't changed later
+ jQuery.data( elem, key, data );
+
+ } else {
+ data = undefined;
+ }
+ }
+
+ return data;
+}
+
+// TODO: This is a hack for 1.5 ONLY to allow objects with a single toJSON
+// property to be considered empty objects; this property always exists in
+// order to make sure JSON.stringify does not expose internal metadata
+function isEmptyDataObject( obj ) {
+ for ( var name in obj ) {
+ if ( name !== "toJSON" ) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+
+
+function handleQueueMarkDefer( elem, type, src ) {
+ var deferDataKey = type + "defer",
+ queueDataKey = type + "queue",
+ markDataKey = type + "mark",
+ defer = jQuery.data( elem, deferDataKey, undefined, true );
+ if ( defer &&
+ ( src === "queue" || !jQuery.data( elem, queueDataKey, undefined, true ) ) &&
+ ( src === "mark" || !jQuery.data( elem, markDataKey, undefined, true ) ) ) {
+ // Give room for hard-coded callbacks to fire first
+ // and eventually mark/queue something else on the element
+ setTimeout( function() {
+ if ( !jQuery.data( elem, queueDataKey, undefined, true ) &&
+ !jQuery.data( elem, markDataKey, undefined, true ) ) {
+ jQuery.removeData( elem, deferDataKey, true );
+ defer.resolve();
+ }
+ }, 0 );
+ }
+}
+
+jQuery.extend({
+
+ _mark: function( elem, type ) {
+ if ( elem ) {
+ type = (type || "fx") + "mark";
+ jQuery.data( elem, type, (jQuery.data(elem,type,undefined,true) || 0) + 1, true );
+ }
+ },
+
+ _unmark: function( force, elem, type ) {
+ if ( force !== true ) {
+ type = elem;
+ elem = force;
+ force = false;
+ }
+ if ( elem ) {
+ type = type || "fx";
+ var key = type + "mark",
+ count = force ? 0 : ( (jQuery.data( elem, key, undefined, true) || 1 ) - 1 );
+ if ( count ) {
+ jQuery.data( elem, key, count, true );
+ } else {
+ jQuery.removeData( elem, key, true );
+ handleQueueMarkDefer( elem, type, "mark" );
+ }
+ }
+ },
+
+ queue: function( elem, type, data ) {
+ if ( elem ) {
+ type = (type || "fx") + "queue";
+ var q = jQuery.data( elem, type, undefined, true );
+ // Speed up dequeue by getting out quickly if this is just a lookup
+ if ( data ) {
+ if ( !q || jQuery.isArray(data) ) {
+ q = jQuery.data( elem, type, jQuery.makeArray(data), true );
+ } else {
+ q.push( data );
+ }
+ }
+ return q || [];
+ }
+ },
+
+ dequeue: function( elem, type ) {
+ type = type || "fx";
+
+ var queue = jQuery.queue( elem, type ),
+ fn = queue.shift(),
+ defer;
+
+ // If the fx queue is dequeued, always remove the progress sentinel
+ if ( fn === "inprogress" ) {
+ fn = queue.shift();
+ }
+
+ if ( fn ) {
+ // Add a progress sentinel to prevent the fx queue from being
+ // automatically dequeued
+ if ( type === "fx" ) {
+ queue.unshift("inprogress");
+ }
+
+ fn.call(elem, function() {
+ jQuery.dequeue(elem, type);
+ });
+ }
+
+ if ( !queue.length ) {
+ jQuery.removeData( elem, type + "queue", true );
+ handleQueueMarkDefer( elem, type, "queue" );
+ }
+ }
+});
+
+jQuery.fn.extend({
+ queue: function( type, data ) {
+ if ( typeof type !== "string" ) {
+ data = type;
+ type = "fx";
+ }
+
+ if ( data === undefined ) {
+ return jQuery.queue( this[0], type );
+ }
+ return this.each(function() {
+ var queue = jQuery.queue( this, type, data );
+
+ if ( type === "fx" && queue[0] !== "inprogress" ) {
+ jQuery.dequeue( this, type );
+ }
+ });
+ },
+ dequeue: function( type ) {
+ return this.each(function() {
+ jQuery.dequeue( this, type );
+ });
+ },
+ // Based off of the plugin by Clint Helfers, with permission.
+ // http://blindsignals.com/index.php/2009/07/jquery-delay/
+ delay: function( time, type ) {
+ time = jQuery.fx ? jQuery.fx.speeds[time] || time : time;
+ type = type || "fx";
+
+ return this.queue( type, function() {
+ var elem = this;
+ setTimeout(function() {
+ jQuery.dequeue( elem, type );
+ }, time );
+ });
+ },
+ clearQueue: function( type ) {
+ return this.queue( type || "fx", [] );
+ },
+ // Get a promise resolved when queues of a certain type
+ // are emptied (fx is the type by default)
+ promise: function( type, object ) {
+ if ( typeof type !== "string" ) {
+ object = type;
+ type = undefined;
+ }
+ type = type || "fx";
+ var defer = jQuery.Deferred(),
+ elements = this,
+ i = elements.length,
+ count = 1,
+ deferDataKey = type + "defer",
+ queueDataKey = type + "queue",
+ markDataKey = type + "mark",
+ tmp;
+ function resolve() {
+ if ( !( --count ) ) {
+ defer.resolveWith( elements, [ elements ] );
+ }
+ }
+ while( i-- ) {
+ if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) ||
+ ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) ||
+ jQuery.data( elements[ i ], markDataKey, undefined, true ) ) &&
+ jQuery.data( elements[ i ], deferDataKey, jQuery._Deferred(), true ) )) {
+ count++;
+ tmp.done( resolve );
+ }
+ }
+ resolve();
+ return defer.promise();
+ }
+});
+
+
+
+
+var rclass = /[\n\t\r]/g,
+ rspace = /\s+/,
+ rreturn = /\r/g,
+ rtype = /^(?:button|input)$/i,
+ rfocusable = /^(?:button|input|object|select|textarea)$/i,
+ rclickable = /^a(?:rea)?$/i,
+ rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,
+ rinvalidChar = /\:|^on/,
+ formHook, boolHook;
+
+jQuery.fn.extend({
+ attr: function( name, value ) {
+ return jQuery.access( this, name, value, true, jQuery.attr );
+ },
+
+ removeAttr: function( name ) {
+ return this.each(function() {
+ jQuery.removeAttr( this, name );
+ });
+ },
+
+ prop: function( name, value ) {
+ return jQuery.access( this, name, value, true, jQuery.prop );
+ },
+
+ removeProp: function( name ) {
+ name = jQuery.propFix[ name ] || name;
+ return this.each(function() {
+ // try/catch handles cases where IE balks (such as removing a property on window)
+ try {
+ this[ name ] = undefined;
+ delete this[ name ];
+ } catch( e ) {}
+ });
+ },
+
+ addClass: function( value ) {
+ var classNames, i, l, elem,
+ setClass, c, cl;
+
+ if ( jQuery.isFunction( value ) ) {
+ return this.each(function( j ) {
+ jQuery( this ).addClass( value.call(this, j, this.className) );
+ });
+ }
+
+ if ( value && typeof value === "string" ) {
+ classNames = value.split( rspace );
+
+ for ( i = 0, l = this.length; i < l; i++ ) {
+ elem = this[ i ];
+
+ if ( elem.nodeType === 1 ) {
+ if ( !elem.className && classNames.length === 1 ) {
+ elem.className = value;
+
+ } else {
+ setClass = " " + elem.className + " ";
+
+ for ( c = 0, cl = classNames.length; c < cl; c++ ) {
+ if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) {
+ setClass += classNames[ c ] + " ";
+ }
+ }
+ elem.className = jQuery.trim( setClass );
+ }
+ }
+ }
+ }
+
+ return this;
+ },
+
+ removeClass: function( value ) {
+ var classNames, i, l, elem, className, c, cl;
+
+ if ( jQuery.isFunction( value ) ) {
+ return this.each(function( j ) {
+ jQuery( this ).removeClass( value.call(this, j, this.className) );
+ });
+ }
+
+ if ( (value && typeof value === "string") || value === undefined ) {
+ classNames = (value || "").split( rspace );
+
+ for ( i = 0, l = this.length; i < l; i++ ) {
+ elem = this[ i ];
+
+ if ( elem.nodeType === 1 && elem.className ) {
+ if ( value ) {
+ className = (" " + elem.className + " ").replace( rclass, " " );
+ for ( c = 0, cl = classNames.length; c < cl; c++ ) {
+ className = className.replace(" " + classNames[ c ] + " ", " ");
+ }
+ elem.className = jQuery.trim( className );
+
+ } else {
+ elem.className = "";
+ }
+ }
+ }
+ }
+
+ return this;
+ },
+
+ toggleClass: function( value, stateVal ) {
+ var type = typeof value,
+ isBool = typeof stateVal === "boolean";
+
+ if ( jQuery.isFunction( value ) ) {
+ return this.each(function( i ) {
+ jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );
+ });
+ }
+
+ return this.each(function() {
+ if ( type === "string" ) {
+ // toggle individual class names
+ var className,
+ i = 0,
+ self = jQuery( this ),
+ state = stateVal,
+ classNames = value.split( rspace );
+
+ while ( (className = classNames[ i++ ]) ) {
+ // check each className given, space seperated list
+ state = isBool ? state : !self.hasClass( className );
+ self[ state ? "addClass" : "removeClass" ]( className );
+ }
+
+ } else if ( type === "undefined" || type === "boolean" ) {
+ if ( this.className ) {
+ // store className if set
+ jQuery._data( this, "__className__", this.className );
+ }
+
+ // toggle whole className
+ this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || "";
+ }
+ });
+ },
+
+ hasClass: function( selector ) {
+ var className = " " + selector + " ";
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ val: function( value ) {
+ var hooks, ret,
+ elem = this[0];
+
+ if ( !arguments.length ) {
+ if ( elem ) {
+ hooks = jQuery.valHooks[ elem.nodeName.toLowerCase() ] || jQuery.valHooks[ elem.type ];
+
+ if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) {
+ return ret;
+ }
+
+ ret = elem.value;
+
+ return typeof ret === "string" ?
+ // handle most common string cases
+ ret.replace(rreturn, "") :
+ // handle cases where value is null/undef or number
+ ret == null ? "" : ret;
+ }
+
+ return undefined;
+ }
+
+ var isFunction = jQuery.isFunction( value );
+
+ return this.each(function( i ) {
+ var self = jQuery(this), val;
+
+ if ( this.nodeType !== 1 ) {
+ return;
+ }
+
+ if ( isFunction ) {
+ val = value.call( this, i, self.val() );
+ } else {
+ val = value;
+ }
+
+ // Treat null/undefined as ""; convert numbers to string
+ if ( val == null ) {
+ val = "";
+ } else if ( typeof val === "number" ) {
+ val += "";
+ } else if ( jQuery.isArray( val ) ) {
+ val = jQuery.map(val, function ( value ) {
+ return value == null ? "" : value + "";
+ });
+ }
+
+ hooks = jQuery.valHooks[ this.nodeName.toLowerCase() ] || jQuery.valHooks[ this.type ];
+
+ // If set returns undefined, fall back to normal setting
+ if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) {
+ this.value = val;
+ }
+ });
+ }
+});
+
+jQuery.extend({
+ valHooks: {
+ option: {
+ get: function( elem ) {
+ // attributes.value is undefined in Blackberry 4.7 but
+ // uses .value. See #6932
+ var val = elem.attributes.value;
+ return !val || val.specified ? elem.value : elem.text;
+ }
+ },
+ select: {
+ get: function( elem ) {
+ var value,
+ index = elem.selectedIndex,
+ values = [],
+ options = elem.options,
+ one = elem.type === "select-one";
+
+ // Nothing was selected
+ if ( index < 0 ) {
+ return null;
+ }
+
+ // Loop through all the selected options
+ for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) {
+ var option = options[ i ];
+
+ // Don't return options that are disabled or in a disabled optgroup
+ if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) &&
+ (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) {
+
+ // Get the specific value for the option
+ value = jQuery( option ).val();
+
+ // We don't need an array for one selects
+ if ( one ) {
+ return value;
+ }
+
+ // Multi-Selects return an array
+ values.push( value );
+ }
+ }
+
+ // Fixes Bug #2551 -- select.val() broken in IE after form.reset()
+ if ( one && !values.length && options.length ) {
+ return jQuery( options[ index ] ).val();
+ }
+
+ return values;
+ },
+
+ set: function( elem, value ) {
+ var values = jQuery.makeArray( value );
+
+ jQuery(elem).find("option").each(function() {
+ this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;
+ });
+
+ if ( !values.length ) {
+ elem.selectedIndex = -1;
+ }
+ return values;
+ }
+ }
+ },
+
+ attrFn: {
+ val: true,
+ css: true,
+ html: true,
+ text: true,
+ data: true,
+ width: true,
+ height: true,
+ offset: true
+ },
+
+ attrFix: {
+ // Always normalize to ensure hook usage
+ tabindex: "tabIndex"
+ },
+
+ attr: function( elem, name, value, pass ) {
+ var nType = elem.nodeType;
+
+ // don't get/set attributes on text, comment and attribute nodes
+ if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+ return undefined;
+ }
+
+ if ( pass && name in jQuery.attrFn ) {
+ return jQuery( elem )[ name ]( value );
+ }
+
+ // Fallback to prop when attributes are not supported
+ if ( !("getAttribute" in elem) ) {
+ return jQuery.prop( elem, name, value );
+ }
+
+ var ret, hooks,
+ notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
+
+ // Normalize the name if needed
+ if ( notxml ) {
+ name = jQuery.attrFix[ name ] || name;
+
+ hooks = jQuery.attrHooks[ name ];
+
+ if ( !hooks ) {
+ // Use boolHook for boolean attributes
+ if ( rboolean.test( name ) ) {
+
+ hooks = boolHook;
+
+ // Use formHook for forms and if the name contains certain characters
+ } else if ( formHook && name !== "className" &&
+ (jQuery.nodeName( elem, "form" ) || rinvalidChar.test( name )) ) {
+
+ hooks = formHook;
+ }
+ }
+ }
+
+ if ( value !== undefined ) {
+
+ if ( value === null ) {
+ jQuery.removeAttr( elem, name );
+ return undefined;
+
+ } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) {
+ return ret;
+
+ } else {
+ elem.setAttribute( name, "" + value );
+ return value;
+ }
+
+ } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) {
+ return ret;
+
+ } else {
+
+ ret = elem.getAttribute( name );
+
+ // Non-existent attributes return null, we normalize to undefined
+ return ret === null ?
+ undefined :
+ ret;
+ }
+ },
+
+ removeAttr: function( elem, name ) {
+ var propName;
+ if ( elem.nodeType === 1 ) {
+ name = jQuery.attrFix[ name ] || name;
+
+ if ( jQuery.support.getSetAttribute ) {
+ // Use removeAttribute in browsers that support it
+ elem.removeAttribute( name );
+ } else {
+ jQuery.attr( elem, name, "" );
+ elem.removeAttributeNode( elem.getAttributeNode( name ) );
+ }
+
+ // Set corresponding property to false for boolean attributes
+ if ( rboolean.test( name ) && (propName = jQuery.propFix[ name ] || name) in elem ) {
+ elem[ propName ] = false;
+ }
+ }
+ },
+
+ attrHooks: {
+ type: {
+ set: function( elem, value ) {
+ // We can't allow the type property to be changed (since it causes problems in IE)
+ if ( rtype.test( elem.nodeName ) && elem.parentNode ) {
+ jQuery.error( "type property can't be changed" );
+ } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) {
+ // Setting the type on a radio button after the value resets the value in IE6-9
+ // Reset value to it's default in case type is set after value
+ // This is for element creation
+ var val = elem.value;
+ elem.setAttribute( "type", value );
+ if ( val ) {
+ elem.value = val;
+ }
+ return value;
+ }
+ }
+ },
+ tabIndex: {
+ get: function( elem ) {
+ // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
+ // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
+ var attributeNode = elem.getAttributeNode("tabIndex");
+
+ return attributeNode && attributeNode.specified ?
+ parseInt( attributeNode.value, 10 ) :
+ rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?
+ 0 :
+ undefined;
+ }
+ },
+ // Use the value property for back compat
+ // Use the formHook for button elements in IE6/7 (#1954)
+ value: {
+ get: function( elem, name ) {
+ if ( formHook && jQuery.nodeName( elem, "button" ) ) {
+ return formHook.get( elem, name );
+ }
+ return name in elem ?
+ elem.value :
+ null;
+ },
+ set: function( elem, value, name ) {
+ if ( formHook && jQuery.nodeName( elem, "button" ) ) {
+ return formHook.set( elem, value, name );
+ }
+ // Does not return so that setAttribute is also used
+ elem.value = value;
+ }
+ }
+ },
+
+ propFix: {
+ tabindex: "tabIndex",
+ readonly: "readOnly",
+ "for": "htmlFor",
+ "class": "className",
+ maxlength: "maxLength",
+ cellspacing: "cellSpacing",
+ cellpadding: "cellPadding",
+ rowspan: "rowSpan",
+ colspan: "colSpan",
+ usemap: "useMap",
+ frameborder: "frameBorder",
+ contenteditable: "contentEditable"
+ },
+
+ prop: function( elem, name, value ) {
+ var nType = elem.nodeType;
+
+ // don't get/set properties on text, comment and attribute nodes
+ if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
+ return undefined;
+ }
+
+ var ret, hooks,
+ notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
+
+ if ( notxml ) {
+ // Fix name and attach hooks
+ name = jQuery.propFix[ name ] || name;
+ hooks = jQuery.propHooks[ name ];
+ }
+
+ if ( value !== undefined ) {
+ if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
+ return ret;
+
+ } else {
+ return (elem[ name ] = value);
+ }
+
+ } else {
+ if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== undefined ) {
+ return ret;
+
+ } else {
+ return elem[ name ];
+ }
+ }
+ },
+
+ propHooks: {}
+});
+
+// Hook for boolean attributes
+boolHook = {
+ get: function( elem, name ) {
+ // Align boolean attributes with corresponding properties
+ return jQuery.prop( elem, name ) ?
+ name.toLowerCase() :
+ undefined;
+ },
+ set: function( elem, value, name ) {
+ var propName;
+ if ( value === false ) {
+ // Remove boolean attributes when set to false
+ jQuery.removeAttr( elem, name );
+ } else {
+ // value is true since we know at this point it's type boolean and not false
+ // Set boolean attributes to the same name and set the DOM property
+ propName = jQuery.propFix[ name ] || name;
+ if ( propName in elem ) {
+ // Only set the IDL specifically if it already exists on the element
+ elem[ propName ] = true;
+ }
+
+ elem.setAttribute( name, name.toLowerCase() );
+ }
+ return name;
+ }
+};
+
+// IE6/7 do not support getting/setting some attributes with get/setAttribute
+if ( !jQuery.support.getSetAttribute ) {
+
+ // propFix is more comprehensive and contains all fixes
+ jQuery.attrFix = jQuery.propFix;
+
+ // Use this for any attribute on a form in IE6/7
+ formHook = jQuery.attrHooks.name = jQuery.attrHooks.title = jQuery.valHooks.button = {
+ get: function( elem, name ) {
+ var ret;
+ ret = elem.getAttributeNode( name );
+ // Return undefined if nodeValue is empty string
+ return ret && ret.nodeValue !== "" ?
+ ret.nodeValue :
+ undefined;
+ },
+ set: function( elem, value, name ) {
+ // Check form objects in IE (multiple bugs related)
+ // Only use nodeValue if the attribute node exists on the form
+ var ret = elem.getAttributeNode( name );
+ if ( ret ) {
+ ret.nodeValue = value;
+ return value;
+ }
+ }
+ };
+
+ // Set width and height to auto instead of 0 on empty string( Bug #8150 )
+ // This is for removals
+ jQuery.each([ "width", "height" ], function( i, name ) {
+ jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
+ set: function( elem, value ) {
+ if ( value === "" ) {
+ elem.setAttribute( name, "auto" );
+ return value;
+ }
+ }
+ });
+ });
+}
+
+
+// Some attributes require a special call on IE
+if ( !jQuery.support.hrefNormalized ) {
+ jQuery.each([ "href", "src", "width", "height" ], function( i, name ) {
+ jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {
+ get: function( elem ) {
+ var ret = elem.getAttribute( name, 2 );
+ return ret === null ? undefined : ret;
+ }
+ });
+ });
+}
+
+if ( !jQuery.support.style ) {
+ jQuery.attrHooks.style = {
+ get: function( elem ) {
+ // Return undefined in the case of empty string
+ // Normalize to lowercase since IE uppercases css property names
+ return elem.style.cssText.toLowerCase() || undefined;
+ },
+ set: function( elem, value ) {
+ return (elem.style.cssText = "" + value);
+ }
+ };
+}
+
+// Safari mis-reports the default selected property of an option
+// Accessing the parent's selectedIndex property fixes it
+if ( !jQuery.support.optSelected ) {
+ jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, {
+ get: function( elem ) {
+ var parent = elem.parentNode;
+
+ if ( parent ) {
+ parent.selectedIndex;
+
+ // Make sure that it also works with optgroups, see #5701
+ if ( parent.parentNode ) {
+ parent.parentNode.selectedIndex;
+ }
+ }
+ }
+ });
+}
+
+// Radios and checkboxes getter/setter
+if ( !jQuery.support.checkOn ) {
+ jQuery.each([ "radio", "checkbox" ], function() {
+ jQuery.valHooks[ this ] = {
+ get: function( elem ) {
+ // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified
+ return elem.getAttribute("value") === null ? "on" : elem.value;
+ }
+ };
+ });
+}
+jQuery.each([ "radio", "checkbox" ], function() {
+ jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], {
+ set: function( elem, value ) {
+ if ( jQuery.isArray( value ) ) {
+ return (elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0);
+ }
+ }
+ });
+});
+
+
+
+
+var rnamespaces = /\.(.*)$/,
+ rformElems = /^(?:textarea|input|select)$/i,
+ rperiod = /\./g,
+ rspaces = / /g,
+ rescape = /[^\w\s.|`]/g,
+ fcleanup = function( nm ) {
+ return nm.replace(rescape, "\\$&");
+ };
+
+/*
+ * A number of helper functions used for managing events.
+ * Many of the ideas behind this code originated from
+ * Dean Edwards' addEvent library.
+ */
+jQuery.event = {
+
+ // Bind an event to an element
+ // Original by Dean Edwards
+ add: function( elem, types, handler, data ) {
+ if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+ return;
+ }
+
+ if ( handler === false ) {
+ handler = returnFalse;
+ } else if ( !handler ) {
+ // Fixes bug #7229. Fix recommended by jdalton
+ return;
+ }
+
+ var handleObjIn, handleObj;
+
+ if ( handler.handler ) {
+ handleObjIn = handler;
+ handler = handleObjIn.handler;
+ }
+
+ // Make sure that the function being executed has a unique ID
+ if ( !handler.guid ) {
+ handler.guid = jQuery.guid++;
+ }
+
+ // Init the element's event structure
+ var elemData = jQuery._data( elem );
+
+ // If no elemData is found then we must be trying to bind to one of the
+ // banned noData elements
+ if ( !elemData ) {
+ return;
+ }
+
+ var events = elemData.events,
+ eventHandle = elemData.handle;
+
+ if ( !events ) {
+ elemData.events = events = {};
+ }
+
+ if ( !eventHandle ) {
+ elemData.handle = eventHandle = function( e ) {
+ // Discard the second event of a jQuery.event.trigger() and
+ // when an event is called after a page has unloaded
+ return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ?
+ jQuery.event.handle.apply( eventHandle.elem, arguments ) :
+ undefined;
+ };
+ }
+
+ // Add elem as a property of the handle function
+ // This is to prevent a memory leak with non-native events in IE.
+ eventHandle.elem = elem;
+
+ // Handle multiple events separated by a space
+ // jQuery(...).bind("mouseover mouseout", fn);
+ types = types.split(" ");
+
+ var type, i = 0, namespaces;
+
+ while ( (type = types[ i++ ]) ) {
+ handleObj = handleObjIn ?
+ jQuery.extend({}, handleObjIn) :
+ { handler: handler, data: data };
+
+ // Namespaced event handlers
+ if ( type.indexOf(".") > -1 ) {
+ namespaces = type.split(".");
+ type = namespaces.shift();
+ handleObj.namespace = namespaces.slice(0).sort().join(".");
+
+ } else {
+ namespaces = [];
+ handleObj.namespace = "";
+ }
+
+ handleObj.type = type;
+ if ( !handleObj.guid ) {
+ handleObj.guid = handler.guid;
+ }
+
+ // Get the current list of functions bound to this event
+ var handlers = events[ type ],
+ special = jQuery.event.special[ type ] || {};
+
+ // Init the event handler queue
+ if ( !handlers ) {
+ handlers = events[ type ] = [];
+
+ // Check for a special event handler
+ // Only use addEventListener/attachEvent if the special
+ // events handler returns false
+ if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
+ // Bind the global event handler to the element
+ if ( elem.addEventListener ) {
+ elem.addEventListener( type, eventHandle, false );
+
+ } else if ( elem.attachEvent ) {
+ elem.attachEvent( "on" + type, eventHandle );
+ }
+ }
+ }
+
+ if ( special.add ) {
+ special.add.call( elem, handleObj );
+
+ if ( !handleObj.handler.guid ) {
+ handleObj.handler.guid = handler.guid;
+ }
+ }
+
+ // Add the function to the element's handler list
+ handlers.push( handleObj );
+
+ // Keep track of which events have been used, for event optimization
+ jQuery.event.global[ type ] = true;
+ }
+
+ // Nullify elem to prevent memory leaks in IE
+ elem = null;
+ },
+
+ global: {},
+
+ // Detach an event or set of events from an element
+ remove: function( elem, types, handler, pos ) {
+ // don't do events on text and comment nodes
+ if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+ return;
+ }
+
+ if ( handler === false ) {
+ handler = returnFalse;
+ }
+
+ var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType,
+ elemData = jQuery.hasData( elem ) && jQuery._data( elem ),
+ events = elemData && elemData.events;
+
+ if ( !elemData || !events ) {
+ return;
+ }
+
+ // types is actually an event object here
+ if ( types && types.type ) {
+ handler = types.handler;
+ types = types.type;
+ }
+
+ // Unbind all events for the element
+ if ( !types || typeof types === "string" && types.charAt(0) === "." ) {
+ types = types || "";
+
+ for ( type in events ) {
+ jQuery.event.remove( elem, type + types );
+ }
+
+ return;
+ }
+
+ // Handle multiple events separated by a space
+ // jQuery(...).unbind("mouseover mouseout", fn);
+ types = types.split(" ");
+
+ while ( (type = types[ i++ ]) ) {
+ origType = type;
+ handleObj = null;
+ all = type.indexOf(".") < 0;
+ namespaces = [];
+
+ if ( !all ) {
+ // Namespaced event handlers
+ namespaces = type.split(".");
+ type = namespaces.shift();
+
+ namespace = new RegExp("(^|\\.)" +
+ jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)");
+ }
+
+ eventType = events[ type ];
+
+ if ( !eventType ) {
+ continue;
+ }
+
+ if ( !handler ) {
+ for ( j = 0; j < eventType.length; j++ ) {
+ handleObj = eventType[ j ];
+
+ if ( all || namespace.test( handleObj.namespace ) ) {
+ jQuery.event.remove( elem, origType, handleObj.handler, j );
+ eventType.splice( j--, 1 );
+ }
+ }
+
+ continue;
+ }
+
+ special = jQuery.event.special[ type ] || {};
+
+ for ( j = pos || 0; j < eventType.length; j++ ) {
+ handleObj = eventType[ j ];
+
+ if ( handler.guid === handleObj.guid ) {
+ // remove the given handler for the given type
+ if ( all || namespace.test( handleObj.namespace ) ) {
+ if ( pos == null ) {
+ eventType.splice( j--, 1 );
+ }
+
+ if ( special.remove ) {
+ special.remove.call( elem, handleObj );
+ }
+ }
+
+ if ( pos != null ) {
+ break;
+ }
+ }
+ }
+
+ // remove generic event handler if no more handlers exist
+ if ( eventType.length === 0 || pos != null && eventType.length === 1 ) {
+ if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) {
+ jQuery.removeEvent( elem, type, elemData.handle );
+ }
+
+ ret = null;
+ delete events[ type ];
+ }
+ }
+
+ // Remove the expando if it's no longer used
+ if ( jQuery.isEmptyObject( events ) ) {
+ var handle = elemData.handle;
+ if ( handle ) {
+ handle.elem = null;
+ }
+
+ delete elemData.events;
+ delete elemData.handle;
+
+ if ( jQuery.isEmptyObject( elemData ) ) {
+ jQuery.removeData( elem, undefined, true );
+ }
+ }
+ },
+
+ // Events that are safe to short-circuit if no handlers are attached.
+ // Native DOM events should not be added, they may have inline handlers.
+ customEvent: {
+ "getData": true,
+ "setData": true,
+ "changeData": true
+ },
+
+ trigger: function( event, data, elem, onlyHandlers ) {
+ // Event object or event type
+ var type = event.type || event,
+ namespaces = [],
+ exclusive;
+
+ if ( type.indexOf("!") >= 0 ) {
+ // Exclusive events trigger only for the exact event (no namespaces)
+ type = type.slice(0, -1);
+ exclusive = true;
+ }
+
+ if ( type.indexOf(".") >= 0 ) {
+ // Namespaced trigger; create a regexp to match event type in handle()
+ namespaces = type.split(".");
+ type = namespaces.shift();
+ namespaces.sort();
+ }
+
+ if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) {
+ // No jQuery handlers for this event type, and it can't have inline handlers
+ return;
+ }
+
+ // Caller can pass in an Event, Object, or just an event type string
+ event = typeof event === "object" ?
+ // jQuery.Event object
+ event[ jQuery.expando ] ? event :
+ // Object literal
+ new jQuery.Event( type, event ) :
+ // Just the event type (string)
+ new jQuery.Event( type );
+
+ event.type = type;
+ event.exclusive = exclusive;
+ event.namespace = namespaces.join(".");
+ event.namespace_re = new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)");
+
+ // triggerHandler() and global events don't bubble or run the default action
+ if ( onlyHandlers || !elem ) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ // Handle a global trigger
+ if ( !elem ) {
+ // TODO: Stop taunting the data cache; remove global events and always attach to document
+ jQuery.each( jQuery.cache, function() {
+ // internalKey variable is just used to make it easier to find
+ // and potentially change this stuff later; currently it just
+ // points to jQuery.expando
+ var internalKey = jQuery.expando,
+ internalCache = this[ internalKey ];
+ if ( internalCache && internalCache.events && internalCache.events[ type ] ) {
+ jQuery.event.trigger( event, data, internalCache.handle.elem );
+ }
+ });
+ return;
+ }
+
+ // Don't do events on text and comment nodes
+ if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
+ return;
+ }
+
+ // Clean up the event in case it is being reused
+ event.result = undefined;
+ event.target = elem;
+
+ // Clone any incoming data and prepend the event, creating the handler arg list
+ data = data != null ? jQuery.makeArray( data ) : [];
+ data.unshift( event );
+
+ var cur = elem,
+ // IE doesn't like method names with a colon (#3533, #8272)
+ ontype = type.indexOf(":") < 0 ? "on" + type : "";
+
+ // Fire event on the current element, then bubble up the DOM tree
+ do {
+ var handle = jQuery._data( cur, "handle" );
+
+ event.currentTarget = cur;
+ if ( handle ) {
+ handle.apply( cur, data );
+ }
+
+ // Trigger an inline bound script
+ if ( ontype && jQuery.acceptData( cur ) && cur[ ontype ] && cur[ ontype ].apply( cur, data ) === false ) {
+ event.result = false;
+ event.preventDefault();
+ }
+
+ // Bubble up to document, then to window
+ cur = cur.parentNode || cur.ownerDocument || cur === event.target.ownerDocument && window;
+ } while ( cur && !event.isPropagationStopped() );
+
+ // If nobody prevented the default action, do it now
+ if ( !event.isDefaultPrevented() ) {
+ var old,
+ special = jQuery.event.special[ type ] || {};
+
+ if ( (!special._default || special._default.call( elem.ownerDocument, event ) === false) &&
+ !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) {
+
+ // Call a native DOM method on the target with the same name name as the event.
+ // Can't use an .isFunction)() check here because IE6/7 fails that test.
+ // IE<9 dies on focus to hidden element (#1486), may want to revisit a try/catch.
+ try {
+ if ( ontype && elem[ type ] ) {
+ // Don't re-trigger an onFOO event when we call its FOO() method
+ old = elem[ ontype ];
+
+ if ( old ) {
+ elem[ ontype ] = null;
+ }
+
+ jQuery.event.triggered = type;
+ elem[ type ]();
+ }
+ } catch ( ieError ) {}
+
+ if ( old ) {
+ elem[ ontype ] = old;
+ }
+
+ jQuery.event.triggered = undefined;
+ }
+ }
+
+ return event.result;
+ },
+
+ handle: function( event ) {
+ event = jQuery.event.fix( event || window.event );
+ // Snapshot the handlers list since a called handler may add/remove events.
+ var handlers = ((jQuery._data( this, "events" ) || {})[ event.type ] || []).slice(0),
+ run_all = !event.exclusive && !event.namespace,
+ args = Array.prototype.slice.call( arguments, 0 );
+
+ // Use the fix-ed Event rather than the (read-only) native event
+ args[0] = event;
+ event.currentTarget = this;
+
+ for ( var j = 0, l = handlers.length; j < l; j++ ) {
+ var handleObj = handlers[ j ];
+
+ // Triggered event must 1) be non-exclusive and have no namespace, or
+ // 2) have namespace(s) a subset or equal to those in the bound event.
+ if ( run_all || event.namespace_re.test( handleObj.namespace ) ) {
+ // Pass in a reference to the handler function itself
+ // So that we can later remove it
+ event.handler = handleObj.handler;
+ event.data = handleObj.data;
+ event.handleObj = handleObj;
+
+ var ret = handleObj.handler.apply( this, args );
+
+ if ( ret !== undefined ) {
+ event.result = ret;
+ if ( ret === false ) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ if ( event.isImmediatePropagationStopped() ) {
+ break;
+ }
+ }
+ }
+ return event.result;
+ },
+
+ props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
+
+ fix: function( event ) {
+ if ( event[ jQuery.expando ] ) {
+ return event;
+ }
+
+ // store a copy of the original event object
+ // and "clone" to set read-only properties
+ var originalEvent = event;
+ event = jQuery.Event( originalEvent );
+
+ for ( var i = this.props.length, prop; i; ) {
+ prop = this.props[ --i ];
+ event[ prop ] = originalEvent[ prop ];
+ }
+
+ // Fix target property, if necessary
+ if ( !event.target ) {
+ // Fixes #1925 where srcElement might not be defined either
+ event.target = event.srcElement || document;
+ }
+
+ // check if target is a textnode (safari)
+ if ( event.target.nodeType === 3 ) {
+ event.target = event.target.parentNode;
+ }
+
+ // Add relatedTarget, if necessary
+ if ( !event.relatedTarget && event.fromElement ) {
+ event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement;
+ }
+
+ // Calculate pageX/Y if missing and clientX/Y available
+ if ( event.pageX == null && event.clientX != null ) {
+ var eventDocument = event.target.ownerDocument || document,
+ doc = eventDocument.documentElement,
+ body = eventDocument.body;
+
+ event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0);
+ event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0);
+ }
+
+ // Add which for key events
+ if ( event.which == null && (event.charCode != null || event.keyCode != null) ) {
+ event.which = event.charCode != null ? event.charCode : event.keyCode;
+ }
+
+ // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs)
+ if ( !event.metaKey && event.ctrlKey ) {
+ event.metaKey = event.ctrlKey;
+ }
+
+ // Add which for click: 1 === left; 2 === middle; 3 === right
+ // Note: button is not normalized, so don't use it
+ if ( !event.which && event.button !== undefined ) {
+ event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) ));
+ }
+
+ return event;
+ },
+
+ // Deprecated, use jQuery.guid instead
+ guid: 1E8,
+
+ // Deprecated, use jQuery.proxy instead
+ proxy: jQuery.proxy,
+
+ special: {
+ ready: {
+ // Make sure the ready event is setup
+ setup: jQuery.bindReady,
+ teardown: jQuery.noop
+ },
+
+ live: {
+ add: function( handleObj ) {
+ jQuery.event.add( this,
+ liveConvert( handleObj.origType, handleObj.selector ),
+ jQuery.extend({}, handleObj, {handler: liveHandler, guid: handleObj.handler.guid}) );
+ },
+
+ remove: function( handleObj ) {
+ jQuery.event.remove( this, liveConvert( handleObj.origType, handleObj.selector ), handleObj );
+ }
+ },
+
+ beforeunload: {
+ setup: function( data, namespaces, eventHandle ) {
+ // We only want to do this special case on windows
+ if ( jQuery.isWindow( this ) ) {
+ this.onbeforeunload = eventHandle;
+ }
+ },
+
+ teardown: function( namespaces, eventHandle ) {
+ if ( this.onbeforeunload === eventHandle ) {
+ this.onbeforeunload = null;
+ }
+ }
+ }
+ }
+};
+
+jQuery.removeEvent = document.removeEventListener ?
+ function( elem, type, handle ) {
+ if ( elem.removeEventListener ) {
+ elem.removeEventListener( type, handle, false );
+ }
+ } :
+ function( elem, type, handle ) {
+ if ( elem.detachEvent ) {
+ elem.detachEvent( "on" + type, handle );
+ }
+ };
+
+jQuery.Event = function( src, props ) {
+ // Allow instantiation without the 'new' keyword
+ if ( !this.preventDefault ) {
+ return new jQuery.Event( src, props );
+ }
+
+ // Event object
+ if ( src && src.type ) {
+ this.originalEvent = src;
+ this.type = src.type;
+
+ // Events bubbling up the document may have been marked as prevented
+ // by a handler lower down the tree; reflect the correct value.
+ this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false ||
+ src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse;
+
+ // Event type
+ } else {
+ this.type = src;
+ }
+
+ // Put explicitly provided properties onto the event object
+ if ( props ) {
+ jQuery.extend( this, props );
+ }
+
+ // timeStamp is buggy for some events on Firefox(#3843)
+ // So we won't rely on the native value
+ this.timeStamp = jQuery.now();
+
+ // Mark it as fixed
+ this[ jQuery.expando ] = true;
+};
+
+function returnFalse() {
+ return false;
+}
+function returnTrue() {
+ return true;
+}
+
+// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
+// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
+jQuery.Event.prototype = {
+ preventDefault: function() {
+ this.isDefaultPrevented = returnTrue;
+
+ var e = this.originalEvent;
+ if ( !e ) {
+ return;
+ }
+
+ // if preventDefault exists run it on the original event
+ if ( e.preventDefault ) {
+ e.preventDefault();
+
+ // otherwise set the returnValue property of the original event to false (IE)
+ } else {
+ e.returnValue = false;
+ }
+ },
+ stopPropagation: function() {
+ this.isPropagationStopped = returnTrue;
+
+ var e = this.originalEvent;
+ if ( !e ) {
+ return;
+ }
+ // if stopPropagation exists run it on the original event
+ if ( e.stopPropagation ) {
+ e.stopPropagation();
+ }
+ // otherwise set the cancelBubble property of the original event to true (IE)
+ e.cancelBubble = true;
+ },
+ stopImmediatePropagation: function() {
+ this.isImmediatePropagationStopped = returnTrue;
+ this.stopPropagation();
+ },
+ isDefaultPrevented: returnFalse,
+ isPropagationStopped: returnFalse,
+ isImmediatePropagationStopped: returnFalse
+};
+
+// Checks if an event happened on an element within another element
+// Used in jQuery.event.special.mouseenter and mouseleave handlers
+var withinElement = function( event ) {
+
+ // Check if mouse(over|out) are still within the same parent element
+ var related = event.relatedTarget,
+ inside = false,
+ eventType = event.type;
+
+ event.type = event.data;
+
+ if ( related !== this ) {
+
+ if ( related ) {
+ inside = jQuery.contains( this, related );
+ }
+
+ if ( !inside ) {
+
+ jQuery.event.handle.apply( this, arguments );
+
+ event.type = eventType;
+ }
+ }
+},
+
+// In case of event delegation, we only need to rename the event.type,
+// liveHandler will take care of the rest.
+delegate = function( event ) {
+ event.type = event.data;
+ jQuery.event.handle.apply( this, arguments );
+};
+
+// Create mouseenter and mouseleave events
+jQuery.each({
+ mouseenter: "mouseover",
+ mouseleave: "mouseout"
+}, function( orig, fix ) {
+ jQuery.event.special[ orig ] = {
+ setup: function( data ) {
+ jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig );
+ },
+ teardown: function( data ) {
+ jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement );
+ }
+ };
+});
+
+// submit delegation
+if ( !jQuery.support.submitBubbles ) {
+
+ jQuery.event.special.submit = {
+ setup: function( data, namespaces ) {
+ if ( !jQuery.nodeName( this, "form" ) ) {
+ jQuery.event.add(this, "click.specialSubmit", function( e ) {
+ var elem = e.target,
+ type = elem.type;
+
+ if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) {
+ trigger( "submit", this, arguments );
+ }
+ });
+
+ jQuery.event.add(this, "keypress.specialSubmit", function( e ) {
+ var elem = e.target,
+ type = elem.type;
+
+ if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) {
+ trigger( "submit", this, arguments );
+ }
+ });
+
+ } else {
+ return false;
+ }
+ },
+
+ teardown: function( namespaces ) {
+ jQuery.event.remove( this, ".specialSubmit" );
+ }
+ };
+
+}
+
+// change delegation, happens here so we have bind.
+if ( !jQuery.support.changeBubbles ) {
+
+ var changeFilters,
+
+ getVal = function( elem ) {
+ var type = elem.type, val = elem.value;
+
+ if ( type === "radio" || type === "checkbox" ) {
+ val = elem.checked;
+
+ } else if ( type === "select-multiple" ) {
+ val = elem.selectedIndex > -1 ?
+ jQuery.map( elem.options, function( elem ) {
+ return elem.selected;
+ }).join("-") :
+ "";
+
+ } else if ( jQuery.nodeName( elem, "select" ) ) {
+ val = elem.selectedIndex;
+ }
+
+ return val;
+ },
+
+ testChange = function testChange( e ) {
+ var elem = e.target, data, val;
+
+ if ( !rformElems.test( elem.nodeName ) || elem.readOnly ) {
+ return;
+ }
+
+ data = jQuery._data( elem, "_change_data" );
+ val = getVal(elem);
+
+ // the current data will be also retrieved by beforeactivate
+ if ( e.type !== "focusout" || elem.type !== "radio" ) {
+ jQuery._data( elem, "_change_data", val );
+ }
+
+ if ( data === undefined || val === data ) {
+ return;
+ }
+
+ if ( data != null || val ) {
+ e.type = "change";
+ e.liveFired = undefined;
+ jQuery.event.trigger( e, arguments[1], elem );
+ }
+ };
+
+ jQuery.event.special.change = {
+ filters: {
+ focusout: testChange,
+
+ beforedeactivate: testChange,
+
+ click: function( e ) {
+ var elem = e.target, type = jQuery.nodeName( elem, "input" ) ? elem.type : "";
+
+ if ( type === "radio" || type === "checkbox" || jQuery.nodeName( elem, "select" ) ) {
+ testChange.call( this, e );
+ }
+ },
+
+ // Change has to be called before submit
+ // Keydown will be called before keypress, which is used in submit-event delegation
+ keydown: function( e ) {
+ var elem = e.target, type = jQuery.nodeName( elem, "input" ) ? elem.type : "";
+
+ if ( (e.keyCode === 13 && !jQuery.nodeName( elem, "textarea" ) ) ||
+ (e.keyCode === 32 && (type === "checkbox" || type === "radio")) ||
+ type === "select-multiple" ) {
+ testChange.call( this, e );
+ }
+ },
+
+ // Beforeactivate happens also before the previous element is blurred
+ // with this event you can't trigger a change event, but you can store
+ // information
+ beforeactivate: function( e ) {
+ var elem = e.target;
+ jQuery._data( elem, "_change_data", getVal(elem) );
+ }
+ },
+
+ setup: function( data, namespaces ) {
+ if ( this.type === "file" ) {
+ return false;
+ }
+
+ for ( var type in changeFilters ) {
+ jQuery.event.add( this, type + ".specialChange", changeFilters[type] );
+ }
+
+ return rformElems.test( this.nodeName );
+ },
+
+ teardown: function( namespaces ) {
+ jQuery.event.remove( this, ".specialChange" );
+
+ return rformElems.test( this.nodeName );
+ }
+ };
+
+ changeFilters = jQuery.event.special.change.filters;
+
+ // Handle when the input is .focus()'d
+ changeFilters.focus = changeFilters.beforeactivate;
+}
+
+function trigger( type, elem, args ) {
+ // Piggyback on a donor event to simulate a different one.
+ // Fake originalEvent to avoid donor's stopPropagation, but if the
+ // simulated event prevents default then we do the same on the donor.
+ // Don't pass args or remember liveFired; they apply to the donor event.
+ var event = jQuery.extend( {}, args[ 0 ] );
+ event.type = type;
+ event.originalEvent = {};
+ event.liveFired = undefined;
+ jQuery.event.handle.call( elem, event );
+ if ( event.isDefaultPrevented() ) {
+ args[ 0 ].preventDefault();
+ }
+}
+
+// Create "bubbling" focus and blur events
+if ( !jQuery.support.focusinBubbles ) {
+ jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
+
+ // Attach a single capturing handler while someone wants focusin/focusout
+ var attaches = 0;
+
+ jQuery.event.special[ fix ] = {
+ setup: function() {
+ if ( attaches++ === 0 ) {
+ document.addEventListener( orig, handler, true );
+ }
+ },
+ teardown: function() {
+ if ( --attaches === 0 ) {
+ document.removeEventListener( orig, handler, true );
+ }
+ }
+ };
+
+ function handler( donor ) {
+ // Donor event is always a native one; fix it and switch its type.
+ // Let focusin/out handler cancel the donor focus/blur event.
+ var e = jQuery.event.fix( donor );
+ e.type = fix;
+ e.originalEvent = {};
+ jQuery.event.trigger( e, null, e.target );
+ if ( e.isDefaultPrevented() ) {
+ donor.preventDefault();
+ }
+ }
+ });
+}
+
+jQuery.each(["bind", "one"], function( i, name ) {
+ jQuery.fn[ name ] = function( type, data, fn ) {
+ var handler;
+
+ // Handle object literals
+ if ( typeof type === "object" ) {
+ for ( var key in type ) {
+ this[ name ](key, data, type[key], fn);
+ }
+ return this;
+ }
+
+ if ( arguments.length === 2 || data === false ) {
+ fn = data;
+ data = undefined;
+ }
+
+ if ( name === "one" ) {
+ handler = function( event ) {
+ jQuery( this ).unbind( event, handler );
+ return fn.apply( this, arguments );
+ };
+ handler.guid = fn.guid || jQuery.guid++;
+ } else {
+ handler = fn;
+ }
+
+ if ( type === "unload" && name !== "one" ) {
+ this.one( type, data, fn );
+
+ } else {
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ jQuery.event.add( this[i], type, handler, data );
+ }
+ }
+
+ return this;
+ };
+});
+
+jQuery.fn.extend({
+ unbind: function( type, fn ) {
+ // Handle object literals
+ if ( typeof type === "object" && !type.preventDefault ) {
+ for ( var key in type ) {
+ this.unbind(key, type[key]);
+ }
+
+ } else {
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ jQuery.event.remove( this[i], type, fn );
+ }
+ }
+
+ return this;
+ },
+
+ delegate: function( selector, types, data, fn ) {
+ return this.live( types, data, fn, selector );
+ },
+
+ undelegate: function( selector, types, fn ) {
+ if ( arguments.length === 0 ) {
+ return this.unbind( "live" );
+
+ } else {
+ return this.die( types, null, fn, selector );
+ }
+ },
+
+ trigger: function( type, data ) {
+ return this.each(function() {
+ jQuery.event.trigger( type, data, this );
+ });
+ },
+
+ triggerHandler: function( type, data ) {
+ if ( this[0] ) {
+ return jQuery.event.trigger( type, data, this[0], true );
+ }
+ },
+
+ toggle: function( fn ) {
+ // Save reference to arguments for access in closure
+ var args = arguments,
+ guid = fn.guid || jQuery.guid++,
+ i = 0,
+ toggler = function( event ) {
+ // Figure out which function to execute
+ var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i;
+ jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 );
+
+ // Make sure that clicks stop
+ event.preventDefault();
+
+ // and execute the function
+ return args[ lastToggle ].apply( this, arguments ) || false;
+ };
+
+ // link all the functions, so any of them can unbind this click handler
+ toggler.guid = guid;
+ while ( i < args.length ) {
+ args[ i++ ].guid = guid;
+ }
+
+ return this.click( toggler );
+ },
+
+ hover: function( fnOver, fnOut ) {
+ return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
+ }
+});
+
+var liveMap = {
+ focus: "focusin",
+ blur: "focusout",
+ mouseenter: "mouseover",
+ mouseleave: "mouseout"
+};
+
+jQuery.each(["live", "die"], function( i, name ) {
+ jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) {
+ var type, i = 0, match, namespaces, preType,
+ selector = origSelector || this.selector,
+ context = origSelector ? this : jQuery( this.context );
+
+ if ( typeof types === "object" && !types.preventDefault ) {
+ for ( var key in types ) {
+ context[ name ]( key, data, types[key], selector );
+ }
+
+ return this;
+ }
+
+ if ( name === "die" && !types &&
+ origSelector && origSelector.charAt(0) === "." ) {
+
+ context.unbind( origSelector );
+
+ return this;
+ }
+
+ if ( data === false || jQuery.isFunction( data ) ) {
+ fn = data || returnFalse;
+ data = undefined;
+ }
+
+ types = (types || "").split(" ");
+
+ while ( (type = types[ i++ ]) != null ) {
+ match = rnamespaces.exec( type );
+ namespaces = "";
+
+ if ( match ) {
+ namespaces = match[0];
+ type = type.replace( rnamespaces, "" );
+ }
+
+ if ( type === "hover" ) {
+ types.push( "mouseenter" + namespaces, "mouseleave" + namespaces );
+ continue;
+ }
+
+ preType = type;
+
+ if ( liveMap[ type ] ) {
+ types.push( liveMap[ type ] + namespaces );
+ type = type + namespaces;
+
+ } else {
+ type = (liveMap[ type ] || type) + namespaces;
+ }
+
+ if ( name === "live" ) {
+ // bind live handler
+ for ( var j = 0, l = context.length; j < l; j++ ) {
+ jQuery.event.add( context[j], "live." + liveConvert( type, selector ),
+ { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } );
+ }
+
+ } else {
+ // unbind live handler
+ context.unbind( "live." + liveConvert( type, selector ), fn );
+ }
+ }
+
+ return this;
+ };
+});
+
+function liveHandler( event ) {
+ var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret,
+ elems = [],
+ selectors = [],
+ events = jQuery._data( this, "events" );
+
+ // Make sure we avoid non-left-click bubbling in Firefox (#3861) and disabled elements in IE (#6911)
+ if ( event.liveFired === this || !events || !events.live || event.target.disabled || event.button && event.type === "click" ) {
+ return;
+ }
+
+ if ( event.namespace ) {
+ namespace = new RegExp("(^|\\.)" + event.namespace.split(".").join("\\.(?:.*\\.)?") + "(\\.|$)");
+ }
+
+ event.liveFired = this;
+
+ var live = events.live.slice(0);
+
+ for ( j = 0; j < live.length; j++ ) {
+ handleObj = live[j];
+
+ if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) {
+ selectors.push( handleObj.selector );
+
+ } else {
+ live.splice( j--, 1 );
+ }
+ }
+
+ match = jQuery( event.target ).closest( selectors, event.currentTarget );
+
+ for ( i = 0, l = match.length; i < l; i++ ) {
+ close = match[i];
+
+ for ( j = 0; j < live.length; j++ ) {
+ handleObj = live[j];
+
+ if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) && !close.elem.disabled ) {
+ elem = close.elem;
+ related = null;
+
+ // Those two events require additional checking
+ if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) {
+ event.type = handleObj.preType;
+ related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0];
+
+ // Make sure not to accidentally match a child element with the same selector
+ if ( related && jQuery.contains( elem, related ) ) {
+ related = elem;
+ }
+ }
+
+ if ( !related || related !== elem ) {
+ elems.push({ elem: elem, handleObj: handleObj, level: close.level });
+ }
+ }
+ }
+ }
+
+ for ( i = 0, l = elems.length; i < l; i++ ) {
+ match = elems[i];
+
+ if ( maxLevel && match.level > maxLevel ) {
+ break;
+ }
+
+ event.currentTarget = match.elem;
+ event.data = match.handleObj.data;
+ event.handleObj = match.handleObj;
+
+ ret = match.handleObj.origHandler.apply( match.elem, arguments );
+
+ if ( ret === false || event.isPropagationStopped() ) {
+ maxLevel = match.level;
+
+ if ( ret === false ) {
+ stop = false;
+ }
+ if ( event.isImmediatePropagationStopped() ) {
+ break;
+ }
+ }
+ }
+
+ return stop;
+}
+
+function liveConvert( type, selector ) {
+ return (type && type !== "*" ? type + "." : "") + selector.replace(rperiod, "`").replace(rspaces, "&");
+}
+
+jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
+ "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
+ "change select submit keydown keypress keyup error").split(" "), function( i, name ) {
+
+ // Handle event binding
+ jQuery.fn[ name ] = function( data, fn ) {
+ if ( fn == null ) {
+ fn = data;
+ data = null;
+ }
+
+ return arguments.length > 0 ?
+ this.bind( name, data, fn ) :
+ this.trigger( name );
+ };
+
+ if ( jQuery.attrFn ) {
+ jQuery.attrFn[ name ] = true;
+ }
+});
+
+
+
+/*!
+ * Sizzle CSS Selector Engine
+ * Copyright 2011, The Dojo Foundation
+ * Released under the MIT, BSD, and GPL Licenses.
+ * More information: http://sizzlejs.com/
+ */
+(function(){
+
+var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
+ done = 0,
+ toString = Object.prototype.toString,
+ hasDuplicate = false,
+ baseHasDuplicate = true,
+ rBackslash = /\\/g,
+ rNonWord = /\W/;
+
+// Here we check if the JavaScript engine is using some sort of
+// optimization where it does not always call our comparision
+// function. If that is the case, discard the hasDuplicate value.
+// Thus far that includes Google Chrome.
+[0, 0].sort(function() {
+ baseHasDuplicate = false;
+ return 0;
+});
+
+var Sizzle = function( selector, context, results, seed ) {
+ results = results || [];
+ context = context || document;
+
+ var origContext = context;
+
+ if ( context.nodeType !== 1 && context.nodeType !== 9 ) {
+ return [];
+ }
+
+ if ( !selector || typeof selector !== "string" ) {
+ return results;
+ }
+
+ var m, set, checkSet, extra, ret, cur, pop, i,
+ prune = true,
+ contextXML = Sizzle.isXML( context ),
+ parts = [],
+ soFar = selector;
+
+ // Reset the position of the chunker regexp (start from head)
+ do {
+ chunker.exec( "" );
+ m = chunker.exec( soFar );
+
+ if ( m ) {
+ soFar = m[3];
+
+ parts.push( m[1] );
+
+ if ( m[2] ) {
+ extra = m[3];
+ break;
+ }
+ }
+ } while ( m );
+
+ if ( parts.length > 1 && origPOS.exec( selector ) ) {
+
+ if ( parts.length === 2 && Expr.relative[ parts[0] ] ) {
+ set = posProcess( parts[0] + parts[1], context );
+
+ } else {
+ set = Expr.relative[ parts[0] ] ?
+ [ context ] :
+ Sizzle( parts.shift(), context );
+
+ while ( parts.length ) {
+ selector = parts.shift();
+
+ if ( Expr.relative[ selector ] ) {
+ selector += parts.shift();
+ }
+
+ set = posProcess( selector, set );
+ }
+ }
+
+ } else {
+ // Take a shortcut and set the context if the root selector is an ID
+ // (but not if it'll be faster if the inner selector is an ID)
+ if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&
+ Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {
+
+ ret = Sizzle.find( parts.shift(), context, contextXML );
+ context = ret.expr ?
+ Sizzle.filter( ret.expr, ret.set )[0] :
+ ret.set[0];
+ }
+
+ if ( context ) {
+ ret = seed ?
+ { expr: parts.pop(), set: makeArray(seed) } :
+ Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML );
+
+ set = ret.expr ?
+ Sizzle.filter( ret.expr, ret.set ) :
+ ret.set;
+
+ if ( parts.length > 0 ) {
+ checkSet = makeArray( set );
+
+ } else {
+ prune = false;
+ }
+
+ while ( parts.length ) {
+ cur = parts.pop();
+ pop = cur;
+
+ if ( !Expr.relative[ cur ] ) {
+ cur = "";
+ } else {
+ pop = parts.pop();
+ }
+
+ if ( pop == null ) {
+ pop = context;
+ }
+
+ Expr.relative[ cur ]( checkSet, pop, contextXML );
+ }
+
+ } else {
+ checkSet = parts = [];
+ }
+ }
+
+ if ( !checkSet ) {
+ checkSet = set;
+ }
+
+ if ( !checkSet ) {
+ Sizzle.error( cur || selector );
+ }
+
+ if ( toString.call(checkSet) === "[object Array]" ) {
+ if ( !prune ) {
+ results.push.apply( results, checkSet );
+
+ } else if ( context && context.nodeType === 1 ) {
+ for ( i = 0; checkSet[i] != null; i++ ) {
+ if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) {
+ results.push( set[i] );
+ }
+ }
+
+ } else {
+ for ( i = 0; checkSet[i] != null; i++ ) {
+ if ( checkSet[i] && checkSet[i].nodeType === 1 ) {
+ results.push( set[i] );
+ }
+ }
+ }
+
+ } else {
+ makeArray( checkSet, results );
+ }
+
+ if ( extra ) {
+ Sizzle( extra, origContext, results, seed );
+ Sizzle.uniqueSort( results );
+ }
+
+ return results;
+};
+
+Sizzle.uniqueSort = function( results ) {
+ if ( sortOrder ) {
+ hasDuplicate = baseHasDuplicate;
+ results.sort( sortOrder );
+
+ if ( hasDuplicate ) {
+ for ( var i = 1; i < results.length; i++ ) {
+ if ( results[i] === results[ i - 1 ] ) {
+ results.splice( i--, 1 );
+ }
+ }
+ }
+ }
+
+ return results;
+};
+
+Sizzle.matches = function( expr, set ) {
+ return Sizzle( expr, null, null, set );
+};
+
+Sizzle.matchesSelector = function( node, expr ) {
+ return Sizzle( expr, null, null, [node] ).length > 0;
+};
+
+Sizzle.find = function( expr, context, isXML ) {
+ var set;
+
+ if ( !expr ) {
+ return [];
+ }
+
+ for ( var i = 0, l = Expr.order.length; i < l; i++ ) {
+ var match,
+ type = Expr.order[i];
+
+ if ( (match = Expr.leftMatch[ type ].exec( expr )) ) {
+ var left = match[1];
+ match.splice( 1, 1 );
+
+ if ( left.substr( left.length - 1 ) !== "\\" ) {
+ match[1] = (match[1] || "").replace( rBackslash, "" );
+ set = Expr.find[ type ]( match, context, isXML );
+
+ if ( set != null ) {
+ expr = expr.replace( Expr.match[ type ], "" );
+ break;
+ }
+ }
+ }
+ }
+
+ if ( !set ) {
+ set = typeof context.getElementsByTagName !== "undefined" ?
+ context.getElementsByTagName( "*" ) :
+ [];
+ }
+
+ return { set: set, expr: expr };
+};
+
+Sizzle.filter = function( expr, set, inplace, not ) {
+ var match, anyFound,
+ old = expr,
+ result = [],
+ curLoop = set,
+ isXMLFilter = set && set[0] && Sizzle.isXML( set[0] );
+
+ while ( expr && set.length ) {
+ for ( var type in Expr.filter ) {
+ if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {
+ var found, item,
+ filter = Expr.filter[ type ],
+ left = match[1];
+
+ anyFound = false;
+
+ match.splice(1,1);
+
+ if ( left.substr( left.length - 1 ) === "\\" ) {
+ continue;
+ }
+
+ if ( curLoop === result ) {
+ result = [];
+ }
+
+ if ( Expr.preFilter[ type ] ) {
+ match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );
+
+ if ( !match ) {
+ anyFound = found = true;
+
+ } else if ( match === true ) {
+ continue;
+ }
+ }
+
+ if ( match ) {
+ for ( var i = 0; (item = curLoop[i]) != null; i++ ) {
+ if ( item ) {
+ found = filter( item, match, i, curLoop );
+ var pass = not ^ !!found;
+
+ if ( inplace && found != null ) {
+ if ( pass ) {
+ anyFound = true;
+
+ } else {
+ curLoop[i] = false;
+ }
+
+ } else if ( pass ) {
+ result.push( item );
+ anyFound = true;
+ }
+ }
+ }
+ }
+
+ if ( found !== undefined ) {
+ if ( !inplace ) {
+ curLoop = result;
+ }
+
+ expr = expr.replace( Expr.match[ type ], "" );
+
+ if ( !anyFound ) {
+ return [];
+ }
+
+ break;
+ }
+ }
+ }
+
+ // Improper expression
+ if ( expr === old ) {
+ if ( anyFound == null ) {
+ Sizzle.error( expr );
+
+ } else {
+ break;
+ }
+ }
+
+ old = expr;
+ }
+
+ return curLoop;
+};
+
+Sizzle.error = function( msg ) {
+ throw "Syntax error, unrecognized expression: " + msg;
+};
+
+var Expr = Sizzle.selectors = {
+ order: [ "ID", "NAME", "TAG" ],
+
+ match: {
+ ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
+ CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
+ NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,
+ ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,
+ TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,
+ CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,
+ POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,
+ PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/
+ },
+
+ leftMatch: {},
+
+ attrMap: {
+ "class": "className",
+ "for": "htmlFor"
+ },
+
+ attrHandle: {
+ href: function( elem ) {
+ return elem.getAttribute( "href" );
+ },
+ type: function( elem ) {
+ return elem.getAttribute( "type" );
+ }
+ },
+
+ relative: {
+ "+": function(checkSet, part){
+ var isPartStr = typeof part === "string",
+ isTag = isPartStr && !rNonWord.test( part ),
+ isPartStrNotTag = isPartStr && !isTag;
+
+ if ( isTag ) {
+ part = part.toLowerCase();
+ }
+
+ for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {
+ if ( (elem = checkSet[i]) ) {
+ while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}
+
+ checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?
+ elem || false :
+ elem === part;
+ }
+ }
+
+ if ( isPartStrNotTag ) {
+ Sizzle.filter( part, checkSet, true );
+ }
+ },
+
+ ">": function( checkSet, part ) {
+ var elem,
+ isPartStr = typeof part === "string",
+ i = 0,
+ l = checkSet.length;
+
+ if ( isPartStr && !rNonWord.test( part ) ) {
+ part = part.toLowerCase();
+
+ for ( ; i < l; i++ ) {
+ elem = checkSet[i];
+
+ if ( elem ) {
+ var parent = elem.parentNode;
+ checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false;
+ }
+ }
+
+ } else {
+ for ( ; i < l; i++ ) {
+ elem = checkSet[i];
+
+ if ( elem ) {
+ checkSet[i] = isPartStr ?
+ elem.parentNode :
+ elem.parentNode === part;
+ }
+ }
+
+ if ( isPartStr ) {
+ Sizzle.filter( part, checkSet, true );
+ }
+ }
+ },
+
+ "": function(checkSet, part, isXML){
+ var nodeCheck,
+ doneName = done++,
+ checkFn = dirCheck;
+
+ if ( typeof part === "string" && !rNonWord.test( part ) ) {
+ part = part.toLowerCase();
+ nodeCheck = part;
+ checkFn = dirNodeCheck;
+ }
+
+ checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML );
+ },
+
+ "~": function( checkSet, part, isXML ) {
+ var nodeCheck,
+ doneName = done++,
+ checkFn = dirCheck;
+
+ if ( typeof part === "string" && !rNonWord.test( part ) ) {
+ part = part.toLowerCase();
+ nodeCheck = part;
+ checkFn = dirNodeCheck;
+ }
+
+ checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML );
+ }
+ },
+
+ find: {
+ ID: function( match, context, isXML ) {
+ if ( typeof context.getElementById !== "undefined" && !isXML ) {
+ var m = context.getElementById(match[1]);
+ // Check parentNode to catch when Blackberry 4.6 returns
+ // nodes that are no longer in the document #6963
+ return m && m.parentNode ? [m] : [];
+ }
+ },
+
+ NAME: function( match, context ) {
+ if ( typeof context.getElementsByName !== "undefined" ) {
+ var ret = [],
+ results = context.getElementsByName( match[1] );
+
+ for ( var i = 0, l = results.length; i < l; i++ ) {
+ if ( results[i].getAttribute("name") === match[1] ) {
+ ret.push( results[i] );
+ }
+ }
+
+ return ret.length === 0 ? null : ret;
+ }
+ },
+
+ TAG: function( match, context ) {
+ if ( typeof context.getElementsByTagName !== "undefined" ) {
+ return context.getElementsByTagName( match[1] );
+ }
+ }
+ },
+ preFilter: {
+ CLASS: function( match, curLoop, inplace, result, not, isXML ) {
+ match = " " + match[1].replace( rBackslash, "" ) + " ";
+
+ if ( isXML ) {
+ return match;
+ }
+
+ for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {
+ if ( elem ) {
+ if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) {
+ if ( !inplace ) {
+ result.push( elem );
+ }
+
+ } else if ( inplace ) {
+ curLoop[i] = false;
+ }
+ }
+ }
+
+ return false;
+ },
+
+ ID: function( match ) {
+ return match[1].replace( rBackslash, "" );
+ },
+
+ TAG: function( match, curLoop ) {
+ return match[1].replace( rBackslash, "" ).toLowerCase();
+ },
+
+ CHILD: function( match ) {
+ if ( match[1] === "nth" ) {
+ if ( !match[2] ) {
+ Sizzle.error( match[0] );
+ }
+
+ match[2] = match[2].replace(/^\+|\s*/g, '');
+
+ // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
+ var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec(
+ match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" ||
+ !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]);
+
+ // calculate the numbers (first)n+(last) including if they are negative
+ match[2] = (test[1] + (test[2] || 1)) - 0;
+ match[3] = test[3] - 0;
+ }
+ else if ( match[2] ) {
+ Sizzle.error( match[0] );
+ }
+
+ // TODO: Move to normal caching system
+ match[0] = done++;
+
+ return match;
+ },
+
+ ATTR: function( match, curLoop, inplace, result, not, isXML ) {
+ var name = match[1] = match[1].replace( rBackslash, "" );
+
+ if ( !isXML && Expr.attrMap[name] ) {
+ match[1] = Expr.attrMap[name];
+ }
+
+ // Handle if an un-quoted value was used
+ match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" );
+
+ if ( match[2] === "~=" ) {
+ match[4] = " " + match[4] + " ";
+ }
+
+ return match;
+ },
+
+ PSEUDO: function( match, curLoop, inplace, result, not ) {
+ if ( match[1] === "not" ) {
+ // If we're dealing with a complex expression, or a simple one
+ if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) {
+ match[3] = Sizzle(match[3], null, null, curLoop);
+
+ } else {
+ var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
+
+ if ( !inplace ) {
+ result.push.apply( result, ret );
+ }
+
+ return false;
+ }
+
+ } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) {
+ return true;
+ }
+
+ return match;
+ },
+
+ POS: function( match ) {
+ match.unshift( true );
+
+ return match;
+ }
+ },
+
+ filters: {
+ enabled: function( elem ) {
+ return elem.disabled === false && elem.type !== "hidden";
+ },
+
+ disabled: function( elem ) {
+ return elem.disabled === true;
+ },
+
+ checked: function( elem ) {
+ return elem.checked === true;
+ },
+
+ selected: function( elem ) {
+ // Accessing this property makes selected-by-default
+ // options in Safari work properly
+ if ( elem.parentNode ) {
+ elem.parentNode.selectedIndex;
+ }
+
+ return elem.selected === true;
+ },
+
+ parent: function( elem ) {
+ return !!elem.firstChild;
+ },
+
+ empty: function( elem ) {
+ return !elem.firstChild;
+ },
+
+ has: function( elem, i, match ) {
+ return !!Sizzle( match[3], elem ).length;
+ },
+
+ header: function( elem ) {
+ return (/h\d/i).test( elem.nodeName );
+ },
+
+ text: function( elem ) {
+ var attr = elem.getAttribute( "type" ), type = elem.type;
+ // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc)
+ // use getAttribute instead to test this case
+ return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null );
+ },
+
+ radio: function( elem ) {
+ return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type;
+ },
+
+ checkbox: function( elem ) {
+ return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type;
+ },
+
+ file: function( elem ) {
+ return elem.nodeName.toLowerCase() === "input" && "file" === elem.type;
+ },
+
+ password: function( elem ) {
+ return elem.nodeName.toLowerCase() === "input" && "password" === elem.type;
+ },
+
+ submit: function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return (name === "input" || name === "button") && "submit" === elem.type;
+ },
+
+ image: function( elem ) {
+ return elem.nodeName.toLowerCase() === "input" && "image" === elem.type;
+ },
+
+ reset: function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return (name === "input" || name === "button") && "reset" === elem.type;
+ },
+
+ button: function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && "button" === elem.type || name === "button";
+ },
+
+ input: function( elem ) {
+ return (/input|select|textarea|button/i).test( elem.nodeName );
+ },
+
+ focus: function( elem ) {
+ return elem === elem.ownerDocument.activeElement;
+ }
+ },
+ setFilters: {
+ first: function( elem, i ) {
+ return i === 0;
+ },
+
+ last: function( elem, i, match, array ) {
+ return i === array.length - 1;
+ },
+
+ even: function( elem, i ) {
+ return i % 2 === 0;
+ },
+
+ odd: function( elem, i ) {
+ return i % 2 === 1;
+ },
+
+ lt: function( elem, i, match ) {
+ return i < match[3] - 0;
+ },
+
+ gt: function( elem, i, match ) {
+ return i > match[3] - 0;
+ },
+
+ nth: function( elem, i, match ) {
+ return match[3] - 0 === i;
+ },
+
+ eq: function( elem, i, match ) {
+ return match[3] - 0 === i;
+ }
+ },
+ filter: {
+ PSEUDO: function( elem, match, i, array ) {
+ var name = match[1],
+ filter = Expr.filters[ name ];
+
+ if ( filter ) {
+ return filter( elem, i, match, array );
+
+ } else if ( name === "contains" ) {
+ return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0;
+
+ } else if ( name === "not" ) {
+ var not = match[3];
+
+ for ( var j = 0, l = not.length; j < l; j++ ) {
+ if ( not[j] === elem ) {
+ return false;
+ }
+ }
+
+ return true;
+
+ } else {
+ Sizzle.error( name );
+ }
+ },
+
+ CHILD: function( elem, match ) {
+ var type = match[1],
+ node = elem;
+
+ switch ( type ) {
+ case "only":
+ case "first":
+ while ( (node = node.previousSibling) ) {
+ if ( node.nodeType === 1 ) {
+ return false;
+ }
+ }
+
+ if ( type === "first" ) {
+ return true;
+ }
+
+ node = elem;
+
+ case "last":
+ while ( (node = node.nextSibling) ) {
+ if ( node.nodeType === 1 ) {
+ return false;
+ }
+ }
+
+ return true;
+
+ case "nth":
+ var first = match[2],
+ last = match[3];
+
+ if ( first === 1 && last === 0 ) {
+ return true;
+ }
+
+ var doneName = match[0],
+ parent = elem.parentNode;
+
+ if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) {
+ var count = 0;
+
+ for ( node = parent.firstChild; node; node = node.nextSibling ) {
+ if ( node.nodeType === 1 ) {
+ node.nodeIndex = ++count;
+ }
+ }
+
+ parent.sizcache = doneName;
+ }
+
+ var diff = elem.nodeIndex - last;
+
+ if ( first === 0 ) {
+ return diff === 0;
+
+ } else {
+ return ( diff % first === 0 && diff / first >= 0 );
+ }
+ }
+ },
+
+ ID: function( elem, match ) {
+ return elem.nodeType === 1 && elem.getAttribute("id") === match;
+ },
+
+ TAG: function( elem, match ) {
+ return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match;
+ },
+
+ CLASS: function( elem, match ) {
+ return (" " + (elem.className || elem.getAttribute("class")) + " ")
+ .indexOf( match ) > -1;
+ },
+
+ ATTR: function( elem, match ) {
+ var name = match[1],
+ result = Expr.attrHandle[ name ] ?
+ Expr.attrHandle[ name ]( elem ) :
+ elem[ name ] != null ?
+ elem[ name ] :
+ elem.getAttribute( name ),
+ value = result + "",
+ type = match[2],
+ check = match[4];
+
+ return result == null ?
+ type === "!=" :
+ type === "=" ?
+ value === check :
+ type === "*=" ?
+ value.indexOf(check) >= 0 :
+ type === "~=" ?
+ (" " + value + " ").indexOf(check) >= 0 :
+ !check ?
+ value && result !== false :
+ type === "!=" ?
+ value !== check :
+ type === "^=" ?
+ value.indexOf(check) === 0 :
+ type === "$=" ?
+ value.substr(value.length - check.length) === check :
+ type === "|=" ?
+ value === check || value.substr(0, check.length + 1) === check + "-" :
+ false;
+ },
+
+ POS: function( elem, match, i, array ) {
+ var name = match[2],
+ filter = Expr.setFilters[ name ];
+
+ if ( filter ) {
+ return filter( elem, i, match, array );
+ }
+ }
+ }
+};
+
+var origPOS = Expr.match.POS,
+ fescape = function(all, num){
+ return "\\" + (num - 0 + 1);
+ };
+
+for ( var type in Expr.match ) {
+ Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) );
+ Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) );
+}
+
+var makeArray = function( array, results ) {
+ array = Array.prototype.slice.call( array, 0 );
+
+ if ( results ) {
+ results.push.apply( results, array );
+ return results;
+ }
+
+ return array;
+};
+
+// Perform a simple check to determine if the browser is capable of
+// converting a NodeList to an array using builtin methods.
+// Also verifies that the returned array holds DOM nodes
+// (which is not the case in the Blackberry browser)
+try {
+ Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType;
+
+// Provide a fallback method if it does not work
+} catch( e ) {
+ makeArray = function( array, results ) {
+ var i = 0,
+ ret = results || [];
+
+ if ( toString.call(array) === "[object Array]" ) {
+ Array.prototype.push.apply( ret, array );
+
+ } else {
+ if ( typeof array.length === "number" ) {
+ for ( var l = array.length; i < l; i++ ) {
+ ret.push( array[i] );
+ }
+
+ } else {
+ for ( ; array[i]; i++ ) {
+ ret.push( array[i] );
+ }
+ }
+ }
+
+ return ret;
+ };
+}
+
+var sortOrder, siblingCheck;
+
+if ( document.documentElement.compareDocumentPosition ) {
+ sortOrder = function( a, b ) {
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {
+ return a.compareDocumentPosition ? -1 : 1;
+ }
+
+ return a.compareDocumentPosition(b) & 4 ? -1 : 1;
+ };
+
+} else {
+ sortOrder = function( a, b ) {
+ // The nodes are identical, we can exit early
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+
+ // Fallback to using sourceIndex (in IE) if it's available on both nodes
+ } else if ( a.sourceIndex && b.sourceIndex ) {
+ return a.sourceIndex - b.sourceIndex;
+ }
+
+ var al, bl,
+ ap = [],
+ bp = [],
+ aup = a.parentNode,
+ bup = b.parentNode,
+ cur = aup;
+
+ // If the nodes are siblings (or identical) we can do a quick check
+ if ( aup === bup ) {
+ return siblingCheck( a, b );
+
+ // If no parents were found then the nodes are disconnected
+ } else if ( !aup ) {
+ return -1;
+
+ } else if ( !bup ) {
+ return 1;
+ }
+
+ // Otherwise they're somewhere else in the tree so we need
+ // to build up a full list of the parentNodes for comparison
+ while ( cur ) {
+ ap.unshift( cur );
+ cur = cur.parentNode;
+ }
+
+ cur = bup;
+
+ while ( cur ) {
+ bp.unshift( cur );
+ cur = cur.parentNode;
+ }
+
+ al = ap.length;
+ bl = bp.length;
+
+ // Start walking down the tree looking for a discrepancy
+ for ( var i = 0; i < al && i < bl; i++ ) {
+ if ( ap[i] !== bp[i] ) {
+ return siblingCheck( ap[i], bp[i] );
+ }
+ }
+
+ // We ended someplace up the tree so do a sibling check
+ return i === al ?
+ siblingCheck( a, bp[i], -1 ) :
+ siblingCheck( ap[i], b, 1 );
+ };
+
+ siblingCheck = function( a, b, ret ) {
+ if ( a === b ) {
+ return ret;
+ }
+
+ var cur = a.nextSibling;
+
+ while ( cur ) {
+ if ( cur === b ) {
+ return -1;
+ }
+
+ cur = cur.nextSibling;
+ }
+
+ return 1;
+ };
+}
+
+// Utility function for retreiving the text value of an array of DOM nodes
+Sizzle.getText = function( elems ) {
+ var ret = "", elem;
+
+ for ( var i = 0; elems[i]; i++ ) {
+ elem = elems[i];
+
+ // Get the text from text nodes and CDATA nodes
+ if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
+ ret += elem.nodeValue;
+
+ // Traverse everything else, except comment nodes
+ } else if ( elem.nodeType !== 8 ) {
+ ret += Sizzle.getText( elem.childNodes );
+ }
+ }
+
+ return ret;
+};
+
+// Check to see if the browser returns elements by name when
+// querying by getElementById (and provide a workaround)
+(function(){
+ // We're going to inject a fake input element with a specified name
+ var form = document.createElement("div"),
+ id = "script" + (new Date()).getTime(),
+ root = document.documentElement;
+
+ form.innerHTML = "<a name='" + id + "'/>";
+
+ // Inject it into the root element, check its status, and remove it quickly
+ root.insertBefore( form, root.firstChild );
+
+ // The workaround has to do additional checks after a getElementById
+ // Which slows things down for other browsers (hence the branching)
+ if ( document.getElementById( id ) ) {
+ Expr.find.ID = function( match, context, isXML ) {
+ if ( typeof context.getElementById !== "undefined" && !isXML ) {
+ var m = context.getElementById(match[1]);
+
+ return m ?
+ m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ?
+ [m] :
+ undefined :
+ [];
+ }
+ };
+
+ Expr.filter.ID = function( elem, match ) {
+ var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id");
+
+ return elem.nodeType === 1 && node && node.nodeValue === match;
+ };
+ }
+
+ root.removeChild( form );
+
+ // release memory in IE
+ root = form = null;
+})();
+
+(function(){
+ // Check to see if the browser returns only elements
+ // when doing getElementsByTagName("*")
+
+ // Create a fake element
+ var div = document.createElement("div");
+ div.appendChild( document.createComment("") );
+
+ // Make sure no comments are found
+ if ( div.getElementsByTagName("*").length > 0 ) {
+ Expr.find.TAG = function( match, context ) {
+ var results = context.getElementsByTagName( match[1] );
+
+ // Filter out possible comments
+ if ( match[1] === "*" ) {
+ var tmp = [];
+
+ for ( var i = 0; results[i]; i++ ) {
+ if ( results[i].nodeType === 1 ) {
+ tmp.push( results[i] );
+ }
+ }
+
+ results = tmp;
+ }
+
+ return results;
+ };
+ }
+
+ // Check to see if an attribute returns normalized href attributes
+ div.innerHTML = "<a href='#'></a>";
+
+ if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" &&
+ div.firstChild.getAttribute("href") !== "#" ) {
+
+ Expr.attrHandle.href = function( elem ) {
+ return elem.getAttribute( "href", 2 );
+ };
+ }
+
+ // release memory in IE
+ div = null;
+})();
+
+if ( document.querySelectorAll ) {
+ (function(){
+ var oldSizzle = Sizzle,
+ div = document.createElement("div"),
+ id = "__sizzle__";
+
+ div.innerHTML = "<p class='TEST'></p>";
+
+ // Safari can't handle uppercase or unicode characters when
+ // in quirks mode.
+ if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {
+ return;
+ }
+
+ Sizzle = function( query, context, extra, seed ) {
+ context = context || document;
+
+ // Only use querySelectorAll on non-XML documents
+ // (ID selectors don't work in non-HTML documents)
+ if ( !seed && !Sizzle.isXML(context) ) {
+ // See if we find a selector to speed up
+ var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query );
+
+ if ( match && (context.nodeType === 1 || context.nodeType === 9) ) {
+ // Speed-up: Sizzle("TAG")
+ if ( match[1] ) {
+ return makeArray( context.getElementsByTagName( query ), extra );
+
+ // Speed-up: Sizzle(".CLASS")
+ } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) {
+ return makeArray( context.getElementsByClassName( match[2] ), extra );
+ }
+ }
+
+ if ( context.nodeType === 9 ) {
+ // Speed-up: Sizzle("body")
+ // The body element only exists once, optimize finding it
+ if ( query === "body" && context.body ) {
+ return makeArray( [ context.body ], extra );
+
+ // Speed-up: Sizzle("#ID")
+ } else if ( match && match[3] ) {
+ var elem = context.getElementById( match[3] );
+
+ // Check parentNode to catch when Blackberry 4.6 returns
+ // nodes that are no longer in the document #6963
+ if ( elem && elem.parentNode ) {
+ // Handle the case where IE and Opera return items
+ // by name instead of ID
+ if ( elem.id === match[3] ) {
+ return makeArray( [ elem ], extra );
+ }
+
+ } else {
+ return makeArray( [], extra );
+ }
+ }
+
+ try {
+ return makeArray( context.querySelectorAll(query), extra );
+ } catch(qsaError) {}
+
+ // qSA works strangely on Element-rooted queries
+ // We can work around this by specifying an extra ID on the root
+ // and working up from there (Thanks to Andrew Dupont for the technique)
+ // IE 8 doesn't work on object elements
+ } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
+ var oldContext = context,
+ old = context.getAttribute( "id" ),
+ nid = old || id,
+ hasParent = context.parentNode,
+ relativeHierarchySelector = /^\s*[+~]/.test( query );
+
+ if ( !old ) {
+ context.setAttribute( "id", nid );
+ } else {
+ nid = nid.replace( /'/g, "\\$&" );
+ }
+ if ( relativeHierarchySelector && hasParent ) {
+ context = context.parentNode;
+ }
+
+ try {
+ if ( !relativeHierarchySelector || hasParent ) {
+ return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra );
+ }
+
+ } catch(pseudoError) {
+ } finally {
+ if ( !old ) {
+ oldContext.removeAttribute( "id" );
+ }
+ }
+ }
+ }
+
+ return oldSizzle(query, context, extra, seed);
+ };
+
+ for ( var prop in oldSizzle ) {
+ Sizzle[ prop ] = oldSizzle[ prop ];
+ }
+
+ // release memory in IE
+ div = null;
+ })();
+}
+
+(function(){
+ var html = document.documentElement,
+ matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector;
+
+ if ( matches ) {
+ // Check to see if it's possible to do matchesSelector
+ // on a disconnected node (IE 9 fails this)
+ var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ),
+ pseudoWorks = false;
+
+ try {
+ // This should fail with an exception
+ // Gecko does not error, returns false instead
+ matches.call( document.documentElement, "[test!='']:sizzle" );
+
+ } catch( pseudoError ) {
+ pseudoWorks = true;
+ }
+
+ Sizzle.matchesSelector = function( node, expr ) {
+ // Make sure that attribute selectors are quoted
+ expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']");
+
+ if ( !Sizzle.isXML( node ) ) {
+ try {
+ if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) {
+ var ret = matches.call( node, expr );
+
+ // IE 9's matchesSelector returns false on disconnected nodes
+ if ( ret || !disconnectedMatch ||
+ // As well, disconnected nodes are said to be in a document
+ // fragment in IE 9, so check for that
+ node.document && node.document.nodeType !== 11 ) {
+ return ret;
+ }
+ }
+ } catch(e) {}
+ }
+
+ return Sizzle(expr, null, null, [node]).length > 0;
+ };
+ }
+})();
+
+(function(){
+ var div = document.createElement("div");
+
+ div.innerHTML = "<div class='test e'></div><div class='test'></div>";
+
+ // Opera can't find a second classname (in 9.6)
+ // Also, make sure that getElementsByClassName actually exists
+ if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) {
+ return;
+ }
+
+ // Safari caches class attributes, doesn't catch changes (in 3.2)
+ div.lastChild.className = "e";
+
+ if ( div.getElementsByClassName("e").length === 1 ) {
+ return;
+ }
+
+ Expr.order.splice(1, 0, "CLASS");
+ Expr.find.CLASS = function( match, context, isXML ) {
+ if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) {
+ return context.getElementsByClassName(match[1]);
+ }
+ };
+
+ // release memory in IE
+ div = null;
+})();
+
+function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
+ for ( var i = 0, l = checkSet.length; i < l; i++ ) {
+ var elem = checkSet[i];
+
+ if ( elem ) {
+ var match = false;
+
+ elem = elem[dir];
+
+ while ( elem ) {
+ if ( elem.sizcache === doneName ) {
+ match = checkSet[elem.sizset];
+ break;
+ }
+
+ if ( elem.nodeType === 1 && !isXML ){
+ elem.sizcache = doneName;
+ elem.sizset = i;
+ }
+
+ if ( elem.nodeName.toLowerCase() === cur ) {
+ match = elem;
+ break;
+ }
+
+ elem = elem[dir];
+ }
+
+ checkSet[i] = match;
+ }
+ }
+}
+
+function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {
+ for ( var i = 0, l = checkSet.length; i < l; i++ ) {
+ var elem = checkSet[i];
+
+ if ( elem ) {
+ var match = false;
+
+ elem = elem[dir];
+
+ while ( elem ) {
+ if ( elem.sizcache === doneName ) {
+ match = checkSet[elem.sizset];
+ break;
+ }
+
+ if ( elem.nodeType === 1 ) {
+ if ( !isXML ) {
+ elem.sizcache = doneName;
+ elem.sizset = i;
+ }
+
+ if ( typeof cur !== "string" ) {
+ if ( elem === cur ) {
+ match = true;
+ break;
+ }
+
+ } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {
+ match = elem;
+ break;
+ }
+ }
+
+ elem = elem[dir];
+ }
+
+ checkSet[i] = match;
+ }
+ }
+}
+
+if ( document.documentElement.contains ) {
+ Sizzle.contains = function( a, b ) {
+ return a !== b && (a.contains ? a.contains(b) : true);
+ };
+
+} else if ( document.documentElement.compareDocumentPosition ) {
+ Sizzle.contains = function( a, b ) {
+ return !!(a.compareDocumentPosition(b) & 16);
+ };
+
+} else {
+ Sizzle.contains = function() {
+ return false;
+ };
+}
+
+Sizzle.isXML = function( elem ) {
+ // documentElement is verified for cases where it doesn't yet exist
+ // (such as loading iframes in IE - #4833)
+ var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;
+
+ return documentElement ? documentElement.nodeName !== "HTML" : false;
+};
+
+var posProcess = function( selector, context ) {
+ var match,
+ tmpSet = [],
+ later = "",
+ root = context.nodeType ? [context] : context;
+
+ // Position selectors must be done after the filter
+ // And so must :not(positional) so we move all PSEUDOs to the end
+ while ( (match = Expr.match.PSEUDO.exec( selector )) ) {
+ later += match[0];
+ selector = selector.replace( Expr.match.PSEUDO, "" );
+ }
+
+ selector = Expr.relative[selector] ? selector + "*" : selector;
+
+ for ( var i = 0, l = root.length; i < l; i++ ) {
+ Sizzle( selector, root[i], tmpSet );
+ }
+
+ return Sizzle.filter( later, tmpSet );
+};
+
+// EXPOSE
+jQuery.find = Sizzle;
+jQuery.expr = Sizzle.selectors;
+jQuery.expr[":"] = jQuery.expr.filters;
+jQuery.unique = Sizzle.uniqueSort;
+jQuery.text = Sizzle.getText;
+jQuery.isXMLDoc = Sizzle.isXML;
+jQuery.contains = Sizzle.contains;
+
+
+})();
+
+
+var runtil = /Until$/,
+ rparentsprev = /^(?:parents|prevUntil|prevAll)/,
+ // Note: This RegExp should be improved, or likely pulled from Sizzle
+ rmultiselector = /,/,
+ isSimple = /^.[^:#\[\.,]*$/,
+ slice = Array.prototype.slice,
+ POS = jQuery.expr.match.POS,
+ // methods guaranteed to produce a unique set when starting from a unique set
+ guaranteedUnique = {
+ children: true,
+ contents: true,
+ next: true,
+ prev: true
+ };
+
+jQuery.fn.extend({
+ find: function( selector ) {
+ var self = this,
+ i, l;
+
+ if ( typeof selector !== "string" ) {
+ return jQuery( selector ).filter(function() {
+ for ( i = 0, l = self.length; i < l; i++ ) {
+ if ( jQuery.contains( self[ i ], this ) ) {
+ return true;
+ }
+ }
+ });
+ }
+
+ var ret = this.pushStack( "", "find", selector ),
+ length, n, r;
+
+ for ( i = 0, l = this.length; i < l; i++ ) {
+ length = ret.length;
+ jQuery.find( selector, this[i], ret );
+
+ if ( i > 0 ) {
+ // Make sure that the results are unique
+ for ( n = length; n < ret.length; n++ ) {
+ for ( r = 0; r < length; r++ ) {
+ if ( ret[r] === ret[n] ) {
+ ret.splice(n--, 1);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return ret;
+ },
+
+ has: function( target ) {
+ var targets = jQuery( target );
+ return this.filter(function() {
+ for ( var i = 0, l = targets.length; i < l; i++ ) {
+ if ( jQuery.contains( this, targets[i] ) ) {
+ return true;
+ }
+ }
+ });
+ },
+
+ not: function( selector ) {
+ return this.pushStack( winnow(this, selector, false), "not", selector);
+ },
+
+ filter: function( selector ) {
+ return this.pushStack( winnow(this, selector, true), "filter", selector );
+ },
+
+ is: function( selector ) {
+ return !!selector && ( typeof selector === "string" ?
+ jQuery.filter( selector, this ).length > 0 :
+ this.filter( selector ).length > 0 );
+ },
+
+ closest: function( selectors, context ) {
+ var ret = [], i, l, cur = this[0];
+
+ // Array
+ if ( jQuery.isArray( selectors ) ) {
+ var match, selector,
+ matches = {},
+ level = 1;
+
+ if ( cur && selectors.length ) {
+ for ( i = 0, l = selectors.length; i < l; i++ ) {
+ selector = selectors[i];
+
+ if ( !matches[ selector ] ) {
+ matches[ selector ] = POS.test( selector ) ?
+ jQuery( selector, context || this.context ) :
+ selector;
+ }
+ }
+
+ while ( cur && cur.ownerDocument && cur !== context ) {
+ for ( selector in matches ) {
+ match = matches[ selector ];
+
+ if ( match.jquery ? match.index( cur ) > -1 : jQuery( cur ).is( match ) ) {
+ ret.push({ selector: selector, elem: cur, level: level });
+ }
+ }
+
+ cur = cur.parentNode;
+ level++;
+ }
+ }
+
+ return ret;
+ }
+
+ // String
+ var pos = POS.test( selectors ) || typeof selectors !== "string" ?
+ jQuery( selectors, context || this.context ) :
+ 0;
+
+ for ( i = 0, l = this.length; i < l; i++ ) {
+ cur = this[i];
+
+ while ( cur ) {
+ if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) {
+ ret.push( cur );
+ break;
+
+ } else {
+ cur = cur.parentNode;
+ if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) {
+ break;
+ }
+ }
+ }
+ }
+
+ ret = ret.length > 1 ? jQuery.unique( ret ) : ret;
+
+ return this.pushStack( ret, "closest", selectors );
+ },
+
+ // Determine the position of an element within
+ // the matched set of elements
+ index: function( elem ) {
+ if ( !elem || typeof elem === "string" ) {
+ return jQuery.inArray( this[0],
+ // If it receives a string, the selector is used
+ // If it receives nothing, the siblings are used
+ elem ? jQuery( elem ) : this.parent().children() );
+ }
+ // Locate the position of the desired element
+ return jQuery.inArray(
+ // If it receives a jQuery object, the first element is used
+ elem.jquery ? elem[0] : elem, this );
+ },
+
+ add: function( selector, context ) {
+ var set = typeof selector === "string" ?
+ jQuery( selector, context ) :
+ jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ),
+ all = jQuery.merge( this.get(), set );
+
+ return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ?
+ all :
+ jQuery.unique( all ) );
+ },
+
+ andSelf: function() {
+ return this.add( this.prevObject );
+ }
+});
+
+// A painfully simple check to see if an element is disconnected
+// from a document (should be improved, where feasible).
+function isDisconnected( node ) {
+ return !node || !node.parentNode || node.parentNode.nodeType === 11;
+}
+
+jQuery.each({
+ parent: function( elem ) {
+ var parent = elem.parentNode;
+ return parent && parent.nodeType !== 11 ? parent : null;
+ },
+ parents: function( elem ) {
+ return jQuery.dir( elem, "parentNode" );
+ },
+ parentsUntil: function( elem, i, until ) {
+ return jQuery.dir( elem, "parentNode", until );
+ },
+ next: function( elem ) {
+ return jQuery.nth( elem, 2, "nextSibling" );
+ },
+ prev: function( elem ) {
+ return jQuery.nth( elem, 2, "previousSibling" );
+ },
+ nextAll: function( elem ) {
+ return jQuery.dir( elem, "nextSibling" );
+ },
+ prevAll: function( elem ) {
+ return jQuery.dir( elem, "previousSibling" );
+ },
+ nextUntil: function( elem, i, until ) {
+ return jQuery.dir( elem, "nextSibling", until );
+ },
+ prevUntil: function( elem, i, until ) {
+ return jQuery.dir( elem, "previousSibling", until );
+ },
+ siblings: function( elem ) {
+ return jQuery.sibling( elem.parentNode.firstChild, elem );
+ },
+ children: function( elem ) {
+ return jQuery.sibling( elem.firstChild );
+ },
+ contents: function( elem ) {
+ return jQuery.nodeName( elem, "iframe" ) ?
+ elem.contentDocument || elem.contentWindow.document :
+ jQuery.makeArray( elem.childNodes );
+ }
+}, function( name, fn ) {
+ jQuery.fn[ name ] = function( until, selector ) {
+ var ret = jQuery.map( this, fn, until ),
+ // The variable 'args' was introduced in
+ // https://github.com/jquery/jquery/commit/52a0238
+ // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed.
+ // http://code.google.com/p/v8/issues/detail?id=1050
+ args = slice.call(arguments);
+
+ if ( !runtil.test( name ) ) {
+ selector = until;
+ }
+
+ if ( selector && typeof selector === "string" ) {
+ ret = jQuery.filter( selector, ret );
+ }
+
+ ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;
+
+ if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {
+ ret = ret.reverse();
+ }
+
+ return this.pushStack( ret, name, args.join(",") );
+ };
+});
+
+jQuery.extend({
+ filter: function( expr, elems, not ) {
+ if ( not ) {
+ expr = ":not(" + expr + ")";
+ }
+
+ return elems.length === 1 ?
+ jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] :
+ jQuery.find.matches(expr, elems);
+ },
+
+ dir: function( elem, dir, until ) {
+ var matched = [],
+ cur = elem[ dir ];
+
+ while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {
+ if ( cur.nodeType === 1 ) {
+ matched.push( cur );
+ }
+ cur = cur[dir];
+ }
+ return matched;
+ },
+
+ nth: function( cur, result, dir, elem ) {
+ result = result || 1;
+ var num = 0;
+
+ for ( ; cur; cur = cur[dir] ) {
+ if ( cur.nodeType === 1 && ++num === result ) {
+ break;
+ }
+ }
+
+ return cur;
+ },
+
+ sibling: function( n, elem ) {
+ var r = [];
+
+ for ( ; n; n = n.nextSibling ) {
+ if ( n.nodeType === 1 && n !== elem ) {
+ r.push( n );
+ }
+ }
+
+ return r;
+ }
+});
+
+// Implement the identical functionality for filter and not
+function winnow( elements, qualifier, keep ) {
+
+ // Can't pass null or undefined to indexOf in Firefox 4
+ // Set to 0 to skip string check
+ qualifier = qualifier || 0;
+
+ if ( jQuery.isFunction( qualifier ) ) {
+ return jQuery.grep(elements, function( elem, i ) {
+ var retVal = !!qualifier.call( elem, i, elem );
+ return retVal === keep;
+ });
+
+ } else if ( qualifier.nodeType ) {
+ return jQuery.grep(elements, function( elem, i ) {
+ return (elem === qualifier) === keep;
+ });
+
+ } else if ( typeof qualifier === "string" ) {
+ var filtered = jQuery.grep(elements, function( elem ) {
+ return elem.nodeType === 1;
+ });
+
+ if ( isSimple.test( qualifier ) ) {
+ return jQuery.filter(qualifier, filtered, !keep);
+ } else {
+ qualifier = jQuery.filter( qualifier, filtered );
+ }
+ }
+
+ return jQuery.grep(elements, function( elem, i ) {
+ return (jQuery.inArray( elem, qualifier ) >= 0) === keep;
+ });
+}
+
+
+
+
+var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g,
+ rleadingWhitespace = /^\s+/,
+ rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
+ rtagName = /<([\w:]+)/,
+ rtbody = /<tbody/i,
+ rhtml = /<|&#?\w+;/,
+ rnocache = /<(?:script|object|embed|option|style)/i,
+ // checked="checked" or checked
+ rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
+ rscriptType = /\/(java|ecma)script/i,
+ rcleanScript = /^\s*<!(?:\[CDATA\[|\-\-)/,
+ wrapMap = {
+ option: [ 1, "<select multiple='multiple'>", "</select>" ],
+ legend: [ 1, "<fieldset>", "</fieldset>" ],
+ thead: [ 1, "<table>", "</table>" ],
+ tr: [ 2, "<table><tbody>", "</tbody></table>" ],
+ td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
+ col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ],
+ area: [ 1, "<map>", "</map>" ],
+ _default: [ 0, "", "" ]
+ };
+
+wrapMap.optgroup = wrapMap.option;
+wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
+wrapMap.th = wrapMap.td;
+
+// IE can't serialize <link> and <script> tags normally
+if ( !jQuery.support.htmlSerialize ) {
+ wrapMap._default = [ 1, "div<div>", "</div>" ];
+}
+
+jQuery.fn.extend({
+ text: function( text ) {
+ if ( jQuery.isFunction(text) ) {
+ return this.each(function(i) {
+ var self = jQuery( this );
+
+ self.text( text.call(this, i, self.text()) );
+ });
+ }
+
+ if ( typeof text !== "object" && text !== undefined ) {
+ return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );
+ }
+
+ return jQuery.text( this );
+ },
+
+ wrapAll: function( html ) {
+ if ( jQuery.isFunction( html ) ) {
+ return this.each(function(i) {
+ jQuery(this).wrapAll( html.call(this, i) );
+ });
+ }
+
+ if ( this[0] ) {
+ // The elements to wrap the target around
+ var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);
+
+ if ( this[0].parentNode ) {
+ wrap.insertBefore( this[0] );
+ }
+
+ wrap.map(function() {
+ var elem = this;
+
+ while ( elem.firstChild && elem.firstChild.nodeType === 1 ) {
+ elem = elem.firstChild;
+ }
+
+ return elem;
+ }).append( this );
+ }
+
+ return this;
+ },
+
+ wrapInner: function( html ) {
+ if ( jQuery.isFunction( html ) ) {
+ return this.each(function(i) {
+ jQuery(this).wrapInner( html.call(this, i) );
+ });
+ }
+
+ return this.each(function() {
+ var self = jQuery( this ),
+ contents = self.contents();
+
+ if ( contents.length ) {
+ contents.wrapAll( html );
+
+ } else {
+ self.append( html );
+ }
+ });
+ },
+
+ wrap: function( html ) {
+ return this.each(function() {
+ jQuery( this ).wrapAll( html );
+ });
+ },
+
+ unwrap: function() {
+ return this.parent().each(function() {
+ if ( !jQuery.nodeName( this, "body" ) ) {
+ jQuery( this ).replaceWith( this.childNodes );
+ }
+ }).end();
+ },
+
+ append: function() {
+ return this.domManip(arguments, true, function( elem ) {
+ if ( this.nodeType === 1 ) {
+ this.appendChild( elem );
+ }
+ });
+ },
+
+ prepend: function() {
+ return this.domManip(arguments, true, function( elem ) {
+ if ( this.nodeType === 1 ) {
+ this.insertBefore( elem, this.firstChild );
+ }
+ });
+ },
+
+ before: function() {
+ if ( this[0] && this[0].parentNode ) {
+ return this.domManip(arguments, false, function( elem ) {
+ this.parentNode.insertBefore( elem, this );
+ });
+ } else if ( arguments.length ) {
+ var set = jQuery(arguments[0]);
+ set.push.apply( set, this.toArray() );
+ return this.pushStack( set, "before", arguments );
+ }
+ },
+
+ after: function() {
+ if ( this[0] && this[0].parentNode ) {
+ return this.domManip(arguments, false, function( elem ) {
+ this.parentNode.insertBefore( elem, this.nextSibling );
+ });
+ } else if ( arguments.length ) {
+ var set = this.pushStack( this, "after", arguments );
+ set.push.apply( set, jQuery(arguments[0]).toArray() );
+ return set;
+ }
+ },
+
+ // keepData is for internal use only--do not document
+ remove: function( selector, keepData ) {
+ for ( var i = 0, elem; (elem = this[i]) != null; i++ ) {
+ if ( !selector || jQuery.filter( selector, [ elem ] ).length ) {
+ if ( !keepData && elem.nodeType === 1 ) {
+ jQuery.cleanData( elem.getElementsByTagName("*") );
+ jQuery.cleanData( [ elem ] );
+ }
+
+ if ( elem.parentNode ) {
+ elem.parentNode.removeChild( elem );
+ }
+ }
+ }
+
+ return this;
+ },
+
+ empty: function() {
+ for ( var i = 0, elem; (elem = this[i]) != null; i++ ) {
+ // Remove element nodes and prevent memory leaks
+ if ( elem.nodeType === 1 ) {
+ jQuery.cleanData( elem.getElementsByTagName("*") );
+ }
+
+ // Remove any remaining nodes
+ while ( elem.firstChild ) {
+ elem.removeChild( elem.firstChild );
+ }
+ }
+
+ return this;
+ },
+
+ clone: function( dataAndEvents, deepDataAndEvents ) {
+ dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
+ deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
+
+ return this.map( function () {
+ return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
+ });
+ },
+
+ html: function( value ) {
+ if ( value === undefined ) {
+ return this[0] && this[0].nodeType === 1 ?
+ this[0].innerHTML.replace(rinlinejQuery, "") :
+ null;
+
+ // See if we can take a shortcut and just use innerHTML
+ } else if ( typeof value === "string" && !rnocache.test( value ) &&
+ (jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) &&
+ !wrapMap[ (rtagName.exec( value ) || ["", ""])[1].toLowerCase() ] ) {
+
+ value = value.replace(rxhtmlTag, "<$1></$2>");
+
+ try {
+ for ( var i = 0, l = this.length; i < l; i++ ) {
+ // Remove element nodes and prevent memory leaks
+ if ( this[i].nodeType === 1 ) {
+ jQuery.cleanData( this[i].getElementsByTagName("*") );
+ this[i].innerHTML = value;
+ }
+ }
+
+ // If using innerHTML throws an exception, use the fallback method
+ } catch(e) {
+ this.empty().append( value );
+ }
+
+ } else if ( jQuery.isFunction( value ) ) {
+ this.each(function(i){
+ var self = jQuery( this );
+
+ self.html( value.call(this, i, self.html()) );
+ });
+
+ } else {
+ this.empty().append( value );
+ }
+
+ return this;
+ },
+
+ replaceWith: function( value ) {
+ if ( this[0] && this[0].parentNode ) {
+ // Make sure that the elements are removed from the DOM before they are inserted
+ // this can help fix replacing a parent with child elements
+ if ( jQuery.isFunction( value ) ) {
+ return this.each(function(i) {
+ var self = jQuery(this), old = self.html();
+ self.replaceWith( value.call( this, i, old ) );
+ });
+ }
+
+ if ( typeof value !== "string" ) {
+ value = jQuery( value ).detach();
+ }
+
+ return this.each(function() {
+ var next = this.nextSibling,
+ parent = this.parentNode;
+
+ jQuery( this ).remove();
+
+ if ( next ) {
+ jQuery(next).before( value );
+ } else {
+ jQuery(parent).append( value );
+ }
+ });
+ } else {
+ return this.length ?
+ this.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), "replaceWith", value ) :
+ this;
+ }
+ },
+
+ detach: function( selector ) {
+ return this.remove( selector, true );
+ },
+
+ domManip: function( args, table, callback ) {
+ var results, first, fragment, parent,
+ value = args[0],
+ scripts = [];
+
+ // We can't cloneNode fragments that contain checked, in WebKit
+ if ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === "string" && rchecked.test( value ) ) {
+ return this.each(function() {
+ jQuery(this).domManip( args, table, callback, true );
+ });
+ }
+
+ if ( jQuery.isFunction(value) ) {
+ return this.each(function(i) {
+ var self = jQuery(this);
+ args[0] = value.call(this, i, table ? self.html() : undefined);
+ self.domManip( args, table, callback );
+ });
+ }
+
+ if ( this[0] ) {
+ parent = value && value.parentNode;
+
+ // If we're in a fragment, just use that instead of building a new one
+ if ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) {
+ results = { fragment: parent };
+
+ } else {
+ results = jQuery.buildFragment( args, this, scripts );
+ }
+
+ fragment = results.fragment;
+
+ if ( fragment.childNodes.length === 1 ) {
+ first = fragment = fragment.firstChild;
+ } else {
+ first = fragment.firstChild;
+ }
+
+ if ( first ) {
+ table = table && jQuery.nodeName( first, "tr" );
+
+ for ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) {
+ callback.call(
+ table ?
+ root(this[i], first) :
+ this[i],
+ // Make sure that we do not leak memory by inadvertently discarding
+ // the original fragment (which might have attached data) instead of
+ // using it; in addition, use the original fragment object for the last
+ // item instead of first because it can end up being emptied incorrectly
+ // in certain situations (Bug #8070).
+ // Fragments from the fragment cache must always be cloned and never used
+ // in place.
+ results.cacheable || (l > 1 && i < lastIndex) ?
+ jQuery.clone( fragment, true, true ) :
+ fragment
+ );
+ }
+ }
+
+ if ( scripts.length ) {
+ jQuery.each( scripts, evalScript );
+ }
+ }
+
+ return this;
+ }
+});
+
+function root( elem, cur ) {
+ return jQuery.nodeName(elem, "table") ?
+ (elem.getElementsByTagName("tbody")[0] ||
+ elem.appendChild(elem.ownerDocument.createElement("tbody"))) :
+ elem;
+}
+
+function cloneCopyEvent( src, dest ) {
+
+ if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {
+ return;
+ }
+
+ var internalKey = jQuery.expando,
+ oldData = jQuery.data( src ),
+ curData = jQuery.data( dest, oldData );
+
+ // Switch to use the internal data object, if it exists, for the next
+ // stage of data copying
+ if ( (oldData = oldData[ internalKey ]) ) {
+ var events = oldData.events;
+ curData = curData[ internalKey ] = jQuery.extend({}, oldData);
+
+ if ( events ) {
+ delete curData.handle;
+ curData.events = {};
+
+ for ( var type in events ) {
+ for ( var i = 0, l = events[ type ].length; i < l; i++ ) {
+ jQuery.event.add( dest, type + ( events[ type ][ i ].namespace ? "." : "" ) + events[ type ][ i ].namespace, events[ type ][ i ], events[ type ][ i ].data );
+ }
+ }
+ }
+ }
+}
+
+function cloneFixAttributes( src, dest ) {
+ var nodeName;
+
+ // We do not need to do anything for non-Elements
+ if ( dest.nodeType !== 1 ) {
+ return;
+ }
+
+ // clearAttributes removes the attributes, which we don't want,
+ // but also removes the attachEvent events, which we *do* want
+ if ( dest.clearAttributes ) {
+ dest.clearAttributes();
+ }
+
+ // mergeAttributes, in contrast, only merges back on the
+ // original attributes, not the events
+ if ( dest.mergeAttributes ) {
+ dest.mergeAttributes( src );
+ }
+
+ nodeName = dest.nodeName.toLowerCase();
+
+ // IE6-8 fail to clone children inside object elements that use
+ // the proprietary classid attribute value (rather than the type
+ // attribute) to identify the type of content to display
+ if ( nodeName === "object" ) {
+ dest.outerHTML = src.outerHTML;
+
+ } else if ( nodeName === "input" && (src.type === "checkbox" || src.type === "radio") ) {
+ // IE6-8 fails to persist the checked state of a cloned checkbox
+ // or radio button. Worse, IE6-7 fail to give the cloned element
+ // a checked appearance if the defaultChecked value isn't also set
+ if ( src.checked ) {
+ dest.defaultChecked = dest.checked = src.checked;
+ }
+
+ // IE6-7 get confused and end up setting the value of a cloned
+ // checkbox/radio button to an empty string instead of "on"
+ if ( dest.value !== src.value ) {
+ dest.value = src.value;
+ }
+
+ // IE6-8 fails to return the selected option to the default selected
+ // state when cloning options
+ } else if ( nodeName === "option" ) {
+ dest.selected = src.defaultSelected;
+
+ // IE6-8 fails to set the defaultValue to the correct value when
+ // cloning other types of input fields
+ } else if ( nodeName === "input" || nodeName === "textarea" ) {
+ dest.defaultValue = src.defaultValue;
+ }
+
+ // Event data gets referenced instead of copied if the expando
+ // gets copied too
+ dest.removeAttribute( jQuery.expando );
+}
+
+jQuery.buildFragment = function( args, nodes, scripts ) {
+ var fragment, cacheable, cacheresults, doc;
+
+ // nodes may contain either an explicit document object,
+ // a jQuery collection or context object.
+ // If nodes[0] contains a valid object to assign to doc
+ if ( nodes && nodes[0] ) {
+ doc = nodes[0].ownerDocument || nodes[0];
+ }
+
+ // Ensure that an attr object doesn't incorrectly stand in as a document object
+ // Chrome and Firefox seem to allow this to occur and will throw exception
+ // Fixes #8950
+ if ( !doc.createDocumentFragment ) {
+ doc = document;
+ }
+
+ // Only cache "small" (1/2 KB) HTML strings that are associated with the main document
+ // Cloning options loses the selected state, so don't cache them
+ // IE 6 doesn't like it when you put <object> or <embed> elements in a fragment
+ // Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache
+ if ( args.length === 1 && typeof args[0] === "string" && args[0].length < 512 && doc === document &&
+ args[0].charAt(0) === "<" && !rnocache.test( args[0] ) && (jQuery.support.checkClone || !rchecked.test( args[0] )) ) {
+
+ cacheable = true;
+
+ cacheresults = jQuery.fragments[ args[0] ];
+ if ( cacheresults && cacheresults !== 1 ) {
+ fragment = cacheresults;
+ }
+ }
+
+ if ( !fragment ) {
+ fragment = doc.createDocumentFragment();
+ jQuery.clean( args, doc, fragment, scripts );
+ }
+
+ if ( cacheable ) {
+ jQuery.fragments[ args[0] ] = cacheresults ? fragment : 1;
+ }
+
+ return { fragment: fragment, cacheable: cacheable };
+};
+
+jQuery.fragments = {};
+
+jQuery.each({
+ appendTo: "append",
+ prependTo: "prepend",
+ insertBefore: "before",
+ insertAfter: "after",
+ replaceAll: "replaceWith"
+}, function( name, original ) {
+ jQuery.fn[ name ] = function( selector ) {
+ var ret = [],
+ insert = jQuery( selector ),
+ parent = this.length === 1 && this[0].parentNode;
+
+ if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
+ insert[ original ]( this[0] );
+ return this;
+
+ } else {
+ for ( var i = 0, l = insert.length; i < l; i++ ) {
+ var elems = (i > 0 ? this.clone(true) : this).get();
+ jQuery( insert[i] )[ original ]( elems );
+ ret = ret.concat( elems );
+ }
+
+ return this.pushStack( ret, name, insert.selector );
+ }
+ };
+});
+
+function getAll( elem ) {
+ if ( "getElementsByTagName" in elem ) {
+ return elem.getElementsByTagName( "*" );
+
+ } else if ( "querySelectorAll" in elem ) {
+ return elem.querySelectorAll( "*" );
+
+ } else {
+ return [];
+ }
+}
+
+// Used in clean, fixes the defaultChecked property
+function fixDefaultChecked( elem ) {
+ if ( elem.type === "checkbox" || elem.type === "radio" ) {
+ elem.defaultChecked = elem.checked;
+ }
+}
+// Finds all inputs and passes them to fixDefaultChecked
+function findInputs( elem ) {
+ if ( jQuery.nodeName( elem, "input" ) ) {
+ fixDefaultChecked( elem );
+ } else if ( "getElementsByTagName" in elem ) {
+ jQuery.grep( elem.getElementsByTagName("input"), fixDefaultChecked );
+ }
+}
+
+jQuery.extend({
+ clone: function( elem, dataAndEvents, deepDataAndEvents ) {
+ var clone = elem.cloneNode(true),
+ srcElements,
+ destElements,
+ i;
+
+ if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) &&
+ (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) {
+ // IE copies events bound via attachEvent when using cloneNode.
+ // Calling detachEvent on the clone will also remove the events
+ // from the original. In order to get around this, we use some
+ // proprietary methods to clear the events. Thanks to MooTools
+ // guys for this hotness.
+
+ cloneFixAttributes( elem, clone );
+
+ // Using Sizzle here is crazy slow, so we use getElementsByTagName
+ // instead
+ srcElements = getAll( elem );
+ destElements = getAll( clone );
+
+ // Weird iteration because IE will replace the length property
+ // with an element if you are cloning the body and one of the
+ // elements on the page has a name or id of "length"
+ for ( i = 0; srcElements[i]; ++i ) {
+ cloneFixAttributes( srcElements[i], destElements[i] );
+ }
+ }
+
+ // Copy the events from the original to the clone
+ if ( dataAndEvents ) {
+ cloneCopyEvent( elem, clone );
+
+ if ( deepDataAndEvents ) {
+ srcElements = getAll( elem );
+ destElements = getAll( clone );
+
+ for ( i = 0; srcElements[i]; ++i ) {
+ cloneCopyEvent( srcElements[i], destElements[i] );
+ }
+ }
+ }
+
+ srcElements = destElements = null;
+
+ // Return the cloned set
+ return clone;
+ },
+
+ clean: function( elems, context, fragment, scripts ) {
+ var checkScriptType;
+
+ context = context || document;
+
+ // !context.createElement fails in IE with an error but returns typeof 'object'
+ if ( typeof context.createElement === "undefined" ) {
+ context = context.ownerDocument || context[0] && context[0].ownerDocument || document;
+ }
+
+ var ret = [], j;
+
+ for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
+ if ( typeof elem === "number" ) {
+ elem += "";
+ }
+
+ if ( !elem ) {
+ continue;
+ }
+
+ // Convert html string into DOM nodes
+ if ( typeof elem === "string" ) {
+ if ( !rhtml.test( elem ) ) {
+ elem = context.createTextNode( elem );
+ } else {
+ // Fix "XHTML"-style tags in all browsers
+ elem = elem.replace(rxhtmlTag, "<$1></$2>");
+
+ // Trim whitespace, otherwise indexOf won't work as expected
+ var tag = (rtagName.exec( elem ) || ["", ""])[1].toLowerCase(),
+ wrap = wrapMap[ tag ] || wrapMap._default,
+ depth = wrap[0],
+ div = context.createElement("div");
+
+ // Go to html and back, then peel off extra wrappers
+ div.innerHTML = wrap[1] + elem + wrap[2];
+
+ // Move to the right depth
+ while ( depth-- ) {
+ div = div.lastChild;
+ }
+
+ // Remove IE's autoinserted <tbody> from table fragments
+ if ( !jQuery.support.tbody ) {
+
+ // String was a <table>, *may* have spurious <tbody>
+ var hasBody = rtbody.test(elem),
+ tbody = tag === "table" && !hasBody ?
+ div.firstChild && div.firstChild.childNodes :
+
+ // String was a bare <thead> or <tfoot>
+ wrap[1] === "<table>" && !hasBody ?
+ div.childNodes :
+ [];
+
+ for ( j = tbody.length - 1; j >= 0 ; --j ) {
+ if ( jQuery.nodeName( tbody[ j ], "tbody" ) && !tbody[ j ].childNodes.length ) {
+ tbody[ j ].parentNode.removeChild( tbody[ j ] );
+ }
+ }
+ }
+
+ // IE completely kills leading whitespace when innerHTML is used
+ if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {
+ div.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );
+ }
+
+ elem = div.childNodes;
+ }
+ }
+
+ // Resets defaultChecked for any radios and checkboxes
+ // about to be appended to the DOM in IE 6/7 (#8060)
+ var len;
+ if ( !jQuery.support.appendChecked ) {
+ if ( elem[0] && typeof (len = elem.length) === "number" ) {
+ for ( j = 0; j < len; j++ ) {
+ findInputs( elem[j] );
+ }
+ } else {
+ findInputs( elem );
+ }
+ }
+
+ if ( elem.nodeType ) {
+ ret.push( elem );
+ } else {
+ ret = jQuery.merge( ret, elem );
+ }
+ }
+
+ if ( fragment ) {
+ checkScriptType = function( elem ) {
+ return !elem.type || rscriptType.test( elem.type );
+ };
+ for ( i = 0; ret[i]; i++ ) {
+ if ( scripts && jQuery.nodeName( ret[i], "script" ) && (!ret[i].type || ret[i].type.toLowerCase() === "text/javascript") ) {
+ scripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );
+
+ } else {
+ if ( ret[i].nodeType === 1 ) {
+ var jsTags = jQuery.grep( ret[i].getElementsByTagName( "script" ), checkScriptType );
+
+ ret.splice.apply( ret, [i + 1, 0].concat( jsTags ) );
+ }
+ fragment.appendChild( ret[i] );
+ }
+ }
+ }
+
+ return ret;
+ },
+
+ cleanData: function( elems ) {
+ var data, id, cache = jQuery.cache, internalKey = jQuery.expando, special = jQuery.event.special,
+ deleteExpando = jQuery.support.deleteExpando;
+
+ for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
+ if ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) {
+ continue;
+ }
+
+ id = elem[ jQuery.expando ];
+
+ if ( id ) {
+ data = cache[ id ] && cache[ id ][ internalKey ];
+
+ if ( data && data.events ) {
+ for ( var type in data.events ) {
+ if ( special[ type ] ) {
+ jQuery.event.remove( elem, type );
+
+ // This is a shortcut to avoid jQuery.event.remove's overhead
+ } else {
+ jQuery.removeEvent( elem, type, data.handle );
+ }
+ }
+
+ // Null the DOM reference to avoid IE6/7/8 leak (#7054)
+ if ( data.handle ) {
+ data.handle.elem = null;
+ }
+ }
+
+ if ( deleteExpando ) {
+ delete elem[ jQuery.expando ];
+
+ } else if ( elem.removeAttribute ) {
+ elem.removeAttribute( jQuery.expando );
+ }
+
+ delete cache[ id ];
+ }
+ }
+ }
+});
+
+function evalScript( i, elem ) {
+ if ( elem.src ) {
+ jQuery.ajax({
+ url: elem.src,
+ async: false,
+ dataType: "script"
+ });
+ } else {
+ jQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || "" ).replace( rcleanScript, "/*$0*/" ) );
+ }
+
+ if ( elem.parentNode ) {
+ elem.parentNode.removeChild( elem );
+ }
+}
+
+
+
+var ralpha = /alpha\([^)]*\)/i,
+ ropacity = /opacity=([^)]*)/,
+ // fixed for IE9, see #8346
+ rupper = /([A-Z]|^ms)/g,
+ rnumpx = /^-?\d+(?:px)?$/i,
+ rnum = /^-?\d/,
+ rrelNum = /^[+\-]=/,
+ rrelNumFilter = /[^+\-\.\de]+/g,
+
+ cssShow = { position: "absolute", visibility: "hidden", display: "block" },
+ cssWidth = [ "Left", "Right" ],
+ cssHeight = [ "Top", "Bottom" ],
+ curCSS,
+
+ getComputedStyle,
+ currentStyle;
+
+jQuery.fn.css = function( name, value ) {
+ // Setting 'undefined' is a no-op
+ if ( arguments.length === 2 && value === undefined ) {
+ return this;
+ }
+
+ return jQuery.access( this, name, value, true, function( elem, name, value ) {
+ return value !== undefined ?
+ jQuery.style( elem, name, value ) :
+ jQuery.css( elem, name );
+ });
+};
+
+jQuery.extend({
+ // Add in style property hooks for overriding the default
+ // behavior of getting and setting a style property
+ cssHooks: {
+ opacity: {
+ get: function( elem, computed ) {
+ if ( computed ) {
+ // We should always get a number back from opacity
+ var ret = curCSS( elem, "opacity", "opacity" );
+ return ret === "" ? "1" : ret;
+
+ } else {
+ return elem.style.opacity;
+ }
+ }
+ }
+ },
+
+ // Exclude the following css properties to add px
+ cssNumber: {
+ "fillOpacity": true,
+ "fontWeight": true,
+ "lineHeight": true,
+ "opacity": true,
+ "orphans": true,
+ "widows": true,
+ "zIndex": true,
+ "zoom": true
+ },
+
+ // Add in properties whose names you wish to fix before
+ // setting or getting the value
+ cssProps: {
+ // normalize float css property
+ "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat"
+ },
+
+ // Get and set the style property on a DOM Node
+ style: function( elem, name, value, extra ) {
+ // Don't set styles on text and comment nodes
+ if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
+ return;
+ }
+
+ // Make sure that we're working with the right name
+ var ret, type, origName = jQuery.camelCase( name ),
+ style = elem.style, hooks = jQuery.cssHooks[ origName ];
+
+ name = jQuery.cssProps[ origName ] || origName;
+
+ // Check if we're setting a value
+ if ( value !== undefined ) {
+ type = typeof value;
+
+ // Make sure that NaN and null values aren't set. See: #7116
+ if ( type === "number" && isNaN( value ) || value == null ) {
+ return;
+ }
+
+ // convert relative number strings (+= or -=) to relative numbers. #7345
+ if ( type === "string" && rrelNum.test( value ) ) {
+ value = +value.replace( rrelNumFilter, "" ) + parseFloat( jQuery.css( elem, name ) );
+ // Fixes bug #9237
+ type = "number";
+ }
+
+ // If a number was passed in, add 'px' to the (except for certain CSS properties)
+ if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
+ value += "px";
+ }
+
+ // If a hook was provided, use that value, otherwise just set the specified value
+ if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value )) !== undefined ) {
+ // Wrapped to prevent IE from throwing errors when 'invalid' values are provided
+ // Fixes bug #5509
+ try {
+ style[ name ] = value;
+ } catch(e) {}
+ }
+
+ } else {
+ // If a hook was provided get the non-computed value from there
+ if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {
+ return ret;
+ }
+
+ // Otherwise just get the value from the style object
+ return style[ name ];
+ }
+ },
+
+ css: function( elem, name, extra ) {
+ var ret, hooks;
+
+ // Make sure that we're working with the right name
+ name = jQuery.camelCase( name );
+ hooks = jQuery.cssHooks[ name ];
+ name = jQuery.cssProps[ name ] || name;
+
+ // cssFloat needs a special treatment
+ if ( name === "cssFloat" ) {
+ name = "float";
+ }
+
+ // If a hook was provided get the computed value from there
+ if ( hooks && "get" in hooks && (ret = hooks.get( elem, true, extra )) !== undefined ) {
+ return ret;
+
+ // Otherwise, if a way to get the computed value exists, use that
+ } else if ( curCSS ) {
+ return curCSS( elem, name );
+ }
+ },
+
+ // A method for quickly swapping in/out CSS properties to get correct calculations
+ swap: function( elem, options, callback ) {
+ var old = {};
+
+ // Remember the old values, and insert the new ones
+ for ( var name in options ) {
+ old[ name ] = elem.style[ name ];
+ elem.style[ name ] = options[ name ];
+ }
+
+ callback.call( elem );
+
+ // Revert the old values
+ for ( name in options ) {
+ elem.style[ name ] = old[ name ];
+ }
+ }
+});
+
+// DEPRECATED, Use jQuery.css() instead
+jQuery.curCSS = jQuery.css;
+
+jQuery.each(["height", "width"], function( i, name ) {
+ jQuery.cssHooks[ name ] = {
+ get: function( elem, computed, extra ) {
+ var val;
+
+ if ( computed ) {
+ if ( elem.offsetWidth !== 0 ) {
+ return getWH( elem, name, extra );
+ } else {
+ jQuery.swap( elem, cssShow, function() {
+ val = getWH( elem, name, extra );
+ });
+ }
+
+ return val;
+ }
+ },
+
+ set: function( elem, value ) {
+ if ( rnumpx.test( value ) ) {
+ // ignore negative width and height values #1599
+ value = parseFloat( value );
+
+ if ( value >= 0 ) {
+ return value + "px";
+ }
+
+ } else {
+ return value;
+ }
+ }
+ };
+});
+
+if ( !jQuery.support.opacity ) {
+ jQuery.cssHooks.opacity = {
+ get: function( elem, computed ) {
+ // IE uses filters for opacity
+ return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ?
+ ( parseFloat( RegExp.$1 ) / 100 ) + "" :
+ computed ? "1" : "";
+ },
+
+ set: function( elem, value ) {
+ var style = elem.style,
+ currentStyle = elem.currentStyle;
+
+ // IE has trouble with opacity if it does not have layout
+ // Force it by setting the zoom level
+ style.zoom = 1;
+
+ // Set the alpha filter to set the opacity
+ var opacity = jQuery.isNaN( value ) ?
+ "" :
+ "alpha(opacity=" + value * 100 + ")",
+ filter = currentStyle && currentStyle.filter || style.filter || "";
+
+ style.filter = ralpha.test( filter ) ?
+ filter.replace( ralpha, opacity ) :
+ filter + " " + opacity;
+ }
+ };
+}
+
+jQuery(function() {
+ // This hook cannot be added until DOM ready because the support test
+ // for it is not run until after DOM ready
+ if ( !jQuery.support.reliableMarginRight ) {
+ jQuery.cssHooks.marginRight = {
+ get: function( elem, computed ) {
+ // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
+ // Work around by temporarily setting element display to inline-block
+ var ret;
+ jQuery.swap( elem, { "display": "inline-block" }, function() {
+ if ( computed ) {
+ ret = curCSS( elem, "margin-right", "marginRight" );
+ } else {
+ ret = elem.style.marginRight;
+ }
+ });
+ return ret;
+ }
+ };
+ }
+});
+
+if ( document.defaultView && document.defaultView.getComputedStyle ) {
+ getComputedStyle = function( elem, name ) {
+ var ret, defaultView, computedStyle;
+
+ name = name.replace( rupper, "-$1" ).toLowerCase();
+
+ if ( !(defaultView = elem.ownerDocument.defaultView) ) {
+ return undefined;
+ }
+
+ if ( (computedStyle = defaultView.getComputedStyle( elem, null )) ) {
+ ret = computedStyle.getPropertyValue( name );
+ if ( ret === "" && !jQuery.contains( elem.ownerDocument.documentElement, elem ) ) {
+ ret = jQuery.style( elem, name );
+ }
+ }
+
+ return ret;
+ };
+}
+
+if ( document.documentElement.currentStyle ) {
+ currentStyle = function( elem, name ) {
+ var left,
+ ret = elem.currentStyle && elem.currentStyle[ name ],
+ rsLeft = elem.runtimeStyle && elem.runtimeStyle[ name ],
+ style = elem.style;
+
+ // From the awesome hack by Dean Edwards
+ // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
+
+ // If we're not dealing with a regular pixel number
+ // but a number that has a weird ending, we need to convert it to pixels
+ if ( !rnumpx.test( ret ) && rnum.test( ret ) ) {
+ // Remember the original values
+ left = style.left;
+
+ // Put in the new values to get a computed value out
+ if ( rsLeft ) {
+ elem.runtimeStyle.left = elem.currentStyle.left;
+ }
+ style.left = name === "fontSize" ? "1em" : (ret || 0);
+ ret = style.pixelLeft + "px";
+
+ // Revert the changed values
+ style.left = left;
+ if ( rsLeft ) {
+ elem.runtimeStyle.left = rsLeft;
+ }
+ }
+
+ return ret === "" ? "auto" : ret;
+ };
+}
+
+curCSS = getComputedStyle || currentStyle;
+
+function getWH( elem, name, extra ) {
+
+ // Start with offset property
+ var val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
+ which = name === "width" ? cssWidth : cssHeight;
+
+ if ( val > 0 ) {
+ if ( extra !== "border" ) {
+ jQuery.each( which, function() {
+ if ( !extra ) {
+ val -= parseFloat( jQuery.css( elem, "padding" + this ) ) || 0;
+ }
+ if ( extra === "margin" ) {
+ val += parseFloat( jQuery.css( elem, extra + this ) ) || 0;
+ } else {
+ val -= parseFloat( jQuery.css( elem, "border" + this + "Width" ) ) || 0;
+ }
+ });
+ }
+
+ return val + "px";
+ }
+
+ // Fall back to computed then uncomputed css if necessary
+ val = curCSS( elem, name, name );
+ if ( val < 0 || val == null ) {
+ val = elem.style[ name ] || 0;
+ }
+ // Normalize "", auto, and prepare for extra
+ val = parseFloat( val ) || 0;
+
+ // Add padding, border, margin
+ if ( extra ) {
+ jQuery.each( which, function() {
+ val += parseFloat( jQuery.css( elem, "padding" + this ) ) || 0;
+ if ( extra !== "padding" ) {
+ val += parseFloat( jQuery.css( elem, "border" + this + "Width" ) ) || 0;
+ }
+ if ( extra === "margin" ) {
+ val += parseFloat( jQuery.css( elem, extra + this ) ) || 0;
+ }
+ });
+ }
+
+ return val + "px";
+}
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+ jQuery.expr.filters.hidden = function( elem ) {
+ var width = elem.offsetWidth,
+ height = elem.offsetHeight;
+
+ return (width === 0 && height === 0) || (!jQuery.support.reliableHiddenOffsets && (elem.style.display || jQuery.css( elem, "display" )) === "none");
+ };
+
+ jQuery.expr.filters.visible = function( elem ) {
+ return !jQuery.expr.filters.hidden( elem );
+ };
+}
+
+
+
+
+var r20 = /%20/g,
+ rbracket = /\[\]$/,
+ rCRLF = /\r?\n/g,
+ rhash = /#.*$/,
+ rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL
+ rinput = /^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,
+ // #7653, #8125, #8152: local protocol detection
+ rlocalProtocol = /^(?:about|app|app\-storage|.+\-extension|file|widget):$/,
+ rnoContent = /^(?:GET|HEAD)$/,
+ rprotocol = /^\/\//,
+ rquery = /\?/,
+ rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
+ rselectTextarea = /^(?:select|textarea)/i,
+ rspacesAjax = /\s+/,
+ rts = /([?&])_=[^&]*/,
+ rurl = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,
+
+ // Keep a copy of the old load method
+ _load = jQuery.fn.load,
+
+ /* Prefilters
+ * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
+ * 2) These are called:
+ * - BEFORE asking for a transport
+ * - AFTER param serialization (s.data is a string if s.processData is true)
+ * 3) key is the dataType
+ * 4) the catchall symbol "*" can be used
+ * 5) execution will start with transport dataType and THEN continue down to "*" if needed
+ */
+ prefilters = {},
+
+ /* Transports bindings
+ * 1) key is the dataType
+ * 2) the catchall symbol "*" can be used
+ * 3) selection will start with transport dataType and THEN go to "*" if needed
+ */
+ transports = {},
+
+ // Document location
+ ajaxLocation,
+
+ // Document location segments
+ ajaxLocParts;
+
+// #8138, IE may throw an exception when accessing
+// a field from window.location if document.domain has been set
+try {
+ ajaxLocation = location.href;
+} catch( e ) {
+ // Use the href attribute of an A element
+ // since IE will modify it given document.location
+ ajaxLocation = document.createElement( "a" );
+ ajaxLocation.href = "";
+ ajaxLocation = ajaxLocation.href;
+}
+
+// Segment location into parts
+ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
+
+// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
+function addToPrefiltersOrTransports( structure ) {
+
+ // dataTypeExpression is optional and defaults to "*"
+ return function( dataTypeExpression, func ) {
+
+ if ( typeof dataTypeExpression !== "string" ) {
+ func = dataTypeExpression;
+ dataTypeExpression = "*";
+ }
+
+ if ( jQuery.isFunction( func ) ) {
+ var dataTypes = dataTypeExpression.toLowerCase().split( rspacesAjax ),
+ i = 0,
+ length = dataTypes.length,
+ dataType,
+ list,
+ placeBefore;
+
+ // For each dataType in the dataTypeExpression
+ for(; i < length; i++ ) {
+ dataType = dataTypes[ i ];
+ // We control if we're asked to add before
+ // any existing element
+ placeBefore = /^\+/.test( dataType );
+ if ( placeBefore ) {
+ dataType = dataType.substr( 1 ) || "*";
+ }
+ list = structure[ dataType ] = structure[ dataType ] || [];
+ // then we add to the structure accordingly
+ list[ placeBefore ? "unshift" : "push" ]( func );
+ }
+ }
+ };
+}
+
+// Base inspection function for prefilters and transports
+function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR,
+ dataType /* internal */, inspected /* internal */ ) {
+
+ dataType = dataType || options.dataTypes[ 0 ];
+ inspected = inspected || {};
+
+ inspected[ dataType ] = true;
+
+ var list = structure[ dataType ],
+ i = 0,
+ length = list ? list.length : 0,
+ executeOnly = ( structure === prefilters ),
+ selection;
+
+ for(; i < length && ( executeOnly || !selection ); i++ ) {
+ selection = list[ i ]( options, originalOptions, jqXHR );
+ // If we got redirected to another dataType
+ // we try there if executing only and not done already
+ if ( typeof selection === "string" ) {
+ if ( !executeOnly || inspected[ selection ] ) {
+ selection = undefined;
+ } else {
+ options.dataTypes.unshift( selection );
+ selection = inspectPrefiltersOrTransports(
+ structure, options, originalOptions, jqXHR, selection, inspected );
+ }
+ }
+ }
+ // If we're only executing or nothing was selected
+ // we try the catchall dataType if not done already
+ if ( ( executeOnly || !selection ) && !inspected[ "*" ] ) {
+ selection = inspectPrefiltersOrTransports(
+ structure, options, originalOptions, jqXHR, "*", inspected );
+ }
+ // unnecessary when only executing (prefilters)
+ // but it'll be ignored by the caller in that case
+ return selection;
+}
+
+jQuery.fn.extend({
+ load: function( url, params, callback ) {
+ if ( typeof url !== "string" && _load ) {
+ return _load.apply( this, arguments );
+
+ // Don't do a request if no elements are being requested
+ } else if ( !this.length ) {
+ return this;
+ }
+
+ var off = url.indexOf( " " );
+ if ( off >= 0 ) {
+ var selector = url.slice( off, url.length );
+ url = url.slice( 0, off );
+ }
+
+ // Default to a GET request
+ var type = "GET";
+
+ // If the second parameter was provided
+ if ( params ) {
+ // If it's a function
+ if ( jQuery.isFunction( params ) ) {
+ // We assume that it's the callback
+ callback = params;
+ params = undefined;
+
+ // Otherwise, build a param string
+ } else if ( typeof params === "object" ) {
+ params = jQuery.param( params, jQuery.ajaxSettings.traditional );
+ type = "POST";
+ }
+ }
+
+ var self = this;
+
+ // Request the remote document
+ jQuery.ajax({
+ url: url,
+ type: type,
+ dataType: "html",
+ data: params,
+ // Complete callback (responseText is used internally)
+ complete: function( jqXHR, status, responseText ) {
+ // Store the response as specified by the jqXHR object
+ responseText = jqXHR.responseText;
+ // If successful, inject the HTML into all the matched elements
+ if ( jqXHR.isResolved() ) {
+ // #4825: Get the actual response in case
+ // a dataFilter is present in ajaxSettings
+ jqXHR.done(function( r ) {
+ responseText = r;
+ });
+ // See if a selector was specified
+ self.html( selector ?
+ // Create a dummy div to hold the results
+ jQuery("<div>")
+ // inject the contents of the document in, removing the scripts
+ // to avoid any 'Permission Denied' errors in IE
+ .append(responseText.replace(rscript, ""))
+
+ // Locate the specified elements
+ .find(selector) :
+
+ // If not, just inject the full result
+ responseText );
+ }
+
+ if ( callback ) {
+ self.each( callback, [ responseText, status, jqXHR ] );
+ }
+ }
+ });
+
+ return this;
+ },
+
+ serialize: function() {
+ return jQuery.param( this.serializeArray() );
+ },
+
+ serializeArray: function() {
+ return this.map(function(){
+ return this.elements ? jQuery.makeArray( this.elements ) : this;
+ })
+ .filter(function(){
+ return this.name && !this.disabled &&
+ ( this.checked || rselectTextarea.test( this.nodeName ) ||
+ rinput.test( this.type ) );
+ })
+ .map(function( i, elem ){
+ var val = jQuery( this ).val();
+
+ return val == null ?
+ null :
+ jQuery.isArray( val ) ?
+ jQuery.map( val, function( val, i ){
+ return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+ }) :
+ { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
+ }).get();
+ }
+});
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( "ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split( " " ), function( i, o ){
+ jQuery.fn[ o ] = function( f ){
+ return this.bind( o, f );
+ };
+});
+
+jQuery.each( [ "get", "post" ], function( i, method ) {
+ jQuery[ method ] = function( url, data, callback, type ) {
+ // shift arguments if data argument was omitted
+ if ( jQuery.isFunction( data ) ) {
+ type = type || callback;
+ callback = data;
+ data = undefined;
+ }
+
+ return jQuery.ajax({
+ type: method,
+ url: url,
+ data: data,
+ success: callback,
+ dataType: type
+ });
+ };
+});
+
+jQuery.extend({
+
+ getScript: function( url, callback ) {
+ return jQuery.get( url, undefined, callback, "script" );
+ },
+
+ getJSON: function( url, data, callback ) {
+ return jQuery.get( url, data, callback, "json" );
+ },
+
+ // Creates a full fledged settings object into target
+ // with both ajaxSettings and settings fields.
+ // If target is omitted, writes into ajaxSettings.
+ ajaxSetup: function ( target, settings ) {
+ if ( !settings ) {
+ // Only one parameter, we extend ajaxSettings
+ settings = target;
+ target = jQuery.extend( true, jQuery.ajaxSettings, settings );
+ } else {
+ // target was provided, we extend into it
+ jQuery.extend( true, target, jQuery.ajaxSettings, settings );
+ }
+ // Flatten fields we don't want deep extended
+ for( var field in { context: 1, url: 1 } ) {
+ if ( field in settings ) {
+ target[ field ] = settings[ field ];
+ } else if( field in jQuery.ajaxSettings ) {
+ target[ field ] = jQuery.ajaxSettings[ field ];
+ }
+ }
+ return target;
+ },
+
+ ajaxSettings: {
+ url: ajaxLocation,
+ isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),
+ global: true,
+ type: "GET",
+ contentType: "application/x-www-form-urlencoded",
+ processData: true,
+ async: true,
+ /*
+ timeout: 0,
+ data: null,
+ dataType: null,
+ username: null,
+ password: null,
+ cache: null,
+ traditional: false,
+ headers: {},
+ */
+
+ accepts: {
+ xml: "application/xml, text/xml",
+ html: "text/html",
+ text: "text/plain",
+ json: "application/json, text/javascript",
+ "*": "*/*"
+ },
+
+ contents: {
+ xml: /xml/,
+ html: /html/,
+ json: /json/
+ },
+
+ responseFields: {
+ xml: "responseXML",
+ text: "responseText"
+ },
+
+ // List of data converters
+ // 1) key format is "source_type destination_type" (a single space in-between)
+ // 2) the catchall symbol "*" can be used for source_type
+ converters: {
+
+ // Convert anything to text
+ "* text": window.String,
+
+ // Text to html (true = no transformation)
+ "text html": true,
+
+ // Evaluate text as a json expression
+ "text json": jQuery.parseJSON,
+
+ // Parse text as xml
+ "text xml": jQuery.parseXML
+ }
+ },
+
+ ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
+ ajaxTransport: addToPrefiltersOrTransports( transports ),
+
+ // Main method
+ ajax: function( url, options ) {
+
+ // If url is an object, simulate pre-1.5 signature
+ if ( typeof url === "object" ) {
+ options = url;
+ url = undefined;
+ }
+
+ // Force options to be an object
+ options = options || {};
+
+ var // Create the final options object
+ s = jQuery.ajaxSetup( {}, options ),
+ // Callbacks context
+ callbackContext = s.context || s,
+ // Context for global events
+ // It's the callbackContext if one was provided in the options
+ // and if it's a DOM node or a jQuery collection
+ globalEventContext = callbackContext !== s &&
+ ( callbackContext.nodeType || callbackContext instanceof jQuery ) ?
+ jQuery( callbackContext ) : jQuery.event,
+ // Deferreds
+ deferred = jQuery.Deferred(),
+ completeDeferred = jQuery._Deferred(),
+ // Status-dependent callbacks
+ statusCode = s.statusCode || {},
+ // ifModified key
+ ifModifiedKey,
+ // Headers (they are sent all at once)
+ requestHeaders = {},
+ requestHeadersNames = {},
+ // Response headers
+ responseHeadersString,
+ responseHeaders,
+ // transport
+ transport,
+ // timeout handle
+ timeoutTimer,
+ // Cross-domain detection vars
+ parts,
+ // The jqXHR state
+ state = 0,
+ // To know if global events are to be dispatched
+ fireGlobals,
+ // Loop variable
+ i,
+ // Fake xhr
+ jqXHR = {
+
+ readyState: 0,
+
+ // Caches the header
+ setRequestHeader: function( name, value ) {
+ if ( !state ) {
+ var lname = name.toLowerCase();
+ name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
+ requestHeaders[ name ] = value;
+ }
+ return this;
+ },
+
+ // Raw string
+ getAllResponseHeaders: function() {
+ return state === 2 ? responseHeadersString : null;
+ },
+
+ // Builds headers hashtable if needed
+ getResponseHeader: function( key ) {
+ var match;
+ if ( state === 2 ) {
+ if ( !responseHeaders ) {
+ responseHeaders = {};
+ while( ( match = rheaders.exec( responseHeadersString ) ) ) {
+ responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];
+ }
+ }
+ match = responseHeaders[ key.toLowerCase() ];
+ }
+ return match === undefined ? null : match;
+ },
+
+ // Overrides response content-type header
+ overrideMimeType: function( type ) {
+ if ( !state ) {
+ s.mimeType = type;
+ }
+ return this;
+ },
+
+ // Cancel the request
+ abort: function( statusText ) {
+ statusText = statusText || "abort";
+ if ( transport ) {
+ transport.abort( statusText );
+ }
+ done( 0, statusText );
+ return this;
+ }
+ };
+
+ // Callback for when everything is done
+ // It is defined here because jslint complains if it is declared
+ // at the end of the function (which would be more logical and readable)
+ function done( status, statusText, responses, headers ) {
+
+ // Called once
+ if ( state === 2 ) {
+ return;
+ }
+
+ // State is "done" now
+ state = 2;
+
+ // Clear timeout if it exists
+ if ( timeoutTimer ) {
+ clearTimeout( timeoutTimer );
+ }
+
+ // Dereference transport for early garbage collection
+ // (no matter how long the jqXHR object will be used)
+ transport = undefined;
+
+ // Cache response headers
+ responseHeadersString = headers || "";
+
+ // Set readyState
+ jqXHR.readyState = status ? 4 : 0;
+
+ var isSuccess,
+ success,
+ error,
+ response = responses ? ajaxHandleResponses( s, jqXHR, responses ) : undefined,
+ lastModified,
+ etag;
+
+ // If successful, handle type chaining
+ if ( status >= 200 && status < 300 || status === 304 ) {
+
+ // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+ if ( s.ifModified ) {
+
+ if ( ( lastModified = jqXHR.getResponseHeader( "Last-Modified" ) ) ) {
+ jQuery.lastModified[ ifModifiedKey ] = lastModified;
+ }
+ if ( ( etag = jqXHR.getResponseHeader( "Etag" ) ) ) {
+ jQuery.etag[ ifModifiedKey ] = etag;
+ }
+ }
+
+ // If not modified
+ if ( status === 304 ) {
+
+ statusText = "notmodified";
+ isSuccess = true;
+
+ // If we have data
+ } else {
+
+ try {
+ success = ajaxConvert( s, response );
+ statusText = "success";
+ isSuccess = true;
+ } catch(e) {
+ // We have a parsererror
+ statusText = "parsererror";
+ error = e;
+ }
+ }
+ } else {
+ // We extract error from statusText
+ // then normalize statusText and status for non-aborts
+ error = statusText;
+ if( !statusText || status ) {
+ statusText = "error";
+ if ( status < 0 ) {
+ status = 0;
+ }
+ }
+ }
+
+ // Set data for the fake xhr object
+ jqXHR.status = status;
+ jqXHR.statusText = statusText;
+
+ // Success/Error
+ if ( isSuccess ) {
+ deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+ } else {
+ deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+ }
+
+ // Status-dependent callbacks
+ jqXHR.statusCode( statusCode );
+ statusCode = undefined;
+
+ if ( fireGlobals ) {
+ globalEventContext.trigger( "ajax" + ( isSuccess ? "Success" : "Error" ),
+ [ jqXHR, s, isSuccess ? success : error ] );
+ }
+
+ // Complete
+ completeDeferred.resolveWith( callbackContext, [ jqXHR, statusText ] );
+
+ if ( fireGlobals ) {
+ globalEventContext.trigger( "ajaxComplete", [ jqXHR, s] );
+ // Handle the global AJAX counter
+ if ( !( --jQuery.active ) ) {
+ jQuery.event.trigger( "ajaxStop" );
+ }
+ }
+ }
+
+ // Attach deferreds
+ deferred.promise( jqXHR );
+ jqXHR.success = jqXHR.done;
+ jqXHR.error = jqXHR.fail;
+ jqXHR.complete = completeDeferred.done;
+
+ // Status-dependent callbacks
+ jqXHR.statusCode = function( map ) {
+ if ( map ) {
+ var tmp;
+ if ( state < 2 ) {
+ for( tmp in map ) {
+ statusCode[ tmp ] = [ statusCode[tmp], map[tmp] ];
+ }
+ } else {
+ tmp = map[ jqXHR.status ];
+ jqXHR.then( tmp, tmp );
+ }
+ }
+ return this;
+ };
+
+ // Remove hash character (#7531: and string promotion)
+ // Add protocol if not provided (#5866: IE7 issue with protocol-less urls)
+ // We also use the url parameter if available
+ s.url = ( ( url || s.url ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
+
+ // Extract dataTypes list
+ s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().split( rspacesAjax );
+
+ // Determine if a cross-domain request is in order
+ if ( s.crossDomain == null ) {
+ parts = rurl.exec( s.url.toLowerCase() );
+ s.crossDomain = !!( parts &&
+ ( parts[ 1 ] != ajaxLocParts[ 1 ] || parts[ 2 ] != ajaxLocParts[ 2 ] ||
+ ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) !=
+ ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) )
+ );
+ }
+
+ // Convert data if not already a string
+ if ( s.data && s.processData && typeof s.data !== "string" ) {
+ s.data = jQuery.param( s.data, s.traditional );
+ }
+
+ // Apply prefilters
+ inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+ // If request was aborted inside a prefiler, stop there
+ if ( state === 2 ) {
+ return false;
+ }
+
+ // We can fire global events as of now if asked to
+ fireGlobals = s.global;
+
+ // Uppercase the type
+ s.type = s.type.toUpperCase();
+
+ // Determine if request has content
+ s.hasContent = !rnoContent.test( s.type );
+
+ // Watch for a new set of requests
+ if ( fireGlobals && jQuery.active++ === 0 ) {
+ jQuery.event.trigger( "ajaxStart" );
+ }
+
+ // More options handling for requests with no content
+ if ( !s.hasContent ) {
+
+ // If data is available, append data to url
+ if ( s.data ) {
+ s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.data;
+ }
+
+ // Get ifModifiedKey before adding the anti-cache parameter
+ ifModifiedKey = s.url;
+
+ // Add anti-cache in url if needed
+ if ( s.cache === false ) {
+
+ var ts = jQuery.now(),
+ // try replacing _= if it is there
+ ret = s.url.replace( rts, "$1_=" + ts );
+
+ // if nothing was replaced, add timestamp to the end
+ s.url = ret + ( (ret === s.url ) ? ( rquery.test( s.url ) ? "&" : "?" ) + "_=" + ts : "" );
+ }
+ }
+
+ // Set the correct header, if data is being sent
+ if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+ jqXHR.setRequestHeader( "Content-Type", s.contentType );
+ }
+
+ // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+ if ( s.ifModified ) {
+ ifModifiedKey = ifModifiedKey || s.url;
+ if ( jQuery.lastModified[ ifModifiedKey ] ) {
+ jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ ifModifiedKey ] );
+ }
+ if ( jQuery.etag[ ifModifiedKey ] ) {
+ jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ ifModifiedKey ] );
+ }
+ }
+
+ // Set the Accepts header for the server, depending on the dataType
+ jqXHR.setRequestHeader(
+ "Accept",
+ s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
+ s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", */*; q=0.01" : "" ) :
+ s.accepts[ "*" ]
+ );
+
+ // Check for headers option
+ for ( i in s.headers ) {
+ jqXHR.setRequestHeader( i, s.headers[ i ] );
+ }
+
+ // Allow custom headers/mimetypes and early abort
+ if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
+ // Abort if not done already
+ jqXHR.abort();
+ return false;
+
+ }
+
+ // Install callbacks on deferreds
+ for ( i in { success: 1, error: 1, complete: 1 } ) {
+ jqXHR[ i ]( s[ i ] );
+ }
+
+ // Get transport
+ transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+ // If no transport, we auto-abort
+ if ( !transport ) {
+ done( -1, "No Transport" );
+ } else {
+ jqXHR.readyState = 1;
+ // Send global event
+ if ( fireGlobals ) {
+ globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+ }
+ // Timeout
+ if ( s.async && s.timeout > 0 ) {
+ timeoutTimer = setTimeout( function(){
+ jqXHR.abort( "timeout" );
+ }, s.timeout );
+ }
+
+ try {
+ state = 1;
+ transport.send( requestHeaders, done );
+ } catch (e) {
+ // Propagate exception as error if not done
+ if ( status < 2 ) {
+ done( -1, e );
+ // Simply rethrow otherwise
+ } else {
+ jQuery.error( e );
+ }
+ }
+ }
+
+ return jqXHR;
+ },
+
+ // Serialize an array of form elements or a set of
+ // key/values into a query string
+ param: function( a, traditional ) {
+ var s = [],
+ add = function( key, value ) {
+ // If value is a function, invoke it and return its value
+ value = jQuery.isFunction( value ) ? value() : value;
+ s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
+ };
+
+ // Set traditional to true for jQuery <= 1.3.2 behavior.
+ if ( traditional === undefined ) {
+ traditional = jQuery.ajaxSettings.traditional;
+ }
+
+ // If an array was passed in, assume that it is an array of form elements.
+ if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
+ // Serialize the form elements
+ jQuery.each( a, function() {
+ add( this.name, this.value );
+ });
+
+ } else {
+ // If traditional, encode the "old" way (the way 1.3.2 or older
+ // did it), otherwise encode params recursively.
+ for ( var prefix in a ) {
+ buildParams( prefix, a[ prefix ], traditional, add );
+ }
+ }
+
+ // Return the resulting serialization
+ return s.join( "&" ).replace( r20, "+" );
+ }
+});
+
+function buildParams( prefix, obj, traditional, add ) {
+ if ( jQuery.isArray( obj ) ) {
+ // Serialize array item.
+ jQuery.each( obj, function( i, v ) {
+ if ( traditional || rbracket.test( prefix ) ) {
+ // Treat each array item as a scalar.
+ add( prefix, v );
+
+ } else {
+ // If array item is non-scalar (array or object), encode its
+ // numeric index to resolve deserialization ambiguity issues.
+ // Note that rack (as of 1.0.0) can't currently deserialize
+ // nested arrays properly, and attempting to do so may cause
+ // a server error. Possible fixes are to modify rack's
+ // deserialization algorithm or to provide an option or flag
+ // to force array serialization to be shallow.
+ buildParams( prefix + "[" + ( typeof v === "object" || jQuery.isArray(v) ? i : "" ) + "]", v, traditional, add );
+ }
+ });
+
+ } else if ( !traditional && obj != null && typeof obj === "object" ) {
+ // Serialize object item.
+ for ( var name in obj ) {
+ buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
+ }
+
+ } else {
+ // Serialize scalar item.
+ add( prefix, obj );
+ }
+}
+
+// This is still on the jQuery object... for now
+// Want to move this to jQuery.ajax some day
+jQuery.extend({
+
+ // Counter for holding the number of active queries
+ active: 0,
+
+ // Last-Modified header cache for next request
+ lastModified: {},
+ etag: {}
+
+});
+
+/* Handles responses to an ajax request:
+ * - sets all responseXXX fields accordingly
+ * - finds the right dataType (mediates between content-type and expected dataType)
+ * - returns the corresponding response
+ */
+function ajaxHandleResponses( s, jqXHR, responses ) {
+
+ var contents = s.contents,
+ dataTypes = s.dataTypes,
+ responseFields = s.responseFields,
+ ct,
+ type,
+ finalDataType,
+ firstDataType;
+
+ // Fill responseXXX fields
+ for( type in responseFields ) {
+ if ( type in responses ) {
+ jqXHR[ responseFields[type] ] = responses[ type ];
+ }
+ }
+
+ // Remove auto dataType and get content-type in the process
+ while( dataTypes[ 0 ] === "*" ) {
+ dataTypes.shift();
+ if ( ct === undefined ) {
+ ct = s.mimeType || jqXHR.getResponseHeader( "content-type" );
+ }
+ }
+
+ // Check if we're dealing with a known content-type
+ if ( ct ) {
+ for ( type in contents ) {
+ if ( contents[ type ] && contents[ type ].test( ct ) ) {
+ dataTypes.unshift( type );
+ break;
+ }
+ }
+ }
+
+ // Check to see if we have a response for the expected dataType
+ if ( dataTypes[ 0 ] in responses ) {
+ finalDataType = dataTypes[ 0 ];
+ } else {
+ // Try convertible dataTypes
+ for ( type in responses ) {
+ if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) {
+ finalDataType = type;
+ break;
+ }
+ if ( !firstDataType ) {
+ firstDataType = type;
+ }
+ }
+ // Or just use first one
+ finalDataType = finalDataType || firstDataType;
+ }
+
+ // If we found a dataType
+ // We add the dataType to the list if needed
+ // and return the corresponding response
+ if ( finalDataType ) {
+ if ( finalDataType !== dataTypes[ 0 ] ) {
+ dataTypes.unshift( finalDataType );
+ }
+ return responses[ finalDataType ];
+ }
+}
+
+// Chain conversions given the request and the original response
+function ajaxConvert( s, response ) {
+
+ // Apply the dataFilter if provided
+ if ( s.dataFilter ) {
+ response = s.dataFilter( response, s.dataType );
+ }
+
+ var dataTypes = s.dataTypes,
+ converters = {},
+ i,
+ key,
+ length = dataTypes.length,
+ tmp,
+ // Current and previous dataTypes
+ current = dataTypes[ 0 ],
+ prev,
+ // Conversion expression
+ conversion,
+ // Conversion function
+ conv,
+ // Conversion functions (transitive conversion)
+ conv1,
+ conv2;
+
+ // For each dataType in the chain
+ for( i = 1; i < length; i++ ) {
+
+ // Create converters map
+ // with lowercased keys
+ if ( i === 1 ) {
+ for( key in s.converters ) {
+ if( typeof key === "string" ) {
+ converters[ key.toLowerCase() ] = s.converters[ key ];
+ }
+ }
+ }
+
+ // Get the dataTypes
+ prev = current;
+ current = dataTypes[ i ];
+
+ // If current is auto dataType, update it to prev
+ if( current === "*" ) {
+ current = prev;
+ // If no auto and dataTypes are actually different
+ } else if ( prev !== "*" && prev !== current ) {
+
+ // Get the converter
+ conversion = prev + " " + current;
+ conv = converters[ conversion ] || converters[ "* " + current ];
+
+ // If there is no direct converter, search transitively
+ if ( !conv ) {
+ conv2 = undefined;
+ for( conv1 in converters ) {
+ tmp = conv1.split( " " );
+ if ( tmp[ 0 ] === prev || tmp[ 0 ] === "*" ) {
+ conv2 = converters[ tmp[1] + " " + current ];
+ if ( conv2 ) {
+ conv1 = converters[ conv1 ];
+ if ( conv1 === true ) {
+ conv = conv2;
+ } else if ( conv2 === true ) {
+ conv = conv1;
+ }
+ break;
+ }
+ }
+ }
+ }
+ // If we found no converter, dispatch an error
+ if ( !( conv || conv2 ) ) {
+ jQuery.error( "No conversion from " + conversion.replace(" "," to ") );
+ }
+ // If found converter is not an equivalence
+ if ( conv !== true ) {
+ // Convert with 1 or 2 converters accordingly
+ response = conv ? conv( response ) : conv2( conv1(response) );
+ }
+ }
+ }
+ return response;
+}
+
+
+
+
+var jsc = jQuery.now(),
+ jsre = /(\=)\?(&|$)|\?\?/i;
+
+// Default jsonp settings
+jQuery.ajaxSetup({
+ jsonp: "callback",
+ jsonpCallback: function() {
+ return jQuery.expando + "_" + ( jsc++ );
+ }
+});
+
+// Detect, normalize options and install callbacks for jsonp requests
+jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
+
+ var inspectData = s.contentType === "application/x-www-form-urlencoded" &&
+ ( typeof s.data === "string" );
+
+ if ( s.dataTypes[ 0 ] === "jsonp" ||
+ s.jsonp !== false && ( jsre.test( s.url ) ||
+ inspectData && jsre.test( s.data ) ) ) {
+
+ var responseContainer,
+ jsonpCallback = s.jsonpCallback =
+ jQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback,
+ previous = window[ jsonpCallback ],
+ url = s.url,
+ data = s.data,
+ replace = "$1" + jsonpCallback + "$2";
+
+ if ( s.jsonp !== false ) {
+ url = url.replace( jsre, replace );
+ if ( s.url === url ) {
+ if ( inspectData ) {
+ data = data.replace( jsre, replace );
+ }
+ if ( s.data === data ) {
+ // Add callback manually
+ url += (/\?/.test( url ) ? "&" : "?") + s.jsonp + "=" + jsonpCallback;
+ }
+ }
+ }
+
+ s.url = url;
+ s.data = data;
+
+ // Install callback
+ window[ jsonpCallback ] = function( response ) {
+ responseContainer = [ response ];
+ };
+
+ // Clean-up function
+ jqXHR.always(function() {
+ // Set callback back to previous value
+ window[ jsonpCallback ] = previous;
+ // Call if it was a function and we have a response
+ if ( responseContainer && jQuery.isFunction( previous ) ) {
+ window[ jsonpCallback ]( responseContainer[ 0 ] );
+ }
+ });
+
+ // Use data converter to retrieve json after script execution
+ s.converters["script json"] = function() {
+ if ( !responseContainer ) {
+ jQuery.error( jsonpCallback + " was not called" );
+ }
+ return responseContainer[ 0 ];
+ };
+
+ // force json dataType
+ s.dataTypes[ 0 ] = "json";
+
+ // Delegate to script
+ return "script";
+ }
+});
+
+
+
+
+// Install script dataType
+jQuery.ajaxSetup({
+ accepts: {
+ script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
+ },
+ contents: {
+ script: /javascript|ecmascript/
+ },
+ converters: {
+ "text script": function( text ) {
+ jQuery.globalEval( text );
+ return text;
+ }
+ }
+});
+
+// Handle cache's special case and global
+jQuery.ajaxPrefilter( "script", function( s ) {
+ if ( s.cache === undefined ) {
+ s.cache = false;
+ }
+ if ( s.crossDomain ) {
+ s.type = "GET";
+ s.global = false;
+ }
+});
+
+// Bind script tag hack transport
+jQuery.ajaxTransport( "script", function(s) {
+
+ // This transport only deals with cross domain requests
+ if ( s.crossDomain ) {
+
+ var script,
+ head = document.head || document.getElementsByTagName( "head" )[0] || document.documentElement;
+
+ return {
+
+ send: function( _, callback ) {
+
+ script = document.createElement( "script" );
+
+ script.async = "async";
+
+ if ( s.scriptCharset ) {
+ script.charset = s.scriptCharset;
+ }
+
+ script.src = s.url;
+
+ // Attach handlers for all browsers
+ script.onload = script.onreadystatechange = function( _, isAbort ) {
+
+ if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
+
+ // Handle memory leak in IE
+ script.onload = script.onreadystatechange = null;
+
+ // Remove the script
+ if ( head && script.parentNode ) {
+ head.removeChild( script );
+ }
+
+ // Dereference the script
+ script = undefined;
+
+ // Callback if not abort
+ if ( !isAbort ) {
+ callback( 200, "success" );
+ }
+ }
+ };
+ // Use insertBefore instead of appendChild to circumvent an IE6 bug.
+ // This arises when a base node is used (#2709 and #4378).
+ head.insertBefore( script, head.firstChild );
+ },
+
+ abort: function() {
+ if ( script ) {
+ script.onload( 0, 1 );
+ }
+ }
+ };
+ }
+});
+
+
+
+
+var // #5280: Internet Explorer will keep connections alive if we don't abort on unload
+ xhrOnUnloadAbort = window.ActiveXObject ? function() {
+ // Abort all pending requests
+ for ( var key in xhrCallbacks ) {
+ xhrCallbacks[ key ]( 0, 1 );
+ }
+ } : false,
+ xhrId = 0,
+ xhrCallbacks;
+
+// Functions to create xhrs
+function createStandardXHR() {
+ try {
+ return new window.XMLHttpRequest();
+ } catch( e ) {}
+}
+
+function createActiveXHR() {
+ try {
+ return new window.ActiveXObject( "Microsoft.XMLHTTP" );
+ } catch( e ) {}
+}
+
+// Create the request object
+// (This is still attached to ajaxSettings for backward compatibility)
+jQuery.ajaxSettings.xhr = window.ActiveXObject ?
+ /* Microsoft failed to properly
+ * implement the XMLHttpRequest in IE7 (can't request local files),
+ * so we use the ActiveXObject when it is available
+ * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
+ * we need a fallback.
+ */
+ function() {
+ return !this.isLocal && createStandardXHR() || createActiveXHR();
+ } :
+ // For all other browsers, use the standard XMLHttpRequest object
+ createStandardXHR;
+
+// Determine support properties
+(function( xhr ) {
+ jQuery.extend( jQuery.support, {
+ ajax: !!xhr,
+ cors: !!xhr && ( "withCredentials" in xhr )
+ });
+})( jQuery.ajaxSettings.xhr() );
+
+// Create transport if the browser can provide an xhr
+if ( jQuery.support.ajax ) {
+
+ jQuery.ajaxTransport(function( s ) {
+ // Cross domain only allowed if supported through XMLHttpRequest
+ if ( !s.crossDomain || jQuery.support.cors ) {
+
+ var callback;
+
+ return {
+ send: function( headers, complete ) {
+
+ // Get a new xhr
+ var xhr = s.xhr(),
+ handle,
+ i;
+
+ // Open the socket
+ // Passing null username, generates a login popup on Opera (#2865)
+ if ( s.username ) {
+ xhr.open( s.type, s.url, s.async, s.username, s.password );
+ } else {
+ xhr.open( s.type, s.url, s.async );
+ }
+
+ // Apply custom fields if provided
+ if ( s.xhrFields ) {
+ for ( i in s.xhrFields ) {
+ xhr[ i ] = s.xhrFields[ i ];
+ }
+ }
+
+ // Override mime type if needed
+ if ( s.mimeType && xhr.overrideMimeType ) {
+ xhr.overrideMimeType( s.mimeType );
+ }
+
+ // X-Requested-With header
+ // For cross-domain requests, seeing as conditions for a preflight are
+ // akin to a jigsaw puzzle, we simply never set it to be sure.
+ // (it can always be set on a per-request basis or even using ajaxSetup)
+ // For same-domain requests, won't change header if already provided.
+ if ( !s.crossDomain && !headers["X-Requested-With"] ) {
+ headers[ "X-Requested-With" ] = "XMLHttpRequest";
+ }
+
+ // Need an extra try/catch for cross domain requests in Firefox 3
+ try {
+ for ( i in headers ) {
+ xhr.setRequestHeader( i, headers[ i ] );
+ }
+ } catch( _ ) {}
+
+ // Do send the request
+ // This may raise an exception which is actually
+ // handled in jQuery.ajax (so no try/catch here)
+ xhr.send( ( s.hasContent && s.data ) || null );
+
+ // Listener
+ callback = function( _, isAbort ) {
+
+ var status,
+ statusText,
+ responseHeaders,
+ responses,
+ xml;
+
+ // Firefox throws exceptions when accessing properties
+ // of an xhr when a network error occured
+ // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
+ try {
+
+ // Was never called and is aborted or complete
+ if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
+
+ // Only called once
+ callback = undefined;
+
+ // Do not keep as active anymore
+ if ( handle ) {
+ xhr.onreadystatechange = jQuery.noop;
+ if ( xhrOnUnloadAbort ) {
+ delete xhrCallbacks[ handle ];
+ }
+ }
+
+ // If it's an abort
+ if ( isAbort ) {
+ // Abort it manually if needed
+ if ( xhr.readyState !== 4 ) {
+ xhr.abort();
+ }
+ } else {
+ status = xhr.status;
+ responseHeaders = xhr.getAllResponseHeaders();
+ responses = {};
+ xml = xhr.responseXML;
+
+ // Construct response list
+ if ( xml && xml.documentElement /* #4958 */ ) {
+ responses.xml = xml;
+ }
+ responses.text = xhr.responseText;
+
+ // Firefox throws an exception when accessing
+ // statusText for faulty cross-domain requests
+ try {
+ statusText = xhr.statusText;
+ } catch( e ) {
+ // We normalize with Webkit giving an empty statusText
+ statusText = "";
+ }
+
+ // Filter status for non standard behaviors
+
+ // If the request is local and we have data: assume a success
+ // (success with no data won't get notified, that's the best we
+ // can do given current implementations)
+ if ( !status && s.isLocal && !s.crossDomain ) {
+ status = responses.text ? 200 : 404;
+ // IE - #1450: sometimes returns 1223 when it should be 204
+ } else if ( status === 1223 ) {
+ status = 204;
+ }
+ }
+ }
+ } catch( firefoxAccessException ) {
+ if ( !isAbort ) {
+ complete( -1, firefoxAccessException );
+ }
+ }
+
+ // Call complete if needed
+ if ( responses ) {
+ complete( status, statusText, responses, responseHeaders );
+ }
+ };
+
+ // if we're in sync mode or it's in cache
+ // and has been retrieved directly (IE6 & IE7)
+ // we need to manually fire the callback
+ if ( !s.async || xhr.readyState === 4 ) {
+ callback();
+ } else {
+ handle = ++xhrId;
+ if ( xhrOnUnloadAbort ) {
+ // Create the active xhrs callbacks list if needed
+ // and attach the unload handler
+ if ( !xhrCallbacks ) {
+ xhrCallbacks = {};
+ jQuery( window ).unload( xhrOnUnloadAbort );
+ }
+ // Add to list of active xhrs callbacks
+ xhrCallbacks[ handle ] = callback;
+ }
+ xhr.onreadystatechange = callback;
+ }
+ },
+
+ abort: function() {
+ if ( callback ) {
+ callback(0,1);
+ }
+ }
+ };
+ }
+ });
+}
+
+
+
+
+var elemdisplay = {},
+ iframe, iframeDoc,
+ rfxtypes = /^(?:toggle|show|hide)$/,
+ rfxnum = /^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,
+ timerId,
+ fxAttrs = [
+ // height animations
+ [ "height", "marginTop", "marginBottom", "paddingTop", "paddingBottom" ],
+ // width animations
+ [ "width", "marginLeft", "marginRight", "paddingLeft", "paddingRight" ],
+ // opacity animations
+ [ "opacity" ]
+ ],
+ fxNow,
+ requestAnimationFrame = window.webkitRequestAnimationFrame ||
+ window.mozRequestAnimationFrame ||
+ window.oRequestAnimationFrame;
+
+jQuery.fn.extend({
+ show: function( speed, easing, callback ) {
+ var elem, display;
+
+ if ( speed || speed === 0 ) {
+ return this.animate( genFx("show", 3), speed, easing, callback);
+
+ } else {
+ for ( var i = 0, j = this.length; i < j; i++ ) {
+ elem = this[i];
+
+ if ( elem.style ) {
+ display = elem.style.display;
+
+ // Reset the inline display of this element to learn if it is
+ // being hidden by cascaded rules or not
+ if ( !jQuery._data(elem, "olddisplay") && display === "none" ) {
+ display = elem.style.display = "";
+ }
+
+ // Set elements which have been overridden with display: none
+ // in a stylesheet to whatever the default browser style is
+ // for such an element
+ if ( display === "" && jQuery.css( elem, "display" ) === "none" ) {
+ jQuery._data(elem, "olddisplay", defaultDisplay(elem.nodeName));
+ }
+ }
+ }
+
+ // Set the display of most of the elements in a second loop
+ // to avoid the constant reflow
+ for ( i = 0; i < j; i++ ) {
+ elem = this[i];
+
+ if ( elem.style ) {
+ display = elem.style.display;
+
+ if ( display === "" || display === "none" ) {
+ elem.style.display = jQuery._data(elem, "olddisplay") || "";
+ }
+ }
+ }
+
+ return this;
+ }
+ },
+
+ hide: function( speed, easing, callback ) {
+ if ( speed || speed === 0 ) {
+ return this.animate( genFx("hide", 3), speed, easing, callback);
+
+ } else {
+ for ( var i = 0, j = this.length; i < j; i++ ) {
+ if ( this[i].style ) {
+ var display = jQuery.css( this[i], "display" );
+
+ if ( display !== "none" && !jQuery._data( this[i], "olddisplay" ) ) {
+ jQuery._data( this[i], "olddisplay", display );
+ }
+ }
+ }
+
+ // Set the display of the elements in a second loop
+ // to avoid the constant reflow
+ for ( i = 0; i < j; i++ ) {
+ if ( this[i].style ) {
+ this[i].style.display = "none";
+ }
+ }
+
+ return this;
+ }
+ },
+
+ // Save the old toggle function
+ _toggle: jQuery.fn.toggle,
+
+ toggle: function( fn, fn2, callback ) {
+ var bool = typeof fn === "boolean";
+
+ if ( jQuery.isFunction(fn) && jQuery.isFunction(fn2) ) {
+ this._toggle.apply( this, arguments );
+
+ } else if ( fn == null || bool ) {
+ this.each(function() {
+ var state = bool ? fn : jQuery(this).is(":hidden");
+ jQuery(this)[ state ? "show" : "hide" ]();
+ });
+
+ } else {
+ this.animate(genFx("toggle", 3), fn, fn2, callback);
+ }
+
+ return this;
+ },
+
+ fadeTo: function( speed, to, easing, callback ) {
+ return this.filter(":hidden").css("opacity", 0).show().end()
+ .animate({opacity: to}, speed, easing, callback);
+ },
+
+ animate: function( prop, speed, easing, callback ) {
+ var optall = jQuery.speed(speed, easing, callback);
+
+ if ( jQuery.isEmptyObject( prop ) ) {
+ return this.each( optall.complete, [ false ] );
+ }
+
+ // Do not change referenced properties as per-property easing will be lost
+ prop = jQuery.extend( {}, prop );
+
+ return this[ optall.queue === false ? "each" : "queue" ](function() {
+ // XXX 'this' does not always have a nodeName when running the
+ // test suite
+
+ if ( optall.queue === false ) {
+ jQuery._mark( this );
+ }
+
+ var opt = jQuery.extend( {}, optall ),
+ isElement = this.nodeType === 1,
+ hidden = isElement && jQuery(this).is(":hidden"),
+ name, val, p,
+ display, e,
+ parts, start, end, unit;
+
+ // will store per property easing and be used to determine when an animation is complete
+ opt.animatedProperties = {};
+
+ for ( p in prop ) {
+
+ // property name normalization
+ name = jQuery.camelCase( p );
+ if ( p !== name ) {
+ prop[ name ] = prop[ p ];
+ delete prop[ p ];
+ }
+
+ val = prop[ name ];
+
+ // easing resolution: per property > opt.specialEasing > opt.easing > 'swing' (default)
+ if ( jQuery.isArray( val ) ) {
+ opt.animatedProperties[ name ] = val[ 1 ];
+ val = prop[ name ] = val[ 0 ];
+ } else {
+ opt.animatedProperties[ name ] = opt.specialEasing && opt.specialEasing[ name ] || opt.easing || 'swing';
+ }
+
+ if ( val === "hide" && hidden || val === "show" && !hidden ) {
+ return opt.complete.call( this );
+ }
+
+ if ( isElement && ( name === "height" || name === "width" ) ) {
+ // Make sure that nothing sneaks out
+ // Record all 3 overflow attributes because IE does not
+ // change the overflow attribute when overflowX and
+ // overflowY are set to the same value
+ opt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ];
+
+ // Set display property to inline-block for height/width
+ // animations on inline elements that are having width/height
+ // animated
+ if ( jQuery.css( this, "display" ) === "inline" &&
+ jQuery.css( this, "float" ) === "none" ) {
+ if ( !jQuery.support.inlineBlockNeedsLayout ) {
+ this.style.display = "inline-block";
+
+ } else {
+ display = defaultDisplay( this.nodeName );
+
+ // inline-level elements accept inline-block;
+ // block-level elements need to be inline with layout
+ if ( display === "inline" ) {
+ this.style.display = "inline-block";
+
+ } else {
+ this.style.display = "inline";
+ this.style.zoom = 1;
+ }
+ }
+ }
+ }
+ }
+
+ if ( opt.overflow != null ) {
+ this.style.overflow = "hidden";
+ }
+
+ for ( p in prop ) {
+ e = new jQuery.fx( this, opt, p );
+ val = prop[ p ];
+
+ if ( rfxtypes.test(val) ) {
+ e[ val === "toggle" ? hidden ? "show" : "hide" : val ]();
+
+ } else {
+ parts = rfxnum.exec( val );
+ start = e.cur();
+
+ if ( parts ) {
+ end = parseFloat( parts[2] );
+ unit = parts[3] || ( jQuery.cssNumber[ p ] ? "" : "px" );
+
+ // We need to compute starting value
+ if ( unit !== "px" ) {
+ jQuery.style( this, p, (end || 1) + unit);
+ start = ((end || 1) / e.cur()) * start;
+ jQuery.style( this, p, start + unit);
+ }
+
+ // If a +=/-= token was provided, we're doing a relative animation
+ if ( parts[1] ) {
+ end = ( (parts[ 1 ] === "-=" ? -1 : 1) * end ) + start;
+ }
+
+ e.custom( start, end, unit );
+
+ } else {
+ e.custom( start, val, "" );
+ }
+ }
+ }
+
+ // For JS strict compliance
+ return true;
+ });
+ },
+
+ stop: function( clearQueue, gotoEnd ) {
+ if ( clearQueue ) {
+ this.queue([]);
+ }
+
+ this.each(function() {
+ var timers = jQuery.timers,
+ i = timers.length;
+ // clear marker counters if we know they won't be
+ if ( !gotoEnd ) {
+ jQuery._unmark( true, this );
+ }
+ while ( i-- ) {
+ if ( timers[i].elem === this ) {
+ if (gotoEnd) {
+ // force the next step to be the last
+ timers[i](true);
+ }
+
+ timers.splice(i, 1);
+ }
+ }
+ });
+
+ // start the next in the queue if the last step wasn't forced
+ if ( !gotoEnd ) {
+ this.dequeue();
+ }
+
+ return this;
+ }
+
+});
+
+// Animations created synchronously will run synchronously
+function createFxNow() {
+ setTimeout( clearFxNow, 0 );
+ return ( fxNow = jQuery.now() );
+}
+
+function clearFxNow() {
+ fxNow = undefined;
+}
+
+// Generate parameters to create a standard animation
+function genFx( type, num ) {
+ var obj = {};
+
+ jQuery.each( fxAttrs.concat.apply([], fxAttrs.slice(0,num)), function() {
+ obj[ this ] = type;
+ });
+
+ return obj;
+}
+
+// Generate shortcuts for custom animations
+jQuery.each({
+ slideDown: genFx("show", 1),
+ slideUp: genFx("hide", 1),
+ slideToggle: genFx("toggle", 1),
+ fadeIn: { opacity: "show" },
+ fadeOut: { opacity: "hide" },
+ fadeToggle: { opacity: "toggle" }
+}, function( name, props ) {
+ jQuery.fn[ name ] = function( speed, easing, callback ) {
+ return this.animate( props, speed, easing, callback );
+ };
+});
+
+jQuery.extend({
+ speed: function( speed, easing, fn ) {
+ var opt = speed && typeof speed === "object" ? jQuery.extend({}, speed) : {
+ complete: fn || !fn && easing ||
+ jQuery.isFunction( speed ) && speed,
+ duration: speed,
+ easing: fn && easing || easing && !jQuery.isFunction(easing) && easing
+ };
+
+ opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
+ opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[opt.duration] : jQuery.fx.speeds._default;
+
+ // Queueing
+ opt.old = opt.complete;
+ opt.complete = function( noUnmark ) {
+ if ( jQuery.isFunction( opt.old ) ) {
+ opt.old.call( this );
+ }
+
+ if ( opt.queue !== false ) {
+ jQuery.dequeue( this );
+ } else if ( noUnmark !== false ) {
+ jQuery._unmark( this );
+ }
+ };
+
+ return opt;
+ },
+
+ easing: {
+ linear: function( p, n, firstNum, diff ) {
+ return firstNum + diff * p;
+ },
+ swing: function( p, n, firstNum, diff ) {
+ return ((-Math.cos(p*Math.PI)/2) + 0.5) * diff + firstNum;
+ }
+ },
+
+ timers: [],
+
+ fx: function( elem, options, prop ) {
+ this.options = options;
+ this.elem = elem;
+ this.prop = prop;
+
+ options.orig = options.orig || {};
+ }
+
+});
+
+jQuery.fx.prototype = {
+ // Simple function for setting a style value
+ update: function() {
+ if ( this.options.step ) {
+ this.options.step.call( this.elem, this.now, this );
+ }
+
+ (jQuery.fx.step[this.prop] || jQuery.fx.step._default)( this );
+ },
+
+ // Get the current size
+ cur: function() {
+ if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) ) {
+ return this.elem[ this.prop ];
+ }
+
+ var parsed,
+ r = jQuery.css( this.elem, this.prop );
+ // Empty strings, null, undefined and "auto" are converted to 0,
+ // complex values such as "rotate(1rad)" are returned as is,
+ // simple values such as "10px" are parsed to Float.
+ return isNaN( parsed = parseFloat( r ) ) ? !r || r === "auto" ? 0 : r : parsed;
+ },
+
+ // Start an animation from one number to another
+ custom: function( from, to, unit ) {
+ var self = this,
+ fx = jQuery.fx,
+ raf;
+
+ this.startTime = fxNow || createFxNow();
+ this.start = from;
+ this.end = to;
+ this.unit = unit || this.unit || ( jQuery.cssNumber[ this.prop ] ? "" : "px" );
+ this.now = this.start;
+ this.pos = this.state = 0;
+
+ function t( gotoEnd ) {
+ return self.step(gotoEnd);
+ }
+
+ t.elem = this.elem;
+
+ if ( t() && jQuery.timers.push(t) && !timerId ) {
+ // Use requestAnimationFrame instead of setInterval if available
+ if ( requestAnimationFrame ) {
+ timerId = true;
+ raf = function() {
+ // When timerId gets set to null at any point, this stops
+ if ( timerId ) {
+ requestAnimationFrame( raf );
+ fx.tick();
+ }
+ };
+ requestAnimationFrame( raf );
+ } else {
+ timerId = setInterval( fx.tick, fx.interval );
+ }
+ }
+ },
+
+ // Simple 'show' function
+ show: function() {
+ // Remember where we started, so that we can go back to it later
+ this.options.orig[this.prop] = jQuery.style( this.elem, this.prop );
+ this.options.show = true;
+
+ // Begin the animation
+ // Make sure that we start at a small width/height to avoid any
+ // flash of content
+ this.custom(this.prop === "width" || this.prop === "height" ? 1 : 0, this.cur());
+
+ // Start by showing the element
+ jQuery( this.elem ).show();
+ },
+
+ // Simple 'hide' function
+ hide: function() {
+ // Remember where we started, so that we can go back to it later
+ this.options.orig[this.prop] = jQuery.style( this.elem, this.prop );
+ this.options.hide = true;
+
+ // Begin the animation
+ this.custom(this.cur(), 0);
+ },
+
+ // Each step of an animation
+ step: function( gotoEnd ) {
+ var t = fxNow || createFxNow(),
+ done = true,
+ elem = this.elem,
+ options = this.options,
+ i, n;
+
+ if ( gotoEnd || t >= options.duration + this.startTime ) {
+ this.now = this.end;
+ this.pos = this.state = 1;
+ this.update();
+
+ options.animatedProperties[ this.prop ] = true;
+
+ for ( i in options.animatedProperties ) {
+ if ( options.animatedProperties[i] !== true ) {
+ done = false;
+ }
+ }
+
+ if ( done ) {
+ // Reset the overflow
+ if ( options.overflow != null && !jQuery.support.shrinkWrapBlocks ) {
+
+ jQuery.each( [ "", "X", "Y" ], function (index, value) {
+ elem.style[ "overflow" + value ] = options.overflow[index];
+ });
+ }
+
+ // Hide the element if the "hide" operation was done
+ if ( options.hide ) {
+ jQuery(elem).hide();
+ }
+
+ // Reset the properties, if the item has been hidden or shown
+ if ( options.hide || options.show ) {
+ for ( var p in options.animatedProperties ) {
+ jQuery.style( elem, p, options.orig[p] );
+ }
+ }
+
+ // Execute the complete function
+ options.complete.call( elem );
+ }
+
+ return false;
+
+ } else {
+ // classical easing cannot be used with an Infinity duration
+ if ( options.duration == Infinity ) {
+ this.now = t;
+ } else {
+ n = t - this.startTime;
+ this.state = n / options.duration;
+
+ // Perform the easing function, defaults to swing
+ this.pos = jQuery.easing[ options.animatedProperties[ this.prop ] ]( this.state, n, 0, 1, options.duration );
+ this.now = this.start + ((this.end - this.start) * this.pos);
+ }
+ // Perform the next step of the animation
+ this.update();
+ }
+
+ return true;
+ }
+};
+
+jQuery.extend( jQuery.fx, {
+ tick: function() {
+ for ( var timers = jQuery.timers, i = 0 ; i < timers.length ; ++i ) {
+ if ( !timers[i]() ) {
+ timers.splice(i--, 1);
+ }
+ }
+
+ if ( !timers.length ) {
+ jQuery.fx.stop();
+ }
+ },
+
+ interval: 13,
+
+ stop: function() {
+ clearInterval( timerId );
+ timerId = null;
+ },
+
+ speeds: {
+ slow: 600,
+ fast: 200,
+ // Default speed
+ _default: 400
+ },
+
+ step: {
+ opacity: function( fx ) {
+ jQuery.style( fx.elem, "opacity", fx.now );
+ },
+
+ _default: function( fx ) {
+ if ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) {
+ fx.elem.style[ fx.prop ] = (fx.prop === "width" || fx.prop === "height" ? Math.max(0, fx.now) : fx.now) + fx.unit;
+ } else {
+ fx.elem[ fx.prop ] = fx.now;
+ }
+ }
+ }
+});
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+ jQuery.expr.filters.animated = function( elem ) {
+ return jQuery.grep(jQuery.timers, function( fn ) {
+ return elem === fn.elem;
+ }).length;
+ };
+}
+
+// Try to restore the default display value of an element
+function defaultDisplay( nodeName ) {
+
+ if ( !elemdisplay[ nodeName ] ) {
+
+ var body = document.body,
+ elem = jQuery( "<" + nodeName + ">" ).appendTo( body ),
+ display = elem.css( "display" );
+
+ elem.remove();
+
+ // If the simple way fails,
+ // get element's real default display by attaching it to a temp iframe
+ if ( display === "none" || display === "" ) {
+ // No iframe to use yet, so create it
+ if ( !iframe ) {
+ iframe = document.createElement( "iframe" );
+ iframe.frameBorder = iframe.width = iframe.height = 0;
+ }
+
+ body.appendChild( iframe );
+
+ // Create a cacheable copy of the iframe document on first call.
+ // IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML
+ // document to it; WebKit & Firefox won't allow reusing the iframe document.
+ if ( !iframeDoc || !iframe.createElement ) {
+ iframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document;
+ iframeDoc.write( ( document.compatMode === "CSS1Compat" ? "<!doctype html>" : "" ) + "<html><body>" );
+ iframeDoc.close();
+ }
+
+ elem = iframeDoc.createElement( nodeName );
+
+ iframeDoc.body.appendChild( elem );
+
+ display = jQuery.css( elem, "display" );
+
+ body.removeChild( iframe );
+ }
+
+ // Store the correct default display
+ elemdisplay[ nodeName ] = display;
+ }
+
+ return elemdisplay[ nodeName ];
+}
+
+
+
+
+var rtable = /^t(?:able|d|h)$/i,
+ rroot = /^(?:body|html)$/i;
+
+if ( "getBoundingClientRect" in document.documentElement ) {
+ jQuery.fn.offset = function( options ) {
+ var elem = this[0], box;
+
+ if ( options ) {
+ return this.each(function( i ) {
+ jQuery.offset.setOffset( this, options, i );
+ });
+ }
+
+ if ( !elem || !elem.ownerDocument ) {
+ return null;
+ }
+
+ if ( elem === elem.ownerDocument.body ) {
+ return jQuery.offset.bodyOffset( elem );
+ }
+
+ try {
+ box = elem.getBoundingClientRect();
+ } catch(e) {}
+
+ var doc = elem.ownerDocument,
+ docElem = doc.documentElement;
+
+ // Make sure we're not dealing with a disconnected DOM node
+ if ( !box || !jQuery.contains( docElem, elem ) ) {
+ return box ? { top: box.top, left: box.left } : { top: 0, left: 0 };
+ }
+
+ var body = doc.body,
+ win = getWindow(doc),
+ clientTop = docElem.clientTop || body.clientTop || 0,
+ clientLeft = docElem.clientLeft || body.clientLeft || 0,
+ scrollTop = win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop || body.scrollTop,
+ scrollLeft = win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft,
+ top = box.top + scrollTop - clientTop,
+ left = box.left + scrollLeft - clientLeft;
+
+ return { top: top, left: left };
+ };
+
+} else {
+ jQuery.fn.offset = function( options ) {
+ var elem = this[0];
+
+ if ( options ) {
+ return this.each(function( i ) {
+ jQuery.offset.setOffset( this, options, i );
+ });
+ }
+
+ if ( !elem || !elem.ownerDocument ) {
+ return null;
+ }
+
+ if ( elem === elem.ownerDocument.body ) {
+ return jQuery.offset.bodyOffset( elem );
+ }
+
+ jQuery.offset.initialize();
+
+ var computedStyle,
+ offsetParent = elem.offsetParent,
+ prevOffsetParent = elem,
+ doc = elem.ownerDocument,
+ docElem = doc.documentElement,
+ body = doc.body,
+ defaultView = doc.defaultView,
+ prevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle,
+ top = elem.offsetTop,
+ left = elem.offsetLeft;
+
+ while ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) {
+ if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) {
+ break;
+ }
+
+ computedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle;
+ top -= elem.scrollTop;
+ left -= elem.scrollLeft;
+
+ if ( elem === offsetParent ) {
+ top += elem.offsetTop;
+ left += elem.offsetLeft;
+
+ if ( jQuery.offset.doesNotAddBorder && !(jQuery.offset.doesAddBorderForTableAndCells && rtable.test(elem.nodeName)) ) {
+ top += parseFloat( computedStyle.borderTopWidth ) || 0;
+ left += parseFloat( computedStyle.borderLeftWidth ) || 0;
+ }
+
+ prevOffsetParent = offsetParent;
+ offsetParent = elem.offsetParent;
+ }
+
+ if ( jQuery.offset.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== "visible" ) {
+ top += parseFloat( computedStyle.borderTopWidth ) || 0;
+ left += parseFloat( computedStyle.borderLeftWidth ) || 0;
+ }
+
+ prevComputedStyle = computedStyle;
+ }
+
+ if ( prevComputedStyle.position === "relative" || prevComputedStyle.position === "static" ) {
+ top += body.offsetTop;
+ left += body.offsetLeft;
+ }
+
+ if ( jQuery.offset.supportsFixedPosition && prevComputedStyle.position === "fixed" ) {
+ top += Math.max( docElem.scrollTop, body.scrollTop );
+ left += Math.max( docElem.scrollLeft, body.scrollLeft );
+ }
+
+ return { top: top, left: left };
+ };
+}
+
+jQuery.offset = {
+ initialize: function() {
+ var body = document.body, container = document.createElement("div"), innerDiv, checkDiv, table, td, bodyMarginTop = parseFloat( jQuery.css(body, "marginTop") ) || 0,
+ html = "<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";
+
+ jQuery.extend( container.style, { position: "absolute", top: 0, left: 0, margin: 0, border: 0, width: "1px", height: "1px", visibility: "hidden" } );
+
+ container.innerHTML = html;
+ body.insertBefore( container, body.firstChild );
+ innerDiv = container.firstChild;
+ checkDiv = innerDiv.firstChild;
+ td = innerDiv.nextSibling.firstChild.firstChild;
+
+ this.doesNotAddBorder = (checkDiv.offsetTop !== 5);
+ this.doesAddBorderForTableAndCells = (td.offsetTop === 5);
+
+ checkDiv.style.position = "fixed";
+ checkDiv.style.top = "20px";
+
+ // safari subtracts parent border width here which is 5px
+ this.supportsFixedPosition = (checkDiv.offsetTop === 20 || checkDiv.offsetTop === 15);
+ checkDiv.style.position = checkDiv.style.top = "";
+
+ innerDiv.style.overflow = "hidden";
+ innerDiv.style.position = "relative";
+
+ this.subtractsBorderForOverflowNotVisible = (checkDiv.offsetTop === -5);
+
+ this.doesNotIncludeMarginInBodyOffset = (body.offsetTop !== bodyMarginTop);
+
+ body.removeChild( container );
+ jQuery.offset.initialize = jQuery.noop;
+ },
+
+ bodyOffset: function( body ) {
+ var top = body.offsetTop,
+ left = body.offsetLeft;
+
+ jQuery.offset.initialize();
+
+ if ( jQuery.offset.doesNotIncludeMarginInBodyOffset ) {
+ top += parseFloat( jQuery.css(body, "marginTop") ) || 0;
+ left += parseFloat( jQuery.css(body, "marginLeft") ) || 0;
+ }
+
+ return { top: top, left: left };
+ },
+
+ setOffset: function( elem, options, i ) {
+ var position = jQuery.css( elem, "position" );
+
+ // set position first, in-case top/left are set even on static elem
+ if ( position === "static" ) {
+ elem.style.position = "relative";
+ }
+
+ var curElem = jQuery( elem ),
+ curOffset = curElem.offset(),
+ curCSSTop = jQuery.css( elem, "top" ),
+ curCSSLeft = jQuery.css( elem, "left" ),
+ calculatePosition = (position === "absolute" || position === "fixed") && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1,
+ props = {}, curPosition = {}, curTop, curLeft;
+
+ // need to be able to calculate position if either top or left is auto and position is either absolute or fixed
+ if ( calculatePosition ) {
+ curPosition = curElem.position();
+ curTop = curPosition.top;
+ curLeft = curPosition.left;
+ } else {
+ curTop = parseFloat( curCSSTop ) || 0;
+ curLeft = parseFloat( curCSSLeft ) || 0;
+ }
+
+ if ( jQuery.isFunction( options ) ) {
+ options = options.call( elem, i, curOffset );
+ }
+
+ if (options.top != null) {
+ props.top = (options.top - curOffset.top) + curTop;
+ }
+ if (options.left != null) {
+ props.left = (options.left - curOffset.left) + curLeft;
+ }
+
+ if ( "using" in options ) {
+ options.using.call( elem, props );
+ } else {
+ curElem.css( props );
+ }
+ }
+};
+
+
+jQuery.fn.extend({
+ position: function() {
+ if ( !this[0] ) {
+ return null;
+ }
+
+ var elem = this[0],
+
+ // Get *real* offsetParent
+ offsetParent = this.offsetParent(),
+
+ // Get correct offsets
+ offset = this.offset(),
+ parentOffset = rroot.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset();
+
+ // Subtract element margins
+ // note: when an element has margin: auto the offsetLeft and marginLeft
+ // are the same in Safari causing offset.left to incorrectly be 0
+ offset.top -= parseFloat( jQuery.css(elem, "marginTop") ) || 0;
+ offset.left -= parseFloat( jQuery.css(elem, "marginLeft") ) || 0;
+
+ // Add offsetParent borders
+ parentOffset.top += parseFloat( jQuery.css(offsetParent[0], "borderTopWidth") ) || 0;
+ parentOffset.left += parseFloat( jQuery.css(offsetParent[0], "borderLeftWidth") ) || 0;
+
+ // Subtract the two offsets
+ return {
+ top: offset.top - parentOffset.top,
+ left: offset.left - parentOffset.left
+ };
+ },
+
+ offsetParent: function() {
+ return this.map(function() {
+ var offsetParent = this.offsetParent || document.body;
+ while ( offsetParent && (!rroot.test(offsetParent.nodeName) && jQuery.css(offsetParent, "position") === "static") ) {
+ offsetParent = offsetParent.offsetParent;
+ }
+ return offsetParent;
+ });
+ }
+});
+
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( ["Left", "Top"], function( i, name ) {
+ var method = "scroll" + name;
+
+ jQuery.fn[ method ] = function( val ) {
+ var elem, win;
+
+ if ( val === undefined ) {
+ elem = this[ 0 ];
+
+ if ( !elem ) {
+ return null;
+ }
+
+ win = getWindow( elem );
+
+ // Return the scroll offset
+ return win ? ("pageXOffset" in win) ? win[ i ? "pageYOffset" : "pageXOffset" ] :
+ jQuery.support.boxModel && win.document.documentElement[ method ] ||
+ win.document.body[ method ] :
+ elem[ method ];
+ }
+
+ // Set the scroll offset
+ return this.each(function() {
+ win = getWindow( this );
+
+ if ( win ) {
+ win.scrollTo(
+ !i ? val : jQuery( win ).scrollLeft(),
+ i ? val : jQuery( win ).scrollTop()
+ );
+
+ } else {
+ this[ method ] = val;
+ }
+ });
+ };
+});
+
+function getWindow( elem ) {
+ return jQuery.isWindow( elem ) ?
+ elem :
+ elem.nodeType === 9 ?
+ elem.defaultView || elem.parentWindow :
+ false;
+}
+
+
+
+
+// Create width, height, innerHeight, innerWidth, outerHeight and outerWidth methods
+jQuery.each([ "Height", "Width" ], function( i, name ) {
+
+ var type = name.toLowerCase();
+
+ // innerHeight and innerWidth
+ jQuery.fn[ "inner" + name ] = function() {
+ var elem = this[0];
+ return elem && elem.style ?
+ parseFloat( jQuery.css( elem, type, "padding" ) ) :
+ null;
+ };
+
+ // outerHeight and outerWidth
+ jQuery.fn[ "outer" + name ] = function( margin ) {
+ var elem = this[0];
+ return elem && elem.style ?
+ parseFloat( jQuery.css( elem, type, margin ? "margin" : "border" ) ) :
+ null;
+ };
+
+ jQuery.fn[ type ] = function( size ) {
+ // Get window width or height
+ var elem = this[0];
+ if ( !elem ) {
+ return size == null ? null : this;
+ }
+
+ if ( jQuery.isFunction( size ) ) {
+ return this.each(function( i ) {
+ var self = jQuery( this );
+ self[ type ]( size.call( this, i, self[ type ]() ) );
+ });
+ }
+
+ if ( jQuery.isWindow( elem ) ) {
+ // Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode
+ // 3rd condition allows Nokia support, as it supports the docElem prop but not CSS1Compat
+ var docElemProp = elem.document.documentElement[ "client" + name ];
+ return elem.document.compatMode === "CSS1Compat" && docElemProp ||
+ elem.document.body[ "client" + name ] || docElemProp;
+
+ // Get document width or height
+ } else if ( elem.nodeType === 9 ) {
+ // Either scroll[Width/Height] or offset[Width/Height], whichever is greater
+ return Math.max(
+ elem.documentElement["client" + name],
+ elem.body["scroll" + name], elem.documentElement["scroll" + name],
+ elem.body["offset" + name], elem.documentElement["offset" + name]
+ );
+
+ // Get or set width or height on the element
+ } else if ( size === undefined ) {
+ var orig = jQuery.css( elem, type ),
+ ret = parseFloat( orig );
+
+ return jQuery.isNaN( ret ) ? orig : ret;
+
+ // Set the width or height on the element (default to pixels if value is unitless)
+ } else {
+ return this.css( type, typeof size === "string" ? size : size + "px" );
+ }
+ };
+
+});
+
+
+// Expose jQuery to the global object
+window.jQuery = window.$ = jQuery;
+})(window);
diff --git a/www/js/jquery.corner.js b/www/js/jquery.corner.js
new file mode 100644
index 0000000..c416613
--- /dev/null
+++ b/www/js/jquery.corner.js
@@ -0,0 +1,249 @@
+/*!
+ * jQuery corner plugin: simple corner rounding
+ * Examples and documentation at: http://jquery.malsup.com/corner/
+ * version 2.12 (23-MAY-2011)
+ * Requires jQuery v1.3.2 or later
+ * Dual licensed under the MIT and GPL licenses:
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.gnu.org/licenses/gpl.html
+ * Authors: Dave Methvin and Mike Alsup
+ */
+
+/**
+ * corner() takes a single string argument: $('#myDiv').corner("effect corners width")
+ *
+ * effect: name of the effect to apply, such as round, bevel, notch, bite, etc (default is round).
+ * corners: one or more of: top, bottom, tr, tl, br, or bl. (default is all corners)
+ * width: width of the effect; in the case of rounded corners this is the radius.
+ * specify this value using the px suffix such as 10px (yes, it must be pixels).
+ */
+;(function($) {
+
+var style = document.createElement('div').style,
+ moz = style['MozBorderRadius'] !== undefined,
+ webkit = style['WebkitBorderRadius'] !== undefined,
+ radius = style['borderRadius'] !== undefined || style['BorderRadius'] !== undefined,
+ mode = document.documentMode || 0,
+ noBottomFold = $.browser.msie && (($.browser.version < 8 && !mode) || mode < 8),
+
+ expr = $.browser.msie && (function() {
+ var div = document.createElement('div');
+ try { div.style.setExpression('width','0+0'); div.style.removeExpression('width'); }
+ catch(e) { return false; }
+ return true;
+ })();
+
+$.support = $.support || {};
+$.support.borderRadius = moz || webkit || radius; // so you can do: if (!$.support.borderRadius) $('#myDiv').corner();
+
+function sz(el, p) {
+ return parseInt($.css(el,p))||0;
+};
+function hex2(s) {
+ s = parseInt(s).toString(16);
+ return ( s.length < 2 ) ? '0'+s : s;
+};
+function gpc(node) {
+ while(node) {
+ var v = $.css(node,'backgroundColor'), rgb;
+ if (v && v != 'transparent' && v != 'rgba(0, 0, 0, 0)') {
+ if (v.indexOf('rgb') >= 0) {
+ rgb = v.match(/\d+/g);
+ return '#'+ hex2(rgb[0]) + hex2(rgb[1]) + hex2(rgb[2]);
+ }
+ return v;
+ }
+ if (node.nodeName.toLowerCase() == 'html')
+ break;
+ node = node.parentNode; // keep walking if transparent
+ }
+ return '#ffffff';
+};
+
+function getWidth(fx, i, width) {
+ switch(fx) {
+ case 'round': return Math.round(width*(1-Math.cos(Math.asin(i/width))));
+ case 'cool': return Math.round(width*(1+Math.cos(Math.asin(i/width))));
+ case 'sharp': return width-i;
+ case 'bite': return Math.round(width*(Math.cos(Math.asin((width-i-1)/width))));
+ case 'slide': return Math.round(width*(Math.atan2(i,width/i)));
+ case 'jut': return Math.round(width*(Math.atan2(width,(width-i-1))));
+ case 'curl': return Math.round(width*(Math.atan(i)));
+ case 'tear': return Math.round(width*(Math.cos(i)));
+ case 'wicked': return Math.round(width*(Math.tan(i)));
+ case 'long': return Math.round(width*(Math.sqrt(i)));
+ case 'sculpt': return Math.round(width*(Math.log((width-i-1),width)));
+ case 'dogfold':
+ case 'dog': return (i&1) ? (i+1) : width;
+ case 'dog2': return (i&2) ? (i+1) : width;
+ case 'dog3': return (i&3) ? (i+1) : width;
+ case 'fray': return (i%2)*width;
+ case 'notch': return width;
+ case 'bevelfold':
+ case 'bevel': return i+1;
+ case 'steep': return i/2 + 1;
+ case 'invsteep':return (width-i)/2+1;
+ }
+};
+
+$.fn.corner = function(options) {
+ // in 1.3+ we can fix mistakes with the ready state
+ if (this.length == 0) {
+ if (!$.isReady && this.selector) {
+ var s = this.selector, c = this.context;
+ $(function() {
+ $(s,c).corner(options);
+ });
+ }
+ return this;
+ }
+
+ return this.each(function(index){
+ var $this = $(this),
+ // meta values override options
+ o = [$this.attr($.fn.corner.defaults.metaAttr) || '', options || ''].join(' ').toLowerCase(),
+ keep = /keep/.test(o), // keep borders?
+ cc = ((o.match(/cc:(#[0-9a-f]+)/)||[])[1]), // corner color
+ sc = ((o.match(/sc:(#[0-9a-f]+)/)||[])[1]), // strip color
+ width = parseInt((o.match(/(\d+)px/)||[])[1]) || 10, // corner width
+ re = /round|bevelfold|bevel|notch|bite|cool|sharp|slide|jut|curl|tear|fray|wicked|sculpt|long|dog3|dog2|dogfold|dog|invsteep|steep/,
+ fx = ((o.match(re)||['round'])[0]),
+ fold = /dogfold|bevelfold/.test(o),
+ edges = { T:0, B:1 },
+ opts = {
+ TL: /top|tl|left/.test(o), TR: /top|tr|right/.test(o),
+ BL: /bottom|bl|left/.test(o), BR: /bottom|br|right/.test(o)
+ },
+ // vars used in func later
+ strip, pad, cssHeight, j, bot, d, ds, bw, i, w, e, c, common, $horz;
+
+ if ( !opts.TL && !opts.TR && !opts.BL && !opts.BR )
+ opts = { TL:1, TR:1, BL:1, BR:1 };
+
+ // support native rounding
+ if ($.fn.corner.defaults.useNative && fx == 'round' && (radius || moz || webkit) && !cc && !sc) {
+ if (opts.TL)
+ $this.css(radius ? 'border-top-left-radius' : moz ? '-moz-border-radius-topleft' : '-webkit-border-top-left-radius', width + 'px');
+ if (opts.TR)
+ $this.css(radius ? 'border-top-right-radius' : moz ? '-moz-border-radius-topright' : '-webkit-border-top-right-radius', width + 'px');
+ if (opts.BL)
+ $this.css(radius ? 'border-bottom-left-radius' : moz ? '-moz-border-radius-bottomleft' : '-webkit-border-bottom-left-radius', width + 'px');
+ if (opts.BR)
+ $this.css(radius ? 'border-bottom-right-radius' : moz ? '-moz-border-radius-bottomright' : '-webkit-border-bottom-right-radius', width + 'px');
+ return;
+ }
+
+ strip = document.createElement('div');
+ $(strip).css({
+ overflow: 'hidden',
+ height: '1px',
+ minHeight: '1px',
+ fontSize: '1px',
+ backgroundColor: sc || 'transparent',
+ borderStyle: 'solid'
+ });
+
+ pad = {
+ T: parseInt($.css(this,'paddingTop'))||0, R: parseInt($.css(this,'paddingRight'))||0,
+ B: parseInt($.css(this,'paddingBottom'))||0, L: parseInt($.css(this,'paddingLeft'))||0
+ };
+
+ if (typeof this.style.zoom != undefined) this.style.zoom = 1; // force 'hasLayout' in IE
+ if (!keep) this.style.border = 'none';
+ strip.style.borderColor = cc || gpc(this.parentNode);
+ cssHeight = $(this).outerHeight();
+
+ for (j in edges) {
+ bot = edges[j];
+ // only add stips if needed
+ if ((bot && (opts.BL || opts.BR)) || (!bot && (opts.TL || opts.TR))) {
+ strip.style.borderStyle = 'none '+(opts[j+'R']?'solid':'none')+' none '+(opts[j+'L']?'solid':'none');
+ d = document.createElement('div');
+ $(d).addClass('jquery-corner');
+ ds = d.style;
+
+ bot ? this.appendChild(d) : this.insertBefore(d, this.firstChild);
+
+ if (bot && cssHeight != 'auto') {
+ if ($.css(this,'position') == 'static')
+ this.style.position = 'relative';
+ ds.position = 'absolute';
+ ds.bottom = ds.left = ds.padding = ds.margin = '0';
+ if (expr)
+ ds.setExpression('width', 'this.parentNode.offsetWidth');
+ else
+ ds.width = '100%';
+ }
+ else if (!bot && $.browser.msie) {
+ if ($.css(this,'position') == 'static')
+ this.style.position = 'relative';
+ ds.position = 'absolute';
+ ds.top = ds.left = ds.right = ds.padding = ds.margin = '0';
+
+ // fix ie6 problem when blocked element has a border width
+ if (expr) {
+ bw = sz(this,'borderLeftWidth') + sz(this,'borderRightWidth');
+ ds.setExpression('width', 'this.parentNode.offsetWidth - '+bw+'+ "px"');
+ }
+ else
+ ds.width = '100%';
+ }
+ else {
+ ds.position = 'relative';
+ ds.margin = !bot ? '-'+pad.T+'px -'+pad.R+'px '+(pad.T-width)+'px -'+pad.L+'px' :
+ (pad.B-width)+'px -'+pad.R+'px -'+pad.B+'px -'+pad.L+'px';
+ }
+
+ for (i=0; i < width; i++) {
+ w = Math.max(0,getWidth(fx,i, width));
+ e = strip.cloneNode(false);
+ e.style.borderWidth = '0 '+(opts[j+'R']?w:0)+'px 0 '+(opts[j+'L']?w:0)+'px';
+ bot ? d.appendChild(e) : d.insertBefore(e, d.firstChild);
+ }
+
+ if (fold && $.support.boxModel) {
+ if (bot && noBottomFold) continue;
+ for (c in opts) {
+ if (!opts[c]) continue;
+ if (bot && (c == 'TL' || c == 'TR')) continue;
+ if (!bot && (c == 'BL' || c == 'BR')) continue;
+
+ common = { position: 'absolute', border: 'none', margin: 0, padding: 0, overflow: 'hidden', backgroundColor: strip.style.borderColor };
+ $horz = $('<div/>').css(common).css({ width: width + 'px', height: '1px' });
+ switch(c) {
+ case 'TL': $horz.css({ bottom: 0, left: 0 }); break;
+ case 'TR': $horz.css({ bottom: 0, right: 0 }); break;
+ case 'BL': $horz.css({ top: 0, left: 0 }); break;
+ case 'BR': $horz.css({ top: 0, right: 0 }); break;
+ }
+ d.appendChild($horz[0]);
+
+ var $vert = $('<div/>').css(common).css({ top: 0, bottom: 0, width: '1px', height: width + 'px' });
+ switch(c) {
+ case 'TL': $vert.css({ left: width }); break;
+ case 'TR': $vert.css({ right: width }); break;
+ case 'BL': $vert.css({ left: width }); break;
+ case 'BR': $vert.css({ right: width }); break;
+ }
+ d.appendChild($vert[0]);
+ }
+ }
+ }
+ }
+ });
+};
+
+$.fn.uncorner = function() {
+ if (radius || moz || webkit)
+ this.css(radius ? 'border-radius' : moz ? '-moz-border-radius' : '-webkit-border-radius', 0);
+ $('div.jquery-corner', this).remove();
+ return this;
+};
+
+// expose options
+$.fn.corner.defaults = {
+ useNative: true, // true if plugin should attempt to use native browser support for border radius rounding
+ metaAttr: 'data-corner' // name of meta attribute to use for options
+};
+
+})(jQuery);
diff --git a/www/js/jquery.quicksand.js b/www/js/jquery.quicksand.js
new file mode 100644
index 0000000..626f490
--- /dev/null
+++ b/www/js/jquery.quicksand.js
@@ -0,0 +1,307 @@
+/*
+
+Quicksand 1.2.2
+
+Reorder and filter items with a nice shuffling animation.
+
+Copyright (c) 2010 Jacek Galanciak (razorjack.net) and agilope.com
+Big thanks for Piotr Petrus (riddle.pl) for deep code review and wonderful docs & demos.
+
+Dual licensed under the MIT and GPL version 2 licenses.
+http://github.com/jquery/jquery/blob/master/MIT-LICENSE.txt
+http://github.com/jquery/jquery/blob/master/GPL-LICENSE.txt
+
+Project site: http://razorjack.net/quicksand
+Github site: http://github.com/razorjack/quicksand
+
+*/
+
+(function ($) {
+ $.fn.quicksand = function (collection, customOptions) {
+ var options = {
+ duration: 750,
+ easing: 'swing',
+ attribute: 'data-id', // attribute to recognize same items within source and dest
+ adjustHeight: 'auto', // 'dynamic' animates height during shuffling (slow), 'auto' adjusts it before or after the animation, false leaves height constant
+ useScaling: true, // disable it if you're not using scaling effect or want to improve performance
+ enhancement: function(c) {}, // Visual enhacement (eg. font replacement) function for cloned elements
+ selector: '> *',
+ dx: 0,
+ dy: 0
+ };
+ $.extend(options, customOptions);
+
+ if ($.browser.msie || (typeof($.fn.scale) == 'undefined')) {
+ // Got IE and want scaling effect? Kiss my ass.
+ options.useScaling = false;
+ }
+
+ var callbackFunction;
+ if (typeof(arguments[1]) == 'function') {
+ var callbackFunction = arguments[1];
+ } else if (typeof(arguments[2] == 'function')) {
+ var callbackFunction = arguments[2];
+ }
+
+
+ return this.each(function (i) {
+ var val;
+ var animationQueue = []; // used to store all the animation params before starting the animation; solves initial animation slowdowns
+ var $collection = $(collection).clone(); // destination (target) collection
+ var $sourceParent = $(this); // source, the visible container of source collection
+ var sourceHeight = $(this).css('height'); // used to keep height and document flow during the animation
+
+ var destHeight;
+ var adjustHeightOnCallback = false;
+
+ var offset = $($sourceParent).offset(); // offset of visible container, used in animation calculations
+ var offsets = []; // coordinates of every source collection item
+
+ var $source = $(this).find(options.selector); // source collection items
+
+ // Replace the collection and quit if IE6
+ if ($.browser.msie && $.browser.version.substr(0,1)<7) {
+ $sourceParent.html('').append($collection);
+ return;
+ }
+
+ // Gets called when any animation is finished
+ var postCallbackPerformed = 0; // prevents the function from being called more than one time
+ var postCallback = function () {
+
+ if (!postCallbackPerformed) {
+ postCallbackPerformed = 1;
+
+ // hack:
+ // used to be: $sourceParent.html($dest.html()); // put target HTML into visible source container
+ // but new webkit builds cause flickering when replacing the collections
+ $toDelete = $sourceParent.find('> *');
+ $sourceParent.prepend($dest.find('> *'));
+ $toDelete.remove();
+
+ if (adjustHeightOnCallback) {
+ $sourceParent.css('height', destHeight);
+ }
+ options.enhancement($sourceParent); // Perform custom visual enhancements on a newly replaced collection
+ if (typeof callbackFunction == 'function') {
+ callbackFunction.call(this);
+ }
+ }
+ };
+
+ // Position: relative situations
+ var $correctionParent = $sourceParent.offsetParent();
+ var correctionOffset = $correctionParent.offset();
+ if ($correctionParent.css('position') == 'relative') {
+ if ($correctionParent.get(0).nodeName.toLowerCase() == 'body') {
+
+ } else {
+ correctionOffset.top += (parseFloat($correctionParent.css('border-top-width')) || 0);
+ correctionOffset.left +=( parseFloat($correctionParent.css('border-left-width')) || 0);
+ }
+ } else {
+ correctionOffset.top -= (parseFloat($correctionParent.css('border-top-width')) || 0);
+ correctionOffset.left -= (parseFloat($correctionParent.css('border-left-width')) || 0);
+ correctionOffset.top -= (parseFloat($correctionParent.css('margin-top')) || 0);
+ correctionOffset.left -= (parseFloat($correctionParent.css('margin-left')) || 0);
+ }
+
+ // perform custom corrections from options (use when Quicksand fails to detect proper correction)
+ if (isNaN(correctionOffset.left)) {
+ correctionOffset.left = 0;
+ }
+ if (isNaN(correctionOffset.top)) {
+ correctionOffset.top = 0;
+ }
+
+ correctionOffset.left -= options.dx;
+ correctionOffset.top -= options.dy;
+
+ // keeps nodes after source container, holding their position
+ $sourceParent.css('height', $(this).height());
+
+ // get positions of source collections
+ $source.each(function (i) {
+ offsets[i] = $(this).offset();
+ });
+
+ // stops previous animations on source container
+ $(this).stop();
+ var dx = 0; var dy = 0;
+ $source.each(function (i) {
+ $(this).stop(); // stop animation of collection items
+ var rawObj = $(this).get(0);
+ if (rawObj.style.position == 'absolute') {
+ dx = -options.dx;
+ dy = -options.dy;
+ } else {
+ dx = options.dx;
+ dy = options.dy;
+ }
+
+ rawObj.style.position = 'absolute';
+ rawObj.style.margin = '0';
+
+ rawObj.style.top = (offsets[i].top - parseFloat(rawObj.style.marginTop) - correctionOffset.top + dy) + 'px';
+ rawObj.style.left = (offsets[i].left - parseFloat(rawObj.style.marginLeft) - correctionOffset.left + dx) + 'px';
+ });
+
+ // create temporary container with destination collection
+ var $dest = $($sourceParent).clone();
+ var rawDest = $dest.get(0);
+ rawDest.innerHTML = '';
+ rawDest.setAttribute('id', '');
+ rawDest.style.height = 'auto';
+ rawDest.style.width = $sourceParent.width() + 'px';
+ $dest.append($collection);
+ // insert node into HTML
+ // Note that the node is under visible source container in the exactly same position
+ // The browser render all the items without showing them (opacity: 0.0)
+ // No offset calculations are needed, the browser just extracts position from underlayered destination items
+ // and sets animation to destination positions.
+ $dest.insertBefore($sourceParent);
+ $dest.css('opacity', 0.0);
+ rawDest.style.zIndex = -1;
+
+ rawDest.style.margin = '0';
+ rawDest.style.position = 'absolute';
+ rawDest.style.top = offset.top - correctionOffset.top + 'px';
+ rawDest.style.left = offset.left - correctionOffset.left + 'px';
+
+
+
+
+
+ if (options.adjustHeight === 'dynamic') {
+ // If destination container has different height than source container
+ // the height can be animated, adjusting it to destination height
+ $sourceParent.animate({height: $dest.height()}, options.duration, options.easing);
+ } else if (options.adjustHeight === 'auto') {
+ destHeight = $dest.height();
+ if (parseFloat(sourceHeight) < parseFloat(destHeight)) {
+ // Adjust the height now so that the items don't move out of the container
+ $sourceParent.css('height', destHeight);
+ } else {
+ // Adjust later, on callback
+ adjustHeightOnCallback = true;
+ }
+ }
+
+ // Now it's time to do shuffling animation
+ // First of all, we need to identify same elements within source and destination collections
+ $source.each(function (i) {
+ var destElement = [];
+ if (typeof(options.attribute) == 'function') {
+
+ val = options.attribute($(this));
+ $collection.each(function() {
+ if (options.attribute(this) == val) {
+ destElement = $(this);
+ return false;
+ }
+ });
+ } else {
+ destElement = $collection.filter('[' + options.attribute + '=' + $(this).attr(options.attribute) + ']');
+ }
+ if (destElement.length) {
+ // The item is both in source and destination collections
+ // It it's under different position, let's move it
+ if (!options.useScaling) {
+ animationQueue.push(
+ {
+ element: $(this),
+ animation:
+ {top: destElement.offset().top - correctionOffset.top,
+ left: destElement.offset().left - correctionOffset.left,
+ opacity: 1.0
+ }
+ });
+
+ } else {
+ animationQueue.push({
+ element: $(this),
+ animation: {top: destElement.offset().top - correctionOffset.top,
+ left: destElement.offset().left - correctionOffset.left,
+ opacity: 1.0,
+ scale: '1.0'
+ }
+ });
+
+ }
+ } else {
+ // The item from source collection is not present in destination collections
+ // Let's remove it
+ if (!options.useScaling) {
+ animationQueue.push({element: $(this),
+ animation: {opacity: '0.0'}});
+ } else {
+ animationQueue.push({element: $(this), animation: {opacity: '0.0',
+ scale: '0.0'}});
+ }
+ }
+ });
+
+ $collection.each(function (i) {
+ // Grab all items from target collection not present in visible source collection
+
+ var sourceElement = [];
+ var destElement = [];
+ if (typeof(options.attribute) == 'function') {
+ val = options.attribute($(this));
+ $source.each(function() {
+ if (options.attribute(this) == val) {
+ sourceElement = $(this);
+ return false;
+ }
+ });
+
+ $collection.each(function() {
+ if (options.attribute(this) == val) {
+ destElement = $(this);
+ return false;
+ }
+ });
+ } else {
+ sourceElement = $source.filter('[' + options.attribute + '=' + $(this).attr(options.attribute) + ']');
+ destElement = $collection.filter('[' + options.attribute + '=' + $(this).attr(options.attribute) + ']');
+ }
+
+ var animationOptions;
+ if (sourceElement.length === 0) {
+ // No such element in source collection...
+ if (!options.useScaling) {
+ animationOptions = {
+ opacity: '1.0'
+ };
+ } else {
+ animationOptions = {
+ opacity: '1.0',
+ scale: '1.0'
+ };
+ }
+ // Let's create it
+ d = destElement.clone();
+ var rawDestElement = d.get(0);
+ rawDestElement.style.position = 'absolute';
+ rawDestElement.style.margin = '0';
+ rawDestElement.style.top = destElement.offset().top - correctionOffset.top + 'px';
+ rawDestElement.style.left = destElement.offset().left - correctionOffset.left + 'px';
+ d.css('opacity', 0.0); // IE
+ if (options.useScaling) {
+ d.css('transform', 'scale(0.0)');
+ }
+ d.appendTo($sourceParent);
+
+ animationQueue.push({element: $(d),
+ animation: animationOptions});
+ }
+ });
+
+ $dest.remove();
+ options.enhancement($sourceParent); // Perform custom visual enhancements during the animation
+ for (i = 0; i < animationQueue.length; i++) {
+ animationQueue[i].element.animate(animationQueue[i].animation, options.duration, options.easing, postCallback);
+ }
+ });
+ };
+})(jQuery); \ No newline at end of file