├── src ├── __init__.py ├── run.sh ├── static │ ├── img │ │ └── bg.jpg │ ├── WebSocketMain.swf │ ├── fonts │ │ ├── icons.woff │ │ ├── basic.icons.woff │ │ └── basic.icons.svg │ ├── css │ │ ├── chat.css │ │ └── body.css │ └── js │ │ ├── chat.js │ │ ├── json2.js │ │ └── underscore.js ├── init_sqlite.py ├── server.py ├── models.py ├── templates │ └── index.html └── handlers.py ├── MANIFEST.in ├── data ├── chat.png ├── login.png └── topics.png ├── requirements.txt ├── .gitignore ├── setup.py ├── deploy-wechat-simple.yml ├── reload.yml ├── README.rst └── LICENSE /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | nohup python server.py > access.log 2>&1 & 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include *.css *.js *.jpg *.gif *.png *.html *.md *.py -------------------------------------------------------------------------------- /data/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/wechat/HEAD/data/chat.png -------------------------------------------------------------------------------- /data/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/wechat/HEAD/data/login.png -------------------------------------------------------------------------------- /data/topics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/wechat/HEAD/data/topics.png -------------------------------------------------------------------------------- /src/static/img/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/wechat/HEAD/src/static/img/bg.jpg -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | web.py 2 | gevent==1.0 3 | gevent-socketio==0.3.6 4 | gevent-websocket==0.9.3 5 | -------------------------------------------------------------------------------- /src/static/WebSocketMain.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/wechat/HEAD/src/static/WebSocketMain.swf -------------------------------------------------------------------------------- /src/static/fonts/icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/wechat/HEAD/src/static/fonts/icons.woff -------------------------------------------------------------------------------- /src/static/fonts/basic.icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/wechat/HEAD/src/static/fonts/basic.icons.woff -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | src/sessions/* 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .tox 29 | nosetests.xml 30 | *.db 31 | *.log 32 | 33 | # Translations 34 | *.mo 35 | 36 | # Mr Developer 37 | .mr.developer.cfg 38 | .project 39 | .pydevproject 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | #!/usr/bin/env python 3 | 4 | from setuptools import setup, find_packages 5 | 6 | readme = open('README.rst').read() 7 | 8 | setup( 9 | name='wecha', 10 | version='${version}', 11 | description='', 12 | long_description=readme, 13 | author='the5fire', 14 | author_email='thefivefire@gmail.com', 15 | url='http://chat.the5fire.com', 16 | packages=['src',], 17 | package_data={ 18 | 'src':['*.py', 'static/*', 'templates/*'], 19 | }, 20 | include_package_data = True, 21 | install_requires=[ 22 | 'web.py', 23 | 'jinja2', 24 | 'gunicorn', 25 | ], 26 | ) 27 | -------------------------------------------------------------------------------- /deploy-wechat-simple.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: speedy-host1 # hosts中指定 3 | remote_user: the5fire # 如果和当前用户一样,则无需指定 4 | tasks: 5 | - name: check out wechat 6 | git: dest=~/wechat-env/wechat repo=https://github.com/the5fire/wechat 7 | update=yes 8 | - name: make virtualenv 9 | shell: 'virtualenv ~/wechat-env' 10 | - name: install requirements 11 | pip: requirements=~/wechat-env/wechat/requirements.txt 12 | virtualenv=~/wechat-env 13 | - name: init database 14 | shell: . ./bin/activate && cd wechat/src && ./init_sqlite.py chdir=~/wechat-env 15 | - name: run server.py 16 | shell: . ./bin/activate && cd wechat/src && ./run.sh chdir=~/wechat-env 17 | -------------------------------------------------------------------------------- /reload.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: speedy-host1 # hosts中指定 3 | remote_user: the5fire # 如果和当前用户一样,则无需指定 4 | tasks: 5 | - name: check out wechat 6 | git: dest=~/wechat-env/wechat repo=https://github.com/the5fire/wechat 7 | update=yes 8 | - name: install requirements 9 | pip: requirements=~/wechat-env/wechat/requirements.txt 10 | virtualenv=~/wechat-env 11 | - name: init database 12 | shell: . ./bin/activate && cd wechat/src && ./init_sqlite.py chdir=~/wechat-env 13 | - name: kill exist pid 14 | shell: ps aux | grep 'python server.py' | grep -v 'grep' | awk '{print $2}' | xargs kill -9 15 | ignore_errors: yes 16 | - name: run server.py 17 | shell: . ./bin/activate && cd wechat/src && ./run.sh chdir=~/wechat-env 18 | -------------------------------------------------------------------------------- /src/init_sqlite.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding:utf-8 3 | 4 | import sqlite3 5 | 6 | con = sqlite3.connect('wechat.db') 7 | cur = con.cursor() 8 | 9 | command = """ 10 | BEGIN; 11 | CREATE TABLE IF NOT EXISTS user( 12 | "id" integer PRIMARY KEY AUTOINCREMENT, 13 | "username" varchar(20) NOT NULL, 14 | "password" varchar(50) NOT NULL, 15 | "registed_time" datetime NOT NULL, 16 | UNIQUE ("username") 17 | ); 18 | CREATE TABLE IF NOT EXISTS topic( 19 | "id" integer PRIMARY KEY AUTOINCREMENT, 20 | "title" varchar(20) NOT NULL, 21 | "created_time" datetime NOT NULL, 22 | "owner_id" integer NOT NULL, 23 | UNIQUE ("title", 'owner_id') 24 | ); 25 | CREATE TABLE IF NOT EXISTS message( 26 | "id" integer PRIMARY KEY AUTOINCREMENT, 27 | "content" text NOT NULL, 28 | "topic_id" integer NOT NULL, 29 | "user_id" integer NOT NULL, 30 | "created_time" datetime NOT NULL 31 | ); 32 | COMMIT; 33 | """ 34 | 35 | try: 36 | cur.executescript(command) 37 | con.commit() 38 | except Exception as e: 39 | print e 40 | 41 | cur.close() 42 | con.close() 43 | -------------------------------------------------------------------------------- /src/static/css/chat.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Open Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; 3 | background: #FCFCFC url(../img/bg.jpg) repeat; 4 | margin: 0px; 5 | padding: 0px; 6 | color: #555555; 7 | min-width: 320px; 8 | } 9 | .container { 10 | width: 915px; 11 | margin: 0px auto; 12 | } 13 | div.right, div.right a.author, div.right div.metadata, div.right>div.text { 14 | float: right; 15 | } 16 | div.content { 17 | width: 250px; 18 | } 19 | #main { 20 | min-height: 700px; 21 | } 22 | .comment { 23 | min-height: 85px; 24 | } 25 | #message_list { 26 | height: 400px; 27 | overflow-y: auto; 28 | } 29 | .label>p { 30 | text-transform: none; 31 | } 32 | /*login style*/ 33 | #wrapper { 34 | position: absolute; 35 | display: none; 36 | width: 100%; 37 | height: 100%; 38 | top: 0; 39 | left: 0; 40 | overflow: auto; 41 | background: #fff; 42 | } 43 | #login { 44 | padding-top: 200px; 45 | } 46 | .hide { 47 | display: none; 48 | } 49 | footer { 50 | position: relative; 51 | margin: 0 -2.5%; 52 | width: 100%; 53 | background: #fff; 54 | z-index: 999; 55 | bottom: 33px; 56 | } 57 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | wechat 2 | ================== 3 | 4 | An online chat room base on backbonejs & webpy & sqlite3 5 | 6 | Css styles Power by `semantic `_ 7 | 8 | Quick Start at local 9 | ------------------------- 10 | 11 | :: 12 | 13 | git clone https://github.com/the5fire/wechat 14 | cd wechat && pip install -r requirements.txt 15 | cd src 16 | python init_sqlite.py 17 | python server.py 18 | 19 | then open your browser and type http://127.0.0.1:8080 . 20 | 21 | Quick Deploy Online 22 | ------------------------ 23 | You should modify hosts/remote_user or other configration in ``deploy-wechat-simple.yml`` first, then:: 24 | 25 | ansible-playbook deploy-wechat-simple.yml 26 | 27 | that all. 28 | 29 | it's running!! 30 | 31 | 32 | Screenshots 33 | --------------------- 34 | 35 | login: 36 | ~~~~~~~~~~~~~~~~~~~~~~~~ 37 | 38 | .. image:: data/login.png 39 | 40 | 41 | topics: 42 | ~~~~~~~~~~~~~~~~~~~~~~~~ 43 | 44 | .. image:: data/topics.png 45 | 46 | 47 | messages: 48 | ~~~~~~~~~~~~~~~~~~~~ 49 | 50 | .. image:: data/chat.png 51 | 52 | 53 | TODO: 54 | ------------------------ 55 | 56 | 1. replace gevent-socketio with tornado. 57 | 2. use socketio save message data. 58 | -------------------------------------------------------------------------------- /src/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding:utf-8 3 | from gevent import monkey 4 | monkey.patch_all() 5 | 6 | import web 7 | from web.httpserver import StaticMiddleware 8 | from socketio import server 9 | 10 | 11 | urls = ( 12 | '/', 'IndexHandler', # 返回首页 13 | '/topic', 'TopicHandler', 14 | '/topic/(\d+)', 'TopicHandler', 15 | '/message', 'MessageHandler', 16 | '/user', 'UserHandler', 17 | '/user/(\d+)', 'UserHandler', 18 | '/login', 'LoginHandler', 19 | '/logout', 'LogoutHandler', 20 | '/socket.io/.*', 'SocketHandler', 21 | ) 22 | 23 | app = web.application(urls, globals()) 24 | application = app.wsgifunc(StaticMiddleware) 25 | 26 | if web.config.get('_session') is None: 27 | session = web.session.Session( 28 | app, 29 | web.session.DiskStore('sessions'), 30 | initializer={'login': False, 'user': None} 31 | ) 32 | web.config._session = session 33 | 34 | #web.config.debug = False 35 | 36 | from handlers import ( # NOQA 37 | IndexHandler, UserHandler, 38 | LoginHandler, LogoutHandler, 39 | TopicHandler, MessageHandler, 40 | SocketHandler, 41 | ) 42 | 43 | 44 | if __name__ == "__main__": 45 | PORT = 8080 46 | print 'http://localhost:%s' % PORT 47 | server.SocketIOServer( 48 | ('localhost', PORT), 49 | application, 50 | resource="socket.io", 51 | policy_server=True, 52 | policy_listener=('0.0.0.0', 10843), 53 | ).serve_forever() 54 | -------------------------------------------------------------------------------- /src/models.py: -------------------------------------------------------------------------------- 1 | #coding:utf-8 2 | import web 3 | 4 | db = web.database(dbn='sqlite', db="wechat.db") 5 | 6 | 7 | class DBManage(object): 8 | @classmethod 9 | def table(cls): 10 | return cls.__name__.lower() 11 | 12 | @classmethod 13 | def get_by_id(cls, id): 14 | itertodo = db.select(cls.table(), where="id=$id", vars=locals()) 15 | # 参考:https://groups.google.com/forum/#!msg/webpy/PP81l8C5kbQ/90Hgx3HUqG0J 16 | return next(iter(itertodo), None) 17 | 18 | 19 | @classmethod 20 | def get_all(cls): 21 | # inspect.ismethod(cls.get_all) 22 | return db.select(cls.table()) 23 | 24 | @classmethod 25 | def create(cls, **kwargs): 26 | return db.insert(cls.table(), **kwargs) 27 | 28 | @classmethod 29 | def update(cls, **kwargs): 30 | db.update(cls.table(), where="id=$id", vars={"id": kwargs.pop('id')}, **kwargs) 31 | 32 | @classmethod 33 | def delete(cls, id): 34 | db.delete(cls.table(), where="id=$id", vars=locals()) 35 | 36 | 37 | class User(DBManage): 38 | id = None 39 | username = None 40 | password = None 41 | registed_time = None 42 | 43 | @classmethod 44 | def get_by_username_password(cls, username, password): 45 | itertodo = db.select(cls.table(), where="username=$username and password=$password", vars=locals()) 46 | return next(iter(itertodo), None) 47 | 48 | 49 | class Topic(DBManage): 50 | id = None 51 | title = None 52 | created_time = None 53 | owner = None 54 | 55 | 56 | class Message(DBManage): 57 | id = None 58 | content = None 59 | top_id = None 60 | user_id = None 61 | reply_to = None 62 | 63 | @classmethod 64 | def get_by_topic(cls, topic_id): 65 | return db.select(cls.table(), where="topic_id=$topic_id", vars=locals()) 66 | -------------------------------------------------------------------------------- /src/static/css/body.css: -------------------------------------------------------------------------------- 1 | /******************************* 2 | Highlighted Colors 3 | *******************************/ 4 | 5 | ::-webkit-selection { 6 | background-color: #FFFFCC; 7 | color: #555555; 8 | } 9 | ::-moz-selection { 10 | background-color: #FFFFCC; 11 | color: #555555; 12 | } 13 | ::selection { 14 | background-color: #FFFFCC; 15 | color: #555555; 16 | } 17 | 18 | h1::-moz-selection, 19 | h2::-moz-selection, 20 | h3::-moz-selection { 21 | background-color: #F1C1C2; 22 | color: #222222; 23 | } 24 | h1::selection, 25 | h2::selection, 26 | h3::selection { 27 | background-color: #F1C1C2; 28 | color: #222222; 29 | } 30 | .ui *::-moz-selection { 31 | background-color: #CCE2FF; 32 | } 33 | .ui *::selection { 34 | background-color: #CCE2FF; 35 | } 36 | 37 | /******************************* 38 | Global 39 | *******************************/ 40 | 41 | html, 42 | body { 43 | font-size: 15px; 44 | } 45 | 46 | body > .content { 47 | background: #FCFCFC url(/static/img/bg.jpg) repeat; 48 | } 49 | 50 | h1, 51 | h2, 52 | h3, 53 | h4, 54 | h5 { 55 | font-family: "Source Sans Pro", "Helvetica Neue", "Helvetica", "Arial", sans-serif; 56 | } 57 | 58 | ul.list { 59 | list-style-type: disc; 60 | } 61 | ul.list li { 62 | list-style-position: outside; 63 | } 64 | 65 | 66 | /* text and headers */ 67 | h1 { 68 | margin: 0px 0px 20px; 69 | padding: 50px 0px 5px; 70 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 71 | } 72 | h4 + p { 73 | margin: 0px 0px 20px; 74 | } 75 | pre { 76 | background-color: #F0F0F0; 77 | } 78 | pre.console { 79 | background-color: transparent; 80 | line-height: 1.6; 81 | font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; 82 | height: 300px; 83 | overflow: auto; 84 | } 85 | code { 86 | background-color: rgba(0, 0, 0, 0.02); 87 | border: 1px solid rgba(0, 0, 0, 0.1); 88 | display: inline-block; 89 | font-family: Courier New; 90 | font-size: 14px; 91 | margin: 0.25em; 92 | padding: 0.125em 0.5em; 93 | vertical-align: middle; 94 | } 95 | pre code { 96 | border: none; 97 | padding: 0px; 98 | } 99 | table pre, 100 | table code { 101 | margin: 0px !important; 102 | padding: 0px; 103 | background-color: transparent; 104 | } 105 | p { 106 | margin: 1em 0em; 107 | } 108 | p:first-child { 109 | margin-top: 0em; 110 | } 111 | p:last-child { 112 | margin-bottom: 0em; 113 | } 114 | /* links */ 115 | a { 116 | color: #009FDA; 117 | text-decoration: none; 118 | } 119 | a:hover { 120 | color: #00BAFF; 121 | } 122 | 123 | /*-------------- 124 | Intro 125 | ---------------*/ 126 | 127 | body.guide .main h3 { 128 | margin: 2rem 0em 0.5rem; 129 | } 130 | body.guide .main.container > * { 131 | max-width: 800px; 132 | } 133 | -------------------------------------------------------------------------------- /src/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Wechat 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | 42 | 43 |
44 |
45 |
46 |
47 |
48 |

登录

49 |
50 | 51 | 52 |
53 |
54 | 55 | 56 |
57 | 58 |
59 |
60 |
61 |
62 |

注册

63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 | 74 |
75 |
76 |
77 | 78 | 79 |
80 |
81 |
注册
82 |
83 |
84 |
85 |
86 |
87 | 88 | 89 |
90 | 91 | 92 |
93 |
94 |
95 |
96 |
97 |
98 | 99 | 100 |
101 |
102 |
Add
103 |
104 |
105 | 106 | 107 | 128 | 129 | 130 | 131 | 146 | 147 | 161 | 162 |
163 | 164 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /src/handlers.py: -------------------------------------------------------------------------------- 1 | #coding:utf-8 2 | import copy 3 | import json 4 | import hashlib 5 | import sqlite3 6 | from datetime import datetime 7 | 8 | import web 9 | from socketio import socketio_manage 10 | from socketio.namespace import BaseNamespace 11 | from socketio.mixins import RoomsMixin, BroadcastMixin 12 | 13 | from models import Message, User, Topic 14 | 15 | session = web.config._session 16 | 17 | CACHE_USER = {} 18 | 19 | 20 | def sha1(data): 21 | return hashlib.sha1(data).hexdigest() 22 | 23 | 24 | def bad_request(message): 25 | raise web.BadRequest(message=message) 26 | 27 | 28 | # 首页 29 | class IndexHandler: 30 | def GET(self): 31 | render = web.template.render('templates/') 32 | return render.index() 33 | 34 | 35 | class UserHandler: 36 | def GET(self): 37 | # 获取当前登录的用户数据 38 | user = session.user 39 | return json.dumps(user) 40 | 41 | def POST(self): 42 | data = web.data() 43 | data = json.loads(data) 44 | username = data.get("username") 45 | password = data.get("password") 46 | password_repeat = data.get("password_repeat") 47 | 48 | if password != password_repeat: 49 | return bad_request('两次密码输入不一致') 50 | 51 | user_data = { 52 | "username": username, 53 | "password": sha1(password), 54 | "registed_time": datetime.now(), 55 | } 56 | 57 | try: 58 | user_id = User.create(**user_data) 59 | except sqlite3.IntegrityError: 60 | return bad_request('用户名已存在!') 61 | 62 | user = User.get_by_id(user_id) 63 | session.login = True 64 | session.user = user 65 | 66 | result = { 67 | 'id': user_id, 68 | 'username': username, 69 | } 70 | return json.dumps(result) 71 | 72 | 73 | class LoginHandler: 74 | def POST(self): 75 | data = web.data() 76 | data = json.loads(data) 77 | username = data.get("username") 78 | password = data.get("password") 79 | user = User.get_by_username_password( 80 | username=username, 81 | password=sha1(password) 82 | ) 83 | if not user: 84 | return bad_request('用户名或密码错误!') 85 | 86 | session.login = True 87 | session.user = user 88 | result = { 89 | 'id': user.get('id'), 90 | 'username': user.get('username'), 91 | } 92 | return json.dumps(result) 93 | 94 | 95 | class LogoutHandler: 96 | def GET(self): 97 | session.login = False 98 | session.user = None 99 | session.kill() 100 | return web.tempredirect('/#login') 101 | 102 | 103 | class TopicHandler: 104 | def GET(self, pk=None): 105 | if pk: 106 | topic = Topic.get_by_id(pk) 107 | return json.dumps(topic) 108 | 109 | topics = Topic.get_all() 110 | result = [] 111 | for t in topics: 112 | topic = dict(t) 113 | try: 114 | user = CACHE_USER[t.owner_id] 115 | except KeyError: 116 | user = User.get_by_id(t.owner_id) 117 | CACHE_USER[t.owner_id] = user 118 | topic['owner_name'] = user.username 119 | result.append(topic) 120 | return json.dumps(result) 121 | 122 | def POST(self): 123 | if not session.user or not session.user.id: 124 | return bad_request('请先登录!') 125 | if session.user.username != 'the5fire': 126 | return bad_request('sorry,你没有创建权限') 127 | 128 | data = web.data() 129 | data = json.loads(data) 130 | 131 | topic_data = { 132 | "title": data.get('title'), 133 | "owner_id": session.user.id, 134 | "created_time": datetime.now(), 135 | } 136 | 137 | try: 138 | topic_id = Topic.create(**topic_data) 139 | except sqlite3.IntegrityError: 140 | return bad_request('你已创建过该名称!') 141 | 142 | result = { 143 | "id": topic_id, 144 | "title": topic_data.get('title'), 145 | "owner_id": session.user.id, 146 | "owner_name": session.user.username, 147 | "created_time": str(topic_data.get('created_time')), 148 | } 149 | return json.dumps(result) 150 | 151 | def PUT(self, obj_id=None): 152 | data = web.data() 153 | print data 154 | 155 | def DELETE(self, obj_id=None): 156 | pass 157 | 158 | 159 | class MessageHandler: 160 | def GET(self): 161 | topic_id = web.input().get('topic_id') 162 | if topic_id: 163 | messages = Message.get_by_topic(topic_id) or [] 164 | else: 165 | messages = Message.get_all() 166 | 167 | result = [] 168 | current_user_id = session.user.id 169 | for m in messages: 170 | try: 171 | user = CACHE_USER[m.user_id] 172 | except KeyError: 173 | user = User.get_by_id(m.user_id) 174 | CACHE_USER[m.user_id] = user 175 | message = dict(m) 176 | message['user_name'] = user.username 177 | message['is_mine'] = (current_user_id == user.id) 178 | result.append(message) 179 | return json.dumps(result) 180 | 181 | def POST(self): 182 | data = web.data() 183 | data = json.loads(data) 184 | if not (session.user and session.user.id): 185 | return bad_request("请先登录!") 186 | 187 | message_data = { 188 | "content": data.get("content"), 189 | "topic_id": data.get("topic_id"), 190 | "user_id": session.user.id, 191 | "created_time": datetime.now(), 192 | } 193 | m_id = Message.create(**message_data) 194 | result = { 195 | "id": m_id, 196 | "content": message_data.get("content"), 197 | "topic_id": message_data.get("topic_id"), 198 | "user_id": session.user.id, 199 | "user_name": session.user.username, 200 | "created_time": str(message_data.get("created_time")), 201 | "is_mine": True, 202 | } 203 | return json.dumps(result) 204 | 205 | 206 | class ChatNamespace(BaseNamespace, RoomsMixin, BroadcastMixin): 207 | def on_go_out(self): 208 | room_num = self.socket.session.get('room') 209 | if room_num: 210 | print 'go_out', room_num 211 | self.leave(room_num) 212 | 213 | def on_topic(self, topic_id): 214 | """ 加入以某个主题id为房间 215 | 216 | 客户端进入聊天室界面先发送此请求,确定房间号 217 | """ 218 | room_num = 'room_%s' % topic_id 219 | self.socket.session['room'] = room_num 220 | print 'join', room_num 221 | self.join(room_num) 222 | 223 | def on_message(self, model): 224 | user = self.environ['user'] 225 | if user is None: 226 | # 手动从store中取出user 227 | session_id = self.environ['session_id'] 228 | _data = session.store[session_id] 229 | user = _data['user'] 230 | model.update({ 231 | "user_id": user.id, 232 | "created_time": datetime.now(), 233 | }) 234 | m_id = Message.create(**model) 235 | model.update({ 236 | "user_name": user.username, 237 | 'id': m_id, 238 | 'created_time': str(model['created_time']), 239 | 'is_mine': True, 240 | }) 241 | # 发送回客户端 242 | self.emit('message', model) 243 | 244 | # 发送给其他人 245 | model['is_mine'] = False 246 | self.emit_to_room( 247 | self.socket.session['room'], 248 | 'message', 249 | model, 250 | ) 251 | 252 | def recv_disconnect(self): 253 | print 'DISCONNECT!!!!!!!!!!!!!!!!!!!!!!!' 254 | self.disconnect(silent=True) 255 | 256 | 257 | class SocketHandler: 258 | def GET(self): 259 | context = copy.copy(web.ctx.environ) 260 | context.update({ 261 | "user": session.user, 262 | "session_id": session.session_id, 263 | }) 264 | socketio_manage(context, {'': ChatNamespace}) 265 | # 重新载入session数据,因为session在socket请求中改变了 266 | session._load() 267 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/static/js/chat.js: -------------------------------------------------------------------------------- 1 | /* 2 | * author: the5fire 3 | * blog: the5fire.com 4 | * date: 2014-03-16 5 | * */ 6 | $(function(){ 7 | WEB_SOCKET_SWF_LOCATION = "/static/WebSocketMain.swf"; 8 | WEB_SOCKET_DEBUG = true; 9 | 10 | var socket = io.connect(); 11 | socket.on('connect', function(){ 12 | console.log('connected'); 13 | }); 14 | 15 | $(window).bind("beforeunload", function() { 16 | socket.disconnect(); 17 | }); 18 | 19 | var User = Backbone.Model.extend({ 20 | urlRoot: '/user', 21 | }); 22 | 23 | var Topic = Backbone.Model.extend({ 24 | urlRoot: '/topic', 25 | }); 26 | 27 | var Message = Backbone.Model.extend({ 28 | urlRoot: '/message', 29 | sync: function(method, model, options) { 30 | if (method === 'create') { 31 | socket.emit('message', model.attributes); 32 | // 错误处理没做 33 | $('#comment').val(''); 34 | } else { 35 | return Backbone.sync(method, model, options); 36 | }; 37 | }, 38 | }); 39 | 40 | var Topics = Backbone.Collection.extend({ 41 | url: '/topic', 42 | model: Topic, 43 | }); 44 | 45 | var Messages = Backbone.Collection.extend({ 46 | url: '/message', 47 | model: Message, 48 | }); 49 | 50 | var topics = new Topics; 51 | 52 | var TopicView = Backbone.View.extend({ 53 | tagName: "div class='column'", 54 | templ: _.template($('#topic-template').html()), 55 | 56 | // 渲染列表页模板 57 | render: function() { 58 | $(this.el).html(this.templ(this.model.toJSON())); 59 | return this; 60 | }, 61 | }); 62 | 63 | var messages = new Messages; 64 | 65 | var MessageView = Backbone.View.extend({ 66 | tagName: "div class='comment'", 67 | templ: _.template($('#message-template').html()), 68 | 69 | // 渲染列表页模板 70 | render: function() { 71 | $(this.el).html(this.templ(this.model.toJSON())); 72 | return this; 73 | }, 74 | }); 75 | 76 | 77 | var AppView = Backbone.View.extend({ 78 | el: "#main", 79 | topic_list: $("#topic_list"), 80 | topic_section: $("#topic_section"), 81 | message_section: $("#message_section"), 82 | message_list: $("#message_list"), 83 | message_head: $("#message_head"), 84 | 85 | events: { 86 | 'click .submit': 'saveMessage', 87 | 'click .submit_topic': 'saveTopic', 88 | 'keypress #comment': 'saveMessageEvent', 89 | }, 90 | 91 | initialize: function() { 92 | _.bindAll(this, 'addTopic', 'addMessage'); 93 | 94 | topics.bind('add', this.addTopic); 95 | 96 | // 定义消息列表池,每个topic有自己的message collection 97 | // 这样保证每个主题下得消息不冲突 98 | this.message_pool = {}; 99 | this.socket = null; 100 | 101 | this.message_list_div = document.getElementById('message_list'); 102 | }, 103 | 104 | addTopic: function(topic) { 105 | var view = new TopicView({model: topic}); 106 | this.topic_list.append(view.render().el); 107 | }, 108 | 109 | addMessage: function(message) { 110 | var view = new MessageView({model: message}); 111 | this.message_list.append(view.render().el); 112 | self.message_list.scrollTop(self.message_list_div.scrollHeight); 113 | }, 114 | 115 | saveMessageEvent: function(evt) { 116 | if (evt.keyCode == 13) { 117 | this.saveMessage(evt); 118 | } 119 | }, 120 | saveMessage: function(evt) { 121 | var comment_box = $('#comment') 122 | var content = comment_box.val(); 123 | if (content == '') { 124 | alert('内容不能为空'); 125 | return false; 126 | } 127 | var topic_id = comment_box.attr('topic_id'); 128 | var message = new Message({ 129 | content: content, 130 | topic_id: topic_id, 131 | }); 132 | var messages = this.message_pool[topic_id]; 133 | message.save(); // 依赖上面对sync的重载 134 | }, 135 | 136 | saveTopic: function(evt) { 137 | var topic_title = $('#topic_title'); 138 | if (topic_title.val() == '') { 139 | alert('主题不能为空!'); 140 | return false 141 | } 142 | var topic = new Topic({ 143 | title: topic_title.val(), 144 | }); 145 | self = this; 146 | topic.save(null, { 147 | success: function(model, response, options){ 148 | topics.add(response); 149 | topic_title.val(''); 150 | }, 151 | error: function(model, resp, options) { 152 | alert(resp.responseText); 153 | } 154 | }); 155 | }, 156 | 157 | showTopic: function(){ 158 | topics.fetch(); 159 | this.topic_section.show(); 160 | this.message_section.hide(); 161 | this.message_list.html(''); 162 | 163 | this.goOut() 164 | }, 165 | 166 | goOut: function(){ 167 | // 退出房间 168 | socket.emit('go_out'); 169 | socket.removeAllListeners('message'); 170 | }, 171 | 172 | initMessage: function(topic_id) { 173 | var messages = new Messages; 174 | messages.bind('add', this.addMessage); 175 | this.message_pool[topic_id] = messages; 176 | }, 177 | 178 | showMessage: function(topic_id) { 179 | this.initMessage(topic_id); 180 | 181 | this.message_section.show(); 182 | this.topic_section.hide(); 183 | 184 | this.showMessageHead(topic_id); 185 | $('#comment').attr('topic_id', topic_id); 186 | 187 | var messages = this.message_pool[topic_id]; 188 | // 进入房间 189 | socket.emit('topic', topic_id); 190 | // 监听message事件,添加对话到messages中 191 | socket.on('message', function(response) { 192 | messages.add(response); 193 | }); 194 | messages.fetch({ 195 | data: {topic_id: topic_id}, 196 | success: function(resp) { 197 | self.message_list.scrollTop(self.message_list_div.scrollHeight) 198 | }, 199 | error: function(model, resp, options) { 200 | alert(resp.responseText); 201 | } 202 | }); 203 | }, 204 | 205 | showMessageHead: function(topic_id) { 206 | var topic = new Topic({id: topic_id}); 207 | self = this; 208 | topic.fetch({ 209 | success: function(resp, model, options){ 210 | self.message_head.html(model.title); 211 | }, 212 | error: function(model, resp, options) { 213 | alert(resp.responseText); 214 | } 215 | }); 216 | }, 217 | 218 | }); 219 | 220 | 221 | var LoginView = Backbone.View.extend({ 222 | el: "#login", 223 | wrapper: $('#wrapper'), 224 | 225 | events: { 226 | 'keypress #login_pwd': 'loginEvent', 227 | 'click .login_submit': 'login', 228 | 'keypress #reg_pwd_repeat': 'registeEvent', 229 | 'click .registe_submit': 'registe', 230 | }, 231 | 232 | hide: function() { 233 | this.wrapper.hide(); 234 | }, 235 | 236 | show: function() { 237 | this.wrapper.show(); 238 | }, 239 | 240 | loginEvent: function(evt) { 241 | if (evt.keyCode == 13) { 242 | this.login(evt); 243 | } 244 | }, 245 | 246 | login: function(evt){ 247 | var username_input = $('#login_username'); 248 | var pwd_input = $('#login_pwd'); 249 | var u = new User({ 250 | username: username_input.val(), 251 | password: pwd_input.val(), 252 | }); 253 | u.save(null, { 254 | url: '/login', 255 | success: function(model, resp, options){ 256 | g_user = resp; 257 | // 跳转到index 258 | appRouter.navigate('index', {trigger: true}); 259 | }, 260 | error: function(model, resp, options) { 261 | alert(resp.responseText); 262 | } 263 | }); 264 | }, 265 | 266 | registeEvent: function(evt) { 267 | if (evt.keyCode == 13) { 268 | this.registe(evt); 269 | } 270 | }, 271 | 272 | registe: function(evt){ 273 | var reg_username_input = $('#reg_username'); 274 | var reg_pwd_input = $('#reg_pwd'); 275 | var reg_pwd_repeat_input = $('#reg_pwd_repeat'); 276 | var u = new User({ 277 | username: reg_username_input.val(), 278 | password: reg_pwd_input.val(), 279 | password_repeat: reg_pwd_repeat_input.val(), 280 | }); 281 | u.save(null, { 282 | success: function(model, resp, options){ 283 | g_user = resp; 284 | // 跳转到index 285 | appRouter.navigate('index', {trigger: true}); 286 | }, 287 | error: function(model, resp, options) { 288 | alert(resp.responseText); 289 | } 290 | }); 291 | }, 292 | }); 293 | 294 | var UserView = Backbone.View.extend({ 295 | el: "#user_info", 296 | username: $('#username'), 297 | 298 | show: function(username) { 299 | this.username.html(username); 300 | this.$el.show(); 301 | }, 302 | }); 303 | 304 | var AppRouter = Backbone.Router.extend({ 305 | routes: { 306 | "login": "login", 307 | "index": "index", 308 | "topic/:id" : "topic", 309 | }, 310 | 311 | initialize: function(){ 312 | // 初始化项目, 显示首页 313 | this.appView = new AppView(); 314 | this.loginView = new LoginView(); 315 | this.userView = new UserView(); 316 | this.indexFlag = false; 317 | }, 318 | 319 | login: function(){ 320 | this.loginView.show(); 321 | }, 322 | 323 | index: function(){ 324 | if (g_user && g_user.id != undefined) { 325 | this.appView.showTopic(); 326 | this.userView.show(g_user.username); 327 | this.loginView.hide(); 328 | this.indexFlag = true; // 标志已经到达主页了 329 | } 330 | }, 331 | 332 | topic: function(topic_id) { 333 | if (g_user && g_user.id != undefined) { 334 | this.appView.showMessage(topic_id); 335 | this.userView.show(g_user.username); 336 | this.loginView.hide(); 337 | this.indexFlag = true; // 标志已经到达主页了 338 | } 339 | }, 340 | }); 341 | 342 | var appRouter = new AppRouter(); 343 | var g_user = new User; 344 | g_user.fetch({ 345 | success: function(model, resp, options){ 346 | g_user = resp; 347 | Backbone.history.start({pustState: true}); 348 | 349 | if(g_user === null || g_user.id === undefined) { 350 | // 跳转到登录页面 351 | appRouter.navigate('login', {trigger: true}); 352 | } else if (appRouter.indexFlag == false){ 353 | // 跳转到首页 354 | appRouter.navigate('index', {trigger: true}); 355 | } 356 | }, 357 | error: function(model, resp, options) { 358 | alert(resp.responseText); 359 | } 360 | }); // 获取当前用户 361 | }); 362 | -------------------------------------------------------------------------------- /src/static/js/json2.js: -------------------------------------------------------------------------------- 1 | /* 2 | http://www.JSON.org/json2.js 3 | 2009-09-29 4 | 5 | Public Domain. 6 | 7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 8 | 9 | See http://www.JSON.org/js.html 10 | 11 | 12 | This code should be minified before deployment. 13 | See http://javascript.crockford.com/jsmin.html 14 | 15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 16 | NOT CONTROL. 17 | 18 | 19 | This file creates a global JSON object containing two methods: stringify 20 | and parse. 21 | 22 | JSON.stringify(value, replacer, space) 23 | value any JavaScript value, usually an object or array. 24 | 25 | replacer an optional parameter that determines how object 26 | values are stringified for objects. It can be a 27 | function or an array of strings. 28 | 29 | space an optional parameter that specifies the indentation 30 | of nested structures. If it is omitted, the text will 31 | be packed without extra whitespace. If it is a number, 32 | it will specify the number of spaces to indent at each 33 | level. If it is a string (such as '\t' or ' '), 34 | it contains the characters used to indent at each level. 35 | 36 | This method produces a JSON text from a JavaScript value. 37 | 38 | When an object value is found, if the object contains a toJSON 39 | method, its toJSON method will be called and the result will be 40 | stringified. A toJSON method does not serialize: it returns the 41 | value represented by the name/value pair that should be serialized, 42 | or undefined if nothing should be serialized. The toJSON method 43 | will be passed the key associated with the value, and this will be 44 | bound to the value 45 | 46 | For example, this would serialize Dates as ISO strings. 47 | 48 | Date.prototype.toJSON = function (key) { 49 | function f(n) { 50 | // Format integers to have at least two digits. 51 | return n < 10 ? '0' + n : n; 52 | } 53 | 54 | return this.getUTCFullYear() + '-' + 55 | f(this.getUTCMonth() + 1) + '-' + 56 | f(this.getUTCDate()) + 'T' + 57 | f(this.getUTCHours()) + ':' + 58 | f(this.getUTCMinutes()) + ':' + 59 | f(this.getUTCSeconds()) + 'Z'; 60 | }; 61 | 62 | You can provide an optional replacer method. It will be passed the 63 | key and value of each member, with this bound to the containing 64 | object. The value that is returned from your method will be 65 | serialized. If your method returns undefined, then the member will 66 | be excluded from the serialization. 67 | 68 | If the replacer parameter is an array of strings, then it will be 69 | used to select the members to be serialized. It filters the results 70 | such that only members with keys listed in the replacer array are 71 | stringified. 72 | 73 | Values that do not have JSON representations, such as undefined or 74 | functions, will not be serialized. Such values in objects will be 75 | dropped; in arrays they will be replaced with null. You can use 76 | a replacer function to replace those with JSON values. 77 | JSON.stringify(undefined) returns undefined. 78 | 79 | The optional space parameter produces a stringification of the 80 | value that is filled with line breaks and indentation to make it 81 | easier to read. 82 | 83 | If the space parameter is a non-empty string, then that string will 84 | be used for indentation. If the space parameter is a number, then 85 | the indentation will be that many spaces. 86 | 87 | Example: 88 | 89 | text = JSON.stringify(['e', {pluribus: 'unum'}]); 90 | // text is '["e",{"pluribus":"unum"}]' 91 | 92 | 93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); 94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' 95 | 96 | text = JSON.stringify([new Date()], function (key, value) { 97 | return this[key] instanceof Date ? 98 | 'Date(' + this[key] + ')' : value; 99 | }); 100 | // text is '["Date(---current time---)"]' 101 | 102 | 103 | JSON.parse(text, reviver) 104 | This method parses a JSON text to produce an object or array. 105 | It can throw a SyntaxError exception. 106 | 107 | The optional reviver parameter is a function that can filter and 108 | transform the results. It receives each of the keys and values, 109 | and its return value is used instead of the original value. 110 | If it returns what it received, then the structure is not modified. 111 | If it returns undefined then the member is deleted. 112 | 113 | Example: 114 | 115 | // Parse the text. Values that look like ISO date strings will 116 | // be converted to Date objects. 117 | 118 | myData = JSON.parse(text, function (key, value) { 119 | var a; 120 | if (typeof value === 'string') { 121 | a = 122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); 123 | if (a) { 124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], 125 | +a[5], +a[6])); 126 | } 127 | } 128 | return value; 129 | }); 130 | 131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { 132 | var d; 133 | if (typeof value === 'string' && 134 | value.slice(0, 5) === 'Date(' && 135 | value.slice(-1) === ')') { 136 | d = new Date(value.slice(5, -1)); 137 | if (d) { 138 | return d; 139 | } 140 | } 141 | return value; 142 | }); 143 | 144 | 145 | This is a reference implementation. You are free to copy, modify, or 146 | redistribute. 147 | */ 148 | 149 | /*jslint evil: true, strict: false */ 150 | 151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, 152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, 153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, 154 | lastIndex, length, parse, prototype, push, replace, slice, stringify, 155 | test, toJSON, toString, valueOf 156 | */ 157 | 158 | 159 | // Create a JSON object only if one does not already exist. We create the 160 | // methods in a closure to avoid creating global variables. 161 | 162 | if (!this.JSON) { 163 | this.JSON = {}; 164 | } 165 | 166 | (function () { 167 | 168 | function f(n) { 169 | // Format integers to have at least two digits. 170 | return n < 10 ? '0' + n : n; 171 | } 172 | 173 | if (typeof Date.prototype.toJSON !== 'function') { 174 | 175 | Date.prototype.toJSON = function (key) { 176 | 177 | return isFinite(this.valueOf()) ? 178 | this.getUTCFullYear() + '-' + 179 | f(this.getUTCMonth() + 1) + '-' + 180 | f(this.getUTCDate()) + 'T' + 181 | f(this.getUTCHours()) + ':' + 182 | f(this.getUTCMinutes()) + ':' + 183 | f(this.getUTCSeconds()) + 'Z' : null; 184 | }; 185 | 186 | String.prototype.toJSON = 187 | Number.prototype.toJSON = 188 | Boolean.prototype.toJSON = function (key) { 189 | return this.valueOf(); 190 | }; 191 | } 192 | 193 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 194 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 195 | gap, 196 | indent, 197 | meta = { // table of character substitutions 198 | '\b': '\\b', 199 | '\t': '\\t', 200 | '\n': '\\n', 201 | '\f': '\\f', 202 | '\r': '\\r', 203 | '"' : '\\"', 204 | '\\': '\\\\' 205 | }, 206 | rep; 207 | 208 | 209 | function quote(string) { 210 | 211 | // If the string contains no control characters, no quote characters, and no 212 | // backslash characters, then we can safely slap some quotes around it. 213 | // Otherwise we must also replace the offending characters with safe escape 214 | // sequences. 215 | 216 | escapable.lastIndex = 0; 217 | return escapable.test(string) ? 218 | '"' + string.replace(escapable, function (a) { 219 | var c = meta[a]; 220 | return typeof c === 'string' ? c : 221 | '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 222 | }) + '"' : 223 | '"' + string + '"'; 224 | } 225 | 226 | 227 | function str(key, holder) { 228 | 229 | // Produce a string from holder[key]. 230 | 231 | var i, // The loop counter. 232 | k, // The member key. 233 | v, // The member value. 234 | length, 235 | mind = gap, 236 | partial, 237 | value = holder[key]; 238 | 239 | // If the value has a toJSON method, call it to obtain a replacement value. 240 | 241 | if (value && typeof value === 'object' && 242 | typeof value.toJSON === 'function') { 243 | value = value.toJSON(key); 244 | } 245 | 246 | // If we were called with a replacer function, then call the replacer to 247 | // obtain a replacement value. 248 | 249 | if (typeof rep === 'function') { 250 | value = rep.call(holder, key, value); 251 | } 252 | 253 | // What happens next depends on the value's type. 254 | 255 | switch (typeof value) { 256 | case 'string': 257 | return quote(value); 258 | 259 | case 'number': 260 | 261 | // JSON numbers must be finite. Encode non-finite numbers as null. 262 | 263 | return isFinite(value) ? String(value) : 'null'; 264 | 265 | case 'boolean': 266 | case 'null': 267 | 268 | // If the value is a boolean or null, convert it to a string. Note: 269 | // typeof null does not produce 'null'. The case is included here in 270 | // the remote chance that this gets fixed someday. 271 | 272 | return String(value); 273 | 274 | // If the type is 'object', we might be dealing with an object or an array or 275 | // null. 276 | 277 | case 'object': 278 | 279 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 280 | // so watch out for that case. 281 | 282 | if (!value) { 283 | return 'null'; 284 | } 285 | 286 | // Make an array to hold the partial results of stringifying this object value. 287 | 288 | gap += indent; 289 | partial = []; 290 | 291 | // Is the value an array? 292 | 293 | if (Object.prototype.toString.apply(value) === '[object Array]') { 294 | 295 | // The value is an array. Stringify every element. Use null as a placeholder 296 | // for non-JSON values. 297 | 298 | length = value.length; 299 | for (i = 0; i < length; i += 1) { 300 | partial[i] = str(i, value) || 'null'; 301 | } 302 | 303 | // Join all of the elements together, separated with commas, and wrap them in 304 | // brackets. 305 | 306 | v = partial.length === 0 ? '[]' : 307 | gap ? '[\n' + gap + 308 | partial.join(',\n' + gap) + '\n' + 309 | mind + ']' : 310 | '[' + partial.join(',') + ']'; 311 | gap = mind; 312 | return v; 313 | } 314 | 315 | // If the replacer is an array, use it to select the members to be stringified. 316 | 317 | if (rep && typeof rep === 'object') { 318 | length = rep.length; 319 | for (i = 0; i < length; i += 1) { 320 | k = rep[i]; 321 | if (typeof k === 'string') { 322 | v = str(k, value); 323 | if (v) { 324 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 325 | } 326 | } 327 | } 328 | } else { 329 | 330 | // Otherwise, iterate through all of the keys in the object. 331 | 332 | for (k in value) { 333 | if (Object.hasOwnProperty.call(value, k)) { 334 | v = str(k, value); 335 | if (v) { 336 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 337 | } 338 | } 339 | } 340 | } 341 | 342 | // Join all of the member texts together, separated with commas, 343 | // and wrap them in braces. 344 | 345 | v = partial.length === 0 ? '{}' : 346 | gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + 347 | mind + '}' : '{' + partial.join(',') + '}'; 348 | gap = mind; 349 | return v; 350 | } 351 | } 352 | 353 | // If the JSON object does not yet have a stringify method, give it one. 354 | 355 | if (typeof JSON.stringify !== 'function') { 356 | JSON.stringify = function (value, replacer, space) { 357 | 358 | // The stringify method takes a value and an optional replacer, and an optional 359 | // space parameter, and returns a JSON text. The replacer can be a function 360 | // that can replace values, or an array of strings that will select the keys. 361 | // A default replacer method can be provided. Use of the space parameter can 362 | // produce text that is more easily readable. 363 | 364 | var i; 365 | gap = ''; 366 | indent = ''; 367 | 368 | // If the space parameter is a number, make an indent string containing that 369 | // many spaces. 370 | 371 | if (typeof space === 'number') { 372 | for (i = 0; i < space; i += 1) { 373 | indent += ' '; 374 | } 375 | 376 | // If the space parameter is a string, it will be used as the indent string. 377 | 378 | } else if (typeof space === 'string') { 379 | indent = space; 380 | } 381 | 382 | // If there is a replacer, it must be a function or an array. 383 | // Otherwise, throw an error. 384 | 385 | rep = replacer; 386 | if (replacer && typeof replacer !== 'function' && 387 | (typeof replacer !== 'object' || 388 | typeof replacer.length !== 'number')) { 389 | throw new Error('JSON.stringify'); 390 | } 391 | 392 | // Make a fake root object containing our value under the key of ''. 393 | // Return the result of stringifying the value. 394 | 395 | return str('', {'': value}); 396 | }; 397 | } 398 | 399 | 400 | // If the JSON object does not yet have a parse method, give it one. 401 | 402 | if (typeof JSON.parse !== 'function') { 403 | JSON.parse = function (text, reviver) { 404 | 405 | // The parse method takes a text and an optional reviver function, and returns 406 | // a JavaScript value if the text is a valid JSON text. 407 | 408 | var j; 409 | 410 | function walk(holder, key) { 411 | 412 | // The walk method is used to recursively walk the resulting structure so 413 | // that modifications can be made. 414 | 415 | var k, v, value = holder[key]; 416 | if (value && typeof value === 'object') { 417 | for (k in value) { 418 | if (Object.hasOwnProperty.call(value, k)) { 419 | v = walk(value, k); 420 | if (v !== undefined) { 421 | value[k] = v; 422 | } else { 423 | delete value[k]; 424 | } 425 | } 426 | } 427 | } 428 | return reviver.call(holder, key, value); 429 | } 430 | 431 | 432 | // Parsing happens in four stages. In the first stage, we replace certain 433 | // Unicode characters with escape sequences. JavaScript handles many characters 434 | // incorrectly, either silently deleting them, or treating them as line endings. 435 | 436 | cx.lastIndex = 0; 437 | if (cx.test(text)) { 438 | text = text.replace(cx, function (a) { 439 | return '\\u' + 440 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 441 | }); 442 | } 443 | 444 | // In the second stage, we run the text against regular expressions that look 445 | // for non-JSON patterns. We are especially concerned with '()' and 'new' 446 | // because they can cause invocation, and '=' because it can cause mutation. 447 | // But just to be safe, we want to reject all unexpected forms. 448 | 449 | // We split the second stage into 4 regexp operations in order to work around 450 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 451 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we 452 | // replace all simple value tokens with ']' characters. Third, we delete all 453 | // open brackets that follow a colon or comma or that begin the text. Finally, 454 | // we look to see that the remaining characters are only whitespace or ']' or 455 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. 456 | 457 | if (/^[\],:{}\s]*$/. 458 | test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). 459 | replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). 460 | replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 461 | 462 | // In the third stage we use the eval function to compile the text into a 463 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity 464 | // in JavaScript: it can begin a block or an object literal. We wrap the text 465 | // in parens to eliminate the ambiguity. 466 | 467 | j = eval('(' + text + ')'); 468 | 469 | // In the optional fourth stage, we recursively walk the new structure, passing 470 | // each name/value pair to a reviver function for possible transformation. 471 | 472 | return typeof reviver === 'function' ? 473 | walk({'': j}, '') : j; 474 | } 475 | 476 | // If the text is not JSON parseable, then a SyntaxError is thrown. 477 | 478 | throw new SyntaxError('JSON.parse'); 479 | }; 480 | } 481 | }()); -------------------------------------------------------------------------------- /src/static/js/underscore.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.4.3 2 | // http://underscorejs.org 3 | // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. 4 | // Underscore may be freely distributed under the MIT license. 5 | 6 | (function() { 7 | 8 | // Baseline setup 9 | // -------------- 10 | 11 | // Establish the root object, `window` in the browser, or `global` on the server. 12 | var root = this; 13 | 14 | // Save the previous value of the `_` variable. 15 | var previousUnderscore = root._; 16 | 17 | // Establish the object that gets returned to break out of a loop iteration. 18 | var breaker = {}; 19 | 20 | // Save bytes in the minified (but not gzipped) version: 21 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; 22 | 23 | // Create quick reference variables for speed access to core prototypes. 24 | var push = ArrayProto.push, 25 | slice = ArrayProto.slice, 26 | concat = ArrayProto.concat, 27 | toString = ObjProto.toString, 28 | hasOwnProperty = ObjProto.hasOwnProperty; 29 | 30 | // All **ECMAScript 5** native function implementations that we hope to use 31 | // are declared here. 32 | var 33 | nativeForEach = ArrayProto.forEach, 34 | nativeMap = ArrayProto.map, 35 | nativeReduce = ArrayProto.reduce, 36 | nativeReduceRight = ArrayProto.reduceRight, 37 | nativeFilter = ArrayProto.filter, 38 | nativeEvery = ArrayProto.every, 39 | nativeSome = ArrayProto.some, 40 | nativeIndexOf = ArrayProto.indexOf, 41 | nativeLastIndexOf = ArrayProto.lastIndexOf, 42 | nativeIsArray = Array.isArray, 43 | nativeKeys = Object.keys, 44 | nativeBind = FuncProto.bind; 45 | 46 | // Create a safe reference to the Underscore object for use below. 47 | var _ = function(obj) { 48 | if (obj instanceof _) return obj; 49 | if (!(this instanceof _)) return new _(obj); 50 | this._wrapped = obj; 51 | }; 52 | 53 | // Export the Underscore object for **Node.js**, with 54 | // backwards-compatibility for the old `require()` API. If we're in 55 | // the browser, add `_` as a global object via a string identifier, 56 | // for Closure Compiler "advanced" mode. 57 | if (typeof exports !== 'undefined') { 58 | if (typeof module !== 'undefined' && module.exports) { 59 | exports = module.exports = _; 60 | } 61 | exports._ = _; 62 | } else { 63 | root._ = _; 64 | } 65 | 66 | // Current version. 67 | _.VERSION = '1.4.3'; 68 | 69 | // Collection Functions 70 | // -------------------- 71 | 72 | // The cornerstone, an `each` implementation, aka `forEach`. 73 | // Handles objects with the built-in `forEach`, arrays, and raw objects. 74 | // Delegates to **ECMAScript 5**'s native `forEach` if available. 75 | var each = _.each = _.forEach = function(obj, iterator, context) { 76 | if (obj == null) return; 77 | if (nativeForEach && obj.forEach === nativeForEach) { 78 | obj.forEach(iterator, context); 79 | } else if (obj.length === +obj.length) { 80 | for (var i = 0, l = obj.length; i < l; i++) { 81 | if (iterator.call(context, obj[i], i, obj) === breaker) return; 82 | } 83 | } else { 84 | for (var key in obj) { 85 | if (_.has(obj, key)) { 86 | if (iterator.call(context, obj[key], key, obj) === breaker) return; 87 | } 88 | } 89 | } 90 | }; 91 | 92 | // Return the results of applying the iterator to each element. 93 | // Delegates to **ECMAScript 5**'s native `map` if available. 94 | _.map = _.collect = function(obj, iterator, context) { 95 | var results = []; 96 | if (obj == null) return results; 97 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); 98 | each(obj, function(value, index, list) { 99 | results[results.length] = iterator.call(context, value, index, list); 100 | }); 101 | return results; 102 | }; 103 | 104 | var reduceError = 'Reduce of empty array with no initial value'; 105 | 106 | // **Reduce** builds up a single result from a list of values, aka `inject`, 107 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. 108 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { 109 | var initial = arguments.length > 2; 110 | if (obj == null) obj = []; 111 | if (nativeReduce && obj.reduce === nativeReduce) { 112 | if (context) iterator = _.bind(iterator, context); 113 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); 114 | } 115 | each(obj, function(value, index, list) { 116 | if (!initial) { 117 | memo = value; 118 | initial = true; 119 | } else { 120 | memo = iterator.call(context, memo, value, index, list); 121 | } 122 | }); 123 | if (!initial) throw new TypeError(reduceError); 124 | return memo; 125 | }; 126 | 127 | // The right-associative version of reduce, also known as `foldr`. 128 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available. 129 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) { 130 | var initial = arguments.length > 2; 131 | if (obj == null) obj = []; 132 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { 133 | if (context) iterator = _.bind(iterator, context); 134 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); 135 | } 136 | var length = obj.length; 137 | if (length !== +length) { 138 | var keys = _.keys(obj); 139 | length = keys.length; 140 | } 141 | each(obj, function(value, index, list) { 142 | index = keys ? keys[--length] : --length; 143 | if (!initial) { 144 | memo = obj[index]; 145 | initial = true; 146 | } else { 147 | memo = iterator.call(context, memo, obj[index], index, list); 148 | } 149 | }); 150 | if (!initial) throw new TypeError(reduceError); 151 | return memo; 152 | }; 153 | 154 | // Return the first value which passes a truth test. Aliased as `detect`. 155 | _.find = _.detect = function(obj, iterator, context) { 156 | var result; 157 | any(obj, function(value, index, list) { 158 | if (iterator.call(context, value, index, list)) { 159 | result = value; 160 | return true; 161 | } 162 | }); 163 | return result; 164 | }; 165 | 166 | // Return all the elements that pass a truth test. 167 | // Delegates to **ECMAScript 5**'s native `filter` if available. 168 | // Aliased as `select`. 169 | _.filter = _.select = function(obj, iterator, context) { 170 | var results = []; 171 | if (obj == null) return results; 172 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); 173 | each(obj, function(value, index, list) { 174 | if (iterator.call(context, value, index, list)) results[results.length] = value; 175 | }); 176 | return results; 177 | }; 178 | 179 | // Return all the elements for which a truth test fails. 180 | _.reject = function(obj, iterator, context) { 181 | return _.filter(obj, function(value, index, list) { 182 | return !iterator.call(context, value, index, list); 183 | }, context); 184 | }; 185 | 186 | // Determine whether all of the elements match a truth test. 187 | // Delegates to **ECMAScript 5**'s native `every` if available. 188 | // Aliased as `all`. 189 | _.every = _.all = function(obj, iterator, context) { 190 | iterator || (iterator = _.identity); 191 | var result = true; 192 | if (obj == null) return result; 193 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); 194 | each(obj, function(value, index, list) { 195 | if (!(result = result && iterator.call(context, value, index, list))) return breaker; 196 | }); 197 | return !!result; 198 | }; 199 | 200 | // Determine if at least one element in the object matches a truth test. 201 | // Delegates to **ECMAScript 5**'s native `some` if available. 202 | // Aliased as `any`. 203 | var any = _.some = _.any = function(obj, iterator, context) { 204 | iterator || (iterator = _.identity); 205 | var result = false; 206 | if (obj == null) return result; 207 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); 208 | each(obj, function(value, index, list) { 209 | if (result || (result = iterator.call(context, value, index, list))) return breaker; 210 | }); 211 | return !!result; 212 | }; 213 | 214 | // Determine if the array or object contains a given value (using `===`). 215 | // Aliased as `include`. 216 | _.contains = _.include = function(obj, target) { 217 | if (obj == null) return false; 218 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; 219 | return any(obj, function(value) { 220 | return value === target; 221 | }); 222 | }; 223 | 224 | // Invoke a method (with arguments) on every item in a collection. 225 | _.invoke = function(obj, method) { 226 | var args = slice.call(arguments, 2); 227 | return _.map(obj, function(value) { 228 | return (_.isFunction(method) ? method : value[method]).apply(value, args); 229 | }); 230 | }; 231 | 232 | // Convenience version of a common use case of `map`: fetching a property. 233 | _.pluck = function(obj, key) { 234 | return _.map(obj, function(value){ return value[key]; }); 235 | }; 236 | 237 | // Convenience version of a common use case of `filter`: selecting only objects 238 | // with specific `key:value` pairs. 239 | _.where = function(obj, attrs) { 240 | if (_.isEmpty(attrs)) return []; 241 | return _.filter(obj, function(value) { 242 | for (var key in attrs) { 243 | if (attrs[key] !== value[key]) return false; 244 | } 245 | return true; 246 | }); 247 | }; 248 | 249 | // Return the maximum element or (element-based computation). 250 | // Can't optimize arrays of integers longer than 65,535 elements. 251 | // See: https://bugs.webkit.org/show_bug.cgi?id=80797 252 | _.max = function(obj, iterator, context) { 253 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 254 | return Math.max.apply(Math, obj); 255 | } 256 | if (!iterator && _.isEmpty(obj)) return -Infinity; 257 | var result = {computed : -Infinity, value: -Infinity}; 258 | each(obj, function(value, index, list) { 259 | var computed = iterator ? iterator.call(context, value, index, list) : value; 260 | computed >= result.computed && (result = {value : value, computed : computed}); 261 | }); 262 | return result.value; 263 | }; 264 | 265 | // Return the minimum element (or element-based computation). 266 | _.min = function(obj, iterator, context) { 267 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { 268 | return Math.min.apply(Math, obj); 269 | } 270 | if (!iterator && _.isEmpty(obj)) return Infinity; 271 | var result = {computed : Infinity, value: Infinity}; 272 | each(obj, function(value, index, list) { 273 | var computed = iterator ? iterator.call(context, value, index, list) : value; 274 | computed < result.computed && (result = {value : value, computed : computed}); 275 | }); 276 | return result.value; 277 | }; 278 | 279 | // Shuffle an array. 280 | _.shuffle = function(obj) { 281 | var rand; 282 | var index = 0; 283 | var shuffled = []; 284 | each(obj, function(value) { 285 | rand = _.random(index++); 286 | shuffled[index - 1] = shuffled[rand]; 287 | shuffled[rand] = value; 288 | }); 289 | return shuffled; 290 | }; 291 | 292 | // An internal function to generate lookup iterators. 293 | var lookupIterator = function(value) { 294 | return _.isFunction(value) ? value : function(obj){ return obj[value]; }; 295 | }; 296 | 297 | // Sort the object's values by a criterion produced by an iterator. 298 | _.sortBy = function(obj, value, context) { 299 | var iterator = lookupIterator(value); 300 | return _.pluck(_.map(obj, function(value, index, list) { 301 | return { 302 | value : value, 303 | index : index, 304 | criteria : iterator.call(context, value, index, list) 305 | }; 306 | }).sort(function(left, right) { 307 | var a = left.criteria; 308 | var b = right.criteria; 309 | if (a !== b) { 310 | if (a > b || a === void 0) return 1; 311 | if (a < b || b === void 0) return -1; 312 | } 313 | return left.index < right.index ? -1 : 1; 314 | }), 'value'); 315 | }; 316 | 317 | // An internal function used for aggregate "group by" operations. 318 | var group = function(obj, value, context, behavior) { 319 | var result = {}; 320 | var iterator = lookupIterator(value || _.identity); 321 | each(obj, function(value, index) { 322 | var key = iterator.call(context, value, index, obj); 323 | behavior(result, key, value); 324 | }); 325 | return result; 326 | }; 327 | 328 | // Groups the object's values by a criterion. Pass either a string attribute 329 | // to group by, or a function that returns the criterion. 330 | _.groupBy = function(obj, value, context) { 331 | return group(obj, value, context, function(result, key, value) { 332 | (_.has(result, key) ? result[key] : (result[key] = [])).push(value); 333 | }); 334 | }; 335 | 336 | // Counts instances of an object that group by a certain criterion. Pass 337 | // either a string attribute to count by, or a function that returns the 338 | // criterion. 339 | _.countBy = function(obj, value, context) { 340 | return group(obj, value, context, function(result, key) { 341 | if (!_.has(result, key)) result[key] = 0; 342 | result[key]++; 343 | }); 344 | }; 345 | 346 | // Use a comparator function to figure out the smallest index at which 347 | // an object should be inserted so as to maintain order. Uses binary search. 348 | _.sortedIndex = function(array, obj, iterator, context) { 349 | iterator = iterator == null ? _.identity : lookupIterator(iterator); 350 | var value = iterator.call(context, obj); 351 | var low = 0, high = array.length; 352 | while (low < high) { 353 | var mid = (low + high) >>> 1; 354 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; 355 | } 356 | return low; 357 | }; 358 | 359 | // Safely convert anything iterable into a real, live array. 360 | _.toArray = function(obj) { 361 | if (!obj) return []; 362 | if (_.isArray(obj)) return slice.call(obj); 363 | if (obj.length === +obj.length) return _.map(obj, _.identity); 364 | return _.values(obj); 365 | }; 366 | 367 | // Return the number of elements in an object. 368 | _.size = function(obj) { 369 | if (obj == null) return 0; 370 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; 371 | }; 372 | 373 | // Array Functions 374 | // --------------- 375 | 376 | // Get the first element of an array. Passing **n** will return the first N 377 | // values in the array. Aliased as `head` and `take`. The **guard** check 378 | // allows it to work with `_.map`. 379 | _.first = _.head = _.take = function(array, n, guard) { 380 | if (array == null) return void 0; 381 | return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; 382 | }; 383 | 384 | // Returns everything but the last entry of the array. Especially useful on 385 | // the arguments object. Passing **n** will return all the values in 386 | // the array, excluding the last N. The **guard** check allows it to work with 387 | // `_.map`. 388 | _.initial = function(array, n, guard) { 389 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); 390 | }; 391 | 392 | // Get the last element of an array. Passing **n** will return the last N 393 | // values in the array. The **guard** check allows it to work with `_.map`. 394 | _.last = function(array, n, guard) { 395 | if (array == null) return void 0; 396 | if ((n != null) && !guard) { 397 | return slice.call(array, Math.max(array.length - n, 0)); 398 | } else { 399 | return array[array.length - 1]; 400 | } 401 | }; 402 | 403 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. 404 | // Especially useful on the arguments object. Passing an **n** will return 405 | // the rest N values in the array. The **guard** 406 | // check allows it to work with `_.map`. 407 | _.rest = _.tail = _.drop = function(array, n, guard) { 408 | return slice.call(array, (n == null) || guard ? 1 : n); 409 | }; 410 | 411 | // Trim out all falsy values from an array. 412 | _.compact = function(array) { 413 | return _.filter(array, _.identity); 414 | }; 415 | 416 | // Internal implementation of a recursive `flatten` function. 417 | var flatten = function(input, shallow, output) { 418 | each(input, function(value) { 419 | if (_.isArray(value)) { 420 | shallow ? push.apply(output, value) : flatten(value, shallow, output); 421 | } else { 422 | output.push(value); 423 | } 424 | }); 425 | return output; 426 | }; 427 | 428 | // Return a completely flattened version of an array. 429 | _.flatten = function(array, shallow) { 430 | return flatten(array, shallow, []); 431 | }; 432 | 433 | // Return a version of the array that does not contain the specified value(s). 434 | _.without = function(array) { 435 | return _.difference(array, slice.call(arguments, 1)); 436 | }; 437 | 438 | // Produce a duplicate-free version of the array. If the array has already 439 | // been sorted, you have the option of using a faster algorithm. 440 | // Aliased as `unique`. 441 | _.uniq = _.unique = function(array, isSorted, iterator, context) { 442 | if (_.isFunction(isSorted)) { 443 | context = iterator; 444 | iterator = isSorted; 445 | isSorted = false; 446 | } 447 | var initial = iterator ? _.map(array, iterator, context) : array; 448 | var results = []; 449 | var seen = []; 450 | each(initial, function(value, index) { 451 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { 452 | seen.push(value); 453 | results.push(array[index]); 454 | } 455 | }); 456 | return results; 457 | }; 458 | 459 | // Produce an array that contains the union: each distinct element from all of 460 | // the passed-in arrays. 461 | _.union = function() { 462 | return _.uniq(concat.apply(ArrayProto, arguments)); 463 | }; 464 | 465 | // Produce an array that contains every item shared between all the 466 | // passed-in arrays. 467 | _.intersection = function(array) { 468 | var rest = slice.call(arguments, 1); 469 | return _.filter(_.uniq(array), function(item) { 470 | return _.every(rest, function(other) { 471 | return _.indexOf(other, item) >= 0; 472 | }); 473 | }); 474 | }; 475 | 476 | // Take the difference between one array and a number of other arrays. 477 | // Only the elements present in just the first array will remain. 478 | _.difference = function(array) { 479 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); 480 | return _.filter(array, function(value){ return !_.contains(rest, value); }); 481 | }; 482 | 483 | // Zip together multiple lists into a single array -- elements that share 484 | // an index go together. 485 | _.zip = function() { 486 | var args = slice.call(arguments); 487 | var length = _.max(_.pluck(args, 'length')); 488 | var results = new Array(length); 489 | for (var i = 0; i < length; i++) { 490 | results[i] = _.pluck(args, "" + i); 491 | } 492 | return results; 493 | }; 494 | 495 | // Converts lists into objects. Pass either a single array of `[key, value]` 496 | // pairs, or two parallel arrays of the same length -- one of keys, and one of 497 | // the corresponding values. 498 | _.object = function(list, values) { 499 | if (list == null) return {}; 500 | var result = {}; 501 | for (var i = 0, l = list.length; i < l; i++) { 502 | if (values) { 503 | result[list[i]] = values[i]; 504 | } else { 505 | result[list[i][0]] = list[i][1]; 506 | } 507 | } 508 | return result; 509 | }; 510 | 511 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), 512 | // we need this function. Return the position of the first occurrence of an 513 | // item in an array, or -1 if the item is not included in the array. 514 | // Delegates to **ECMAScript 5**'s native `indexOf` if available. 515 | // If the array is large and already in sort order, pass `true` 516 | // for **isSorted** to use binary search. 517 | _.indexOf = function(array, item, isSorted) { 518 | if (array == null) return -1; 519 | var i = 0, l = array.length; 520 | if (isSorted) { 521 | if (typeof isSorted == 'number') { 522 | i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); 523 | } else { 524 | i = _.sortedIndex(array, item); 525 | return array[i] === item ? i : -1; 526 | } 527 | } 528 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); 529 | for (; i < l; i++) if (array[i] === item) return i; 530 | return -1; 531 | }; 532 | 533 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. 534 | _.lastIndexOf = function(array, item, from) { 535 | if (array == null) return -1; 536 | var hasIndex = from != null; 537 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { 538 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); 539 | } 540 | var i = (hasIndex ? from : array.length); 541 | while (i--) if (array[i] === item) return i; 542 | return -1; 543 | }; 544 | 545 | // Generate an integer Array containing an arithmetic progression. A port of 546 | // the native Python `range()` function. See 547 | // [the Python documentation](http://docs.python.org/library/functions.html#range). 548 | _.range = function(start, stop, step) { 549 | if (arguments.length <= 1) { 550 | stop = start || 0; 551 | start = 0; 552 | } 553 | step = arguments[2] || 1; 554 | 555 | var len = Math.max(Math.ceil((stop - start) / step), 0); 556 | var idx = 0; 557 | var range = new Array(len); 558 | 559 | while(idx < len) { 560 | range[idx++] = start; 561 | start += step; 562 | } 563 | 564 | return range; 565 | }; 566 | 567 | // Function (ahem) Functions 568 | // ------------------ 569 | 570 | // Reusable constructor function for prototype setting. 571 | var ctor = function(){}; 572 | 573 | // Create a function bound to a given object (assigning `this`, and arguments, 574 | // optionally). Binding with arguments is also known as `curry`. 575 | // Delegates to **ECMAScript 5**'s native `Function.bind` if available. 576 | // We check for `func.bind` first, to fail fast when `func` is undefined. 577 | _.bind = function(func, context) { 578 | var args, bound; 579 | if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); 580 | if (!_.isFunction(func)) throw new TypeError; 581 | args = slice.call(arguments, 2); 582 | return bound = function() { 583 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); 584 | ctor.prototype = func.prototype; 585 | var self = new ctor; 586 | ctor.prototype = null; 587 | var result = func.apply(self, args.concat(slice.call(arguments))); 588 | if (Object(result) === result) return result; 589 | return self; 590 | }; 591 | }; 592 | 593 | // Bind all of an object's methods to that object. Useful for ensuring that 594 | // all callbacks defined on an object belong to it. 595 | _.bindAll = function(obj) { 596 | var funcs = slice.call(arguments, 1); 597 | if (funcs.length === 0) funcs = _.functions(obj); 598 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); 599 | return obj; 600 | }; 601 | 602 | // Memoize an expensive function by storing its results. 603 | _.memoize = function(func, hasher) { 604 | var memo = {}; 605 | hasher || (hasher = _.identity); 606 | return function() { 607 | var key = hasher.apply(this, arguments); 608 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); 609 | }; 610 | }; 611 | 612 | // Delays a function for the given number of milliseconds, and then calls 613 | // it with the arguments supplied. 614 | _.delay = function(func, wait) { 615 | var args = slice.call(arguments, 2); 616 | return setTimeout(function(){ return func.apply(null, args); }, wait); 617 | }; 618 | 619 | // Defers a function, scheduling it to run after the current call stack has 620 | // cleared. 621 | _.defer = function(func) { 622 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); 623 | }; 624 | 625 | // Returns a function, that, when invoked, will only be triggered at most once 626 | // during a given window of time. 627 | _.throttle = function(func, wait) { 628 | var context, args, timeout, result; 629 | var previous = 0; 630 | var later = function() { 631 | previous = new Date; 632 | timeout = null; 633 | result = func.apply(context, args); 634 | }; 635 | return function() { 636 | var now = new Date; 637 | var remaining = wait - (now - previous); 638 | context = this; 639 | args = arguments; 640 | if (remaining <= 0) { 641 | clearTimeout(timeout); 642 | timeout = null; 643 | previous = now; 644 | result = func.apply(context, args); 645 | } else if (!timeout) { 646 | timeout = setTimeout(later, remaining); 647 | } 648 | return result; 649 | }; 650 | }; 651 | 652 | // Returns a function, that, as long as it continues to be invoked, will not 653 | // be triggered. The function will be called after it stops being called for 654 | // N milliseconds. If `immediate` is passed, trigger the function on the 655 | // leading edge, instead of the trailing. 656 | _.debounce = function(func, wait, immediate) { 657 | var timeout, result; 658 | return function() { 659 | var context = this, args = arguments; 660 | var later = function() { 661 | timeout = null; 662 | if (!immediate) result = func.apply(context, args); 663 | }; 664 | var callNow = immediate && !timeout; 665 | clearTimeout(timeout); 666 | timeout = setTimeout(later, wait); 667 | if (callNow) result = func.apply(context, args); 668 | return result; 669 | }; 670 | }; 671 | 672 | // Returns a function that will be executed at most one time, no matter how 673 | // often you call it. Useful for lazy initialization. 674 | _.once = function(func) { 675 | var ran = false, memo; 676 | return function() { 677 | if (ran) return memo; 678 | ran = true; 679 | memo = func.apply(this, arguments); 680 | func = null; 681 | return memo; 682 | }; 683 | }; 684 | 685 | // Returns the first function passed as an argument to the second, 686 | // allowing you to adjust arguments, run code before and after, and 687 | // conditionally execute the original function. 688 | _.wrap = function(func, wrapper) { 689 | return function() { 690 | var args = [func]; 691 | push.apply(args, arguments); 692 | return wrapper.apply(this, args); 693 | }; 694 | }; 695 | 696 | // Returns a function that is the composition of a list of functions, each 697 | // consuming the return value of the function that follows. 698 | _.compose = function() { 699 | var funcs = arguments; 700 | return function() { 701 | var args = arguments; 702 | for (var i = funcs.length - 1; i >= 0; i--) { 703 | args = [funcs[i].apply(this, args)]; 704 | } 705 | return args[0]; 706 | }; 707 | }; 708 | 709 | // Returns a function that will only be executed after being called N times. 710 | _.after = function(times, func) { 711 | if (times <= 0) return func(); 712 | return function() { 713 | if (--times < 1) { 714 | return func.apply(this, arguments); 715 | } 716 | }; 717 | }; 718 | 719 | // Object Functions 720 | // ---------------- 721 | 722 | // Retrieve the names of an object's properties. 723 | // Delegates to **ECMAScript 5**'s native `Object.keys` 724 | _.keys = nativeKeys || function(obj) { 725 | if (obj !== Object(obj)) throw new TypeError('Invalid object'); 726 | var keys = []; 727 | for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; 728 | return keys; 729 | }; 730 | 731 | // Retrieve the values of an object's properties. 732 | _.values = function(obj) { 733 | var values = []; 734 | for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); 735 | return values; 736 | }; 737 | 738 | // Convert an object into a list of `[key, value]` pairs. 739 | _.pairs = function(obj) { 740 | var pairs = []; 741 | for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); 742 | return pairs; 743 | }; 744 | 745 | // Invert the keys and values of an object. The values must be serializable. 746 | _.invert = function(obj) { 747 | var result = {}; 748 | for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; 749 | return result; 750 | }; 751 | 752 | // Return a sorted list of the function names available on the object. 753 | // Aliased as `methods` 754 | _.functions = _.methods = function(obj) { 755 | var names = []; 756 | for (var key in obj) { 757 | if (_.isFunction(obj[key])) names.push(key); 758 | } 759 | return names.sort(); 760 | }; 761 | 762 | // Extend a given object with all the properties in passed-in object(s). 763 | _.extend = function(obj) { 764 | each(slice.call(arguments, 1), function(source) { 765 | if (source) { 766 | for (var prop in source) { 767 | obj[prop] = source[prop]; 768 | } 769 | } 770 | }); 771 | return obj; 772 | }; 773 | 774 | // Return a copy of the object only containing the whitelisted properties. 775 | _.pick = function(obj) { 776 | var copy = {}; 777 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 778 | each(keys, function(key) { 779 | if (key in obj) copy[key] = obj[key]; 780 | }); 781 | return copy; 782 | }; 783 | 784 | // Return a copy of the object without the blacklisted properties. 785 | _.omit = function(obj) { 786 | var copy = {}; 787 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); 788 | for (var key in obj) { 789 | if (!_.contains(keys, key)) copy[key] = obj[key]; 790 | } 791 | return copy; 792 | }; 793 | 794 | // Fill in a given object with default properties. 795 | _.defaults = function(obj) { 796 | each(slice.call(arguments, 1), function(source) { 797 | if (source) { 798 | for (var prop in source) { 799 | if (obj[prop] == null) obj[prop] = source[prop]; 800 | } 801 | } 802 | }); 803 | return obj; 804 | }; 805 | 806 | // Create a (shallow-cloned) duplicate of an object. 807 | _.clone = function(obj) { 808 | if (!_.isObject(obj)) return obj; 809 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj); 810 | }; 811 | 812 | // Invokes interceptor with the obj, and then returns obj. 813 | // The primary purpose of this method is to "tap into" a method chain, in 814 | // order to perform operations on intermediate results within the chain. 815 | _.tap = function(obj, interceptor) { 816 | interceptor(obj); 817 | return obj; 818 | }; 819 | 820 | // Internal recursive comparison function for `isEqual`. 821 | var eq = function(a, b, aStack, bStack) { 822 | // Identical objects are equal. `0 === -0`, but they aren't identical. 823 | // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. 824 | if (a === b) return a !== 0 || 1 / a == 1 / b; 825 | // A strict comparison is necessary because `null == undefined`. 826 | if (a == null || b == null) return a === b; 827 | // Unwrap any wrapped objects. 828 | if (a instanceof _) a = a._wrapped; 829 | if (b instanceof _) b = b._wrapped; 830 | // Compare `[[Class]]` names. 831 | var className = toString.call(a); 832 | if (className != toString.call(b)) return false; 833 | switch (className) { 834 | // Strings, numbers, dates, and booleans are compared by value. 835 | case '[object String]': 836 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is 837 | // equivalent to `new String("5")`. 838 | return a == String(b); 839 | case '[object Number]': 840 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for 841 | // other numeric values. 842 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); 843 | case '[object Date]': 844 | case '[object Boolean]': 845 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their 846 | // millisecond representations. Note that invalid dates with millisecond representations 847 | // of `NaN` are not equivalent. 848 | return +a == +b; 849 | // RegExps are compared by their source patterns and flags. 850 | case '[object RegExp]': 851 | return a.source == b.source && 852 | a.global == b.global && 853 | a.multiline == b.multiline && 854 | a.ignoreCase == b.ignoreCase; 855 | } 856 | if (typeof a != 'object' || typeof b != 'object') return false; 857 | // Assume equality for cyclic structures. The algorithm for detecting cyclic 858 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. 859 | var length = aStack.length; 860 | while (length--) { 861 | // Linear search. Performance is inversely proportional to the number of 862 | // unique nested structures. 863 | if (aStack[length] == a) return bStack[length] == b; 864 | } 865 | // Add the first object to the stack of traversed objects. 866 | aStack.push(a); 867 | bStack.push(b); 868 | var size = 0, result = true; 869 | // Recursively compare objects and arrays. 870 | if (className == '[object Array]') { 871 | // Compare array lengths to determine if a deep comparison is necessary. 872 | size = a.length; 873 | result = size == b.length; 874 | if (result) { 875 | // Deep compare the contents, ignoring non-numeric properties. 876 | while (size--) { 877 | if (!(result = eq(a[size], b[size], aStack, bStack))) break; 878 | } 879 | } 880 | } else { 881 | // Objects with different constructors are not equivalent, but `Object`s 882 | // from different frames are. 883 | var aCtor = a.constructor, bCtor = b.constructor; 884 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && 885 | _.isFunction(bCtor) && (bCtor instanceof bCtor))) { 886 | return false; 887 | } 888 | // Deep compare objects. 889 | for (var key in a) { 890 | if (_.has(a, key)) { 891 | // Count the expected number of properties. 892 | size++; 893 | // Deep compare each member. 894 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; 895 | } 896 | } 897 | // Ensure that both objects contain the same number of properties. 898 | if (result) { 899 | for (key in b) { 900 | if (_.has(b, key) && !(size--)) break; 901 | } 902 | result = !size; 903 | } 904 | } 905 | // Remove the first object from the stack of traversed objects. 906 | aStack.pop(); 907 | bStack.pop(); 908 | return result; 909 | }; 910 | 911 | // Perform a deep comparison to check if two objects are equal. 912 | _.isEqual = function(a, b) { 913 | return eq(a, b, [], []); 914 | }; 915 | 916 | // Is a given array, string, or object empty? 917 | // An "empty" object has no enumerable own-properties. 918 | _.isEmpty = function(obj) { 919 | if (obj == null) return true; 920 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; 921 | for (var key in obj) if (_.has(obj, key)) return false; 922 | return true; 923 | }; 924 | 925 | // Is a given value a DOM element? 926 | _.isElement = function(obj) { 927 | return !!(obj && obj.nodeType === 1); 928 | }; 929 | 930 | // Is a given value an array? 931 | // Delegates to ECMA5's native Array.isArray 932 | _.isArray = nativeIsArray || function(obj) { 933 | return toString.call(obj) == '[object Array]'; 934 | }; 935 | 936 | // Is a given variable an object? 937 | _.isObject = function(obj) { 938 | return obj === Object(obj); 939 | }; 940 | 941 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. 942 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { 943 | _['is' + name] = function(obj) { 944 | return toString.call(obj) == '[object ' + name + ']'; 945 | }; 946 | }); 947 | 948 | // Define a fallback version of the method in browsers (ahem, IE), where 949 | // there isn't any inspectable "Arguments" type. 950 | if (!_.isArguments(arguments)) { 951 | _.isArguments = function(obj) { 952 | return !!(obj && _.has(obj, 'callee')); 953 | }; 954 | } 955 | 956 | // Optimize `isFunction` if appropriate. 957 | if (typeof (/./) !== 'function') { 958 | _.isFunction = function(obj) { 959 | return typeof obj === 'function'; 960 | }; 961 | } 962 | 963 | // Is a given object a finite number? 964 | _.isFinite = function(obj) { 965 | return isFinite(obj) && !isNaN(parseFloat(obj)); 966 | }; 967 | 968 | // Is the given value `NaN`? (NaN is the only number which does not equal itself). 969 | _.isNaN = function(obj) { 970 | return _.isNumber(obj) && obj != +obj; 971 | }; 972 | 973 | // Is a given value a boolean? 974 | _.isBoolean = function(obj) { 975 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; 976 | }; 977 | 978 | // Is a given value equal to null? 979 | _.isNull = function(obj) { 980 | return obj === null; 981 | }; 982 | 983 | // Is a given variable undefined? 984 | _.isUndefined = function(obj) { 985 | return obj === void 0; 986 | }; 987 | 988 | // Shortcut function for checking if an object has a given property directly 989 | // on itself (in other words, not on a prototype). 990 | _.has = function(obj, key) { 991 | return hasOwnProperty.call(obj, key); 992 | }; 993 | 994 | // Utility Functions 995 | // ----------------- 996 | 997 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its 998 | // previous owner. Returns a reference to the Underscore object. 999 | _.noConflict = function() { 1000 | root._ = previousUnderscore; 1001 | return this; 1002 | }; 1003 | 1004 | // Keep the identity function around for default iterators. 1005 | _.identity = function(value) { 1006 | return value; 1007 | }; 1008 | 1009 | // Run a function **n** times. 1010 | _.times = function(n, iterator, context) { 1011 | var accum = Array(n); 1012 | for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); 1013 | return accum; 1014 | }; 1015 | 1016 | // Return a random integer between min and max (inclusive). 1017 | _.random = function(min, max) { 1018 | if (max == null) { 1019 | max = min; 1020 | min = 0; 1021 | } 1022 | return min + (0 | Math.random() * (max - min + 1)); 1023 | }; 1024 | 1025 | // List of HTML entities for escaping. 1026 | var entityMap = { 1027 | escape: { 1028 | '&': '&', 1029 | '<': '<', 1030 | '>': '>', 1031 | '"': '"', 1032 | "'": ''', 1033 | '/': '/' 1034 | } 1035 | }; 1036 | entityMap.unescape = _.invert(entityMap.escape); 1037 | 1038 | // Regexes containing the keys and values listed immediately above. 1039 | var entityRegexes = { 1040 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), 1041 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') 1042 | }; 1043 | 1044 | // Functions for escaping and unescaping strings to/from HTML interpolation. 1045 | _.each(['escape', 'unescape'], function(method) { 1046 | _[method] = function(string) { 1047 | if (string == null) return ''; 1048 | return ('' + string).replace(entityRegexes[method], function(match) { 1049 | return entityMap[method][match]; 1050 | }); 1051 | }; 1052 | }); 1053 | 1054 | // If the value of the named property is a function then invoke it; 1055 | // otherwise, return it. 1056 | _.result = function(object, property) { 1057 | if (object == null) return null; 1058 | var value = object[property]; 1059 | return _.isFunction(value) ? value.call(object) : value; 1060 | }; 1061 | 1062 | // Add your own custom functions to the Underscore object. 1063 | _.mixin = function(obj) { 1064 | each(_.functions(obj), function(name){ 1065 | var func = _[name] = obj[name]; 1066 | _.prototype[name] = function() { 1067 | var args = [this._wrapped]; 1068 | push.apply(args, arguments); 1069 | return result.call(this, func.apply(_, args)); 1070 | }; 1071 | }); 1072 | }; 1073 | 1074 | // Generate a unique integer id (unique within the entire client session). 1075 | // Useful for temporary DOM ids. 1076 | var idCounter = 0; 1077 | _.uniqueId = function(prefix) { 1078 | var id = '' + (++idCounter); 1079 | return prefix ? prefix + id : id; 1080 | }; 1081 | 1082 | // By default, Underscore uses ERB-style template delimiters, change the 1083 | // following template settings to use alternative delimiters. 1084 | _.templateSettings = { 1085 | evaluate : /<%([\s\S]+?)%>/g, 1086 | interpolate : /<%=([\s\S]+?)%>/g, 1087 | escape : /<%-([\s\S]+?)%>/g 1088 | }; 1089 | 1090 | // When customizing `templateSettings`, if you don't want to define an 1091 | // interpolation, evaluation or escaping regex, we need one that is 1092 | // guaranteed not to match. 1093 | var noMatch = /(.)^/; 1094 | 1095 | // Certain characters need to be escaped so that they can be put into a 1096 | // string literal. 1097 | var escapes = { 1098 | "'": "'", 1099 | '\\': '\\', 1100 | '\r': 'r', 1101 | '\n': 'n', 1102 | '\t': 't', 1103 | '\u2028': 'u2028', 1104 | '\u2029': 'u2029' 1105 | }; 1106 | 1107 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; 1108 | 1109 | // JavaScript micro-templating, similar to John Resig's implementation. 1110 | // Underscore templating handles arbitrary delimiters, preserves whitespace, 1111 | // and correctly escapes quotes within interpolated code. 1112 | _.template = function(text, data, settings) { 1113 | var render; 1114 | settings = _.defaults({}, settings, _.templateSettings); 1115 | 1116 | // Combine delimiters into one regular expression via alternation. 1117 | var matcher = new RegExp([ 1118 | (settings.escape || noMatch).source, 1119 | (settings.interpolate || noMatch).source, 1120 | (settings.evaluate || noMatch).source 1121 | ].join('|') + '|$', 'g'); 1122 | 1123 | // Compile the template source, escaping string literals appropriately. 1124 | var index = 0; 1125 | var source = "__p+='"; 1126 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { 1127 | source += text.slice(index, offset) 1128 | .replace(escaper, function(match) { return '\\' + escapes[match]; }); 1129 | 1130 | if (escape) { 1131 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; 1132 | } 1133 | if (interpolate) { 1134 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; 1135 | } 1136 | if (evaluate) { 1137 | source += "';\n" + evaluate + "\n__p+='"; 1138 | } 1139 | index = offset + match.length; 1140 | return match; 1141 | }); 1142 | source += "';\n"; 1143 | 1144 | // If a variable is not specified, place data values in local scope. 1145 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; 1146 | 1147 | source = "var __t,__p='',__j=Array.prototype.join," + 1148 | "print=function(){__p+=__j.call(arguments,'');};\n" + 1149 | source + "return __p;\n"; 1150 | 1151 | try { 1152 | render = new Function(settings.variable || 'obj', '_', source); 1153 | } catch (e) { 1154 | e.source = source; 1155 | throw e; 1156 | } 1157 | 1158 | if (data) return render(data, _); 1159 | var template = function(data) { 1160 | return render.call(this, data, _); 1161 | }; 1162 | 1163 | // Provide the compiled function source as a convenience for precompilation. 1164 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; 1165 | 1166 | return template; 1167 | }; 1168 | 1169 | // Add a "chain" function, which will delegate to the wrapper. 1170 | _.chain = function(obj) { 1171 | return _(obj).chain(); 1172 | }; 1173 | 1174 | // OOP 1175 | // --------------- 1176 | // If Underscore is called as a function, it returns a wrapped object that 1177 | // can be used OO-style. This wrapper holds altered versions of all the 1178 | // underscore functions. Wrapped objects may be chained. 1179 | 1180 | // Helper function to continue chaining intermediate results. 1181 | var result = function(obj) { 1182 | return this._chain ? _(obj).chain() : obj; 1183 | }; 1184 | 1185 | // Add all of the Underscore functions to the wrapper object. 1186 | _.mixin(_); 1187 | 1188 | // Add all mutator Array functions to the wrapper. 1189 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { 1190 | var method = ArrayProto[name]; 1191 | _.prototype[name] = function() { 1192 | var obj = this._wrapped; 1193 | method.apply(obj, arguments); 1194 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; 1195 | return result.call(this, obj); 1196 | }; 1197 | }); 1198 | 1199 | // Add all accessor Array functions to the wrapper. 1200 | each(['concat', 'join', 'slice'], function(name) { 1201 | var method = ArrayProto[name]; 1202 | _.prototype[name] = function() { 1203 | return result.call(this, method.apply(this._wrapped, arguments)); 1204 | }; 1205 | }); 1206 | 1207 | _.extend(_.prototype, { 1208 | 1209 | // Start chaining a wrapped Underscore object. 1210 | chain: function() { 1211 | this._chain = true; 1212 | return this; 1213 | }, 1214 | 1215 | // Extracts the result from a wrapped and chained object. 1216 | value: function() { 1217 | return this._wrapped; 1218 | } 1219 | 1220 | }); 1221 | 1222 | }).call(this); 1223 | -------------------------------------------------------------------------------- /src/static/fonts/basic.icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20100429 at Thu Sep 20 22:09:47 2012 6 | By root 7 | Copyright (C) 2012 by original authors @ fontello.com 8 | 9 | 10 | 11 | 24 | 26 | 28 | 30 | 32 | 35 | 37 | 41 | 44 | 47 | 49 | 51 | 53 | 55 | 57 | 59 | 61 | 63 | 65 | 68 | 71 | 74 | 76 | 80 | 86 | 89 | 91 | 101 | 103 | 105 | 108 | 110 | 112 | 115 | 118 | 121 | 123 | 125 | 127 | 129 | 131 | 133 | 135 | 137 | 140 | 142 | 146 | 149 | 153 | 158 | 161 | 163 | 165 | 169 | 172 | 174 | 178 | 181 | 184 | 191 | 193 | 196 | 199 | 203 | 206 | 209 | 213 | 217 | 219 | 221 | 223 | 225 | 228 | 231 | 234 | 237 | 240 | 243 | 245 | 247 | 250 | 253 | 255 | 258 | 262 | 264 | 266 | 268 | 270 | 273 | 275 | 278 | 283 | 289 | 292 | 295 | 299 | 301 | 304 | 307 | 311 | 315 | 317 | 322 | 325 | 329 | 334 | 341 | 344 | 347 | 351 | 354 | 358 | 362 | 367 | 372 | 377 | 380 | 385 | 388 | 392 | 398 | 401 | 403 | 409 | 411 | 413 | 418 | 422 | 425 | 428 | 431 | 434 | 436 | 440 | 443 | 446 | 449 | 450 | 451 | --------------------------------------------------------------------------------