diff --git a/.gitignore b/.gitignore index 3a7302f5eb..f482055c79 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,31 @@ config.js +.cov +coverage node_modules .naeindex -public/user_data coverage.html .monitor + +*.min.*.js +*.min.*.css +assets.json + +# Ignore Mac OS desktop services store +.DS_Store + +# Ignore Windows desktop setting file +desktop.ini + +# Ignore Redis snapshot +dump.rdb + +*.log + +.idea +public/upload/* + +*.sublime-project +*.sublime-workspace +*.swp + +package-lock.json diff --git a/.jshintrc b/.jshintrc index f80d3979bc..e21ccb39f1 100644 --- a/.jshintrc +++ b/.jshintrc @@ -11,13 +11,13 @@ "before", "beforeEach", "after", - "should", - "rewire", + "afterEach", + "ace", "$" ], "browser": true, - "node" : true, + "node": true, "es5": true, "bitwise": true, "curly": true, @@ -39,5 +39,6 @@ "indent": 2, "expr": true, "multistr": true, - "onevar": false -} \ No newline at end of file + "onevar": false, + "unused": "vars" +} diff --git a/.mention-bot b/.mention-bot new file mode 100644 index 0000000000..86a447db50 --- /dev/null +++ b/.mention-bot @@ -0,0 +1,3 @@ +{ + "userBlacklist": ["huacnlee"] +} diff --git a/.naeignore b/.naeignore deleted file mode 100644 index b3c7c441cc..0000000000 --- a/.naeignore +++ /dev/null @@ -1,2 +0,0 @@ -public/user_data/* -.git diff --git a/.snyk b/.snyk new file mode 100644 index 0000000000..2fb1107f87 --- /dev/null +++ b/.snyk @@ -0,0 +1,8 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.12.0 +ignore: {} +# patches apply the minimum changes required to fix a vulnerability +patch: + 'npm:tunnel-agent:20170305': + - jpush-sdk > request > tunnel-agent: + patched: '2018-07-01T04:07:14.342Z' diff --git a/.travis.yml b/.travis.yml index b8e1f17207..78ce352cc9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,27 @@ +sudo: false + language: node_js + +env: + - CXX=g++-4.8 + node_js: - - 0.6 \ No newline at end of file + - stable + +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 + +services: + - mongodb + - redis + +before_install: + - $CXX --version + +script: make test-cov + +after_success: npm i codecov && codecov diff --git a/History.md b/History.md index 5af9a3454c..918c8f4787 100644 Binary files a/History.md and b/History.md differ diff --git a/LICENSE b/LICENSE index 3dfc67bc53..29b5e9c758 100644 --- a/LICENSE +++ b/LICENSE @@ -1,8 +1,8 @@ -Copyright (c) 2012 muyuan, fengmk2 and other nodeclub contributors +(The MIT License) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including +'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to @@ -11,10 +11,10 @@ the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile index 5b1c160c1e..8a408ea27d 100644 --- a/Makefile +++ b/Makefile @@ -1,34 +1,61 @@ -SRC := libs controllers plugins models -TESTS = $(shell find test -type f -name "*.js") -TESTTIMEOUT = 5000 -REPORTER = spec -JSCOVERAGE = ./node_modules/visionmedia-jscoverage/jscoverage - -test: - @npm install +TESTS = $(shell find test -type f -name "*.test.js") +TEST_TIMEOUT = 10000 +MOCHA_REPORTER = spec +# NPM_REGISTRY = "--registry=http://registry.npm.taobao.org" +NPM_REGISTRY = "" + + +all: test + +install: + @npm install $(NPM_REGISTRY) + +pretest: @if ! test -f config.js; then \ cp config.default.js config.js; \ fi - @NODE_ENV=test ./node_modules/.bin/mocha \ - --reporter $(REPORTER) --timeout $(TESTTIMEOUT) $(TESTS) + @if ! test -d public/upload; then \ + mkdir public/upload; \ + fi + +test: install pretest + @NODE_ENV=test ./node_modules/mocha/bin/mocha \ + --reporter $(MOCHA_REPORTER) \ + -r should \ + -r test/env \ + --timeout $(TEST_TIMEOUT) \ + $(TESTS) + +testfile: + @NODE_ENV=test ./node_modules/mocha/bin/mocha \ + --reporter $(MOCHA_REPORTER) \ + -r should \ + -r test/env \ + --timeout $(TEST_TIMEOUT) \ + $(FILE) + +test-cov cov: install pretest + @NODE_ENV=test node \ + node_modules/.bin/istanbul cover --preserve-comments \ + ./node_modules/.bin/_mocha \ + -- \ + -r should \ + -r test/env \ + --reporter $(MOCHA_REPORTER) \ + --timeout $(TEST_TIMEOUT) \ + $(TESTS) + -test-dot: - @$(MAKE) test REPORTER=dot +build: + @./node_modules/loader-builder/bin/builder views . -cov: - @for dir in $(SRC); do \ - mv $$dir $$dir.bak; \ - $(JSCOVERAGE) --encoding=utf-8 $$dir.bak $$dir; \ - done +run: + @node app.js -cov-clean: - @for dir in $(SRC); do \ - rm -rf $$dir; \ - mv $$dir.bak $$dir; \ - done +start: install build + @NODE_ENV=production ./node_modules/.bin/pm2 start app.js -i 0 --name "cnode" --max-memory-restart 400M -test-cov: cov - @-$(MAKE) test REPORTER=html-cov > coverage.html - @$(MAKE) cov-clean +restart: install build + @NODE_ENV=production ./node_modules/.bin/pm2 restart "cnode" -.PHONY: test test-cov test-dot cov cov-clean +.PHONY: install test testfile cov test-cov build run start restart diff --git a/README.md b/README.md index 75f01ebc8d..c4b08f5de4 100644 --- a/README.md +++ b/README.md @@ -1,106 +1,60 @@ -# nodeclub - -[![Build Status](https://secure.travis-ci.org/cnodejs/nodeclub.png?branch=master)](http://travis-ci.org/cnodejs/nodeclub) - -基于nodejs的社区系统 +Nodeclub += + +[![build status][travis-image]][travis-url] +[![codecov.io][codecov-image]][codecov-url] +[![David deps][david-image]][david-url] +[![node version][node-image]][node-url] + +[travis-image]: https://img.shields.io/travis/cnodejs/nodeclub/master.svg?style=flat-square +[travis-url]: https://travis-ci.org/cnodejs/nodeclub +[codecov-image]: https://img.shields.io/codecov/c/github/cnodejs/nodeclub/master.svg?style=flat-square +[codecov-url]: https://codecov.io/github/cnodejs/nodeclub?branch=master +[david-image]: https://img.shields.io/david/cnodejs/nodeclub.svg?style=flat-square +[david-url]: https://david-dm.org/cnodejs/nodeclub +[node-image]: https://img.shields.io/badge/node.js-%3E=_4.2-green.svg?style=flat-square +[node-url]: http://nodejs.org/download/ ## 介绍 -Node Club 是用 **Node.js** 和 **MongoDB** 开发的新型社区软件,界面优雅,功能丰富,小巧迅速, -已在Node.js 中文技术社区 [CNode](http://cnodejs.org) 得到应用,但你完全可以用它搭建自己的社区。 +Nodeclub 是使用 **Node.js** 和 **MongoDB** 开发的社区系统,界面优雅,功能丰富,小巧迅速, +已在Node.js 中文技术社区 [CNode(http://cnodejs.org)](http://cnodejs.org) 得到应用,但你完全可以用它搭建自己的社区。 ## 安装部署 -```bash -// install node npm mongodb -// run mongod -$ npm install -$ cp config.default.js config.js -// modify the config file as yours -$ node app.js -``` - -## TEST - -```bash -$ make test -``` - -## 其它 +*不保证 Windows 系统的兼容性* -小量修改了两个依赖模块:node-markdown,express - -* node-markdown/lib/markdown.js - -allowedTags 添加: +线上跑的是 [Node.js](https://nodejs.org) v8.12.0,[MongoDB](https://www.mongodb.org) 是 v4.0.3,[Redis](http://redis.io) 是 v4.0.9。 ``` -embed //支持 flash 视频 -table|thead|tbody|tr|td|th|caption //支持表格 +1. 安装 `Node.js[必须]` `MongoDB[必须]` `Redis[必须]` +2. 启动 MongoDB 和 Redis +3. `$ make install` 安装 Nodeclub 的依赖包 +4. `cp config.default.js config.js` 请根据需要修改配置文件 +5. `$ make test` 确保各项服务都正常 +6. `$ node app.js` +7. visit `http://localhost:3000` +8. done! ``` - -allowedAttributes 添加: -``` -embed:'src|quality|width|height|align|allowScriptAccess|allowFullScreen|mode|type' -table: 'class' -``` +## 测试 -* express/node_modules/connect/lib/middleware/csrf.js 添加: +跑测试 -```javascript -if (req.body && req.body.user_action === 'upload_image') return next(); +```bash +$ make test ``` -## 关于pull request - -从现在开始,所有提交都要严格遵循[代码规范](https://github.com/windyrobin/iFrame/blob/master/style.md)。 +跑覆盖率测试 -## Authors - -Below is the output from `git-summary`. - -``` - project: nodeclub - commits: 89 - active : 36 days - files : 253 - authors: - 41 fengmk2 46.1% - 10 Kenny Zhao 11.2% - 9 muyuan 10.1% - 8 dead-horse 9.0% - 7 young40 7.9% - 5 ericzhang 5.6% - 3 Json Shen 3.4% - 2 chang 2.2% - 1 roymax 1.1% - 1 thebrecht 1.1% - 1 LeToNode 1.1% - 1 张洋 1.1% +```bash +$ make test-cov ``` -## License - -( The MIT License ) +## 贡献 -Copyright (c) 2012 muyuan, fengmk2 and other nodeclub contributors +有任何意见或建议都欢迎提 issue,或者直接提给 [@alsotang](https://github.com/alsotang) -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +## License -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +MIT diff --git a/api/v1/message.js b/api/v1/message.js new file mode 100644 index 0000000000..b5b9ede9d9 --- /dev/null +++ b/api/v1/message.js @@ -0,0 +1,114 @@ +var eventproxy = require('eventproxy'); +var Message = require('../../proxy').Message; +var at = require('../../common/at'); +var renderHelper = require('../../common/render_helper'); +var _ = require('lodash'); + +var index = function (req, res, next) { + var user_id = req.user._id; + var mdrender = req.query.mdrender === 'false' ? false : true; + var ep = new eventproxy(); + ep.fail(next); + + ep.all('has_read_messages', 'hasnot_read_messages', function (has_read_messages, hasnot_read_messages) { + res.send({ + success: true, + data: { + has_read_messages: has_read_messages, + hasnot_read_messages: hasnot_read_messages + } + }); + }); + + ep.all('has_read', 'unread', function (has_read, unread) { + [has_read, unread].forEach(function (msgs, idx) { + var epfill = new eventproxy(); + epfill.fail(next); + epfill.after('message_ready', msgs.length, function (docs) { + docs = docs.filter(function (doc) { + return !doc.is_invalid; + }); + docs = docs.map(function (doc) { + doc.author = _.pick(doc.author, ['loginname', 'avatar_url']); + doc.topic = _.pick(doc.topic, ['id', 'author', 'title', 'last_reply_at']); + doc.reply = _.pick(doc.reply, ['id', 'content', 'ups', 'create_at']); + if (mdrender) { + doc.reply.content = renderHelper.markdown(at.linkUsers(doc.reply.content)); + } + doc = _.pick(doc, ['id', 'type', 'has_read', 'author', 'topic', 'reply', 'create_at']); + + return doc; + }); + ep.emit(idx === 0 ? 'has_read_messages' : 'hasnot_read_messages', docs); + }); + msgs.forEach(function (doc) { + Message.getMessageById(doc._id, epfill.group('message_ready')); + }); + }); + }); + + Message.getReadMessagesByUserId(user_id, ep.done('has_read')); + + Message.getUnreadMessageByUserId(user_id, ep.done('unread')); +}; + +exports.index = index; + +var markAll = function (req, res, next) { + var user_id = req.user._id; + var ep = new eventproxy(); + ep.fail(next); + Message.getUnreadMessageByUserId(user_id, ep.done('unread', function (docs) { + docs.forEach(function (doc) { + doc.has_read = true; + doc.save(); + }); + return docs; + })); + + ep.all('unread', function (unread) { + unread = unread.map(function (doc) { + doc = _.pick(doc, ['id']); + return doc; + }); + res.send({ + success: true, + marked_msgs: unread + }); + }); +}; + +exports.markAll = markAll; + + +var markOne = function (req, res, next) { + var msg_id = req.params.msg_id; + var ep = new eventproxy(); + ep.fail(next); + Message.updateOneMessageToRead(msg_id, ep.done('marked_result', function (result) { + return result; + })); + + ep.all('marked_result', function (result) { + res.send({ + success: true, + marked_msg_id: msg_id + }); + }); +}; + +exports.markOne = markOne; + + +var count = function (req, res, next) { + var userId = req.user.id; + + var ep = new eventproxy(); + ep.fail(next); + + Message.getMessagesCount(userId, ep.done(function (count) { + res.send({success: true, data: count}); + })); +}; + +exports.count = count; diff --git a/api/v1/middleware.js b/api/v1/middleware.js new file mode 100644 index 0000000000..c97a5f798b --- /dev/null +++ b/api/v1/middleware.js @@ -0,0 +1,52 @@ +var UserModel = require('../../models').User; +var eventproxy = require('eventproxy'); +var validator = require('validator'); + +// 非登录用户直接屏蔽 +var auth = function (req, res, next) { + var ep = new eventproxy(); + ep.fail(next); + + var accessToken = String(req.body.accesstoken || req.query.accesstoken || ''); + accessToken = validator.trim(accessToken); + + UserModel.findOne({accessToken: accessToken}, ep.done(function (user) { + if (!user) { + res.status(401); + return res.send({success: false, error_msg: '错误的accessToken'}); + } + if (user.is_block) { + res.status(403); + return res.send({success: false, error_msg: '您的账户被禁用'}); + } + req.user = user; + next(); + })); + +}; + +exports.auth = auth; + +// 非登录用户也可通过 +var tryAuth = function (req, res, next) { + var ep = new eventproxy(); + ep.fail(next); + + var accessToken = String(req.body.accesstoken || req.query.accesstoken || ''); + accessToken = validator.trim(accessToken); + + UserModel.findOne({accessToken: accessToken}, ep.done(function (user) { + if (!user) { + return next() + } + if (user.is_block) { + res.status(403); + return res.send({success: false, error_msg: '您的账户被禁用'}); + } + req.user = user; + next(); + })); + +}; + +exports.tryAuth = tryAuth; diff --git a/api/v1/reply.js b/api/v1/reply.js new file mode 100644 index 0000000000..82d105c580 --- /dev/null +++ b/api/v1/reply.js @@ -0,0 +1,121 @@ +var eventproxy = require('eventproxy'); +var validator = require('validator'); +var Topic = require('../../proxy').Topic; +var User = require('../../proxy').User; +var Reply = require('../../proxy').Reply; +var at = require('../../common/at'); +var message = require('../../common/message'); +var config = require('../../config'); + +var create = function (req, res, next) { + var topic_id = req.params.topic_id; + var content = req.body.content || ''; + var reply_id = req.body.reply_id; + + var ep = new eventproxy(); + ep.fail(next); + + var str = validator.trim(content); + if (str === '') { + res.status(400); + return res.send({success: false, error_msg: '回复内容不能为空'}); + } + + if (!validator.isMongoId(topic_id)) { + res.status(400); + return res.send({success: false, error_msg: '不是有效的话题id'}); + } + + Topic.getTopic(topic_id, ep.done(function (topic) { + if (!topic) { + res.status(404); + return res.send({success: false, error_msg: '话题不存在'}); + } + if (topic.lock) { + res.status(403); + return res.send({success: false, error_msg: '该话题已被锁定'}); + } + ep.emit('topic', topic); + })); + + ep.all('topic', function (topic) { + User.getUserById(topic.author_id, ep.done('topic_author')); + }); + + ep.all('topic', 'topic_author', function (topic, topicAuthor) { + Reply.newAndSave(content, topic_id, req.user.id, reply_id, ep.done(function (reply) { + Topic.updateLastReply(topic_id, reply._id, ep.done(function () { + ep.emit('reply_saved', reply); + //发送at消息,并防止重复 at 作者 + var newContent = content.replace('@' + topicAuthor.loginname + ' ', ''); + at.sendMessageToMentionUsers(newContent, topic_id, req.user.id, reply._id); + })); + })); + + User.getUserById(req.user.id, ep.done(function (user) { + user.score += 5; + user.reply_count += 1; + user.save(); + ep.emit('score_saved'); + })); + }); + + ep.all('reply_saved', 'topic', function (reply, topic) { + if (topic.author_id.toString() !== req.user.id.toString()) { + message.sendReplyMessage(topic.author_id, req.user.id, topic._id, reply._id); + } + ep.emit('message_saved'); + }); + + ep.all('reply_saved', 'message_saved', 'score_saved', function (reply) { + res.send({ + success: true, + reply_id: reply._id + }); + }); +}; + +exports.create = create; + +var ups = function (req, res, next) { + var replyId = req.params.reply_id; + var userId = req.user.id; + + if (!validator.isMongoId(replyId)) { + res.status(400); + return res.send({success: false, error_msg: '不是有效的评论id'}); + } + + Reply.getReplyById(replyId, function (err, reply) { + if (err) { + return next(err); + } + if (!reply) { + res.status(404); + return res.send({success: false, error_msg: '评论不存在'}); + } + if (reply.author_id.equals(userId) && !config.debug) { + res.status(403); + return res.send({success: false, error_msg: '不能帮自己点赞'}); + } else { + var action; + reply.ups = reply.ups || []; + var upIndex = reply.ups.indexOf(userId); + if (upIndex === -1) { + reply.ups.push(userId); + action = 'up'; + } else { + reply.ups.splice(upIndex, 1); + action = 'down'; + } + reply.save(function () { + res.send({ + success: true, + action: action + }); + }); + } + }); +}; + +exports.ups = ups; diff --git a/api/v1/tools.js b/api/v1/tools.js new file mode 100644 index 0000000000..6c82cd230e --- /dev/null +++ b/api/v1/tools.js @@ -0,0 +1,14 @@ +var eventproxy = require('eventproxy'); + +var accesstoken = function (req, res, next) { + var ep = new eventproxy(); + ep.fail(next); + + res.send({ + success: true, + loginname: req.user.loginname, + avatar_url: req.user.avatar_url, + id: req.user.id + }); +}; +exports.accesstoken = accesstoken; diff --git a/api/v1/topic.js b/api/v1/topic.js new file mode 100644 index 0000000000..2165909f52 --- /dev/null +++ b/api/v1/topic.js @@ -0,0 +1,244 @@ +var models = require('../../models'); +var TopicModel = models.Topic; +var TopicProxy = require('../../proxy').Topic; +var TopicCollect = require('../../proxy').TopicCollect; +var UserProxy = require('../../proxy').User; +var UserModel = models.User; +var config = require('../../config'); +var eventproxy = require('eventproxy'); +var _ = require('lodash'); +var at = require('../../common/at'); +var renderHelper = require('../../common/render_helper'); +var validator = require('validator'); + +var index = function (req, res, next) { + var page = parseInt(req.query.page, 10) || 1; + page = page > 0 ? page : 1; + var tab = req.query.tab || 'all'; + var limit = Number(req.query.limit) || config.list_topic_count; + var mdrender = req.query.mdrender === 'false' ? false : true; + + var query = {}; + if (!tab || tab === 'all') { + query.tab = {$nin: ['job', 'dev']} + } else { + if (tab === 'good') { + query.good = true; + } else { + query.tab = tab; + } + } + query.deleted = false; + var options = { skip: (page - 1) * limit, limit: limit, sort: '-top -last_reply_at'}; + + var ep = new eventproxy(); + ep.fail(next); + + TopicModel.find(query, '', options, ep.done('topics')); + + ep.all('topics', function (topics) { + topics.forEach(function (topic) { + UserModel.findById(topic.author_id, ep.done(function (author) { + if (mdrender) { + topic.content = renderHelper.markdown(at.linkUsers(topic.content)); + } + topic.author = _.pick(author, ['loginname', 'avatar_url']); + ep.emit('author'); + })); + }); + + ep.after('author', topics.length, function () { + topics = topics.map(function (topic) { + return _.pick(topic, ['id', 'author_id', 'tab', 'content', 'title', 'last_reply_at', + 'good', 'top', 'reply_count', 'visit_count', 'create_at', 'author']); + }); + + res.send({success: true, data: topics}); + }); + }); +}; + +exports.index = index; + +var show = function (req, res, next) { + var topicId = String(req.params.id); + + var mdrender = req.query.mdrender === 'false' ? false : true; + var ep = new eventproxy(); + + if (!validator.isMongoId(topicId)) { + res.status(400); + return res.send({success: false, error_msg: '不是有效的话题id'}); + } + + ep.fail(next); + + TopicProxy.getFullTopic(topicId, ep.done(function (msg, topic, author, replies) { + if (!topic) { + res.status(404); + return res.send({success: false, error_msg: '话题不存在'}); + } + + topic.visit_count += 1; + topic.save(); + + topic = _.pick(topic, ['id', 'author_id', 'tab', 'content', 'title', 'last_reply_at', + 'good', 'top', 'reply_count', 'visit_count', 'create_at', 'author']); + + if (mdrender) { + topic.content = renderHelper.markdown(at.linkUsers(topic.content)); + } + topic.author = _.pick(author, ['loginname', 'avatar_url']); + + topic.replies = replies.map(function (reply) { + if (mdrender) { + reply.content = renderHelper.markdown(at.linkUsers(reply.content)); + } + reply.author = _.pick(reply.author, ['loginname', 'avatar_url']); + reply = _.pick(reply, ['id', 'author', 'content', 'ups', 'create_at', 'reply_id']); + reply.reply_id = reply.reply_id || null; + + if (reply.ups && req.user && reply.ups.indexOf(req.user._id) != -1) { + reply.is_uped = true; + } else { + reply.is_uped = false; + } + + return reply; + }); + + ep.emit('full_topic', topic) + })); + + + if (!req.user) { + ep.emitLater('is_collect', null) + } else { + TopicCollect.getTopicCollect(req.user._id, topicId, ep.done('is_collect')) + } + + ep.all('full_topic', 'is_collect', function (full_topic, is_collect) { + full_topic.is_collect = !!is_collect; + + res.send({success: true, data: full_topic}); + }) + +}; + +exports.show = show; + +var create = function (req, res, next) { + var title = validator.trim(req.body.title || ''); + var tab = validator.trim(req.body.tab || ''); + var content = validator.trim(req.body.content || ''); + + // 得到所有的 tab, e.g. ['ask', 'share', ..] + var allTabs = config.tabs.map(function (tPair) { + return tPair[0]; + }); + + // 验证 + var editError; + if (title === '') { + editError = '标题不能为空'; + } else if (title.length < 5 || title.length > 100) { + editError = '标题字数太多或太少'; + } else if (!tab || !_.includes(allTabs, tab)) { + editError = '必须选择一个版块'; + } else if (content === '') { + editError = '内容不可为空'; + } + // END 验证 + + if (editError) { + res.status(400); + return res.send({success: false, error_msg: editError}); + } + + TopicProxy.newAndSave(title, content, tab, req.user.id, function (err, topic) { + if (err) { + return next(err); + } + + var proxy = new eventproxy(); + proxy.fail(next); + + proxy.all('score_saved', function () { + res.send({ + success: true, + topic_id: topic.id + }); + }); + UserProxy.getUserById(req.user.id, proxy.done(function (user) { + user.score += 5; + user.topic_count += 1; + user.save(); + req.user = user; + proxy.emit('score_saved'); + })); + + //发送at消息 + at.sendMessageToMentionUsers(content, topic.id, req.user.id); + }); +}; + +exports.create = create; + +exports.update = function (req, res, next) { + var topic_id = _.trim(req.body.topic_id); + var title = _.trim(req.body.title); + var tab = _.trim(req.body.tab); + var content = _.trim(req.body.content); + + // 得到所有的 tab, e.g. ['ask', 'share', ..] + var allTabs = config.tabs.map(function (tPair) { + return tPair[0]; + }); + + TopicProxy.getTopicById(topic_id, function (err, topic, tags) { + if (!topic) { + res.status(400); + return res.send({success: false, error_msg: '此话题不存在或已被删除。'}); + } + + if (topic.author_id.equals(req.user._id) || req.user.is_admin) { + // 验证 + var editError; + if (title === '') { + editError = '标题不能是空的。'; + } else if (title.length < 5 || title.length > 100) { + editError = '标题字数太多或太少。'; + } else if (!tab || !_.includes(allTabs, tab)) { + editError = '必须选择一个版块。'; + } + // END 验证 + + if (editError) { + return res.send({success: false, error_msg: editError}); + } + + //保存话题 + topic.title = title; + topic.content = content; + topic.tab = tab; + topic.update_at = new Date(); + + topic.save(function (err) { + if (err) { + return next(err); + } + //发送at消息 + at.sendMessageToMentionUsers(content, topic._id, req.user._id); + + res.send({ + success: true, + topic_id: topic.id + }); + }); + } else { + res.status(403) + return res.send({success: false, error_msg: '对不起,你不能编辑此话题。'}); + } + }); +}; + diff --git a/api/v1/topic_collect.js b/api/v1/topic_collect.js new file mode 100644 index 0000000000..5a6d01360c --- /dev/null +++ b/api/v1/topic_collect.js @@ -0,0 +1,141 @@ +var eventproxy = require('eventproxy'); +var TopicProxy = require('../../proxy').Topic; +var TopicCollectProxy = require('../../proxy').TopicCollect; +var UserProxy = require('../../proxy').User; +var _ = require('lodash'); +var validator = require('validator'); + +function list(req, res, next) { + var loginname = req.params.loginname; + var ep = new eventproxy(); + + ep.fail(next); + + UserProxy.getUserByLoginName(loginname, ep.done(function (user) { + if (!user) { + res.status(404); + return res.send({success: false, error_msg: '用户不存在'}); + } + + // api 返回 100 条就好了 + TopicCollectProxy.getTopicCollectsByUserId(user._id, {limit: 100}, ep.done('collected_topics')); + + ep.all('collected_topics', function (collected_topics) { + + var ids = collected_topics.map(function (doc) { + return String(doc.topic_id) + }); + var query = { _id: { '$in': ids } }; + TopicProxy.getTopicsByQuery(query, {}, ep.done('topics', function (topics) { + topics = _.sortBy(topics, function (topic) { + return ids.indexOf(String(topic._id)) + }); + return topics + })); + + }); + + ep.all('topics', function (topics) { + topics = topics.map(function (topic) { + topic.author = _.pick(topic.author, ['loginname', 'avatar_url']); + return _.pick(topic, ['id', 'author_id', 'tab', 'content', 'title', 'last_reply_at', + 'good', 'top', 'reply_count', 'visit_count', 'create_at', 'author']); + }); + res.send({success: true, data: topics}) + + }) + })) +} + +exports.list = list; + +function collect(req, res, next) { + var topic_id = req.body.topic_id; + + if (!validator.isMongoId(topic_id)) { + res.status(400); + return res.send({success: false, error_msg: '不是有效的话题id'}); + } + + TopicProxy.getTopic(topic_id, function (err, topic) { + if (err) { + return next(err); + } + if (!topic) { + res.status(404); + return res.json({success: false, error_msg: '话题不存在'}); + } + + TopicCollectProxy.getTopicCollect(req.user.id, topic._id, function (err, doc) { + if (err) { + return next(err); + } + if (doc) { + res.json({success: false}); + return; + } + + TopicCollectProxy.newAndSave(req.user.id, topic._id, function (err) { + if (err) { + return next(err); + } + res.json({success: true}); + }); + UserProxy.getUserById(req.user.id, function (err, user) { + if (err) { + return next(err); + } + user.collect_topic_count += 1; + user.save(); + }); + + topic.collect_count += 1; + topic.save(); + }); + }); +} + +exports.collect = collect; + +function de_collect(req, res, next) { + var topic_id = req.body.topic_id; + + if (!validator.isMongoId(topic_id)) { + res.status(400); + return res.send({success: false, error_msg: '不是有效的话题id'}); + } + + TopicProxy.getTopic(topic_id, function (err, topic) { + if (err) { + return next(err); + } + if (!topic) { + res.status(404); + return res.json({success: false, error_msg: '话题不存在'}); + } + TopicCollectProxy.remove(req.user.id, topic._id, function (err, removeResult) { + if (err) { + return next(err); + } + if (removeResult.n == 0) { + return res.json({success: false}) + } + + UserProxy.getUserById(req.user.id, function (err, user) { + if (err) { + return next(err); + } + user.collect_topic_count -= 1; + user.save(); + }); + + topic.collect_count -= 1; + topic.save(); + + res.json({success: true}); + }); + + }); +} + +exports.de_collect = de_collect; diff --git a/api/v1/user.js b/api/v1/user.js new file mode 100644 index 0000000000..7696bc27f3 --- /dev/null +++ b/api/v1/user.js @@ -0,0 +1,62 @@ +var _ = require('lodash'); +var eventproxy = require('eventproxy'); +var UserProxy = require('../../proxy').User; +var TopicProxy = require('../../proxy').Topic; +var ReplyProxy = require('../../proxy').Reply; +var TopicCollect = require('../../proxy').TopicCollect; + +var show = function (req, res, next) { + var loginname = req.params.loginname; + var ep = new eventproxy(); + + ep.fail(next); + + UserProxy.getUserByLoginName(loginname, ep.done(function (user) { + if (!user) { + res.status(404); + return res.send({success: false, error_msg: '用户不存在'}); + } + var query = {author_id: user._id}; + var opt = {limit: 15, sort: '-create_at'}; + TopicProxy.getTopicsByQuery(query, opt, ep.done('recent_topics')); + + ReplyProxy.getRepliesByAuthorId(user._id, {limit: 20, sort: '-create_at'}, + ep.done(function (replies) { + var topic_ids = replies.map(function (reply) { + return reply.topic_id.toString() + }); + topic_ids = _.uniq(topic_ids).slice(0, 5); // 只显示最近5条 + + var query = {_id: {'$in': topic_ids}}; + var opt = {}; + TopicProxy.getTopicsByQuery(query, opt, ep.done('recent_replies', function (recent_replies) { + recent_replies = _.sortBy(recent_replies, function (topic) { + return topic_ids.indexOf(topic._id.toString()) + }); + return recent_replies; + })); + })); + + ep.all('recent_topics', 'recent_replies', + function (recent_topics, recent_replies) { + + user = _.pick(user, ['loginname', 'avatar_url', 'githubUsername', + 'create_at', 'score']); + + user.recent_topics = recent_topics.map(function (topic) { + topic.author = _.pick(topic.author, ['loginname', 'avatar_url']); + topic = _.pick(topic, ['id', 'author', 'title', 'last_reply_at']); + return topic; + }); + user.recent_replies = recent_replies.map(function (topic) { + topic.author = _.pick(topic.author, ['loginname', 'avatar_url']); + topic = _.pick(topic, ['id', 'author', 'title', 'last_reply_at']); + return topic; + }); + + res.send({success: true, data: user}); + }); + })); +}; + +exports.show = show; diff --git a/api_router_v1.js b/api_router_v1.js new file mode 100644 index 0000000000..7d876cdf9b --- /dev/null +++ b/api_router_v1.js @@ -0,0 +1,45 @@ +var express = require('express'); +var topicController = require('./api/v1/topic'); +var topicCollectController = require('./api/v1/topic_collect'); +var userController = require('./api/v1/user'); +var toolsController = require('./api/v1/tools'); +var replyController = require('./api/v1/reply'); +var messageController = require('./api/v1/message'); +var middleware = require('./api/v1/middleware'); +var limit = require('./middlewares/limit'); +var config = require('./config'); + +var router = express.Router(); + + +// 主题 +router.get('/topics', topicController.index); +router.get('/topic/:id', middleware.tryAuth, topicController.show); +router.post('/topics', middleware.auth, limit.peruserperday('create_topic', config.create_post_per_day, {showJson: true}), topicController.create); +router.post('/topics/update', middleware.auth, topicController.update); + + +// 主题收藏 +router.post('/topic_collect/collect', middleware.auth, topicCollectController.collect); // 关注某话题 +router.post('/topic_collect/de_collect', middleware.auth, topicCollectController.de_collect); // 取消关注某话题 +router.get('/topic_collect/:loginname', topicCollectController.list); + +// 用户 +router.get('/user/:loginname', userController.show); + + + +// accessToken 测试 +router.post('/accesstoken', middleware.auth, toolsController.accesstoken); + +// 评论 +router.post('/topic/:topic_id/replies', middleware.auth, limit.peruserperday('create_reply', config.create_reply_per_day, {showJson: true}), replyController.create); +router.post('/reply/:reply_id/ups', middleware.auth, replyController.ups); + +// 通知 +router.get('/messages', middleware.auth, messageController.index); +router.get('/message/count', middleware.auth, messageController.count); +router.post('/message/mark_all', middleware.auth, messageController.markAll); +router.post('/message/mark_one/:msg_id', middleware.auth, messageController.markOne); + +module.exports = router; diff --git a/app.js b/app.js index e1a9651aeb..0c20ec938e 100644 --- a/app.js +++ b/app.js @@ -6,96 +6,177 @@ * Module dependencies. */ +var config = require('./config'); + +if (!config.debug && config.oneapm_key) { + require('oneapm'); +} + +require('colors'); var path = require('path'); +var Loader = require('loader'); +var LoaderConnect = require('loader-connect') var express = require('express'); -var ndir = require('ndir'); -var config = require('./config').config; -// host: http://127.0.0.1 +var session = require('express-session'); +var passport = require('passport'); +require('./middlewares/mongoose_log'); // 打印 mongodb 查询日志 +require('./models'); +var GitHubStrategy = require('passport-github').Strategy; +var githubStrategyMiddleware = require('./middlewares/github_strategy'); +var webRouter = require('./web_router'); +var apiRouterV1 = require('./api_router_v1'); +var auth = require('./middlewares/auth'); +var errorPageMiddleware = require('./middlewares/error_page'); +var proxyMiddleware = require('./middlewares/proxy'); +var RedisStore = require('connect-redis')(session); +var _ = require('lodash'); +var csurf = require('csurf'); +var compress = require('compression'); +var bodyParser = require('body-parser'); +var busboy = require('connect-busboy'); +var errorhandler = require('errorhandler'); +var cors = require('cors'); +var requestLog = require('./middlewares/request_log'); +var renderMiddleware = require('./middlewares/render'); +var logger = require('./common/logger'); +var helmet = require('helmet'); +var bytes = require('bytes') + + +// 静态文件目录 +var staticDir = path.join(__dirname, 'public'); +// assets +var assets = {}; + +if (config.mini_assets) { + try { + assets = require('./assets.json'); + } catch (e) { + logger.error('You must execute `make build` before start app when mini_assets is true.'); + throw e; + } +} + var urlinfo = require('url').parse(config.host); config.hostname = urlinfo.hostname || config.host; -var routes = require('./routes'); -config.upload_dir = config.upload_dir || path.join(__dirname, 'public', 'user_data', 'images'); -// ensure upload dir exists -ndir.mkdir(config.upload_dir, function (err) { - if (err) { - throw err; - } +var app = express(); + +// configuration in all env +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'html'); +app.engine('html', require('ejs-mate')); +app.locals._layoutFile = 'layout.html'; +app.enable('trust proxy'); + +// Request logger。请求时间 +app.use(requestLog); + +if (config.debug) { + // 渲染时间 + app.use(renderMiddleware.render); +} + +// 静态资源 +if (config.debug) { + app.use(LoaderConnect.less(__dirname)); // 测试环境用,编译 .less on the fly +} +app.use('/public', express.static(staticDir)); +app.use('/agent', proxyMiddleware.proxy); + +// 通用的中间件 +app.use(require('response-time')()); +app.use(helmet.frameguard('sameorigin')); +app.use(bodyParser.json({limit: '1mb'})); +app.use(bodyParser.urlencoded({ extended: true, limit: '1mb' })); +app.use(require('method-override')()); +app.use(require('cookie-parser')(config.session_secret)); +app.use(compress()); +app.use(session({ + secret: config.session_secret, + store: new RedisStore({ + port: config.redis_port, + host: config.redis_host, + db: config.redis_db, + pass: config.redis_password, + }), + resave: false, + saveUninitialized: false, +})); + +// oauth 中间件 +app.use(passport.initialize()); + +// github oauth +passport.serializeUser(function (user, done) { + done(null, user); +}); +passport.deserializeUser(function (user, done) { + done(null, user); }); +passport.use(new GitHubStrategy(config.GITHUB_OAUTH, githubStrategyMiddleware)); -var app = express.createServer(); +// custom middleware +app.use(auth.authUser); +app.use(auth.blockUser()); -// configuration in all env -app.configure(function () { - var viewsRoot = path.join(__dirname, 'views'); - app.set('view engine', 'html'); - app.set('views', viewsRoot); - app.register('.html', require('ejs')); - app.use(express.bodyParser({ - uploadDir: config.upload_dir - })); - app.use(express.cookieParser()); - app.use(express.session({ - secret: config.session_secret - })); - // custom middleware - app.use(require('./controllers/sign').auth_user); - - var csrf = express.csrf(); +if (!config.debug) { app.use(function (req, res, next) { - // ignore upload image - if (req.body && req.body.user_action === 'upload_image') { - return next(); + if (req.path === '/api' || req.path.indexOf('/api') === -1) { + csurf()(req, res, next); + return; } - csrf(req, res, next); + next(); }); -}); - -if (process.env.NODE_ENV !== 'test') { - // plugins - var plugins = config.plugins || []; - for (var i = 0, l = plugins.length; i < l; i++) { - var p = plugins[i]; - app.use(require('./plugins/' + p.name)(p.options)); - } + app.set('view cache', true); } +// for debug +// app.get('/err', function (req, res, next) { +// next(new Error('haha')) +// }); + // set static, dynamic helpers -app.helpers({ - config: config +_.extend(app.locals, { + config: config, + Loader: Loader, + assets: assets }); -app.dynamicHelpers({ - csrf: function (req, res) { - return req.session ? req.session._csrf : ''; - } -}); - -var maxAge = 3600000 * 24 * 30; -app.use('/upload/', express.static(config.upload_dir, { maxAge: maxAge })); -// old image url: http://host/user_data/images/xxxx -app.use('/user_data/', express.static(path.join(__dirname, 'public', 'user_data'), { maxAge: maxAge })); -var staticDir = path.join(__dirname, 'public'); -app.configure('development', function () { - app.use(express.static(staticDir)); - app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); +app.use(errorPageMiddleware.errorPage); +_.extend(app.locals, require('./common/render_helper')); +app.use(function (req, res, next) { + res.locals.csrf = req.csrfToken ? req.csrfToken() : ''; + next(); }); -app.configure('production', function () { - app.use(express.static(staticDir, { maxAge: maxAge })); - app.use(express.errorHandler()); - app.set('view cache', true); -}); +app.use(busboy({ + limits: { + fileSize: bytes(config.file_limit) + } +})); // routes -routes(app); +app.use('/api/v1', cors(), apiRouterV1); +app.use('/', webRouter); -if (process.env.NODE_ENV !== 'test') { - app.listen(config.port); +// error handler +if (config.debug) { + app.use(errorhandler()); +} else { + app.use(function (err, req, res, next) { + logger.error(err); + return res.status(500).send('500 status'); + }); +} - console.log("NodeClub listening on port %d in %s mode", config.port, app.settings.env); - console.log("God bless love...."); - console.log("You can debug your app with http://" + config.hostname + ':' + config.port); +if (!module.parent) { + app.listen(config.port, function () { + logger.info('NodeClub listening on port', config.port); + logger.info('God bless love....'); + logger.info('You can debug your app with http://' + config.hostname + ':' + config.port); + logger.info(''); + }); } module.exports = app; diff --git a/bin/fix_at_problem.js b/bin/fix_at_problem.js new file mode 100644 index 0000000000..8374a4f654 --- /dev/null +++ b/bin/fix_at_problem.js @@ -0,0 +1,18 @@ +// 一次性脚本 +// 修复之前重复编辑帖子会导致重复 @someone 的渲染问题 +var TopicModel = require('../models').Topic; + +TopicModel.find({content: /\[{2,}@/}).exec(function (err, topics) { + topics.forEach(function (topic) { + topic.content = fix(topic.content); + console.log(topic.id); + topic.save(); + }); +}); + +function fix(str) { + str = str.replace(/\[{1,}(\[@\w+)(\]\(.+?\))\2+/, function (match_text, $1, $2) { + return $1 + $2; + }); + return str; +} diff --git a/bin/fix_topic_collect_count.js b/bin/fix_topic_collect_count.js new file mode 100644 index 0000000000..577dfb35a1 --- /dev/null +++ b/bin/fix_topic_collect_count.js @@ -0,0 +1,61 @@ +var TopicCollect = require('../models').TopicCollect; +var UserModel = require('../models').User; +var TopicModel = require('../models').Topic + +// 修复用户的topic_collect计数 +TopicCollect.aggregate( + [{ + "$group" : + { + _id : {user_id: "$user_id"}, + count : { $sum : 1} + } + }], function (err, result) { + result.forEach(function (row) { + var userId = row._id.user_id; + var count = row.count; + + UserModel.findOne({ + _id: userId + }, function (err, user) { + + if (!user) { + return; + } + + user.collect_topic_count = count; + user.save(function () { + console.log(user.loginname, count) + }); + }) + }) + }) + + // 修复帖子的topic_collect计数 + TopicCollect.aggregate( + [{ + "$group" : + { + _id : {topic_id: "$topic_id"}, + count : { $sum : 1} + } + }], function (err, result) { + result.forEach(function (row) { + var topic_id = row._id.topic_id; + var count = row.count; + + TopicModel.findOne({ + _id: topic_id + }, function (err, topic) { + + if (!topic) { + return; + } + + topic.collect_topic_count = count; + topic.save(function () { + console.log(topic.id, count) + }); + }) + }) + }) diff --git a/bin/generate_accesstoken.js b/bin/generate_accesstoken.js new file mode 100644 index 0000000000..fbe9780988 --- /dev/null +++ b/bin/generate_accesstoken.js @@ -0,0 +1,41 @@ +// 一次性脚本 +// 为所有老用户生成 accessToken + +var uuid = require('node-uuid'); +var mongoose = require('mongoose'); +var config = require('../config'); +var async = require('async'); +require('../models/user'); + +mongoose.connect(config.db, function (err) { + if (err) { + console.error('connect to %s error: ', config.db, err.message); + process.exit(1); + } +}); + +var UserModel = mongoose.model('User'); + +var hasRemain = true; +async.whilst( + function () { + return hasRemain; + }, + function (callback) { + UserModel.findOne({accessToken: {$exists: false}}, function (err, user) { + if (!user) { + hasRemain = false; + callback(); + return; + } + user.accessToken = uuid.v4(); + user.save(function () { + console.log(user.loginname + ' done!'); + callback(); + }); + }); + }, + function (err) { + mongoose.disconnect(); + }); + diff --git a/bin/get_user_topics.js b/bin/get_user_topics.js new file mode 100644 index 0000000000..9a1b3d43ea --- /dev/null +++ b/bin/get_user_topics.js @@ -0,0 +1,14 @@ +var UserModel = require('../models').User; +var TopicModel = require('../models').Topic + +// usage: +// node get_user_topics.js alsotang +UserModel.findOne({ + loginname: process.argv[2] +}, function (err, user) { + TopicModel.find({ + author_id: user._id + }, function (err, topics) { + console.log(topics) + }) +}) \ No newline at end of file diff --git a/common/at.js b/common/at.js new file mode 100644 index 0000000000..b6ddaf16dc --- /dev/null +++ b/common/at.js @@ -0,0 +1,112 @@ +/*! + * nodeclub - topic mention user controller. + * Copyright(c) 2012 fengmk2 + * Copyright(c) 2012 muyuan + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var User = require('../proxy').User; +var Message = require('./message'); +var EventProxy = require('eventproxy'); +var _ = require('lodash'); + +/** + * 从文本中提取出@username 标记的用户名数组 + * @param {String} text 文本内容 + * @return {Array} 用户名数组 + */ +var fetchUsers = function (text) { + if (!text) { + return []; + } + + var ignoreRegexs = [ + /```.+?```/g, // 去除单行的 ``` + /^```[\s\S]+?^```/gm, // ``` 里面的是 pre 标签内容 + /`[\s\S]+?`/g, // 同一行中,`some code` 中内容也不该被解析 + /^ .*/gm, // 4个空格也是 pre 标签,在这里 . 不会匹配换行 + /\b\S*?@[^\s]*?\..+?\b/g, // somebody@gmail.com 会被去除 + /\[@.+?\]\(\/.+?\)/g, // 已经被 link 的 username + /\/@/g, // 一般是url中path的一部分 + ]; + + ignoreRegexs.forEach(function (ignore_regex) { + text = text.replace(ignore_regex, ''); + }); + + var results = text.match(/@[a-z0-9\-_]+\b/igm); + var names = []; + if (results) { + for (var i = 0, l = results.length; i < l; i++) { + var s = results[i]; + //remove leading char @ + s = s.slice(1); + names.push(s); + } + } + names = _.uniq(names); + return names; +}; +exports.fetchUsers = fetchUsers; + +/** + * 根据文本内容中读取用户,并发送消息给提到的用户 + * Callback: + * - err, 数据库异常 + * @param {String} text 文本内容 + * @param {String} topicId 主题ID + * @param {String} authorId 作者ID + * @param {String} reply_id 回复ID + * @param {Function} callback 回调函数 + */ +exports.sendMessageToMentionUsers = function (text, topicId, authorId, reply_id, callback) { + if (typeof reply_id === 'function') { + callback = reply_id; + reply_id = null; + } + callback = callback || _.noop; + + User.getUsersByNames(fetchUsers(text), function (err, users) { + if (err || !users) { + return callback(err); + } + var ep = new EventProxy(); + ep.fail(callback); + + users = users.filter(function (user) { + return !user._id.equals(authorId); + }); + + ep.after('sent', users.length, function () { + callback(); + }); + + users.forEach(function (user) { + Message.sendAtMessage(user._id, authorId, topicId, reply_id, ep.done('sent')); + }); + }); +}; + +/** + * 根据文本内容,替换为数据库中的数据 + * Callback: + * - err, 数据库异常 + * - text, 替换后的文本内容 + * @param {String} text 文本内容 + * @param {Function} callback 回调函数 + */ +exports.linkUsers = function (text, callback) { + var users = fetchUsers(text); + for (var i = 0, l = users.length; i < l; i++) { + var name = users[i]; + text = text.replace(new RegExp('@' + name + '\\b(?!\\])', 'g'), '[@' + name + '](/user/' + name + ')'); + } + if (!callback) { + return text; + } + return callback(null, text); +}; diff --git a/common/cache.js b/common/cache.js new file mode 100644 index 0000000000..e8fd75abaa --- /dev/null +++ b/common/cache.js @@ -0,0 +1,43 @@ +var redis = require('./redis'); +var _ = require('lodash'); +var logger = require('./logger'); + +var get = function (key, callback) { + var t = new Date(); + redis.get(key, function (err, data) { + if (err) { + return callback(err); + } + if (!data) { + return callback(); + } + data = JSON.parse(data); + var duration = (new Date() - t); + logger.debug('Cache', 'get', key, (duration + 'ms').green); + callback(null, data); + }); +}; + +exports.get = get; + +// time 参数可选,秒为单位 +var set = function (key, value, time, callback) { + var t = new Date(); + + if (typeof time === 'function') { + callback = time; + time = null; + } + callback = callback || _.noop; + value = JSON.stringify(value); + + if (!time) { + redis.set(key, value, callback); + } else { + redis.setex(key, time, value, callback); + } + var duration = (new Date() - t); + logger.debug("Cache", "set", key, (duration + 'ms').green); +}; + +exports.set = set; diff --git a/common/logger.js b/common/logger.js new file mode 100644 index 0000000000..ba7fbe71ee --- /dev/null +++ b/common/logger.js @@ -0,0 +1,18 @@ +var config = require('../config'); +var pathLib = require('path') + +var env = process.env.NODE_ENV || "development" + + +var log4js = require('log4js'); +log4js.configure({ + appenders: [ + { type: 'console' }, + { type: 'file', filename: pathLib.join(config.log_dir, 'cheese.log'), category: 'cheese' } + ] +}); + +var logger = log4js.getLogger('cheese'); +logger.setLevel(config.debug && env !== 'test' ? 'DEBUG' : 'ERROR') + +module.exports = logger; diff --git a/common/mail.js b/common/mail.js new file mode 100644 index 0000000000..a6d3b73b0a --- /dev/null +++ b/common/mail.js @@ -0,0 +1,84 @@ +var mailer = require('nodemailer'); +var smtpTransport = require('nodemailer-smtp-transport'); +var config = require('../config'); +var util = require('util'); +var logger = require('./logger'); +var transporter = mailer.createTransport(smtpTransport(config.mail_opts)); +var SITE_ROOT_URL = 'http://' + config.host; +var async = require('async') + +/** + * Send an email + * @param {Object} data 邮件对象 + */ +var sendMail = function (data) { + if (config.debug) { + return; + } + + // 重试5次 + async.retry({times: 5}, function (done) { + transporter.sendMail(data, function (err) { + if (err) { + // 写为日志 + logger.error('send mail error', err, data); + return done(err); + } + return done() + }); + }, function (err) { + if (err) { + return logger.error('send mail finally error', err, data); + } + logger.info('send mail success', data) + }) +}; +exports.sendMail = sendMail; + +/** + * 发送激活通知邮件 + * @param {String} who 接收人的邮件地址 + * @param {String} token 重置用的token字符串 + * @param {String} name 接收人的用户名 + */ +exports.sendActiveMail = function (who, token, name) { + var from = util.format('%s <%s>', config.name, config.mail_opts.auth.user); + var to = who; + var subject = config.name + '社区帐号激活'; + var html = '

您好:' + name + '

' + + '

我们收到您在' + config.name + '社区的注册信息,请点击下面的链接来激活帐户:

' + + '激活链接' + + '

若您没有在' + config.name + '社区填写过注册信息,说明有人滥用了您的电子邮箱,请删除此邮件,我们对给您造成的打扰感到抱歉。

' + + '

' + config.name + '社区 谨上。

'; + + exports.sendMail({ + from: from, + to: to, + subject: subject, + html: html + }); +}; + +/** + * 发送密码重置通知邮件 + * @param {String} who 接收人的邮件地址 + * @param {String} token 重置用的token字符串 + * @param {String} name 接收人的用户名 + */ +exports.sendResetPassMail = function (who, token, name) { + var from = util.format('%s <%s>', config.name, config.mail_opts.auth.user); + var to = who; + var subject = config.name + '社区密码重置'; + var html = '

您好:' + name + '

' + + '

我们收到您在' + config.name + '社区重置密码的请求,请在24小时内单击下面的链接来重置密码:

' + + '重置密码链接' + + '

若您没有在' + config.name + '社区填写过注册信息,说明有人滥用了您的电子邮箱,请删除此邮件,我们对给您造成的打扰感到抱歉。

' + + '

' + config.name + '社区 谨上。

'; + + exports.sendMail({ + from: from, + to: to, + subject: subject, + html: html + }); +}; diff --git a/common/message.js b/common/message.js new file mode 100644 index 0000000000..ccc82212fc --- /dev/null +++ b/common/message.js @@ -0,0 +1,42 @@ +var models = require('../models'); +var eventproxy = require('eventproxy'); +var Message = models.Message; +var User = require('../proxy').User; +var messageProxy = require('../proxy/message'); +var _ = require('lodash'); + +exports.sendReplyMessage = function (master_id, author_id, topic_id, reply_id, callback) { + callback = callback || _.noop; + var ep = new eventproxy(); + ep.fail(callback); + + var message = new Message(); + message.type = 'reply'; + message.master_id = master_id; + message.author_id = author_id; + message.topic_id = topic_id; + message.reply_id = reply_id; + + message.save(ep.done('message_saved')); + ep.all('message_saved', function (msg) { + callback(null, msg); + }); +}; + +exports.sendAtMessage = function (master_id, author_id, topic_id, reply_id, callback) { + callback = callback || _.noop; + var ep = new eventproxy(); + ep.fail(callback); + + var message = new Message(); + message.type = 'at'; + message.master_id = master_id; + message.author_id = author_id; + message.topic_id = topic_id; + message.reply_id = reply_id; + + message.save(ep.done('message_saved')); + ep.all('message_saved', function (msg) { + callback(null, msg); + }); +}; diff --git a/common/redis.js b/common/redis.js new file mode 100644 index 0000000000..01939b070c --- /dev/null +++ b/common/redis.js @@ -0,0 +1,19 @@ +var config = require('../config'); +var Redis = require('ioredis'); +var logger = require('./logger') + +var client = new Redis({ + port: config.redis_port, + host: config.redis_host, + db: config.redis_db, + password: config.redis_password, +}); + +client.on('error', function (err) { + if (err) { + logger.error('connect to redis error, check your redis config', err); + process.exit(1); + } +}) + +exports = module.exports = client; diff --git a/common/render_helper.js b/common/render_helper.js new file mode 100644 index 0000000000..73368d04ca --- /dev/null +++ b/common/render_helper.js @@ -0,0 +1,92 @@ +/*! + * nodeclub - common/render_helpers.js + * Copyright(c) 2013 fengmk2 + * MIT Licensed + */ + +"use strict"; + +/** + * Module dependencies. + */ + +var MarkdownIt = require('markdown-it'); +var _ = require('lodash'); +var config = require('../config'); +var validator = require('validator'); +var jsxss = require('xss'); +var multiline = require('multiline') + +// Set default options +var md = new MarkdownIt(); + +md.set({ + html: false, // Enable HTML tags in source + xhtmlOut: false, // Use '/' to close single tags (
) + breaks: false, // Convert '\n' in paragraphs into
+ linkify: true, // Autoconvert URL-like text to links + typographer: true, // Enable smartypants and other sweet transforms +}); + +md.renderer.rules.fence = function (tokens, idx) { + var token = tokens[idx]; + var language = token.info && ('language-' + token.info) || ''; + language = validator.escape(language); + + return '
'
+    + '' + validator.escape(token.content) + ''
+    + '
'; +}; + +md.renderer.rules.code_block = function (tokens, idx /*, options*/) { + var token = tokens[idx]; + + return '
'
+    + '' + validator.escape(token.content) + ''
+    + '
'; +}; + +var myxss = new jsxss.FilterXSS({ + onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) { + // 让 prettyprint 可以工作 + if (tag === 'pre' && name === 'class') { + return name + '="' + jsxss.escapeAttrValue(value) + '"'; + } + } +}); + +exports.markdown = function (text) { + return '
' + myxss.process(md.render(text || '')) + '
'; +}; + +exports.escapeSignature = function (signature) { + return signature.split('\n').map(function (p) { + return _.escape(p); + }).join('
'); +}; + +exports.staticFile = function (filePath) { + if (filePath.indexOf('http') === 0 || filePath.indexOf('//') === 0) { + return filePath; + } + return config.site_static_host + filePath; +}; + +exports.tabName = function (tab) { + var pair = _.find(config.tabs, function (pair) { + return pair[0] === tab; + }); + if (pair) { + return pair[1]; + } +}; + +exports.proxy = function (url) { + return url; + // 当 google 和 github 封锁严重时,则需要通过服务器代理访问它们的静态资源 + // return '/agent?url=' + encodeURIComponent(url); +}; + +// 为了在 view 中使用 +exports._ = _; +exports.multiline = multiline; diff --git a/common/store.js b/common/store.js new file mode 100644 index 0000000000..c29ed81753 --- /dev/null +++ b/common/store.js @@ -0,0 +1,4 @@ +var qn = require('./store_qn'); +var local = require('./store_local'); + +module.exports = qn || local; diff --git a/common/store_local.js b/common/store_local.js new file mode 100644 index 0000000000..10bbd6e407 --- /dev/null +++ b/common/store_local.js @@ -0,0 +1,24 @@ +var config = require('../config'); +var utility = require('utility'); +var path = require('path'); +var fs = require('fs'); + +exports.upload = function (file, options, callback) { + var filename = options.filename; + + var newFilename = utility.md5(filename + String((new Date()).getTime())) + + path.extname(filename); + + var upload_path = config.upload.path; + var base_url = config.upload.url; + var filePath = path.join(upload_path, newFilename); + var fileUrl = base_url + newFilename; + + file.on('end', function () { + callback(null, { + url: fileUrl + }); + }); + + file.pipe(fs.createWriteStream(filePath)); +}; diff --git a/common/store_qn.js b/common/store_qn.js new file mode 100644 index 0000000000..0d3f73678e --- /dev/null +++ b/common/store_qn.js @@ -0,0 +1,10 @@ +var qn = require('qn'); +var config = require('../config'); + +//7牛 client +var qnClient = null; +if (config.qn_access && config.qn_access.secretKey !== 'your secret key') { + qnClient = qn.create(config.qn_access); +} + +module.exports = qnClient; diff --git a/common/tools.js b/common/tools.js new file mode 100644 index 0000000000..d047c393b9 --- /dev/null +++ b/common/tools.js @@ -0,0 +1,28 @@ +var bcrypt = require('bcryptjs'); +var moment = require('moment'); + +moment.locale('zh-cn'); // 使用中文 + +// 格式化时间 +exports.formatDate = function (date, friendly) { + date = moment(date); + + if (friendly) { + return date.fromNow(); + } else { + return date.format('YYYY-MM-DD HH:mm'); + } + +}; + +exports.validateId = function (str) { + return (/^[a-zA-Z0-9\-_]+$/i).test(str); +}; + +exports.bhash = function (str, callback) { + bcrypt.hash(str, 10, callback); +}; + +exports.bcompare = function (str, hash, callback) { + bcrypt.compare(str, hash, callback); +}; diff --git a/config.default.js b/config.default.js index 4911dcb6d5..3cbd990593 100644 --- a/config.default.js +++ b/config.default.js @@ -4,63 +4,140 @@ var path = require('path'); -exports.config = { +var config = { + // debug 为 true 时,用于本地调试 debug: true, - name: 'Node Club', - description: 'Node Club 是用Node.js开发的社区软件', - version: '0.2.2', - // site settings + get mini_assets() { return !this.debug; }, // 是否启用静态文件的合并压缩,详见视图中的Loader + + name: 'Nodeclub', // 社区名字 + description: 'CNode:Node.js专业中文社区', // 社区的描述 + keywords: 'nodejs, node, express, connect, socket.io', + + // 添加到 html head 中的信息 site_headers: [ - '', + '' ], - host: 'localhost.cnodejs.org', - site_logo: '', // default is `name` + site_logo: '/public/images/cnodejs_light.svg', // default is `name` + site_icon: '/public/images/cnode_icon_32.png', // 默认没有 favicon, 这里填写网址 + // 右上角的导航区 site_navs: [ - // [ path, title, [target=''] ] - [ '/about', '关于' ], + // 格式 [ path, title, [target=''] ] + [ '/about', '关于' ] ], + // cdn host,如 http://cnodejs.qiniudn.com site_static_host: '', // 静态文件存储域名 - site_enable_search_preview: false, // 开启google search preview - - upload_dir: path.join(__dirname, 'public', 'user_data', 'images'), + // 社区的域名 + host: 'localhost', + // 默认的Google tracker ID,自有站点请修改,申请地址:http://www.google.com/analytics/ + google_tracker_id: '', + // 默认的cnzz tracker ID,自有站点请修改 + cnzz_tracker_id: '', + // mongodb 配置 db: 'mongodb://127.0.0.1/node_club_dev', - session_secret: 'node_club', + + // redis 配置,默认是本地 + redis_host: '127.0.0.1', + redis_port: 6379, + redis_db: 0, + redis_password: '', + + session_secret: 'node_club_secret', // 务必修改 auth_cookie_name: 'node_club', + + // 程序运行的端口 port: 3000, // 话题列表显示的话题数量 list_topic_count: 20, - // RSS + // RSS配置 rss: { title: 'CNode:Node.js专业中文社区', link: 'http://cnodejs.org', language: 'zh-cn', description: 'CNode:Node.js专业中文社区', - //最多获取的RSS Item数量 max_rss_items: 50 }, - // mail SMTP - mail_port: 25, - mail_user: 'club', - mail_pass: 'club', - mail_host: 'smtp.126.com', - mail_sender: 'club@126.com', - mail_use_authentication: true, - + log_dir: path.join(__dirname, 'logs'), + + // 邮箱配置 + mail_opts: { + host: 'smtp.126.com', + port: 25, + auth: { + user: 'club@126.com', + pass: 'club' + }, + ignoreTLS: true, + }, + //weibo app key weibo_key: 10000000, + weibo_id: 'your_weibo_id', + + // admin 可删除话题,编辑标签。把 user_login_name 换成你的登录名 + admins: { user_login_name: true }, + + // github 登陆的配置 + GITHUB_OAUTH: { + clientID: 'your GITHUB_CLIENT_ID', + clientSecret: 'your GITHUB_CLIENT_SECRET', + callbackURL: 'http://cnodejs.org/auth/github/callback' + }, + // 是否允许直接注册(否则只能走 github 的方式) + allow_sign_up: true, - // admin 可删除话题,编辑标签,设某人为达人 - admins: { admin: true }, + // oneapm 是个用来监控网站性能的服务 + oneapm_key: '', - // [ { name: 'plugin_name', options: { ... }, ... ] - plugins: [ - // { name: 'onehost', options: { host: 'localhost.cnodejs.org' } }, - // { name: 'wordpress_redirect', options: {} } - ] + // 下面两个配置都是文件上传的配置 + + // 7牛的access信息,用于文件上传 + qn_access: { + accessKey: 'your access key', + secretKey: 'your secret key', + bucket: 'your bucket name', + origin: 'http://your qiniu domain', + // 如果vps在国外,请使用 http://up.qiniug.com/ ,这是七牛的国际节点 + // 如果在国内,此项请留空 + uploadURL: 'http://xxxxxxxx', + }, + + // 文件上传配置 + // 注:如果填写 qn_access,则会上传到 7牛,以下配置无效 + upload: { + path: path.join(__dirname, 'public/upload/'), + url: '/public/upload/' + }, + + file_limit: '1MB', + + // 版块 + tabs: [ + ['share', '分享'], + ['ask', '问答'], + ['job', '招聘'], + ], + + // 极光推送 + jpush: { + appKey: 'YourAccessKeyyyyyyyyyyyy', + masterSecret: 'YourSecretKeyyyyyyyyyyyyy', + isDebug: false, + }, + + create_post_per_day: 1000, // 每个用户一天可以发的主题数 + create_reply_per_day: 1000, // 每个用户一天可以发的评论数 + create_user_per_ip: 1000, // 每个 ip 每天可以注册账号的次数 + visit_per_day: 1000, // 每个 ip 每天能访问的次数 }; + +if (process.env.NODE_ENV === 'test') { + config.db = 'mongodb://127.0.0.1/node_club_test'; +} + +module.exports = config; diff --git a/controllers/at.js b/controllers/at.js deleted file mode 100644 index 9b253e4ec2..0000000000 --- a/controllers/at.js +++ /dev/null @@ -1,74 +0,0 @@ -/*! - * nodeclub - topic mention user controller. - * Copyright(c) 2012 fengmk2 - * Copyright(c) 2012 muyuan - * MIT Licensed - */ - -/** - * Module dependencies. - */ - -var models = require('../models'); -var User = models.User; -var Message = require('./message'); -var EventProxy = require('eventproxy').EventProxy; - - -function searchUsers(text, callback) { - var results = text.match(/@[a-zA-Z0-9]+/ig); - var names = []; - if (results) { - for (var i = 0, l = results.length; i < l; i++) { - var s = results[i]; - //remove char @ - s = s.slice(1); - names.push(s); - } - } - if (names.length === 0) { - return callback(null, names); - } - - User.find({ name: { $in: names } }, callback); -} - -function sendMessageToMentionUsers(text, topicId, authorId, callback) { - searchUsers(text, function (err, users) { - if (err || !users || users.length === 0) { - return callback && callback(err); - } - var ep = EventProxy.create(); - ep.after('sent', users.length, function () { - callback && callback(); - }); - ep.once('error', function (err) { - ep.unbind(); - callback && callback(err); - }); - users.forEach(function (user) { - Message.send_at_message(user._id, authorId, topicId, function (err) { - if (err) { - return ep.emit('error', err); - } - ep.emit('sent'); - }); - }); - }); -} - -function linkUsers(text, callback) { - searchUsers(text, function (err, users) { - if (err) { - return callback(err); - } - for (var i = 0, l = users.length; i < l; i++) { - var name = users[i].name; - text = text.replace(new RegExp('@' + name, 'gmi'), '@' + name + ''); - } - return callback(err, text); - }); -} - -exports.send_at_message = exports.sendMessageToMentionUsers = sendMessageToMentionUsers; -exports.link_at_who = exports.linkUsers = linkUsers; diff --git a/controllers/github.js b/controllers/github.js new file mode 100644 index 0000000000..9fdbaed68b --- /dev/null +++ b/controllers/github.js @@ -0,0 +1,132 @@ +var Models = require('../models'); +var User = Models.User; +var authMiddleWare = require('../middlewares/auth'); +var tools = require('../common/tools'); +var eventproxy = require('eventproxy'); +var uuid = require('node-uuid'); +var validator = require('validator'); + +exports.callback = function (req, res, next) { + var profile = req.user; + var email = profile.emails && profile.emails[0] && profile.emails[0].value; + if (!email) { + return res.status(500) + .render('sign/no_github_email'); + } + User.findOne({githubId: profile.id}, function (err, user) { + if (err) { + return next(err); + } + // 当用户已经是 cnode 用户时,通过 github 登陆将会更新他的资料 + if (user) { + user.githubUsername = profile.username; + user.githubId = profile.id; + user.githubAccessToken = profile.accessToken; + // user.loginname = profile.username; + user.avatar = profile._json.avatar_url; + user.email = email || user.email; + + + user.save(function (err) { + if (err) { + // 根据 err.err 的错误信息决定如何回应用户,这个地方写得很难看 + if (err.message.indexOf('duplicate key error') !== -1) { + if (err.message.indexOf('loginname') !== -1) { + return res.status(500) + .send('您 GitHub 账号的用户名与之前在 CNodejs 注册的用户名重复了'); + } + } + return next(err); + } + authMiddleWare.gen_session(user, res); + return res.redirect('/'); + }); + } else { + // 如果用户还未存在,则建立新用户 + req.session.profile = profile; + return res.redirect('/auth/github/new'); + } + }); +}; + +exports.new = function (req, res, next) { + res.render('sign/new_oauth', {actionPath: '/auth/github/create'}); +}; + +exports.create = function (req, res, next) { + var profile = req.session.profile; + + var isnew = req.body.isnew; + var loginname = validator.trim(req.body.name || '').toLowerCase(); + var password = validator.trim(req.body.pass || ''); + var ep = new eventproxy(); + ep.fail(next); + + if (!profile) { + return res.redirect('/signin'); + } + delete req.session.profile; + + var email = profile.emails && profile.emails[0] && profile.emails[0].value; + if (!email) { + return res.status(500) + .render('sign/no_github_email'); + } + if (isnew) { // 注册新账号 + var user = new User({ + loginname: profile.username, + pass: profile.accessToken, + email: email, + avatar: profile._json.avatar_url, + githubId: profile.id, + githubUsername: profile.username, + githubAccessToken: profile.accessToken, + active: true, + accessToken: uuid.v4(), + }); + user.save(function (err) { + if (err) { + // 根据 err.err 的错误信息决定如何回应用户,这个地方写得很难看 + if (err.message.indexOf('duplicate key error') !== -1) { + if (err.message.indexOf('loginname') !== -1) { + return res.status(500) + .send('您 GitHub 账号的用户名与之前在 CNodejs 注册的用户名重复了'); + } + } + return next(err); + // END 根据 err.err 的错误信息决定如何回应用户,这个地方写得很难看 + } + authMiddleWare.gen_session(user, res); + res.redirect('/'); + }); + } else { // 关联老账号 + ep.on('login_error', function (login_error) { + res.status(403); + res.render('sign/signin', { error: '账号名或密码错误。' }); + }); + User.findOne({loginname: loginname}, + ep.done(function (user) { + if (!user) { + return ep.emit('login_error'); + } + tools.bcompare(password, user.pass, ep.done(function (bool) { + if (!bool) { + return ep.emit('login_error'); + } + user.githubUsername = profile.username; + user.githubId = profile.id; + // user.loginname = profile.username; + user.avatar = profile._json.avatar_url; + user.githubAccessToken = profile.accessToken; + + user.save(function (err) { + if (err) { + return next(err); + } + authMiddleWare.gen_session(user, res); + res.redirect('/'); + }); + })); + })); + } +}; diff --git a/controllers/mail.js b/controllers/mail.js deleted file mode 100644 index bcc61fea1d..0000000000 --- a/controllers/mail.js +++ /dev/null @@ -1,181 +0,0 @@ -var mailer = require('nodemailer'); -var config = require('../config').config; -var EventProxy = require('eventproxy').EventProxy; -var util = require('util'); -mailer.SMTP = { - host: config.mail_host, - port: config.mail_port, - use_authentication: config.mail_use_authentication, - user: config.mail_user, - pass: config.mail_pass -}; - -var SITE_ROOT_URL = 'http://' + config.hostname + (config.port !== 80 ? ':' + config.port : ''); - -/** - * keep all the mails to send - * @type {Array} - */ -var mails = []; -var timer; -/** - * control mailer - * @type {EventProxy} - */ -var mailEvent = new EventProxy(); -/** - * when need to send an email, start to check the mails array and send all of emails. - */ -mailEvent.on("getMail", function () { - if (mails.length === 0) { - return; - } else { - //遍历邮件数组,发送每一封邮件,如果有发送失败的,就再压入数组,同时触发mailEvent事件 - var failed = false; - for (var i = 0, len = mails.length; i < len; ++i) { - var message = mails[i]; - mails.splice(i, 1); - i--; - len--; - var mail; - try { - message.debug = false; - mail = mailer.send_mail(message, function (error, success) { - if (error) { - mails.push(message); - failed = true; - } - }); - } catch(e) { - mails.push(message); - failed = true; - } - if (mail) { - var oldemit = mail.emit; - mail.emit = function () { - oldemit.apply(mail, arguments); - }; - } - } - if (failed) { - clearTimeout(timer); - timer = setTimeout(trigger, 60000); - } - } -}); - -/** - * trigger email event - * @return {[type]} - */ -function trigger() { - mailEvent.trigger("getMail"); -} - -/** - * send an email - * @param {mail} data [info of an email] - */ -function send_mail(data) { - if (!data) { - return; - } - if (config.debug) { - console.log('******************** 在测试环境下,不会真的发送邮件*******************'); - for (var k in data) { - console.log('%s: %s', k, data[k]); - } - return; - } - mails.push(data); - trigger(); -} - -function send_active_mail(who, token, name, email, cb) { - var sender = config.mail_sender; - var to = who; - var subject = config.name + '社区帐号激活'; - var html = '

您好:

' + - '

我们收到您在' + config.name + '社区的注册信息,请点击下面的链接来激活帐户:

' + - '激活链接' + - '

若您没有在' + config.name + '社区填写过注册信息,说明有人滥用了您的电子邮箱,请删除此邮件,我们对给您造成的打扰感到抱歉。

' + - '

' +config.name +'社区 谨上。

'; - var data = { - sender: sender, - to: to, - subject: subject, - html: html - }; - cb (null, true); - send_mail(data); -} -function send_reset_pass_mail(who, token, name, cb) { - var sender = config.mail_sender; - var to = who; - var subject = config.name + '社区密码重置'; - var html = '

您好:

' + - '

我们收到您在' + config.name + '社区重置密码的请求,请在24小时内单击下面的链接来重置密码:

' + - '重置密码链接' + - '

若您没有在' + config.name + '社区填写过注册信息,说明有人滥用了您的电子邮箱,请删除此邮件,我们对给您造成的打扰感到抱歉。

' + - '

' + config.name +'社区 谨上。

'; - - var data = { - sender: sender, - to: to, - subject: subject, - html: html - }; - - cb (null, true); - send_mail(data); -} - -function send_reply_mail(who, msg) { - var sender = config.mail_sender; - var to = who; - var subject = config.name + ' 新消息'; - var html = '

您好:

' + - '

' + - '' + msg.author.name + '' + - ' 在话题 ' + '' + msg.topic.title + '' + - ' 中回复了你。

' + - '

若您没有在' + config.name + '社区填写过注册信息,说明有人滥用了您的电子邮箱,请删除此邮件,我们对给您造成的打扰感到抱歉。

' + - '

' + config.name +'社区 谨上。

'; - - var data = { - sender: sender, - to: to, - subject: subject, - html: html - }; - - send_mail(data); - -} - -function send_at_mail(who, msg) { - var sender = config.mail_sender; - var to = who; - var subject = config.name + ' 新消息'; - var html = '

您好:

' + - '

' + - '' + msg.author.name + '' + - ' 在话题 ' + '' + msg.topic.title + '' + - ' 中@了你。

' + - '

若您没有在' + config.name + '社区填写过注册信息,说明有人滥用了您的电子邮箱,请删除此邮件,我们对给您造成的打扰感到抱歉。

' + - '

' +config.name +'社区 谨上。

'; - - var data = { - sender: sender, - to: to, - subject: subject, - html: html - }; - - send_mail(data); -} - -exports.send_active_mail = send_active_mail; -exports.send_reset_pass_mail = send_reset_pass_mail; -exports.send_reply_mail = send_reply_mail; -exports.send_at_mail = send_at_mail; diff --git a/controllers/message.js b/controllers/message.js index 0db1c43184..ffdaa111e8 100644 --- a/controllers/message.js +++ b/controllers/message.js @@ -1,222 +1,33 @@ -var models = require('../models'), - Message = models.Message; +var Message = require('../proxy').Message; +var eventproxy = require('eventproxy'); -var user_ctrl = require('./user'); -var mail_ctrl = require('./mail'); -var topic_ctrl = require('./topic'); - -var EventProxy = require('eventproxy').EventProxy; - -exports.index = function(req,res,next){ - if(!req.session.user){ - res.redirect('home'); - return; - } - - var message_ids = []; +exports.index = function (req, res, next) { var user_id = req.session.user._id; - Message.find({master_id:user_id},[],{sort:[['create_at','desc']]},function(err,docs){ - if(err) return next(err); - for(var i=0; i=0; i--){ - if(replies[i].reply_id){ - replies2.push(replies[i]); - replies.splice(i,1); - } - } - for(var j=0; j 0) { + reply.content = content; + reply.update_at = new Date(); + reply.save(function (err) { + if (err) { + return next(err); + } + res.redirect('/topic/' + reply.topic_id + '#' + reply._id); + }); + } else { + return res.renderError('回复的字数太少。', 400); } - return cb(err, replies); + } else { + return res.renderError('对不起,你不能编辑此回复。', 403); } - proxy.after('reply_find', replies.length, done); - for(var i=0; i 0) { + ep.emit('prop_err', '用户名或邮箱已被使用。'); return; } - User.find({'$or':[{'loginname':loginname},{'email':email}]},function(err,users){ - if(err) return next(err); - if(users.length > 0){ - res.render('sign/signup', {error:'用户名或邮箱已被使用。',name:name,email:email}); - return; - } - - // md5 the pass - pass = md5(pass); - // create gavatar - var avatar_url = 'http://www.gravatar.com/avatar/' + md5(email) + '?size=48'; - - var user = new User(); - user.name = name; - user.loginname = loginname; - user.pass = pass; - user.email = email; - user.avatar = avatar_url; - user.active = false; - user.save(function(err){ + tools.bhash(pass, ep.done(function (passhash) { + // create gravatar + var avatarUrl = User.makeGravatar(email); + User.newAndSave(loginname, loginname, passhash, email, avatarUrl, false, function (err) { if (err) { return next(err); } - mail_ctrl.send_active_mail(email,md5(email + config.session_secret), name,email,function(err,success){ - if(success){ - res.render('sign/signup', {success:'欢迎加入 ' + config.name + '!我们已给您的注册邮箱发送了一封邮件,请点击里面的链接来激活您的帐号。'}); - return; - } + // 发送激活邮件 + mail.sendActiveMail(email, utility.md5(email + passhash + config.session_secret), loginname); + res.render('sign/signup', { + success: '欢迎加入 ' + config.name + '!我们已给您的注册邮箱发送了一封邮件,请点击里面的链接来激活您的帐号。' }); }); - }); - } + + })); + }); }; /** * Show user login page. - * + * * @param {HttpRequest} req * @param {HttpResponse} res */ -exports.showLogin = function(req, res) { +exports.showLogin = function (req, res) { req.session._loginReferer = req.headers.referer; res.render('sign/signin'); }; + /** * define some page when login just jump to the home page * @type {Array} @@ -112,234 +98,194 @@ var notJump = [ '/signup', //regist page '/search_pass' //serch pass page ]; + /** * Handle user login. - * - * @param {HttpRequest} req - * @param {HttpResponse} res - * @param {Function} next + * + * @param {HttpRequest} req + * @param {HttpResponse} res + * @param {Function} next */ -exports.login = function(req, res, next) { - var loginname = sanitize(req.body.name).trim().toLowerCase(); - var pass = sanitize(req.body.pass).trim(); - +exports.login = function (req, res, next) { + var loginname = validator.trim(req.body.name).toLowerCase(); + var pass = validator.trim(req.body.pass); + var ep = new eventproxy(); + + ep.fail(next); + if (!loginname || !pass) { + res.status(422); return res.render('sign/signin', { error: '信息不完整。' }); } - User.findOne({ 'loginname': loginname }, function(err, user) { - if (err) return next(err); - if (!user) { - return res.render('sign/signin', { error:'这个用户不存在。' }); - } - pass = md5(pass); - if (pass !== user.pass) { - return res.render('sign/signin', { error:'密码错误。' }); + var getUser; + if (loginname.indexOf('@') !== -1) { + getUser = User.getUserByMail; + } else { + getUser = User.getUserByLoginName; + } + + ep.on('login_error', function (login_error) { + res.status(403); + res.render('sign/signin', { error: '用户名或密码错误' }); + }); + + getUser(loginname, function (err, user) { + if (err) { + return next(err); } - if (!user.active) { - res.render('sign/signin', { error:'此帐号还没有被激活。' }); - return; + if (!user) { + return ep.emit('login_error'); } - // store session cookie - gen_session(user, res); - //check at some page just jump to home page - var refer = req.session._loginReferer || 'home'; - for (var i=0, len=notJump.length; i!=len; ++i) { - if (refer.indexOf(notJump[i]) >= 0) { - refer = 'home'; - break; + var passhash = user.pass; + tools.bcompare(pass, passhash, ep.done(function (bool) { + if (!bool) { + return ep.emit('login_error'); } - } - res.redirect(refer); + if (!user.active) { + // 重新发送激活邮件 + mail.sendActiveMail(user.email, utility.md5(user.email + passhash + config.session_secret), user.loginname); + res.status(403); + return res.render('sign/signin', { error: '此帐号还没有被激活,激活链接已发送到 ' + user.email + ' 邮箱,请查收。' }); + } + // store session cookie + authMiddleWare.gen_session(user, res); + //check at some page just jump to home page + var refer = req.session._loginReferer || '/'; + for (var i = 0, len = notJump.length; i !== len; ++i) { + if (refer.indexOf(notJump[i]) >= 0) { + refer = '/'; + break; + } + } + res.redirect(refer); + })); }); }; // sign out -exports.signout = function(req, res, next) { +exports.signout = function (req, res, next) { req.session.destroy(); res.clearCookie(config.auth_cookie_name, { path: '/' }); - res.redirect(req.headers.referer || 'home'); + res.redirect('/'); }; -exports.active_account = function(req,res,next) { - var key = req.query.key; - var name = req.query.name; - var email = req.query.email; +exports.activeAccount = function (req, res, next) { + var key = validator.trim(req.query.key); + var name = validator.trim(req.query.name); - User.findOne({name:name},function(err,user){ - if(!user || md5(email+config.session_secret) != key){ - res.render('notify/notify',{error: '信息有误,帐号无法被激活。'}); - return; + User.getUserByLoginName(name, function (err, user) { + if (err) { + return next(err); } - if(user.active){ - res.render('notify/notify',{error: '帐号已经是激活状态。'}); - return; + if (!user) { + return next(new Error('[ACTIVE_ACCOUNT] no such user: ' + name)); + } + var passhash = user.pass; + if (!user || utility.md5(user.email + passhash + config.session_secret) !== key) { + return res.render('notify/notify', {error: '信息有误,帐号无法被激活。'}); + } + if (user.active) { + return res.render('notify/notify', {error: '帐号已经是激活状态。'}); } user.active = true; - user.save(function(err){ - res.render('notify/notify',{success: '帐号已被激活,请登录'}); - }); + user.save(function (err) { + if (err) { + return next(err); + } + res.render('notify/notify', {success: '帐号已被激活,请登录'}); + }); }); -} +}; + +exports.showSearchPass = function (req, res) { + res.render('sign/search_pass'); +}; -exports.search_pass = function(req,res,next){ - var method = req.method.toLowerCase(); - if(method == 'get'){ - res.render('sign/search_pass'); +exports.updateSearchPass = function (req, res, next) { + var email = validator.trim(req.body.email).toLowerCase(); + if (!validator.isEmail(email)) { + return res.render('sign/search_pass', {error: '邮箱不合法', email: email}); } - if(method == 'post'){ - var email = req.body.email; - email = email.toLowerCase(); - try{ - check(email, '不正确的电子邮箱。').isEmail(); - }catch(e){ - res.render('sign/search_pass', {error:e.message,email:email}); + // 动态生成retrive_key和timestamp到users collection,之后重置密码进行验证 + var retrieveKey = uuid.v4(); + var retrieveTime = new Date().getTime(); + + User.getUserByMail(email, function (err, user) { + if (!user) { + res.render('sign/search_pass', {error: '没有这个电子邮箱。', email: email}); return; } - - // User.findOne({email:email},function(err,user){ - //动态生成retrive_key和timestamp到users collection,之后重置密码进行验证 - var retrieveKey = randomString(15); - var retrieveTime = new Date().getTime(); - User.findOne({email : email}, function(err, user) { - if(!user) { - res.render('sign/search_pass', {error:'没有这个电子邮箱。',email:email}); - return; - } - user.retrieve_key = retrieveKey; - user.retrieve_time = retrieveTime; - user.save(function(err) { - if(err) { - return next(err); - } - mail_ctrl.send_reset_pass_mail(email, retrieveKey, user.name, function(err,success) { - res.render('notify/notify',{success: '我们已给您填写的电子邮箱发送了一封邮件,请在24小时内点击里面的链接来重置密码。'}); - }); - }); + user.retrieve_key = retrieveKey; + user.retrieve_time = retrieveTime; + user.save(function (err) { + if (err) { + return next(err); + } + // 发送重置密码邮件 + mail.sendResetPassMail(email, retrieveKey, user.loginname); + res.render('notify/notify', {success: '我们已给您填写的电子邮箱发送了一封邮件,请在24小时内点击里面的链接来重置密码。'}); }); - } -} + }); +}; + /** * reset password * 'get' to show the page, 'post' to reset password * after reset password, retrieve_key&time will be destroy - * @param {http.req} req - * @param {http.res} res - * @param {Function} next + * @param {http.req} req + * @param {http.res} res + * @param {Function} next */ -exports.reset_pass = function(req,res,next) { - var method = req.method.toLowerCase(); - if(method === 'get') { - var key = req.query.key; - var name = req.query.name; - User.findOne({name:name, retrieve_key:key},function(err,user) { - if(!user) { - return res.render('notify/notify',{error: '信息有误,密码无法重置。'}); - } - var now = new Date().getTime(); - var oneDay = 1000 * 60 * 60 * 24; - if(!user.retrieve_time || now - user.retrieve_time > oneDay) { - return res.render('notify/notify', {error : '该链接已过期,请重新申请。'}); - } - return res.render('sign/reset', {name : name, key : key}); - }); - } else { - var psw = req.body.psw || ''; - var repsw = req.body.repsw || ''; - var key = req.body.key || ''; - var name = req.body.name || ''; - if(psw !== repsw) { - return res.render('sign/reset', {name : name, key : key, error : '两次密码输入不一致。'}); +exports.resetPass = function (req, res, next) { + var key = validator.trim(req.query.key || ''); + var name = validator.trim(req.query.name || ''); + + User.getUserByNameAndKey(name, key, function (err, user) { + if (!user) { + res.status(403); + return res.render('notify/notify', {error: '信息有误,密码无法重置。'}); } - User.findOne({name:name, retrieve_key: key}, function(err, user) { - if(!user) { - return res.render('notify/notify', {error : '错误的激活链接'}); - } - user.pass = md5(psw); - user.retrieve_key = null; - user.retrieve_time = null; - user.active = true; // 用户激活 - user.save(function(err) { - if(err) { - return next(err); - } - return res.render('notify/notify', {success: '你的密码已重置。'}); - }) - }) - } -} + var now = new Date().getTime(); + var oneDay = 1000 * 60 * 60 * 24; + if (!user.retrieve_time || now - user.retrieve_time > oneDay) { + res.status(403); + return res.render('notify/notify', {error: '该链接已过期,请重新申请。'}); + } + return res.render('sign/reset', {name: name, key: key}); + }); +}; -// auth_user middleware -exports.auth_user = function(req,res,next){ - if(req.session.user){ - if(config.admins[req.session.user.name]){ - req.session.user.is_admin = true; +exports.updatePass = function (req, res, next) { + var psw = validator.trim(req.body.psw) || ''; + var repsw = validator.trim(req.body.repsw) || ''; + var key = validator.trim(req.body.key) || ''; + var name = validator.trim(req.body.name) || ''; + + var ep = new eventproxy(); + ep.fail(next); + + if (psw !== repsw) { + return res.render('sign/reset', {name: name, key: key, error: '两次密码输入不一致。'}); + } + User.getUserByNameAndKey(name, key, ep.done(function (user) { + if (!user) { + return res.render('notify/notify', {error: '错误的激活链接'}); } - message_ctrl.get_messages_count(req.session.user._id,function(err,count){ - if(err) return next(err); - req.session.user.messages_count = count; - res.local('current_user',req.session.user); - return next(); - }); - }else{ - var cookie = req.cookies[config.auth_cookie_name]; - if(!cookie) return next(); + tools.bhash(psw, ep.done(function (passhash) { + user.pass = passhash; + user.retrieve_key = null; + user.retrieve_time = null; + user.active = true; // 用户激活 - var auth_token = decrypt(cookie, config.session_secret); - var auth = auth_token.split('\t'); - var user_id = auth[0]; - User.findOne({_id:user_id},function(err,user){ - if(err) return next(err); - if(user){ - if(config.admins[user.name]){ - user.is_admin = true; + user.save(function (err) { + if (err) { + return next(err); } - message_ctrl.get_messages_count(user._id,function(err,count){ - if(err) return next(err); - user.messages_count = count; - req.session.user = user; - res.local('current_user',req.session.user); - return next(); - }); - }else{ - return next(); - } - }); - } + return res.render('notify/notify', {success: '你的密码已重置。'}); + }); + })); + })); }; -// private -function gen_session(user,res) { - var auth_token = encrypt(user._id + '\t'+user.name + '\t' + user.pass +'\t' + user.email, config.session_secret); - res.cookie(config.auth_cookie_name, auth_token, {path: '/',maxAge: 1000*60*60*24*30}); //cookie 有效期30天 -} -function encrypt(str,secret) { - var cipher = crypto.createCipher('aes192', secret); - var enc = cipher.update(str,'utf8','hex'); - enc += cipher.final('hex'); - return enc; -} -function decrypt(str,secret) { - var decipher = crypto.createDecipher('aes192', secret); - var dec = decipher.update(str,'hex','utf8'); - dec += decipher.final('utf8'); - return dec; -} -function md5(str) { - var md5sum = crypto.createHash('md5'); - md5sum.update(str); - str = md5sum.digest('hex'); - return str; -} -function randomString(size) { - size = size || 6; - var code_string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - var max_num = code_string.length + 1; - var new_pass = ''; - while(size>0){ - new_pass += code_string.charAt(Math.floor(Math.random()* max_num)); - size--; - } - return new_pass; -} diff --git a/controllers/site.js b/controllers/site.js index 15b05bdbff..4b7b32138d 100644 --- a/controllers/site.js +++ b/controllers/site.js @@ -9,105 +9,145 @@ * Module dependencies. */ -var tag_ctrl = require('./tag'); -var user_ctrl = require('./user'); -var topic_ctrl = require('./topic'); -var config = require('../config').config; -var EventProxy = require('eventproxy').EventProxy; - +var User = require('../proxy').User; +var Topic = require('../proxy').Topic; +var config = require('../config'); +var eventproxy = require('eventproxy'); +var cache = require('../common/cache'); +var xmlbuilder = require('xmlbuilder'); +var renderHelper = require('../common/render_helper'); +var _ = require('lodash'); +var moment = require('moment'); exports.index = function (req, res, next) { var page = parseInt(req.query.page, 10) || 1; - var keyword = req.query.q || ''; // in-site search - if (Array.isArray(keyword)) { - keyword = keyword.join(' '); + page = page > 0 ? page : 1; + var tab = req.query.tab || 'all'; + + var proxy = new eventproxy(); + proxy.fail(next); + + // 取主题 + var query = {}; + if (!tab || tab === 'all') { + query.tab = {$nin: ['job', 'dev']} + } else { + if (tab === 'good') { + query.good = true; + } else { + query.tab = tab; + } + } + if (!query.good) { + query.create_at = {$gte: moment().subtract(1, 'years').toDate()} } - keyword = keyword.trim(); - var limit = config.list_topic_count; - var render = function (tags, topics, hot_topics, stars, tops, no_reply_topics, pages) { - var all_tags = tags.slice(0); + var limit = config.list_topic_count; + var options = { skip: (page - 1) * limit, limit: limit, sort: '-top -last_reply_at'}; - // 计算最热标签 - tags.sort(function (tag_a, tag_b) { - return tag_b.topic_count - tag_a.topic_count; - }); - var hot_tags = tags.slice(0, 5); + Topic.getTopicsByQuery(query, options, proxy.done('topics', function (topics) { + return topics; + })); - // 计算最新标签 - tags.sort(function (tag_a, tag_b) { - return tag_b.create_at - tag_a.create_at; - }); - var recent_tags = tags.slice(0, 5); - res.render('index', { - tags: all_tags, - topics: topics, - current_page: page, - list_topic_count: limit, - recent_tags: recent_tags, - hot_topics: hot_topics, - stars: stars, - tops: tops, - no_reply_topics: no_reply_topics, - pages: pages, - keyword: keyword - }); - }; - - var proxy = EventProxy.create('tags', 'topics', 'hot_topics', 'stars', 'tops', 'no_reply_topics', 'pages', render); - proxy.once('error', function (err) { - proxy.unbind(); - next(err); - }); - tag_ctrl.get_all_tags(function (err, tags) { - if (err) { - return proxy.emit('error', err); + // 取排行榜上的用户 + cache.get('tops', proxy.done(function (tops) { + if (tops) { + proxy.emit('tops', tops); + } else { + User.getUsersByQuery( + {is_block: false}, + { limit: 10, sort: '-score'}, + proxy.done('tops', function (tops) { + cache.set('tops', tops, 60 * 1); + return tops; + }) + ); } - proxy.emit('tags', tags); - }); + })); + // END 取排行榜上的用户 - var options = { skip: (page - 1) * limit, limit: limit, sort: [ ['top', 'desc' ], [ 'last_reply_at', 'desc' ] ] }; - var query = {}; - if (keyword) { - keyword = keyword.replace(/[\*\^\&\(\)\[\]\+\?\\]/g, ''); - query.title = new RegExp(keyword, 'i'); - } - topic_ctrl.get_topics_by_query(query, options, function (err, topics) { - if (err) { - return proxy.emit('error', err); - } - proxy.emit('topics', topics); - }); - topic_ctrl.get_topics_by_query({}, { limit: 5, sort: [ [ 'visit_count', 'desc' ] ] }, function (err, hot_topics) { - if (err) { - return proxy.emit('error', err); + // 取0回复的主题 + cache.get('no_reply_topics', proxy.done(function (no_reply_topics) { + if (no_reply_topics) { + proxy.emit('no_reply_topics', no_reply_topics); + } else { + Topic.getTopicsByQuery( + { reply_count: 0, tab: {$nin: ['job', 'dev']}}, + { limit: 5, sort: '-create_at'}, + proxy.done('no_reply_topics', function (no_reply_topics) { + cache.set('no_reply_topics', no_reply_topics, 60 * 1); + return no_reply_topics; + })); } - proxy.emit('hot_topics', hot_topics); - }); - user_ctrl.get_users_by_query({ is_star: true }, { limit: 5 }, function (err, users) { - if (err) { - return proxy.emit('error', err); - } - proxy.emit('stars', users); - }); - user_ctrl.get_users_by_query({}, { limit: 10, sort: [ [ 'score', 'desc' ] ] }, function (err, tops) { - if (err) { - return proxy.emit('error', err); - } - proxy.emit('tops', tops); - }); - topic_ctrl.get_topics_by_query({ reply_count: 0 }, { limit: 5, sort: [ [ 'create_at', 'desc' ] ] }, - function (err, no_reply_topics) { - if (err) { - return proxy.emit('error', err); + })); + // END 取0回复的主题 + + // 取分页数据 + var pagesCacheKey = JSON.stringify(query) + 'pages'; + cache.get(pagesCacheKey, proxy.done(function (pages) { + if (pages) { + proxy.emit('pages', pages); + } else { + Topic.getCountByQuery(query, proxy.done(function (all_topics_count) { + var pages = Math.ceil(all_topics_count / limit); + cache.set(pagesCacheKey, pages, 60 * 1); + proxy.emit('pages', pages); + })); } - proxy.emit('no_reply_topics', no_reply_topics); + })); + // END 取分页数据 + + var tabName = renderHelper.tabName(tab); + proxy.all('topics', 'tops', 'no_reply_topics', 'pages', + function (topics, tops, no_reply_topics, pages) { + res.render('index', { + topics: topics, + current_page: page, + list_topic_count: limit, + tops: tops, + no_reply_topics: no_reply_topics, + pages: pages, + tabs: config.tabs, + tab: tab, + pageTitle: tabName && (tabName + '版块'), + }); + }); +}; + +exports.sitemap = function (req, res, next) { + var urlset = xmlbuilder.create('urlset', + {version: '1.0', encoding: 'UTF-8'}); + urlset.att('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9'); + + var ep = new eventproxy(); + ep.fail(next); + + ep.all('sitemap', function (sitemap) { + res.type('xml'); + res.send(sitemap); }); - topic_ctrl.get_count_by_query(query, function (err, all_topics_count) { - if (err) { - return proxy.emit('error', err); + + cache.get('sitemap', ep.done(function (sitemapData) { + if (sitemapData) { + ep.emit('sitemap', sitemapData); + } else { + Topic.getLimit5w(function (err, topics) { + if (err) { + return next(err); + } + topics.forEach(function (topic) { + urlset.ele('url').ele('loc', 'http://cnodejs.org/topic/' + topic._id); + }); + + var sitemapData = urlset.end(); + // 缓存一天 + cache.set('sitemap', sitemapData, 3600 * 24); + ep.emit('sitemap', sitemapData); + }); } - var pages = Math.ceil(all_topics_count / limit); - proxy.emit('pages', pages); - }); + })); +}; + +exports.appDownload = function (req, res, next) { + res.redirect('https://github.com/soliury/noder-react-native/blob/master/README.md') }; diff --git a/controllers/static.js b/controllers/static.js index f432878d50..b1b3f6fe67 100644 --- a/controllers/static.js +++ b/controllers/static.js @@ -1,8 +1,37 @@ +var multiline = require('multiline'); // static page -exports.about = function(req,res,next){ - res.render('static/about'); +// About +exports.about = function (req, res, next) { + res.render('static/about', { + pageTitle: '关于我们' + }); }; -exports.faq = function(req,res,next){ +// FAQ +exports.faq = function (req, res, next) { res.render('static/faq'); }; + +exports.getstart = function (req, res) { + res.render('static/getstart', { + pageTitle: 'Node.js 新手入门' + }); +}; + + +exports.robots = function (req, res, next) { + res.type('text/plain'); + res.send(multiline(function () {; +/* +# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-Agent: * +# Disallow: / +*/ + })); +}; + +exports.api = function (req, res, next) { + res.render('static/api'); +}; diff --git a/controllers/status.js b/controllers/status.js deleted file mode 100644 index 361c7ebd0e..0000000000 --- a/controllers/status.js +++ /dev/null @@ -1,4 +0,0 @@ -// 用于网络监控 -exports.status = function (req, res, next) { - res.json({status: 'success', now: new Date()}); -}; diff --git a/controllers/tag.js b/controllers/tag.js deleted file mode 100644 index d7559eb6e3..0000000000 --- a/controllers/tag.js +++ /dev/null @@ -1,319 +0,0 @@ -var models = require('../models'), - Tag = models.Tag, - TopicTag = models.TopicTag, - TagCollect = models.TagCollect; - -var check = require('validator').check, - sanitize = require('validator').sanitize; - -var user_ctrl = require('./user'); -var topic_ctrl = require('./topic'); -var config = require('../config').config; -var EventProxy = require('eventproxy').EventProxy; - -exports.list_topic = function(req,res,next){ - var tag_name = req.params.name; - var page = Number(req.query.page) || 1; - var limit = config.list_topic_count; - - Tag.findOne({name:tag_name},function(err,tag){ - if(err) return next(err); - if(tag){ - var done = function(topic_ids,collection,hot_topics,no_reply_topics,pages){ - var query = {'_id':{'$in':topic_ids}}; - var opt = {skip:(page-1)*limit, limit:limit, sort:[['create_at','desc']]}; - - topic_ctrl.get_topics_by_query(query,opt,function(err,topics){ - for(var i=0; i 0){ - res.render('notify/notify',{error:'这个标签已存在。'}); - return; - } - - var tag = new Tag(); - tag.name = name; - tag.order = order; - tag.description = description; - tag.save(function(err){ - if(err) return next(err); - res.redirect('/tags/edit'); - }); - }); -}; - -exports.edit = function(req,res,next){ - if(!req.session.user){ - res.render('notify/notify',{error:'你还没有登录。'}); - return; - } - if(!req.session.user.is_admin){ - res.render('notify/notify',{error:'管理员才能编辑标签。'}); - return; - } - var tag_name = req.params.name; - Tag.findOne({name:tag_name},function(err,tag){ - if(err) return next(err); - if(tag){ - var method = req.method.toLowerCase(); - if(method == 'get'){ - get_all_tags(function(err,tags){ - if(err) return next(err); - res.render('tag/edit',{tag:tag,tags:tags}); - return; - }); - } - if(method == 'post'){ - var name = sanitize(req.body.name).trim(); - name = sanitize(name).xss(); - var order = req.body.order; - var description = sanitize(req.body.description).trim(); - description = sanitize(description).xss(); - if(name == ''){ - res.render('notify/notify', {error:'信息不完整。'}); - return; - } - tag.name = name; - tag.order = order; - tag.description = description; - tag.save(function(err){ - if(err) return next(err); - res.redirect('/tags/edit'); - }) - } - }else{ - res.render('notify/notify',{error:'没有这个标签。'}); - return; - } - }); -} - -exports.delete = function(req,res,next){ - if(!req.session.user){ - res.render('notify/notify',{error:'你还没有登录。'}); - return; - } - if(!req.session.user.is_admin){ - res.render('notify/notify',{error:'管理员才能编辑标签。'}); - return; - } - var tag_name = req.params.name; - Tag.findOne({name:tag_name},function(err,tag){ - if(err) return next(err); - if(tag){ - var proxy = new EventProxy(); - var done = function(){ - tag.remove(function(err){ - if(err) return next(err); - res.redirect('/'); - }); - } - proxy.assign('topic_tag_removed','tag_collect_removed',done); - TopicTag.remove({tag_id:tag._id},function(err){ - if(err) return next(err); - proxy.trigger('topic_tag_removed'); - }); - TagCollect.remove({tag_id:tag._id},function(err){ - if(err) return next(err); - proxy.trigger('tag_collect_removed') - }); - }else{ - res.render('notify/notify',{error:'没有这个标签。'}); - return; - } - }); -} - -exports.collect = function(req,res,next){ - if(!req.session || !req.session.user){ - res.send('fobidden!'); - return; - } - var tag_id = req.body.tag_id; - Tag.findOne({_id: tag_id},function(err,tag){ - if(err) return next(err); - if(!tag){ - res.json({status:'failed'}); - } - - TagCollect.findOne({user_id:req.session.user._id,tag_id:tag._id},function(err,doc){ - if(err) return next(err); - if(doc){ - res.json({status:'success'}); - return; - } - var tag_collect = new TagCollect(); - tag_collect.user_id = req.session.user._id; - tag_collect.tag_id = tag._id; - tag_collect.save(function(err){ - if(err) return next(err); - //用户更新collect_tag_count - user_ctrl.get_user_by_id(req.session.user._id,function(err,user){ - if(err) return next(err); - user.collect_tag_count += 1; - user.save(); - req.session.user.collect_tag_count += 1; - //标签更新collect_count - tag.collect_count += 1; - tag.save() - res.json({status:'success'}); - }); - }); - }); - }); -}; - -exports.de_collect = function(req,res,next){ - if(!req.session || !req.session.user){ - res.send('fobidden!'); - return; - } - var tag_id = req.body.tag_id; - Tag.findOne({_id: tag_id},function(err,tag){ - if(err) return next(err); - if(!tag){ - res.json({status:'failed'}); - } - TagCollect.remove({user_id:req.session.user._id,tag_id:tag._id},function(err){ - if(err) return next(err); - //用户更新collect_tag_count - user_ctrl.get_user_by_id(req.session.user._id,function(err,user){ - if(err) return next(err); - user.collect_tag_count -= 1; - user.save() - req.session.user.collect_tag_count -= 1; - tag.collect_count -= 1; - tag.save(); - res.json({status:'success'}); - }); - }); - }); -}; - -function get_all_tags(cb){ - Tag.find({},[],{sort:[['order','asc']]},function(err,tags){ - if(err) return cb(err,[]) - return cb(err,tags); - }); -}; -function get_tag_by_name(name,cb){ - Tag.findOne({name:name},function(err,tag){ - if(err) return cb(err,null); - return cb(err,tag); - }); -} -function get_tag_by_id(id,cb){ - Tag.findOne({_id:id},function(err,tag){ - if(err) return cb(err,null); - return cb(err,tag); - }); -} -function get_tags_by_ids(ids,cb){ - Tag.find({_id:{'$in':ids}},function(err,tags){ - if(err) return cb(err); - return cb(err,tags); - }); -} -function get_tags_by_query(query,opt,cb){ - Tag.find(query,[],opt,function(err,tags){ - if(err) return cb(err); - return cb(err,tags); - }); -} -exports.get_all_tags = get_all_tags; -exports.get_tag_by_name = get_tag_by_name; -exports.get_tag_by_id = get_tag_by_id; -exports.get_tags_by_ids = get_tags_by_ids; -exports.get_tags_by_query = get_tags_by_query; diff --git a/controllers/tools.js b/controllers/tools.js deleted file mode 100644 index 6ac9907b8c..0000000000 --- a/controllers/tools.js +++ /dev/null @@ -1,37 +0,0 @@ -var models = require('../models'), - User = models.User, - Topic = models.Topic, - Reply = models.Reply, - Relation = models.Relation, - Message = models.Message; - -var EventProxy = require('eventproxy').EventProxy; - -exports.run_site_tools = function(req,res,next){ - res.send('

The White Castle

'); -}; - -// exports.reset_data = function(req,res,next){ -// Topic.find({},function(err,topics){ -// for(var i=0; i 100) { + editError = '标题字数太多或太少。'; + } else if (!tab || allTabs.indexOf(tab) === -1) { + editError = '必须选择一个版块。'; + } else if (content === '') { + editError = '内容不可为空'; + } + // END 验证 + + if (editError) { + res.status(422); + return res.render('topic/edit', { + edit_error: editError, + title: title, + content: content, + tabs: config.tabs }); + } - //get author's relationship - if (!req.session.user || req.session.user._id) { - ep.emit('get_relation', null); - } else { - Relation.findOne({user_id:req.session.user._id, follow_id: topic.author_id},function(err, relation) { - if (err) return ep.emit('error', err); - ep.emit('get_relation', relation); - }); + Topic.newAndSave(title, content, tab, req.session.user._id, function (err, topic) { + if (err) { + return next(err); } - // get author other topics - var options = { limit: 5, sort: [ [ 'last_reply_at', 'desc' ] ]}; - var query = { author_id: topic.author_id, _id: { '$nin': [ topic._id ] } }; - get_topics_by_query(query, options, function(err,topics){ - if (err) return ep.emit('error', err); - ep.emit('other_topics', topics); - }); + var proxy = new EventProxy(); - // get no reply topics - var options2 = { limit:5, sort: [ ['create_at', 'desc'] ] }; - get_topics_by_query({ reply_count: 0 }, options2, function(err, topics) { - if (err) return ep.emit('error', err); - ep.emit('no_reply_topics', topics); + proxy.all('score_saved', function () { + res.redirect('/topic/' + topic._id); }); + proxy.fail(next); + User.getUserById(req.session.user._id, proxy.done(function (user) { + user.score += 5; + user.topic_count += 1; + user.save(); + req.session.user = user; + proxy.emit('score_saved'); + })); + + //发送at消息 + at.sendMessageToMentionUsers(content, topic._id, req.session.user._id); }); }; -exports.create = function(req,res,next){ - if(!req.session.user){ - res.render('notify/notify',{error:'未登入用户不能发布话题。'}); - return; - } +exports.showEdit = function (req, res, next) { + var topic_id = req.params.tid; - var method = req.method.toLowerCase(); - if(method == 'get'){ - tag_ctrl.get_all_tags(function(err,tags){ - if(err) return next(err); - res.render('topic/edit',{tags:tags}); + Topic.getTopicById(topic_id, function (err, topic, tags) { + if (!topic) { + res.render404('此话题不存在或已被删除。'); return; - }); - } - - if(method == 'post'){ - var title = sanitize(req.body.title).trim(); - title = sanitize(title).xss(); - var content = req.body.t_content; - var topic_tags=[]; - if(req.body.topic_tags != ''){ - topic_tags = req.body.topic_tags.split(','); - } - - if(title == ''){ - tag_ctrl.get_all_tags(function(err,tags){ - if(err) return next(err); - for(var i=0; i100){ - tag_ctrl.get_all_tags(function(err,tags){ - if(err) return next(err); - for(var i=0; i 100) { + editError = '标题字数太多或太少。'; + } else if (!tab) { + editError = '必须选择一个版块。'; } - if(topic.author_id == req.session.user._id || req.session.user.is_admin){ - tag_ctrl.get_all_tags(function(err,all_tags){ - if(err) return next(err); - for(var i=0; i'); - return; - } - var user_id = req.body.user_id; - get_user_by_id(user_id, function (err, user) { - if (err) { - return next(err); + return next(new Error('user is not exists')); } - user.is_star = !!user.is_star; + user.is_star = !user.is_star; user.save(function (err) { if (err) { return next(err); @@ -356,271 +188,207 @@ exports.toggle_star = function (req, res, next) { }); }; -exports.get_collect_tags = function (req, res, next) { - if (!req.session.user) { - res.redirect('home'); - return; - } - TagCollect.find({ user_id: req.session.user._id }, function (err, docs) { - if (err) { - return next(err); - } - var ids = []; - for (var i = 0; i < docs.length; i++) { - ids.push(docs[i].tag_id); - } - tag_ctrl.get_tags_by_ids(ids, function (err, tags) { - if (err) { - return next(err); - } - res.render('user/collect_tags', { tags: tags }); - }); - }); -}; - -exports.get_collect_topics = function (req, res, next) { - if (!req.session.user) { - res.redirect('home'); - return; - } - +exports.listCollectedTopics = function (req, res, next) { + var name = req.params.name; var page = Number(req.query.page) || 1; var limit = config.list_topic_count; - var render = function (topics, pages) { - res.render('user/collect_topics', { - topics: topics, - current_page: page, - pages: pages - }); - }; - - var proxy = new EventProxy(); - proxy.assign('topics', 'pages', render); - - TopicCollect.find({ user_id: req.session.user._id }, function (err, docs) { - if (err) { + User.getUserByLoginName(name, function (err, user) { + if (err || !user) { return next(err); } - var ids = []; - for (var i = 0; i < docs.length; i++) { - ids.push(docs[i].topic_id); - } - var query = { _id: { '$in': ids } }; - var opt = { - skip: (page - 1) * limit, - limit: limit, - sort: [ [ 'create_at', 'desc' ] ] + var pages = Math.ceil(user.collect_topic_count/limit); + var render = function (topics) { + res.render('user/collect_topics', { + topics: topics, + current_page: page, + pages: pages, + user: user + }); }; - topic_ctrl.get_topics_by_query(query, opt, function (err, topics) { - if (err) { - return next(err); - } - proxy.trigger('topics', topics); - }); - topic_ctrl.get_count_by_query(query, function (err, all_topics_count) { - if (err) { - return next(err); - } - var pages = Math.ceil(all_topics_count / limit); - proxy.trigger('pages', pages); - }); - }); -}; -exports.get_followings = function (req, res, next) { - if (!req.session.user) { - res.redirect('home'); - return; - } - Relation.find({user_id: req.session.user._id}, function (err, docs) { - if (err) { - return next(err); - } - var ids = []; - for (var i = 0; i < docs.length; i++) { - ids.push(docs[i].follow_id); - } - get_users_by_ids(ids, function (err, users) { - if (err) { - return next(err); - } - res.render('user/followings', {users: users}); - }); - }); -}; + var proxy = EventProxy.create('topics', render); + proxy.fail(next); -exports.get_followers = function (req, res, next) { - if (!req.session.user) { - res.redirect('home'); - return; - } - Relation.find({follow_id: req.session.user._id}, function (err, docs) { - if (err) { - return next(err); - } - var ids = []; - for (var i = 0; i < docs.length; i++) { - ids.push(docs[i].user_id); - } - get_users_by_ids(ids, function (err, users) { - if (err) { - return next(err); - } - res.render('user/followers', {users: users}); - }); - }); + var opt = { + skip: (page - 1) * limit, + limit: limit, + }; + + TopicCollect.getTopicCollectsByUserId(user._id, opt, proxy.done(function (docs) { + var ids = docs.map(function (doc) { + return String(doc.topic_id) + }) + var query = { _id: { '$in': ids } }; + + Topic.getTopicsByQuery(query, {}, proxy.done('topics', function (topics) { + topics = _.sortBy(topics, function (topic) { + return ids.indexOf(String(topic._id)) + }) + return topics + })); + })); + }); }; exports.top100 = function (req, res, next) { - var opt = {limit: 100, sort: [['score', 'desc']]}; - get_users_by_query({}, opt, function (err, tops) { + var opt = {limit: 100, sort: '-score'}; + User.getUsersByQuery({is_block: false}, opt, function (err, tops) { if (err) { return next(err); } - res.render('user/top100', {users: tops}); + res.render('user/top100', { + users: tops, + pageTitle: 'top100', + }); }); }; -exports.list_topics = function (req, res, next) { +exports.listTopics = function (req, res, next) { var user_name = req.params.name; var page = Number(req.query.page) || 1; var limit = config.list_topic_count; - get_user_by_name(user_name, function (err, user) { + User.getUserByLoginName(user_name, function (err, user) { if (!user) { - res.render('notify/notify', {error: '这个用户不存在。'}); + res.render404('这个用户不存在。'); return; } - - var render = function (topics, relation, pages) { - user.friendly_create_at = Util.format_date(user.create_at, true); + + var render = function (topics, pages) { res.render('user/topics', { user: user, topics: topics, - relation: relation, current_page: page, pages: pages }); }; var proxy = new EventProxy(); - proxy.assign('topics', 'relation', 'pages', render); + proxy.assign('topics', 'pages', render); + proxy.fail(next); var query = {'author_id': user._id}; - var opt = {skip: (page - 1) * limit, limit: limit, sort: [['create_at', 'desc']]}; - topic_ctrl.get_topics_by_query(query, opt, function (err, topics) { - if (err) { - return next(err); - } - proxy.trigger('topics', topics); - }); + var opt = {skip: (page - 1) * limit, limit: limit, sort: '-create_at'}; + Topic.getTopicsByQuery(query, opt, proxy.done('topics')); - if (!req.session.user) { - proxy.trigger('relation', null); - } else { - Relation.findOne({user_id: req.session.user._id, follow_id: user._id}, function (err, doc) { - if (err) { - return next(err); - } - proxy.trigger('relation', doc); - }); - } - - topic_ctrl.get_count_by_query(query, function (err, all_topics_count) { - if (err) { - return next(err); - } + Topic.getCountByQuery(query, proxy.done(function (all_topics_count) { var pages = Math.ceil(all_topics_count / limit); - proxy.trigger('pages', pages); - }); + proxy.emit('pages', pages); + })); }); }; -exports.list_replies = function (req, res, next) { +exports.listReplies = function (req, res, next) { var user_name = req.params.name; var page = Number(req.query.page) || 1; - var limit = config.list_topic_count; + var limit = 50; - get_user_by_name(user_name, function (err, user) { + User.getUserByLoginName(user_name, function (err, user) { if (!user) { - res.render('notify/notify', {error: '这个用户不存在。'}); + res.render404('这个用户不存在。'); return; } - - var render = function (topics, relation, pages) { - user.friendly_create_at = Util.format_date(user.create_at, true); + + var render = function (topics, pages) { res.render('user/replies', { user: user, topics: topics, - relation: relation, current_page: page, pages: pages }); }; var proxy = new EventProxy(); - proxy.assign('topics', 'relation', 'pages', render); + proxy.assign('topics', 'pages', render); + proxy.fail(next); + + var opt = {skip: (page - 1) * limit, limit: limit, sort: '-create_at'}; + Reply.getRepliesByAuthorId(user._id, opt, proxy.done(function (replies) { + // 获取所有有评论的主题 + var topic_ids = replies.map(function (reply) { + return reply.topic_id.toString(); + }); + topic_ids = _.uniq(topic_ids); - Reply.find({author_id: user._id}, function (err, replies) { - if (err) { - return next(err); - } - var topic_ids = []; - for (var i = 0; i < replies.length; i++) { - if (topic_ids.indexOf(replies[i].topic_id.toString()) < 0) { - topic_ids.push(replies[i].topic_id); - } - } var query = {'_id': {'$in': topic_ids}}; - var opt = {skip: (page - 1) * limit, limit: limit, sort: [['create_at', 'desc']]}; - topic_ctrl.get_topics_by_query(query, opt, function (err, topics) { - if (err) { - return next(err); - } - proxy.trigger('topics', topics); - }); + Topic.getTopicsByQuery(query, {}, proxy.done('topics', function (topics) { + topics = _.sortBy(topics, function (topic) { + return topic_ids.indexOf(topic._id.toString()) + }) + return topics; + })); + })); + + Reply.getCountByAuthorId(user._id, proxy.done('pages', function (count) { + var pages = Math.ceil(count / limit); + return pages; + })); + }); +}; - topic_ctrl.get_count_by_query(query, function (err, all_topics_count) { - if (err) { - return next(err); - } - var pages = Math.ceil(all_topics_count / limit); - proxy.trigger('pages', pages); - }); - }); +exports.block = function (req, res, next) { + var loginname = req.params.name; + var action = req.body.action; - if (!req.session.user) { - proxy.trigger('relation', null); - } else { - Relation.findOne({user_id: req.session.user._id, follow_id: user._id}, function (err, doc) { - if (err) { - return next(err); - } - proxy.trigger('relation', doc); - }); + var ep = EventProxy.create(); + ep.fail(next); + + User.getUserByLoginName(loginname, ep.done(function (user) { + if (!user) { + return next(new Error('user is not exists')); } - }); + if (action === 'set_block') { + ep.all('block_user', + function (user) { + res.json({status: 'success'}); + }); + user.is_block = true; + user.save(ep.done('block_user')); + + } else if (action === 'cancel_block') { + user.is_block = false; + user.save(ep.done(function () { + + res.json({status: 'success'}); + })); + } + })); +}; + +exports.deleteAll = function (req, res, next) { + var loginname = req.params.name; + + var ep = EventProxy.create(); + ep.fail(next); + + User.getUserByLoginName(loginname, ep.done(function (user) { + if (!user) { + return next(new Error('user is not exists')); + } + ep.all('del_topics', 'del_replys', 'del_ups', + function () { + res.json({status: 'success'}); + }); + // 删除主题 + TopicModel.updateMany({author_id: user._id}, {$set: {deleted: true}}, ep.done('del_topics')); + // 删除评论 + ReplyModel.updateMany({author_id: user._id}, {$set: {deleted: true}}, ep.done('del_replys')); + // 点赞数也全部干掉 + ReplyModel.updateMany({}, {$pull: {'ups': user._id}}, ep.done('del_ups')); + })); }; -function get_user_by_id(id, cb) { - User.findOne({_id: id}, cb); -} -function get_user_by_name(name, cb) { - User.findOne({name: name}, cb); -} -function get_user_by_loginname(name, cb) { - User.findOne({loginname: name}, cb); -} - -function get_users_by_ids(ids, cb) { - User.find({'_id': {'$in': ids}}, cb); -} -function get_users_by_query(query, opt, cb) { - User.find(query, [], opt, cb); -} -exports.get_user_by_id = get_user_by_id; -exports.get_user_by_name = get_user_by_name; -exports.get_user_by_loginname = get_user_by_loginname; -exports.get_users_by_ids = get_users_by_ids; -exports.get_users_by_query = get_users_by_query; +exports.refreshToken = function (req, res, next) { + var user_id = req.session.user._id; + + var ep = EventProxy.create(); + ep.fail(next); + + User.getUserById(user_id, ep.done(function (user) { + user.accessToken = uuid.v4(); + user.save(ep.done(function () { + res.json({status: 'success', accessToken: user.accessToken}); + })); + })); +}; \ No newline at end of file diff --git a/libs/util.js b/libs/util.js deleted file mode 100644 index b08e7f3b09..0000000000 --- a/libs/util.js +++ /dev/null @@ -1,33 +0,0 @@ -exports.format_date = function (date, friendly) { - var year = date.getFullYear(); - var month = date.getMonth() + 1; - var day = date.getDate(); - var hour = date.getHours(); - var minute = date.getMinutes(); - var second = date.getSeconds(); - - if (friendly) { - var now = new Date(); - var mseconds = -(date.getTime() - now.getTime()); - var time_std = [ 1000, 60 * 1000, 60 * 60 * 1000, 24 * 60 * 60 * 1000 ]; - if (mseconds < time_std[3]) { - if (mseconds > 0 && mseconds < time_std[1]) { - return Math.floor(mseconds / time_std[0]).toString() + ' 秒前'; - } - if (mseconds > time_std[1] && mseconds < time_std[2]) { - return Math.floor(mseconds / time_std[1]).toString() + ' 分钟前'; - } - if (mseconds > time_std[2]) { - return Math.floor(mseconds / time_std[2]).toString() + ' 小时前'; - } - } - } - - //month = ((month < 10) ? '0' : '') + month; - //day = ((day < 10) ? '0' : '') + day; - hour = ((hour < 10) ? '0' : '') + hour; - minute = ((minute < 10) ? '0' : '') + minute; - second = ((second < 10) ? '0': '') + second; - - return year + '-' + month + '-' + day + ' ' + hour + ':' + minute; -}; diff --git a/public/libs/pagedown/README.txt b/logs/.gitkeep similarity index 100% rename from public/libs/pagedown/README.txt rename to logs/.gitkeep diff --git a/middlewares/auth.js b/middlewares/auth.js new file mode 100644 index 0000000000..3615066859 --- /dev/null +++ b/middlewares/auth.js @@ -0,0 +1,106 @@ +var mongoose = require('mongoose'); +var UserModel = mongoose.model('User'); +var Message = require('../proxy').Message; +var config = require('../config'); +var eventproxy = require('eventproxy'); +var UserProxy = require('../proxy').User; + +/** + * 需要管理员权限 + */ +exports.adminRequired = function (req, res, next) { + if (!req.session.user) { + return res.render('notify/notify', { error: '你还没有登录。' }); + } + + if (!req.session.user.is_admin) { + return res.render('notify/notify', { error: '需要管理员权限。' }); + } + + next(); +}; + +/** + * 需要登录 + */ +exports.userRequired = function (req, res, next) { + if (!req.session || !req.session.user || !req.session.user._id) { + return res.status(403).send('forbidden!'); + } + + next(); +}; + +exports.blockUser = function () { + return function (req, res, next) { + if (req.path === '/signout') { + return next(); + } + + if (req.session.user && req.session.user.is_block && req.method !== 'GET') { + return res.status(403).send('您已被管理员屏蔽了。有疑问请联系 @alsotang。'); + } + next(); + }; +}; + + +function gen_session(user, res) { + var auth_token = user._id + '$$$$'; // 以后可能会存储更多信息,用 $$$$ 来分隔 + var opts = { + path: '/', + maxAge: 1000 * 60 * 60 * 24 * 30, + signed: true, + httpOnly: true + }; + res.cookie(config.auth_cookie_name, auth_token, opts); //cookie 有效期30天 +} + +exports.gen_session = gen_session; + +// 验证用户是否登录 +exports.authUser = function (req, res, next) { + var ep = new eventproxy(); + ep.fail(next); + + // Ensure current_user always has defined. + res.locals.current_user = null; + + if (config.debug && req.cookies['mock_user']) { + var mockUser = JSON.parse(req.cookies['mock_user']); + req.session.user = new UserModel(mockUser); + if (mockUser.is_admin) { + req.session.user.is_admin = true; + } + return next(); + } + + ep.all('get_user', function (user) { + if (!user) { + return next(); + } + user = res.locals.current_user = req.session.user = new UserModel(user); + + if (config.admins.hasOwnProperty(user.loginname)) { + user.is_admin = true; + } + + Message.getMessagesCount(user._id, ep.done(function (count) { + user.messages_count = count; + next(); + })); + }); + + if (req.session.user) { + ep.emit('get_user', req.session.user); + } else { + var auth_token = req.signedCookies[config.auth_cookie_name]; + if (!auth_token) { + return next(); + } + + var auth = auth_token.split('$$$$'); + var user_id = auth[0]; + UserProxy.getUserById(user_id, ep.done('get_user')); + } +}; diff --git a/middlewares/conf.js b/middlewares/conf.js new file mode 100644 index 0000000000..3257170746 --- /dev/null +++ b/middlewares/conf.js @@ -0,0 +1,8 @@ +var config = require('../config'); + +exports.github = function (req, res, next) { + if (config.GITHUB_OAUTH.clientID === 'your GITHUB_CLIENT_ID') { + return res.send('call the admin to set github oauth.'); + } + next(); +}; diff --git a/middlewares/error_page.js b/middlewares/error_page.js new file mode 100644 index 0000000000..c92a4b4ded --- /dev/null +++ b/middlewares/error_page.js @@ -0,0 +1,16 @@ +// ErrorPage middleware +exports.errorPage = function (req, res, next) { + + res.render404 = function (error) { + return res.status(404).render('notify/notify', { error: error }); + }; + + res.renderError = function (error, statusCode) { + if (statusCode === undefined) { + statusCode = 400; + } + return res.status(statusCode).render('notify/notify', { error: error }); + }; + + next(); +}; diff --git a/middlewares/github_strategy.js b/middlewares/github_strategy.js new file mode 100644 index 0000000000..3ae932cb1e --- /dev/null +++ b/middlewares/github_strategy.js @@ -0,0 +1,4 @@ +module.exports = function (accessToken, refreshToken, profile, done) { + profile.accessToken = accessToken; + done(null, profile); +}; diff --git a/middlewares/limit.js b/middlewares/limit.js new file mode 100644 index 0000000000..8556ef2273 --- /dev/null +++ b/middlewares/limit.js @@ -0,0 +1,51 @@ +var config = require('../config'); +var cache = require('../common/cache'); +var moment = require('moment'); + +var SEPARATOR = '^_^@T_T'; + +var makePerDayLimiter = function (identityName, identityFn) { + return function (name, limitCount, options) { + /* + options.showJson = true 表示调用来自API并返回结构化数据;否则表示调用来自前段并渲染错误页面 + */ + return function (req, res, next) { + var identity = identityFn(req); + var YYYYMMDD = moment().format('YYYYMMDD'); + var key = YYYYMMDD + SEPARATOR + identityName + SEPARATOR + name + SEPARATOR + identity; + + cache.get(key, function (err, count) { + if (err) { + return next(err); + } + count = count || 0; + if (count < limitCount) { + count += 1; + cache.set(key, count, 60 * 60 * 24); + res.set('X-RateLimit-Limit', limitCount); + res.set('X-RateLimit-Remaining', limitCount - count); + next(); + } else { + res.status(403); + if (options.showJson) { + res.send({success: false, error_msg: '频率限制:当前操作每天可以进行 ' + limitCount + ' 次'}); + } else { + res.render('notify/notify', { error: '频率限制:当前操作每天可以进行 ' + limitCount + ' 次'}); + } + } + }); + }; + }; +}; + +exports.peruserperday = makePerDayLimiter('peruserperday', function (req) { + return (req.user || req.session.user).loginname; +}); + +exports.peripperday = makePerDayLimiter('peripperday', function (req) { + var realIP = req.get('x-real-ip'); + if (!realIP && !config.debug) { + throw new Error('should provide `x-real-ip` header') + } + return realIP; +}); diff --git a/middlewares/mongoose_log.js b/middlewares/mongoose_log.js new file mode 100644 index 0000000000..67f5af8a6c --- /dev/null +++ b/middlewares/mongoose_log.js @@ -0,0 +1,21 @@ +var mongoose = require('mongoose'); +var logger = require('../common/logger'); +var config = require('../config'); + +if (config.debug) { + var traceMQuery = function (method, info, query) { + return function (err, result, millis) { + if (err) { + logger.error('traceMQuery error:', err) + } + var infos = []; + infos.push(query._collection.collection.name + "." + method.blue); + infos.push(JSON.stringify(info)); + infos.push((millis + 'ms').green); + + logger.debug("MONGO".magenta, infos.join(' ')); + }; + }; + + mongoose.Mongoose.prototype.mquery.setGlobalTraceFunction(traceMQuery); +} diff --git a/middlewares/proxy.js b/middlewares/proxy.js new file mode 100644 index 0000000000..a2102d1005 --- /dev/null +++ b/middlewares/proxy.js @@ -0,0 +1,30 @@ +var urllib = require('url'); +var request = require('request'); +var logger = require('../common/logger') +var _ = require('lodash') + + +var ALLOW_HOSTNAME = [ + 'avatars.githubusercontent.com', 'www.gravatar.com', + 'gravatar.com', 'www.google-analytics.com', +]; +exports.proxy = function (req, res, next) { + var url = decodeURIComponent(req.query.url); + var hostname = urllib.parse(url).hostname; + + if (ALLOW_HOSTNAME.indexOf(hostname) === -1) { + return res.send(hostname + ' is not allowed'); + } + + request.get({ + url: url, + headers: _.omit(req.headers, ['cookie', 'refer']), + }) + .on('response', function (response) { + res.set(response.headers); + }) + .on('error', function (err) { + logger.error(err); + }) + .pipe(res); +}; diff --git a/middlewares/render.js b/middlewares/render.js new file mode 100644 index 0000000000..0c10c27182 --- /dev/null +++ b/middlewares/render.js @@ -0,0 +1,17 @@ +var logger = require('../common/logger'); + +// Patch res.render method to output logger +exports.render = function (req, res, next) { + res._render = res.render; + + res.render = function (view, options, fn) { + var t = new Date(); + + res._render(view, options, fn); + + var duration = (new Date() - t); + logger.info("Render view", view, ("(" + duration + "ms)").green); + }; + + next(); +}; diff --git a/middlewares/request_log.js b/middlewares/request_log.js new file mode 100644 index 0000000000..de551f27ff --- /dev/null +++ b/middlewares/request_log.js @@ -0,0 +1,22 @@ +var logger = require('../common/logger'); + +var ignore = /^\/(public|agent)/; + +exports = module.exports = function (req, res, next) { + // Assets do not out log. + if (ignore.test(req.url)) { + next(); + return; + } + + var t = new Date(); + logger.info('\n\nStarted', t.toISOString(), req.method, req.url, req.ip); + + res.on('finish', function () { + var duration = ((new Date()) - t); + + logger.info('Completed', res.statusCode, ('(' + duration + 'ms)').green); + }); + + next(); +}; diff --git a/models/base_model.js b/models/base_model.js new file mode 100644 index 0000000000..25e4caf0bd --- /dev/null +++ b/models/base_model.js @@ -0,0 +1,15 @@ +/** + * 给所有的 Model 扩展功能 + * http://mongoosejs.com/docs/plugins.html + */ +var tools = require('../common/tools'); + +module.exports = function (schema) { + schema.methods.create_at_ago = function () { + return tools.formatDate(this.create_at, true); + }; + + schema.methods.update_at_ago = function () { + return tools.formatDate(this.update_at, true); + }; +}; diff --git a/models/index.js b/models/index.js index b692553424..b4c478eb6d 100644 --- a/models/index.js +++ b/models/index.js @@ -1,30 +1,27 @@ var mongoose = require('mongoose'); -var config = require('../config').config; - -mongoose.connect(config.db, function (err) { +var config = require('../config'); +var logger = require('../common/logger') + +mongoose.connect(config.db, { + poolSize: 20, + useCreateIndex: true, + useNewUrlParser: true +}, function (err) { if (err) { - console.error('connect to %s error: ', config.db, err.message); + logger.error('connect to %s error: ', config.db, err.message); process.exit(1); } }); // models -require('./tag'); require('./user'); require('./topic'); -require('./topic_tag'); require('./reply'); require('./topic_collect'); -require('./tag_collect'); -require('./relation'); require('./message'); -exports.Tag = mongoose.model('Tag'); -exports.User = mongoose.model('User'); -exports.Topic = mongoose.model('Topic'); -exports.TopicTag = mongoose.model('TopicTag'); -exports.Reply = mongoose.model('Reply'); +exports.User = mongoose.model('User'); +exports.Topic = mongoose.model('Topic'); +exports.Reply = mongoose.model('Reply'); exports.TopicCollect = mongoose.model('TopicCollect'); -exports.TagCollect = mongoose.model('TagCollect'); -exports.Relation = mongoose.model('Relation'); -exports.Message = mongoose.model('Message'); +exports.Message = mongoose.model('Message'); diff --git a/models/message.js b/models/message.js index 0af3449637..dd03cfd226 100644 --- a/models/message.js +++ b/models/message.js @@ -1,7 +1,8 @@ -var mongoose = require('mongoose'); -var Schema = mongoose.Schema; -var ObjectId = Schema.ObjectId; - +var mongoose = require('mongoose'); +var BaseModel = require("./base_model"); +var Schema = mongoose.Schema; +var ObjectId = Schema.ObjectId; + /* * type: * reply: xx 回复了你的话题 @@ -9,14 +10,17 @@ var ObjectId = Schema.ObjectId; * follow: xx 关注了你 * at: xx @了你 */ - + var MessageSchema = new Schema({ type: { type: String }, - master_id: { type: ObjectId, index: true }, + master_id: { type: ObjectId}, author_id: { type: ObjectId }, topic_id: { type: ObjectId }, + reply_id: { type: ObjectId }, has_read: { type: Boolean, default: false }, create_at: { type: Date, default: Date.now } }); +MessageSchema.plugin(BaseModel); +MessageSchema.index({master_id: 1, has_read: -1, create_at: -1}); mongoose.model('Message', MessageSchema); diff --git a/models/relation.js b/models/relation.js deleted file mode 100644 index f7d73331fa..0000000000 --- a/models/relation.js +++ /dev/null @@ -1,11 +0,0 @@ -var mongoose = require('mongoose'); -var Schema = mongoose.Schema; -var ObjectId = Schema.ObjectId; - -var RelationSchema = new Schema({ - user_id: { type: ObjectId }, - follow_id: { type: ObjectId }, - create_at: { type: Date, default: Date.now } -}); - -mongoose.model('Relation', RelationSchema); diff --git a/models/reply.js b/models/reply.js index fd135cc3ae..a79a17e6fe 100644 --- a/models/reply.js +++ b/models/reply.js @@ -1,15 +1,22 @@ -var mongoose = require('mongoose'); -var Schema = mongoose.Schema; -var ObjectId = Schema.ObjectId; - +var mongoose = require('mongoose'); +var BaseModel = require("./base_model"); +var Schema = mongoose.Schema; +var ObjectId = Schema.ObjectId; + var ReplySchema = new Schema({ - content: { type: String }, - topic_id: { type: ObjectId, index: true }, - author_id: { type: ObjectId }, - reply_id : { type: ObjectId }, - create_at: { type: Date, default: Date.now }, - update_at: { type: Date, default: Date.now }, - content_is_html: { type: Boolean } + content: { type: String }, + topic_id: { type: ObjectId}, + author_id: { type: ObjectId }, + reply_id: { type: ObjectId }, + create_at: { type: Date, default: Date.now }, + update_at: { type: Date, default: Date.now }, + content_is_html: { type: Boolean }, + ups: [Schema.Types.ObjectId], + deleted: {type: Boolean, default: false}, }); +ReplySchema.plugin(BaseModel); +ReplySchema.index({topic_id: 1}); +ReplySchema.index({author_id: 1, create_at: -1}); + mongoose.model('Reply', ReplySchema); diff --git a/models/tag.js b/models/tag.js deleted file mode 100644 index 7ab4eb7bf0..0000000000 --- a/models/tag.js +++ /dev/null @@ -1,13 +0,0 @@ -var mongoose = require('mongoose'); -var Schema = mongoose.Schema; - -var TagSchema = new Schema({ - name: { type: String }, - order: { type: Number, default: 1 }, - description: { type: String }, - topic_count: { type: Number, default: 0 }, - collect_count: { type: Number, default: 0 }, - create_at: { type: Date, default: Date.now } -}); - -mongoose.model('Tag', TagSchema); diff --git a/models/tag_collect.js b/models/tag_collect.js deleted file mode 100644 index 1e9ad6e5a6..0000000000 --- a/models/tag_collect.js +++ /dev/null @@ -1,11 +0,0 @@ -var mongoose = require('mongoose'); -var Schema = mongoose.Schema; -var ObjectId = Schema.ObjectId; - -var TagCollectSchema = new Schema({ - user_id: { type: ObjectId, index: true }, - tag_id: { type: ObjectId }, - create_at: { type: Date, default: Date.now } -}); - -mongoose.model('TagCollect', TagCollectSchema); diff --git a/models/topic.js b/models/topic.js index 68f6ea95d3..baecc34840 100644 --- a/models/topic.js +++ b/models/topic.js @@ -1,12 +1,17 @@ -var mongoose = require('mongoose'); -var Schema = mongoose.Schema; -var ObjectId = Schema.ObjectId; - +var mongoose = require('mongoose'); +var BaseModel = require("./base_model"); +var Schema = mongoose.Schema; +var ObjectId = Schema.ObjectId; +var config = require('../config'); +var _ = require('lodash'); + var TopicSchema = new Schema({ title: { type: String }, content: { type: String }, author_id: { type: ObjectId }, - top: { type: Boolean, default: false }, + top: { type: Boolean, default: false }, // 置顶帖 + good: {type: Boolean, default: false}, // 精华帖 + lock: {type: Boolean, default: false}, // 被锁定主题 reply_count: { type: Number, default: 0 }, visit_count: { type: Number, default: 0 }, collect_count: { type: Number, default: 0 }, @@ -14,7 +19,27 @@ var TopicSchema = new Schema({ update_at: { type: Date, default: Date.now }, last_reply: { type: ObjectId }, last_reply_at: { type: Date, default: Date.now }, - content_is_html: { type: Boolean } + content_is_html: { type: Boolean }, + tab: {type: String}, + deleted: {type: Boolean, default: false}, +}); + +TopicSchema.plugin(BaseModel); +TopicSchema.index({create_at: -1}); +TopicSchema.index({top: -1, last_reply_at: -1}); +TopicSchema.index({author_id: 1, create_at: -1}); + +TopicSchema.virtual('tabName').get(function () { + var tab = this.tab; + var pair = _.find(config.tabs, function (_pair) { + return _pair[0] === tab; + }); + + if (pair) { + return pair[1]; + } else { + return ''; + } }); mongoose.model('Topic', TopicSchema); diff --git a/models/topic_collect.js b/models/topic_collect.js index 11e4f8add7..0850dc050c 100644 --- a/models/topic_collect.js +++ b/models/topic_collect.js @@ -1,11 +1,15 @@ -var mongoose = require('mongoose'); -var Schema = mongoose.Schema; -var ObjectId = Schema.ObjectId; - +var mongoose = require('mongoose'); +var BaseModel = require("./base_model"); +var Schema = mongoose.Schema; +var ObjectId = Schema.ObjectId; + var TopicCollectSchema = new Schema({ user_id: { type: ObjectId }, topic_id: { type: ObjectId }, create_at: { type: Date, default: Date.now } }); +TopicCollectSchema.plugin(BaseModel); +TopicCollectSchema.index({user_id: 1, topic_id: 1}, {unique: true}); + mongoose.model('TopicCollect', TopicCollectSchema); diff --git a/models/topic_tag.js b/models/topic_tag.js deleted file mode 100644 index bd92fe67b6..0000000000 --- a/models/topic_tag.js +++ /dev/null @@ -1,11 +0,0 @@ -var mongoose = require('mongoose'); -var Schema = mongoose.Schema; -var ObjectId = Schema.ObjectId; - -var TopicTagSchema = new Schema({ - topic_id: { type: ObjectId }, - tag_id: { type: ObjectId }, - create_at: { type: Date, default: Date.now } -}); - -mongoose.model('TopicTag', TopicTagSchema); diff --git a/models/user.js b/models/user.js index 13d21c1b36..c5c66db372 100644 --- a/models/user.js +++ b/models/user.js @@ -1,18 +1,27 @@ -var mongoose = require('mongoose'); -var Schema = mongoose.Schema; - +var mongoose = require('mongoose'); +var BaseModel = require("./base_model"); +var renderHelper = require('../common/render_helper'); +var Schema = mongoose.Schema; +var utility = require('utility'); +var _ = require('lodash'); + var UserSchema = new Schema({ - name: { type: String, index: true }, - loginname: { type: String, unique: true }, + name: { type: String}, + loginname: { type: String}, pass: { type: String }, - email: { type: String, unique: true }, + email: { type: String}, url: { type: String }, + profile_image_url: {type: String}, location: { type: String }, signature: { type: String }, profile: { type: String }, weibo: { type: String }, avatar: { type: String }, - + githubId: { type: String}, + githubUsername: {type: String}, + githubAccessToken: {type: String}, + is_block: {type: Boolean, default: false}, + score: { type: Number, default: 0 }, topic_count: { type: Number, default: 0 }, reply_count: { type: Number, default: 0 }, @@ -24,14 +33,53 @@ var UserSchema = new Schema({ update_at: { type: Date, default: Date.now }, is_star: { type: Boolean }, level: { type: String }, - active: { type: Boolean, default: true }, - + active: { type: Boolean, default: false }, + receive_reply_mail: {type: Boolean, default: false }, receive_at_mail: { type: Boolean, default: false }, from_wp: { type: Boolean }, - retrieve_time : {type: Number}, - retrieve_key : {type: String} + retrieve_time: {type: Number}, + retrieve_key: {type: String}, + + accessToken: {type: String}, +}); + +UserSchema.plugin(BaseModel); +UserSchema.virtual('avatar_url').get(function () { + var url = this.avatar || ('https://gravatar.com/avatar/' + utility.md5(this.email.toLowerCase()) + '?size=48'); + + // www.gravatar.com 被墙 + url = url.replace('www.gravatar.com', 'gravatar.com'); + + // 让协议自适应 protocol,使用 `//` 开头 + if (url.indexOf('http:') === 0) { + url = url.slice(5); + } + + // 如果是 github 的头像,则限制大小 + if (url.indexOf('githubusercontent') !== -1) { + url += '&s=120'; + } + + return url; +}); + +UserSchema.virtual('isAdvanced').get(function () { + // 积分高于 700 则认为是高级用户 + return this.score > 700 || this.is_star; +}); + +UserSchema.index({loginname: 1}, {unique: true}); +UserSchema.index({email: 1}, {unique: true}); +UserSchema.index({score: -1}); +UserSchema.index({githubId: 1}); +UserSchema.index({accessToken: 1}); + +UserSchema.pre('save', function(next){ + var now = new Date(); + this.update_at = now; + next(); }); mongoose.model('User', UserSchema); diff --git a/oneapm.js b/oneapm.js new file mode 100644 index 0000000000..7471c3e1c7 --- /dev/null +++ b/oneapm.js @@ -0,0 +1,30 @@ +/** + * OneAPM agent configuration. + * + * See lib/config.defaults.js in the agent distribution for a more complete + * description of configuration variables and their potential values. + */ + +var config = require('./config'); + +exports.config = { + /** + * Array of application names. + */ + app_name : [config.name], + /** + * Your OneAPM license key. + */ + license_key : config.oneapm_key, + logging : { + /** + * Level at which to log. 'trace' is most useful to OneAPM when diagnosing + * issues with the agent, 'info' and higher will impose the least overhead on + * production applications. + */ + level : 'info' + }, + transaction_events: { + enabled: true + } +}; diff --git a/package.json b/package.json index 043bc3b766..a7ab0e4f9e 100644 --- a/package.json +++ b/package.json @@ -1,26 +1,73 @@ { - "name": "nodeclub", - "version": "0.2.2", - "main": "./app.js", - "private": true, + "name": "nodeclub", + "version": "2.1.1", + "private": true, + "main": "app.js", + "description": "A Node.js bbs using MongoDB", + "repository": "https://github.com/cnodejs/nodeclub", "dependencies": { - "express": "2.5.1", - "ejs": "0.5.0", - "eventproxy": ">=0.1.0", - "mongoose": "2.4.1", - "node-markdown": "0.1.0", - "validator": "0.3.7", - "ndir": "0.1.2", - "nodemailer": "0.3.5", - "data2xml": "0.4.0" + "async": "1.5.2", + "bcryptjs": "2.3.0", + "body-parser": "1.17.1", + "bytes": "^2.2.0", + "colors": "1.1.2", + "compression": "1.7.0", + "connect-busboy": "0.0.2", + "connect-redis": "3.0.2", + "cookie-parser": "1.4.1", + "cors": "2.7.1", + "csurf": "1.8.3", + "data2xml": "1.2.4", + "ejs-mate": "2.3.0", + "eventproxy": "1.0.0", + "express": "4.16.0", + "express-session": "1.12.1", + "helmet": "1.3.0", + "ioredis": "2.0.0", + "jpush-sdk": "3.3.2", + "loader-builder": "2.4.1", + "loader": "2.1.1", + "lodash": "4.17.21", + "log4js": "^0.6.29", + "markdown-it": "6.0.0", + "memory-cache": "0.1.4", + "method-override": "2.3.5", + "moment": "2.15.2", + "mongoose": "5.3.9", + "multiline": "1.0.2", + "node-uuid": "1.4.7", + "nodemailer": "2.3.0", + "nodemailer-smtp-transport": "2.4.0", + "oneapm": "1.2.20", + "passport": "0.3.2", + "passport-github": "1.1.0", + "pm2": "*", + "qn": "1.3.0", + "ready": "0.1.1", + "request": "2.81.0", + "response-time": "2.3.1", + "superagent": "2.0.0", + "utility": "1.6.0", + "validator": "5.1.0", + "xmlbuilder": "7.0.0", + "xss": "0.2.10", + "snyk": "^1.88.0" }, "devDependencies": { - "should": ">=0.6.0", - "mocha": ">=0.14.1", - "rewire": ">=0.2.0", - "visionmedia-jscoverage": ">=1.0.0" + "errorhandler": "1.4.3", + "istanbul": "0.4.2", + "loader-connect": "1.0.1", + "mm": "1.3.5", + "mocha": "2.4.5", + "nock": "7.5.0", + "pedding": "1.0.0", + "should": "8.3.0", + "supertest": "1.2.0" }, "scripts": { - "test": "make test" - } + "test": "make test", + "snyk-protect": "snyk protect", + "prepare": "npm run snyk-protect" + }, + "snyk": true } diff --git a/plugins/onehost/index.js b/plugins/onehost/index.js deleted file mode 100644 index 5a0a3e15b5..0000000000 --- a/plugins/onehost/index.js +++ /dev/null @@ -1,33 +0,0 @@ -/*! - * nodeclub - One host only - * - * Redirect `HTTP GET` for `club.cnodejs.org` and `www.cnodejs.org` to `cnodejs.org`. - * - * Copyright(c) 2012 fengmk2 - * MIT Licensed - */ - -/** - * Module dependencies. - */ - -module.exports = function onehost(options) { - options = options || {}; - var host = options.host; - var exclude = options.exclude || []; - if (!Array.isArray(exclude)) { - exclude = [ exclude ]; - } - if (host) { - exclude.push(host); - } - return function (req, res, next) { - if (!host || exclude.indexOf(req.headers.host) >= 0 || req.method !== 'GET') { - return next(); - } - res.writeHead(301, { - 'Location': 'http://' + host + req.url - }); - res.end(); - }; -}; \ No newline at end of file diff --git a/plugins/wordpress_redirect/cnodeblog.posts.json b/plugins/wordpress_redirect/cnodeblog.posts.json deleted file mode 100644 index 2428f2807b..0000000000 --- a/plugins/wordpress_redirect/cnodeblog.posts.json +++ /dev/null @@ -1 +0,0 @@ -[{"id":2,"post_title":"关于","post_status":"publish","post_type":"page"},{"id":168,"post_title":"node.js中实现文件的循环写入","post_status":"publish","post_type":"post"},{"id":196,"post_title":"Demystifying events in node.js 阅读笔记","post_status":"publish","post_type":"post"},{"id":42,"post_title":"一分钟node.js","post_status":"publish","post_type":"post"},{"id":8,"post_title":"node.js调研与服务性能测试","post_status":"publish","post_type":"post"},{"id":32,"post_title":"nodejs快速入门","post_status":"publish","post_type":"post"},{"id":52,"post_title":"几个常用字符串hash算法的node封装","post_status":"publish","post_type":"post"},{"id":59,"post_title":"node chat源码解读(一)","post_status":"publish","post_type":"post"},{"id":82,"post_title":"node chat源码解读(三)","post_status":"publish","post_type":"post"},{"id":186,"post_title":"Node大全","post_status":"publish","post_type":"nav_menu_item"},{"id":101,"post_title":"node chat源码解读(二)","post_status":"publish","post_type":"post"},{"id":104,"post_title":"node.js国内外资料集锦(2011.02.09更新)","post_status":"publish","post_type":"post"},{"id":112,"post_title":"Node 下 Http Streaming 的跨浏览器实现","post_status":"publish","post_type":"post"},{"id":244,"post_title":"nodejs异步IO的实现","post_status":"publish","post_type":"post"},{"id":178,"post_title":"","post_status":"publish","post_type":"nav_menu_item"},{"id":179,"post_title":"","post_status":"publish","post_type":"nav_menu_item"},{"id":180,"post_title":"","post_status":"publish","post_type":"nav_menu_item"},{"id":181,"post_title":"","post_status":"publish","post_type":"nav_menu_item"},{"id":182,"post_title":"","post_status":"publish","post_type":"nav_menu_item"},{"id":187,"post_title":"首页","post_status":"publish","post_type":"nav_menu_item"},{"id":229,"post_title":"NodeJS的面向对象编程","post_status":"publish","post_type":"post"},{"id":317,"post_title":"基于node的VPS简易流量监控","post_status":"publish","post_type":"post"},{"id":273,"post_title":"websocket与node.js的完美结合","post_status":"publish","post_type":"post"},{"id":305,"post_title":"Guestbook","post_status":"publish","post_type":"page"},{"id":307,"post_title":"采访Node.JS的创始人Ryan Dahl","post_status":"publish","post_type":"post"},{"id":342,"post_title":"Node.js简单介绍并实现一个简单的Web MVC框架","post_status":"publish","post_type":"post"},{"id":377,"post_title":"node.js发布0.4.0稳定版本","post_status":"publish","post_type":"post"},{"id":418,"post_title":"NodeJS0.4.1(stable)版发布","post_status":"publish","post_type":"post"},{"id":386,"post_title":"有趣的Comet技术选择,论 Is node.js best for Comet?","post_status":"publish","post_type":"post"},{"id":404,"post_title":"nodejs下mysql性能测试","post_status":"publish","post_type":"post"},{"id":425,"post_title":"请求路由模块journey的源码解读和学习心得(一)","post_status":"publish","post_type":"post"},{"id":509,"post_title":"请求路由模块journey的源码解读和学习心得(二)","post_status":"publish","post_type":"post"},{"id":728,"post_title":"NodeParty 4月16日在北京betacafe即将开始","post_status":"publish","post_type":"post"},{"id":596,"post_title":"一个简单的日志module","post_status":"publish","post_type":"post"},{"id":611,"post_title":"异步IO一定更好吗?","post_status":"publish","post_type":"post"},{"id":625,"post_title":"nodejs-kissy 项目简介","post_status":"publish","post_type":"post"},{"id":634,"post_title":"NodeParty - CNodeJS北京聚会,邀请你参加","post_status":"publish","post_type":"post"},{"id":702,"post_title":"nodejs: 真正的一份代码,到处运行","post_status":"publish","post_type":"post"},{"id":649,"post_title":"nodejs下function,new function和this的研究","post_status":"publish","post_type":"post"},{"id":680,"post_title":"究竟怎样OOP?","post_status":"publish","post_type":"post"},{"id":752,"post_title":"NodeParty4月16日在北京成功举办","post_status":"publish","post_type":"post"},{"id":734,"post_title":"使用nodejs搭建最简单的comet原型","post_status":"publish","post_type":"post"},{"id":815,"post_title":"NodeParty 5月中旬杭州聚会,讲师征集中","post_status":"publish","post_type":"post"},{"id":842,"post_title":"nodejs一周动态(2011-04-18/24)","post_status":"publish","post_type":"post"},{"id":780,"post_title":"我为什么向后端工程师推荐NodeJS","post_status":"publish","post_type":"post"},{"id":920,"post_title":"node协作绘图程序在线测试","post_status":"publish","post_type":"post"},{"id":824,"post_title":"Mongoose入门介绍","post_status":"publish","post_type":"post"},{"id":858,"post_title":"CNodeJS T恤","post_status":"publish","post_type":"nav_menu_item"},{"id":872,"post_title":"Javascript模板引擎性能对比及几点优化","post_status":"publish","post_type":"post"},{"id":885,"post_title":"用NodeJS实现HTTP/HTTPS代理","post_status":"publish","post_type":"post"},{"id":897,"post_title":"NodeParty – CNodeJS杭州聚会(5.14),邀请你参加","post_status":"publish","post_type":"post"},{"id":911,"post_title":"用Eclipse调试Node.js代码","post_status":"publish","post_type":"post"},{"id":931,"post_title":"nodejs一周动态(2011-04-25 - 05-01)","post_status":"publish","post_type":"post"},{"id":1014,"post_title":"如何提高NodeJS程序的稳定性","post_status":"publish","post_type":"post"},{"id":940,"post_title":"nodeconf 2011 分享slides大全","post_status":"publish","post_type":"post"},{"id":956,"post_title":"NodeParty在杭州浙大校园召开","post_status":"publish","post_type":"post"},{"id":968,"post_title":"NodeParty杭州站影像","post_status":"publish","post_type":"post"},{"id":997,"post_title":"node中的require和exports","post_status":"publish","post_type":"post"},{"id":1015,"post_title":"续:异步IO一定更好吗?","post_status":"publish","post_type":"post"},{"id":1074,"post_title":"cnodechat聊天室设计及实现介绍","post_status":"publish","post_type":"post"},{"id":1065,"post_title":"Web开发的新势力——服务端JavaScript开发","post_status":"publish","post_type":"post"},{"id":1044,"post_title":"CNode社区介绍","post_status":"publish","post_type":"page"},{"id":1147,"post_title":"cnode社区联合珠三角技术沙龙6月专场开始报名","post_status":"publish","post_type":"post"},{"id":1169,"post_title":"node 0.5.0-pre新增功能汇总","post_status":"publish","post_type":"post"},{"id":1160,"post_title":"NodeParty – CNode社区(北京地区)7月技术沙龙邀请你参加!","post_status":"publish","post_type":"post"},{"id":1182,"post_title":"珠三角技术沙龙/NodeParty广州站圆满举办","post_status":"publish","post_type":"post"},{"id":1203,"post_title":"使用SeaJS实现模块化JavaScript开发","post_status":"publish","post_type":"post"},{"id":1280,"post_title":"node.js源码研究—模块组织加载","post_status":"publish","post_type":"post"},{"id":1310,"post_title":"nodejs web开发入门: Simple-TODO Nodejs 实现版","post_status":"publish","post_type":"post"},{"id":1478,"post_title":"实现自己的require函数","post_status":"publish","post_type":"post"},{"id":1364,"post_title":"Browser Vs. Node(浏览器与Node)","post_status":"publish","post_type":"post"},{"id":1409,"post_title":"记北京静安中心人人网总部cnodejs沙龙","post_status":"publish","post_type":"post"},{"id":1423,"post_title":"7月16日在人人网举办的cnode社区北京第二次线下交流会圆满成功!","post_status":"publish","post_type":"post"},{"id":1621,"post_title":"Javascript里有个C:Part 1 - 基础","post_status":"publish","post_type":"post"},{"id":4581,"post_title":"亚马逊EC2上node.js配置与开发的入门经验","post_status":"publish","post_type":"post"},{"id":1515,"post_title":"社区活动","post_status":"publish","post_type":"page"},{"id":1518,"post_title":"志愿者","post_status":"publish","post_type":"page"},{"id":1520,"post_title":"工作机会","post_status":"publish","post_type":"page"},{"id":1584,"post_title":"为什么CoffeeScript这么美?","post_status":"publish","post_type":"post"},{"id":1590,"post_title":"支持Nodejs的免费服务器简单介绍","post_status":"publish","post_type":"post"},{"id":1833,"post_title":"Javascript里有个C:Part 3 - 深入对象","post_status":"publish","post_type":"post"},{"id":1703,"post_title":"[实践经验+代码]用node.js和express.js和jade搭建轻型cms系统","post_status":"publish","post_type":"post"},{"id":1730,"post_title":"Web.js MVC between client and server","post_status":"publish","post_type":"post"},{"id":1804,"post_title":"Javascript里有个C:Part 2 – 对象","post_status":"publish","post_type":"post"},{"id":1912,"post_title":"Connect介绍(Just Connect it Already)","post_status":"publish","post_type":"post"},{"id":2806,"post_title":"全国首届“Nodejs开发者大赛”启动项目征集 触发梦想","post_status":"publish","post_type":"post"},{"id":2242,"post_title":"让node.js充分利用多核服务器的性能","post_status":"publish","post_type":"post"},{"id":2030,"post_title":"NodeParty-上海分享会第二期(9.17),诚邀您的参与(讲师继续征集中)","post_status":"publish","post_type":"post"},{"id":2045,"post_title":"当 web.js 遇上 Eisa…","post_status":"publish","post_type":"post"},{"id":2080,"post_title":"详解如何在ubuntu上安装nodejs","post_status":"publish","post_type":"post"},{"id":2207,"post_title":"nodejs-post文件上传原理详解","post_status":"publish","post_type":"post"},{"id":3057,"post_title":"在EC2 中搭建node.js 环境","post_status":"publish","post_type":"post"},{"id":2107,"post_title":"为websocket应用实现负载均衡","post_status":"publish","post_type":"post"},{"id":2126,"post_title":"使用xhr-multipart方式实现comet","post_status":"publish","post_type":"post"},{"id":2149,"post_title":"香港 NodeParty 啟動","post_status":"publish","post_type":"post"},{"id":2460,"post_title":"单服务器node.js和php性能测试","post_status":"publish","post_type":"post"},{"id":2572,"post_title":"node通过http.request向其他服务器上传文件","post_status":"publish","post_type":"post"},{"id":2590,"post_title":"用node.js和Websocket来做个多人聊天室吧","post_status":"publish","post_type":"post"},{"id":2426,"post_title":"linux AIO (异步IO) 那点事儿","post_status":"publish","post_type":"post"},{"id":2334,"post_title":"多核单服务器各种配置和业务压力下的node.js性能测试","post_status":"publish","post_type":"post"},{"id":2710,"post_title":"使用node.js建markdown静态博客,文章总汇(8篇)","post_status":"publish","post_type":"post"},{"id":2489,"post_title":"libev 设计分析","post_status":"publish","post_type":"post"},{"id":2521,"post_title":"Node App Engine开始内测","post_status":"publish","post_type":"post"},{"id":2763,"post_title":"NodeParty-SH-01圆满结束(附图文总结)","post_status":"publish","post_type":"post"},{"id":2593,"post_title":"NodeParty上海第一期(9.17)即将进行,倒计时中","post_status":"publish","post_type":"post"},{"id":2610,"post_title":"IIS 运行 Nodejs — 我大脑正常着呢!","post_status":"publish","post_type":"post"},{"id":2661,"post_title":"nodejs 异步之 Timer &Tick 篇","post_status":"publish","post_type":"post"},{"id":2797,"post_title":"【Node.js动态】用Node.js开发应用的公司(系列一)","post_status":"publish","post_type":"post"},{"id":2831,"post_title":"全国首届“Nodejs开发者大赛”评委简介","post_status":"publish","post_type":"post"},{"id":2859,"post_title":"全国首届“Nodejs开发者大赛”活动指南","post_status":"publish","post_type":"post"},{"id":3022,"post_title":"windows下配置node.js","post_status":"publish","post_type":"post"},{"id":3904,"post_title":"用NodeJS打造你的静态文件服务器","post_status":"publish","post_type":"post"},{"id":3616,"post_title":"Call/CC与Node.js","post_status":"publish","post_type":"post"},{"id":3471,"post_title":"NodeJs 多核多进程并行框架实作","post_status":"publish","post_type":"post"},{"id":3391,"post_title":"PhoneGap +Android start 的一天","post_status":"publish","post_type":"post"},{"id":3275,"post_title":"部署Node.js的应用","post_status":"publish","post_type":"post"},{"id":3473,"post_title":"ubuntu安装node.js记录","post_status":"publish","post_type":"post"},{"id":3574,"post_title":"用node作桌面开发","post_status":"publish","post_type":"post"},{"id":4520,"post_title":"构建动态服务器基础","post_status":"publish","post_type":"post"},{"id":5090,"post_title":"NodeParty-SH-2 ChristmasParty[2011-12-25]上海九城","post_status":"publish","post_type":"post"},{"id":4481,"post_title":"NodeJS深圳分享会第一届(NodeParty-SZ)(2012年1月8日)(更新活动当天安排)","post_status":"publish","post_type":"post"},{"id":3839,"post_title":"Node.js v0.6.0发布","post_status":"publish","post_type":"post"},{"id":4305,"post_title":"NodeParty-HZ-2 杭州第二期(ppt更新)","post_status":"publish","post_type":"post"},{"id":3844,"post_title":"三个郁闷的错误……搞了一下午……","post_status":"publish","post_type":"post"},{"id":4136,"post_title":"node v0.8 Roadmap详解","post_status":"publish","post_type":"post"},{"id":3699,"post_title":"【nodejscontest】Nodebook——随时记录,随时分享。","post_status":"publish","post_type":"post"},{"id":4160,"post_title":"Node[Christmas]Party-上海 12.25,邀请中","post_status":"publish","post_type":"post"},{"id":3765,"post_title":"【nodejscontest】SuperSonic","post_status":"publish","post_type":"post"},{"id":4817,"post_title":"node.js与dojo完美的融合-开发完全面向对象化","post_status":"publish","post_type":"post"},{"id":3856,"post_title":"《Node入门》勘误列表","post_status":"publish","post_type":"post"},{"id":4242,"post_title":"通过nginx为forever-webui添加密码验证 实现外部管理","post_status":"publish","post_type":"post"},{"id":4037,"post_title":"Javascript 1.7中的Yield","post_status":"publish","post_type":"post"},{"id":4252,"post_title":"限额报名:HTML5年度Home Party暨首届原创游戏大赛颁奖典礼","post_status":"publish","post_type":"post"},{"id":4337,"post_title":"NodeParty-北京分享会第三期(12.10),诚邀您的参与","post_status":"publish","post_type":"post"},{"id":4186,"post_title":"Node Buffer/Stream 内存策略分析","post_status":"publish","post_type":"post"},{"id":4542,"post_title":"中国首届NodeJS开发者大赛顺利结束","post_status":"publish","post_type":"post"},{"id":4437,"post_title":"用于测试rest api的nodejs测试框架","post_status":"publish","post_type":"post"},{"id":4982,"post_title":"fibonacci(40) benchmark","post_status":"publish","post_type":"post"},{"id":5055,"post_title":"Node.js中相同模块是否会被加载多次?","post_status":"publish","post_type":"post"},{"id":4625,"post_title":" NodeParty-BJ-3 (2011.12.10,易宝支付)","post_status":"publish","post_type":"post"},{"id":4739,"post_title":"Node 编程规范征集","post_status":"publish","post_type":"post"},{"id":4747,"post_title":"Building Hypermedia APIs with HTML5 & Node翻译中(暨简介)","post_status":"publish","post_type":"post"},{"id":4828,"post_title":"node-formidable详解","post_status":"publish","post_type":"post"},{"id":5005,"post_title":"Windows 下的 Nodejs","post_status":"publish","post_type":"post"},{"id":5398,"post_title":"用 VC 做 node 扩展,C++ 的。","post_status":"publish","post_type":"post"},{"id":5166,"post_title":"node.js的循环依赖","post_status":"publish","post_type":"post"},{"id":5425,"post_title":"小心data事件里的chunk拼接","post_status":"publish","post_type":"post"},{"id":5435,"post_title":"简单案例:使用Node.JS在线渲染LESS CSS","post_status":"publish","post_type":"post"},{"id":5480,"post_title":"rrestjs高性能restful框架","post_status":"publish","post_type":"post"},{"id":5461,"post_title":"Nodeparty-SZ 深圳聚会活动回顾总结[2012.01.08] ","post_status":"publish","post_type":"post"},{"id":5553,"post_title":"从异步队列到中间件实现 —— (1)异步队列 asynclist","post_status":"publish","post_type":"post"}] diff --git a/plugins/wordpress_redirect/dump_posts.js b/plugins/wordpress_redirect/dump_posts.js deleted file mode 100644 index 0da6b1c854..0000000000 --- a/plugins/wordpress_redirect/dump_posts.js +++ /dev/null @@ -1,13 +0,0 @@ -var mysql = require('mysql'); - -var client = mysql.createClient(); -client.host = '127.0.0.1'; -client.user = 'user'; -client.password = 'pwd'; -client.query('USE wordpress'); - -var sql = 'select id, post_title, post_status, post_type from nodejs_wp_posts where post_status="publish"'; -client.query(sql, function (err, rows) { - console.log(JSON.stringify(rows)); - console.log('done'); -}); diff --git a/plugins/wordpress_redirect/index.js b/plugins/wordpress_redirect/index.js deleted file mode 100644 index 6e5df905a1..0000000000 --- a/plugins/wordpress_redirect/index.js +++ /dev/null @@ -1,38 +0,0 @@ -/*! - * nodeclub - wordpress_redirect - * handle wordpress http://xxx/blog/?p=$pid redirect to http://nodeclub/topic/xxxx - * - * Copyright(c) 2012 fengmk2 - * MIT Licensed - */ - -/** - * Module dependencies. - */ - -var PostToTopic = require('./model').PostToTopic; - -module.exports = function wordpressRedirect(options) { - options = options || {}; - var root = options.root || '/topic/'; - if (root[root.length - 1] !== '/') { - root += '/'; - } - var URL_RE = options.match || /^\/blog\/?\?p=(\d+)/i; - return function (req, res, next) { - if (!URL_RE.test(req.url)) { - return next(); - } - var m = URL_RE.exec(req.url); - var postId = parseInt(m[1], 10); - PostToTopic.findOne({ _id: postId }, function (err, o) { - if (err) { - return next(err); - } - if (!o) { - return next(); - } - res.redirect(root + o.topic_id, 301); - }); - }; -}; \ No newline at end of file diff --git a/plugins/wordpress_redirect/model.js b/plugins/wordpress_redirect/model.js deleted file mode 100644 index 07227abb29..0000000000 --- a/plugins/wordpress_redirect/model.js +++ /dev/null @@ -1,12 +0,0 @@ -var mongoose = require('mongoose'); -var Schema = mongoose.Schema; -var ObjectId = Schema.ObjectId; - -var PostToTopicSchema = new Schema({ - _id: { type: Number }, - topic_id: { type: ObjectId } -}); - -mongoose.model('PostToTopic', PostToTopicSchema); - -exports.PostToTopic = mongoose.model('PostToTopic'); \ No newline at end of file diff --git a/plugins/wordpress_redirect/sync_post_id_to_topic.js b/plugins/wordpress_redirect/sync_post_id_to_topic.js deleted file mode 100644 index 95e4f17305..0000000000 --- a/plugins/wordpress_redirect/sync_post_id_to_topic.js +++ /dev/null @@ -1,55 +0,0 @@ -/*! - * Sync post id to topic by title. - * - * Usage: node sync_post_id_to_topic.js wordpress.json - * - * Copyright(c) 2012 fengmk2 - * MIT Licensed - */ - -/** - * Module dependencies. - */ - -var PostToTopic = require('./model').PostToTopic; -var Topic = require('../../models').Topic; -var path = require('path'); - -var posts = require(path.join(process.cwd(), process.argv[2])); - -function sync(post, callback) { - var title = post.post_title; - if (!title || post.post_type !== 'post') { - return callback(); - } - Topic.findOne({ title: title }, function (err, topic) { - if (err) { - return callback(err); - } - if (!topic) { - // console.log('%s %s not found', post.id, title); - return callback(); - } - var r = new PostToTopic(); - r._id = post.id; - r.topic_id = topic._id; - r.save(); - console.log('%s %s founed %s', post.id, title, topic._id); - callback(); - }); -} - -function next(i) { - var post = posts[i]; - if (!post) { - process.exit(0); - } - sync(post, function (err) { - if (err) { - throw err; - } - next(--i); - }); -} - -next(posts.length - 1); diff --git a/proxy/index.js b/proxy/index.js new file mode 100644 index 0000000000..fffc4c4feb --- /dev/null +++ b/proxy/index.js @@ -0,0 +1,5 @@ +exports.User = require('./user'); +exports.Message = require('./message'); +exports.Topic = require('./topic'); +exports.Reply = require('./reply'); +exports.TopicCollect = require('./topic_collect'); diff --git a/proxy/message.js b/proxy/message.js new file mode 100644 index 0000000000..36c59e2bdb --- /dev/null +++ b/proxy/message.js @@ -0,0 +1,117 @@ +var EventProxy = require('eventproxy'); +var _ = require('lodash'); + +var Message = require('../models').Message; + +var User = require('./user'); +var Topic = require('./topic'); +var Reply = require('./reply'); + +/** + * 根据用户ID,获取未读消息的数量 + * Callback: + * 回调函数参数列表: + * - err, 数据库错误 + * - count, 未读消息数量 + * @param {String} id 用户ID + * @param {Function} callback 获取消息数量 + */ +exports.getMessagesCount = function (id, callback) { + Message.countDocuments({master_id: id, has_read: false}, callback); +}; + + +/** + * 根据消息Id获取消息 + * Callback: + * - err, 数据库错误 + * - message, 消息对象 + * @param {String} id 消息ID + * @param {Function} callback 回调函数 + */ +exports.getMessageById = function (id, callback) { + Message.findOne({_id: id}, function (err, message) { + if (err) { + return callback(err); + } + getMessageRelations(message, callback); + }); +}; + +var getMessageRelations = exports.getMessageRelations = function (message, callback) { + if (message.type === 'reply' || message.type === 'reply2' || message.type === 'at') { + var proxy = new EventProxy(); + proxy.fail(callback); + proxy.assign('author', 'topic', 'reply', function (author, topic, reply) { + message.author = author; + message.topic = topic; + message.reply = reply; + if (!author || !topic) { + message.is_invalid = true; + } + return callback(null, message); + }); // 接收异常 + User.getUserById(message.author_id, proxy.done('author')); + Topic.getTopicById(message.topic_id, proxy.done('topic')); + Reply.getReplyById(message.reply_id, proxy.done('reply')); + } else { + return callback(null, {is_invalid: true}); + } +}; + +/** + * 根据用户ID,获取已读消息列表 + * Callback: + * - err, 数据库异常 + * - messages, 消息列表 + * @param {String} userId 用户ID + * @param {Function} callback 回调函数 + */ +exports.getReadMessagesByUserId = function (userId, callback) { + Message.find({master_id: userId, has_read: true}, null, + {sort: '-create_at', limit: 20}, callback); +}; + +/** + * 根据用户ID,获取未读消息列表 + * Callback: + * - err, 数据库异常 + * - messages, 未读消息列表 + * @param {String} userId 用户ID + * @param {Function} callback 回调函数 + */ +exports.getUnreadMessageByUserId = function (userId, callback) { + Message.find({master_id: userId, has_read: false}, null, + {sort: '-create_at'}, callback); +}; + + +/** + * 将消息设置成已读 + */ +exports.updateMessagesToRead = function (userId, messages, callback) { + callback = callback || _.noop; + if (messages.length === 0) { + return callback(); + } + + var ids = messages.map(function (m) { + return m.id; + }); + + var query = { master_id: userId, _id: { $in: ids } }; + Message.updateMany(query, { $set: { has_read: true } }).exec(callback); +}; + + +/** + * 将单个消息设置成已读 + */ +exports.updateOneMessageToRead = function (msg_id, callback) { + callback = callback || _.noop; + if (!msg_id) { + return callback(); + } + var query = { _id: msg_id }; + Message.updateMany(query, { $set: { has_read: true } }).exec(callback); +}; diff --git a/proxy/reply.js b/proxy/reply.js new file mode 100644 index 0000000000..ad2b6002a2 --- /dev/null +++ b/proxy/reply.js @@ -0,0 +1,149 @@ +var models = require('../models'); +var Reply = models.Reply; +var EventProxy = require('eventproxy'); +var tools = require('../common/tools'); +var User = require('./user'); +var at = require('../common/at'); + +/** + * 获取一条回复信息 + * @param {String} id 回复ID + * @param {Function} callback 回调函数 + */ +exports.getReply = function (id, callback) { + Reply.findOne({_id: id}, callback); +}; + +/** + * 根据回复ID,获取回复 + * Callback: + * - err, 数据库异常 + * - reply, 回复内容 + * @param {String} id 回复ID + * @param {Function} callback 回调函数 + */ +exports.getReplyById = function (id, callback) { + if (!id) { + return callback(null, null); + } + Reply.findOne({_id: id}, function (err, reply) { + if (err) { + return callback(err); + } + if (!reply) { + return callback(err, null); + } + + var author_id = reply.author_id; + User.getUserById(author_id, function (err, author) { + if (err) { + return callback(err); + } + reply.author = author; + // TODO: 添加更新方法,有些旧帖子可以转换为markdown格式的内容 + if (reply.content_is_html) { + return callback(null, reply); + } + at.linkUsers(reply.content, function (err, str) { + if (err) { + return callback(err); + } + reply.content = str; + return callback(err, reply); + }); + }); + }); +}; + +/** + * 根据主题ID,获取回复列表 + * Callback: + * - err, 数据库异常 + * - replies, 回复列表 + * @param {String} id 主题ID + * @param {Function} callback 回调函数 + */ +exports.getRepliesByTopicId = function (id, cb) { + Reply.find({topic_id: id, deleted: false}, '', {sort: 'create_at'}, function (err, replies) { + if (err) { + return cb(err); + } + if (replies.length === 0) { + return cb(null, []); + } + + var proxy = new EventProxy(); + proxy.after('reply_find', replies.length, function () { + cb(null, replies); + }); + for (var j = 0; j < replies.length; j++) { + (function (i) { + var author_id = replies[i].author_id; + User.getUserById(author_id, function (err, author) { + if (err) { + return cb(err); + } + replies[i].author = author || { _id: '' }; + if (replies[i].content_is_html) { + return proxy.emit('reply_find'); + } + at.linkUsers(replies[i].content, function (err, str) { + if (err) { + return cb(err); + } + replies[i].content = str; + proxy.emit('reply_find'); + }); + }); + })(j); + } + }); +}; + +/** + * 创建并保存一条回复信息 + * @param {String} content 回复内容 + * @param {String} topicId 主题ID + * @param {String} authorId 回复作者 + * @param {String} [replyId] 回复ID,当二级回复时设定该值 + * @param {Function} callback 回调函数 + */ +exports.newAndSave = function (content, topicId, authorId, replyId, callback) { + if (typeof replyId === 'function') { + callback = replyId; + replyId = null; + } + var reply = new Reply(); + reply.content = content; + reply.topic_id = topicId; + reply.author_id = authorId; + + if (replyId) { + reply.reply_id = replyId; + } + reply.save(function (err) { + callback(err, reply); + }); +}; + +/** + * 根据topicId查询到最新的一条未删除回复 + * @param topicId 主题ID + * @param callback 回调函数 + */ +exports.getLastReplyByTopId = function (topicId, callback) { + Reply.find({topic_id: topicId, deleted: false}, '_id', {sort: {create_at : -1}, limit : 1}, callback); +}; + +exports.getRepliesByAuthorId = function (authorId, opt, callback) { + if (!callback) { + callback = opt; + opt = null; + } + Reply.find({author_id: authorId}, {}, opt, callback); +}; + +// 通过 author_id 获取回复总数 +exports.getCountByAuthorId = function (authorId, callback) { + Reply.countDocuments({author_id: authorId}, callback); +}; diff --git a/proxy/topic.js b/proxy/topic.js new file mode 100644 index 0000000000..d1fc6675b5 --- /dev/null +++ b/proxy/topic.js @@ -0,0 +1,226 @@ +var EventProxy = require('eventproxy'); +var models = require('../models'); +var Topic = models.Topic; +var User = require('./user'); +var Reply = require('./reply'); +var tools = require('../common/tools'); +var at = require('../common/at'); +var _ = require('lodash'); + + +/** + * 根据主题ID获取主题 + * Callback: + * - err, 数据库错误 + * - topic, 主题 + * - author, 作者 + * - lastReply, 最后回复 + * @param {String} id 主题ID + * @param {Function} callback 回调函数 + */ +exports.getTopicById = function (id, callback) { + var proxy = new EventProxy(); + var events = ['topic', 'author', 'last_reply']; + proxy.assign(events, function (topic, author, last_reply) { + if (!author) { + return callback(null, null, null, null); + } + return callback(null, topic, author, last_reply); + }).fail(callback); + + Topic.findOne({_id: id}, proxy.done(function (topic) { + if (!topic) { + proxy.emit('topic', null); + proxy.emit('author', null); + proxy.emit('last_reply', null); + return; + } + proxy.emit('topic', topic); + + User.getUserById(topic.author_id, proxy.done('author')); + + if (topic.last_reply) { + Reply.getReplyById(topic.last_reply, proxy.done(function (last_reply) { + proxy.emit('last_reply', last_reply); + })); + } else { + proxy.emit('last_reply', null); + } + })); +}; + +/** + * 获取关键词能搜索到的主题数量 + * Callback: + * - err, 数据库错误 + * - count, 主题数量 + * @param {String} query 搜索关键词 + * @param {Function} callback 回调函数 + */ +exports.getCountByQuery = function (query, callback) { + Topic.countDocuments(query, callback); +}; + +/** + * 根据关键词,获取主题列表 + * Callback: + * - err, 数据库错误 + * - count, 主题列表 + * @param {String} query 搜索关键词 + * @param {Object} opt 搜索选项 + * @param {Function} callback 回调函数 + */ +exports.getTopicsByQuery = function (query, opt, callback) { + query.deleted = false; + Topic.find(query, {}, opt, function (err, topics) { + if (err) { + return callback(err); + } + if (topics.length === 0) { + return callback(null, []); + } + + var proxy = new EventProxy(); + proxy.after('topic_ready', topics.length, function () { + topics = _.compact(topics); // 删除不合规的 topic + return callback(null, topics); + }); + proxy.fail(callback); + + topics.forEach(function (topic, i) { + var ep = new EventProxy(); + ep.all('author', 'reply', function (author, reply) { + // 保证顺序 + // 作者可能已被删除 + if (author) { + topic.author = author; + topic.reply = reply; + } else { + topics[i] = null; + } + proxy.emit('topic_ready'); + }); + + User.getUserById(topic.author_id, ep.done('author')); + // 获取主题的最后回复 + Reply.getReplyById(topic.last_reply, ep.done('reply')); + }); + }); +}; + +// for sitemap +exports.getLimit5w = function (callback) { + Topic.find({deleted: false}, '_id', {limit: 50000, sort: '-create_at'}, callback); +}; + +/** + * 获取所有信息的主题 + * Callback: + * - err, 数据库异常 + * - message, 消息 + * - topic, 主题 + * - author, 主题作者 + * - replies, 主题的回复 + * @param {String} id 主题ID + * @param {Function} callback 回调函数 + */ +exports.getFullTopic = function (id, callback) { + var proxy = new EventProxy(); + var events = ['topic', 'author', 'replies']; + proxy + .assign(events, function (topic, author, replies) { + callback(null, '', topic, author, replies); + }) + .fail(callback); + + Topic.findOne({_id: id, deleted: false}, proxy.done(function (topic) { + if (!topic) { + proxy.unbind(); + return callback(null, '此话题不存在或已被删除。'); + } + at.linkUsers(topic.content, proxy.done('topic', function (str) { + topic.linkedContent = str; + return topic; + })); + + User.getUserById(topic.author_id, proxy.done(function (author) { + if (!author) { + proxy.unbind(); + return callback(null, '话题的作者丢了。'); + } + proxy.emit('author', author); + })); + + Reply.getRepliesByTopicId(topic._id, proxy.done('replies')); + })); +}; + +/** + * 更新主题的最后回复信息 + * @param {String} topicId 主题ID + * @param {String} replyId 回复ID + * @param {Function} callback 回调函数 + */ +exports.updateLastReply = function (topicId, replyId, callback) { + Topic.findOne({_id: topicId}, function (err, topic) { + if (err || !topic) { + return callback(err); + } + topic.last_reply = replyId; + topic.last_reply_at = new Date(); + topic.reply_count += 1; + topic.save(callback); + }); +}; + +/** + * 根据主题ID,查找一条主题 + * @param {String} id 主题ID + * @param {Function} callback 回调函数 + */ +exports.getTopic = function (id, callback) { + Topic.findOne({_id: id}, callback); +}; + +/** + * 将当前主题的回复计数减1,并且更新最后回复的用户,删除回复时用到 + * @param {String} id 主题ID + * @param {Function} callback 回调函数 + */ +exports.reduceCount = function (id, callback) { + Topic.findOne({_id: id}, function (err, topic) { + if (err) { + return callback(err); + } + + if (!topic) { + return callback(new Error('该主题不存在')); + } + topic.reply_count -= 1; + + Reply.getLastReplyByTopId(id, function (err, reply) { + if (err) { + return callback(err); + } + + if (reply.length !== 0) { + topic.last_reply = reply[0]._id; + } else { + topic.last_reply = null; + } + + topic.save(callback); + }); + + }); +}; + +exports.newAndSave = function (title, content, tab, authorId, callback) { + var topic = new Topic(); + topic.title = title; + topic.content = content; + topic.tab = tab; + topic.author_id = authorId; + + topic.save(callback); +}; diff --git a/proxy/topic_collect.js b/proxy/topic_collect.js new file mode 100644 index 0000000000..cfa17d4d4d --- /dev/null +++ b/proxy/topic_collect.js @@ -0,0 +1,24 @@ +var TopicCollect = require('../models').TopicCollect; +var _ = require('lodash') + +exports.getTopicCollect = function (userId, topicId, callback) { + TopicCollect.findOne({user_id: userId, topic_id: topicId}, callback); +}; + +exports.getTopicCollectsByUserId = function (userId, opt, callback) { + var defaultOpt = {sort: '-create_at'}; + opt = _.assign(defaultOpt, opt) + TopicCollect.find({user_id: userId}, '', opt, callback); +}; + +exports.newAndSave = function (userId, topicId, callback) { + var topic_collect = new TopicCollect(); + topic_collect.user_id = userId; + topic_collect.topic_id = topicId; + topic_collect.save(callback); +}; + +exports.remove = function (userId, topicId, callback) { + TopicCollect.deleteOne({user_id: userId, topic_id: topicId}, callback); +}; + diff --git a/proxy/user.js b/proxy/user.js new file mode 100644 index 0000000000..58b8a0a132 --- /dev/null +++ b/proxy/user.js @@ -0,0 +1,118 @@ +var models = require('../models'); +var User = models.User; +var utility = require('utility'); +var uuid = require('node-uuid'); + +/** + * 根据用户名列表查找用户列表 + * Callback: + * - err, 数据库异常 + * - users, 用户列表 + * @param {Array} names 用户名列表 + * @param {Function} callback 回调函数 + */ +exports.getUsersByNames = function (names, callback) { + if (names.length === 0) { + return callback(null, []); + } + User.find({ loginname: { $in: names } }, callback); +}; + +/** + * 根据登录名查找用户 + * Callback: + * - err, 数据库异常 + * - user, 用户 + * @param {String} loginName 登录名 + * @param {Function} callback 回调函数 + */ +exports.getUserByLoginName = function (loginName, callback) { + User.findOne({'loginname': new RegExp('^'+loginName+'$', "i")}, callback); +}; + +/** + * 根据用户ID,查找用户 + * Callback: + * - err, 数据库异常 + * - user, 用户 + * @param {String} id 用户ID + * @param {Function} callback 回调函数 + */ +exports.getUserById = function (id, callback) { + if (!id) { + return callback(); + } + User.findOne({_id: id}, callback); +}; + +/** + * 根据邮箱,查找用户 + * Callback: + * - err, 数据库异常 + * - user, 用户 + * @param {String} email 邮箱地址 + * @param {Function} callback 回调函数 + */ +exports.getUserByMail = function (email, callback) { + User.findOne({email: email}, callback); +}; + +/** + * 根据用户ID列表,获取一组用户 + * Callback: + * - err, 数据库异常 + * - users, 用户列表 + * @param {Array} ids 用户ID列表 + * @param {Function} callback 回调函数 + */ +exports.getUsersByIds = function (ids, callback) { + User.find({'_id': {'$in': ids}}, callback); +}; + +/** + * 根据关键字,获取一组用户 + * Callback: + * - err, 数据库异常 + * - users, 用户列表 + * @param {String} query 关键字 + * @param {Object} opt 选项 + * @param {Function} callback 回调函数 + */ +exports.getUsersByQuery = function (query, opt, callback) { + User.find(query, '', opt, callback); +}; + +/** + * 根据查询条件,获取一个用户 + * Callback: + * - err, 数据库异常 + * - user, 用户 + * @param {String} name 用户名 + * @param {String} key 激活码 + * @param {Function} callback 回调函数 + */ +exports.getUserByNameAndKey = function (loginname, key, callback) { + User.findOne({loginname: loginname, retrieve_key: key}, callback); +}; + +exports.newAndSave = function (name, loginname, pass, email, avatar_url, active, callback) { + var user = new User(); + user.name = loginname; + user.loginname = loginname; + user.pass = pass; + user.email = email; + user.avatar = avatar_url; + user.active = active || false; + user.accessToken = uuid.v4(); + + user.save(callback); +}; + +var makeGravatar = function (email) { + return 'http://www.gravatar.com/avatar/' + utility.md5(email.toLowerCase()) + '?size=48'; +}; +exports.makeGravatar = makeGravatar; + +exports.getGravatar = function (user) { + return user.avatar || makeGravatar(user); +}; diff --git a/public/favicon.ico b/public/favicon.ico old mode 100755 new mode 100644 diff --git a/public/github-card.html b/public/github-card.html new file mode 100644 index 0000000000..a17180c1c4 --- /dev/null +++ b/public/github-card.html @@ -0,0 +1,5 @@ + + + diff --git a/public/images/cert_icon&16.png b/public/images/cert_icon&16.png deleted file mode 100644 index 36fead5e9c..0000000000 Binary files a/public/images/cert_icon&16.png and /dev/null differ diff --git a/public/images/checkmark_icon&16.png b/public/images/checkmark_icon&16.png deleted file mode 100755 index 7da7a71d7b..0000000000 Binary files a/public/images/checkmark_icon&16.png and /dev/null differ diff --git a/public/images/cnode_icon_32.png b/public/images/cnode_icon_32.png new file mode 100644 index 0000000000..9e69485a35 Binary files /dev/null and b/public/images/cnode_icon_32.png differ diff --git a/public/images/cnode_icon_64.png b/public/images/cnode_icon_64.png new file mode 100644 index 0000000000..dbef98015b Binary files /dev/null and b/public/images/cnode_icon_64.png differ diff --git a/public/images/cnode_logo_128.png b/public/images/cnode_logo_128.png new file mode 100644 index 0000000000..802beeaf4f Binary files /dev/null and b/public/images/cnode_logo_128.png differ diff --git a/public/images/cnode_logo_32.png b/public/images/cnode_logo_32.png new file mode 100644 index 0000000000..5beaa4fe31 Binary files /dev/null and b/public/images/cnode_logo_32.png differ diff --git a/public/images/cnodejs.svg b/public/images/cnodejs.svg new file mode 100644 index 0000000000..756fa8ca8b --- /dev/null +++ b/public/images/cnodejs.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + diff --git a/public/images/cnodejs_light.svg b/public/images/cnodejs_light.svg new file mode 100644 index 0000000000..7642b406dc --- /dev/null +++ b/public/images/cnodejs_light.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + diff --git a/public/images/cog_icon&16.png b/public/images/cog_icon&16.png deleted file mode 100644 index 2e62f9a1db..0000000000 Binary files a/public/images/cog_icon&16.png and /dev/null differ diff --git a/public/images/compass_icon&16.png b/public/images/compass_icon&16.png deleted file mode 100644 index d06389d1e4..0000000000 Binary files a/public/images/compass_icon&16.png and /dev/null differ diff --git a/public/images/delete_icon&16.png b/public/images/delete_icon&16.png deleted file mode 100644 index b59c23c8fa..0000000000 Binary files a/public/images/delete_icon&16.png and /dev/null differ diff --git a/public/images/digitalocean.png b/public/images/digitalocean.png new file mode 100644 index 0000000000..cc11d228c3 Binary files /dev/null and b/public/images/digitalocean.png differ diff --git a/public/images/doc_edit_icon&16.png b/public/images/doc_edit_icon&16.png deleted file mode 100644 index 9ec5df06d7..0000000000 Binary files a/public/images/doc_edit_icon&16.png and /dev/null differ diff --git a/public/images/golangtc-logo.png b/public/images/golangtc-logo.png new file mode 100644 index 0000000000..56a44c3733 Binary files /dev/null and b/public/images/golangtc-logo.png differ diff --git a/public/images/home_icon&16.png b/public/images/home_icon&16.png deleted file mode 100644 index f084db96c6..0000000000 Binary files a/public/images/home_icon&16.png and /dev/null differ diff --git a/public/images/iojs-logo-w150h50.png b/public/images/iojs-logo-w150h50.png new file mode 100644 index 0000000000..55f6a9a047 Binary files /dev/null and b/public/images/iojs-logo-w150h50.png differ diff --git a/public/images/iojs-logo.png b/public/images/iojs-logo.png new file mode 100644 index 0000000000..68ad337da5 Binary files /dev/null and b/public/images/iojs-logo.png differ diff --git a/public/images/logo.png b/public/images/logo.png index 8a4009e5a7..c8dcf1a948 100644 Binary files a/public/images/logo.png and b/public/images/logo.png differ diff --git a/test/fixtures/logo.png b/public/images/logo_bak.png similarity index 100% rename from test/fixtures/logo.png rename to public/images/logo_bak.png diff --git a/public/images/mail_icon&16.png b/public/images/mail_icon&16.png deleted file mode 100644 index 53ea8d8751..0000000000 Binary files a/public/images/mail_icon&16.png and /dev/null differ diff --git a/public/images/node_icon&16.png b/public/images/node_icon&16.png deleted file mode 100644 index d351b4b70f..0000000000 Binary files a/public/images/node_icon&16.png and /dev/null differ diff --git a/public/images/on-off_icon&16.png b/public/images/on-off_icon&16.png deleted file mode 100644 index 072d21ce46..0000000000 Binary files a/public/images/on-off_icon&16.png and /dev/null differ diff --git a/public/images/paper_airplane_icon&16.png b/public/images/paper_airplane_icon&16.png deleted file mode 100644 index da42327aa7..0000000000 Binary files a/public/images/paper_airplane_icon&16.png and /dev/null differ diff --git a/public/images/phphub-logo.png b/public/images/phphub-logo.png new file mode 100644 index 0000000000..b42bd4dbec Binary files /dev/null and b/public/images/phphub-logo.png differ diff --git a/public/images/qiniu.png b/public/images/qiniu.png new file mode 100644 index 0000000000..d1c4ea5c80 Binary files /dev/null and b/public/images/qiniu.png differ diff --git a/public/images/rss_icon&40.png b/public/images/rss_icon&40.png deleted file mode 100644 index 883a7ce1b9..0000000000 Binary files a/public/images/rss_icon&40.png and /dev/null differ diff --git a/public/images/ruby-china-20150529.png b/public/images/ruby-china-20150529.png new file mode 100644 index 0000000000..09f2a81bfc Binary files /dev/null and b/public/images/ruby-china-20150529.png differ diff --git a/public/images/ruby-china-logo2.png b/public/images/ruby-china-logo2.png new file mode 100644 index 0000000000..68132f2358 Binary files /dev/null and b/public/images/ruby-china-logo2.png differ diff --git a/public/images/ruby_china_logo.png b/public/images/ruby_china_logo.png deleted file mode 100644 index b5e76a6d11..0000000000 Binary files a/public/images/ruby_china_logo.png and /dev/null differ diff --git a/public/images/spechbubble_2_icon&16.png b/public/images/spechbubble_2_icon&16.png deleted file mode 100644 index a5675fa65f..0000000000 Binary files a/public/images/spechbubble_2_icon&16.png and /dev/null differ diff --git a/public/images/star_fav_empty_icon&16.png b/public/images/star_fav_empty_icon&16.png deleted file mode 100644 index 3708cb7286..0000000000 Binary files a/public/images/star_fav_empty_icon&16.png and /dev/null differ diff --git a/public/images/star_fav_icon&16.png b/public/images/star_fav_icon&16.png deleted file mode 100644 index 3964ca1758..0000000000 Binary files a/public/images/star_fav_icon&16.png and /dev/null differ diff --git a/public/images/tagL.png b/public/images/tagL.png deleted file mode 100644 index a14b11b880..0000000000 Binary files a/public/images/tagL.png and /dev/null differ diff --git a/public/images/tag_icon&16.png b/public/images/tag_icon&16.png deleted file mode 100644 index 26dd94e940..0000000000 Binary files a/public/images/tag_icon&16.png and /dev/null differ diff --git a/public/images/trash_icon&16.png b/public/images/trash_icon&16.png deleted file mode 100644 index b0ef5fc270..0000000000 Binary files a/public/images/trash_icon&16.png and /dev/null differ diff --git a/public/images/twitter_2_icon&16.png b/public/images/twitter_2_icon&16.png deleted file mode 100644 index 29f4796151..0000000000 Binary files a/public/images/twitter_2_icon&16.png and /dev/null differ diff --git a/public/images/ucloud.png b/public/images/ucloud.png new file mode 100644 index 0000000000..b1e133ec7d Binary files /dev/null and b/public/images/ucloud.png differ diff --git a/public/images/user_icon&16.png b/public/images/user_icon&16.png deleted file mode 100644 index 2cab254227..0000000000 Binary files a/public/images/user_icon&16.png and /dev/null differ diff --git a/public/images/user_icon&48.png b/public/images/user_icon&48.png deleted file mode 100644 index d74238cfc5..0000000000 Binary files a/public/images/user_icon&48.png and /dev/null differ diff --git a/public/images/user_icon&48_bak.png b/public/images/user_icon&48_bak.png deleted file mode 100644 index 76403e124b..0000000000 Binary files a/public/images/user_icon&48_bak.png and /dev/null differ diff --git a/public/images/user_icon&48_bak2.png b/public/images/user_icon&48_bak2.png deleted file mode 100644 index 2d75cacef6..0000000000 Binary files a/public/images/user_icon&48_bak2.png and /dev/null differ diff --git a/public/images/user_icon_48.png b/public/images/user_icon_48.png deleted file mode 100644 index d74238cfc5..0000000000 Binary files a/public/images/user_icon_48.png and /dev/null differ diff --git a/public/images/users_icon&16.png b/public/images/users_icon&16.png deleted file mode 100644 index ec415407c6..0000000000 Binary files a/public/images/users_icon&16.png and /dev/null differ diff --git a/public/images/wp_logo.png b/public/images/wp_logo.png deleted file mode 100644 index 224f7c8da3..0000000000 Binary files a/public/images/wp_logo.png and /dev/null differ diff --git a/public/images/wrapper_bg.jpg b/public/images/wrapper_bg.jpg deleted file mode 100644 index 587f7d03c3..0000000000 Binary files a/public/images/wrapper_bg.jpg and /dev/null differ diff --git a/public/images/wrapper_bg.png b/public/images/wrapper_bg.png deleted file mode 100644 index 5ab7b06176..0000000000 Binary files a/public/images/wrapper_bg.png and /dev/null differ diff --git a/public/images/wrapper_bg_1.png b/public/images/wrapper_bg_1.png deleted file mode 100644 index 68b8c2fc64..0000000000 Binary files a/public/images/wrapper_bg_1.png and /dev/null differ diff --git a/public/images/wrapper_bg_2.jpg b/public/images/wrapper_bg_2.jpg deleted file mode 100644 index b64d155101..0000000000 Binary files a/public/images/wrapper_bg_2.jpg and /dev/null differ diff --git a/public/images/wrapper_bg_2.png b/public/images/wrapper_bg_2.png deleted file mode 100644 index 3134aa92ab..0000000000 Binary files a/public/images/wrapper_bg_2.png and /dev/null differ diff --git a/public/images/wrapper_bg_3.png b/public/images/wrapper_bg_3.png deleted file mode 100644 index 2140cce989..0000000000 Binary files a/public/images/wrapper_bg_3.png and /dev/null differ diff --git a/public/images/wrapper_bg_4.png b/public/images/wrapper_bg_4.png deleted file mode 100644 index 7e2fb7fe75..0000000000 Binary files a/public/images/wrapper_bg_4.png and /dev/null differ diff --git a/public/images/wrapper_bg_5.png b/public/images/wrapper_bg_5.png deleted file mode 100644 index b1f58de3a1..0000000000 Binary files a/public/images/wrapper_bg_5.png and /dev/null differ diff --git a/public/images/wrench_icon&16.png b/public/images/wrench_icon&16.png deleted file mode 100644 index 457611726e..0000000000 Binary files a/public/images/wrench_icon&16.png and /dev/null differ diff --git a/public/img/glyphicons-halflings-white.png b/public/img/glyphicons-halflings-white.png new file mode 100644 index 0000000000..3bf6484a29 Binary files /dev/null and b/public/img/glyphicons-halflings-white.png differ diff --git a/public/img/glyphicons-halflings.png b/public/img/glyphicons-halflings.png new file mode 100644 index 0000000000..a996999320 Binary files /dev/null and b/public/img/glyphicons-halflings.png differ diff --git a/public/javascripts/google_search_preview.js b/public/javascripts/google_search_preview.js deleted file mode 100644 index 3b46a4e7c8..0000000000 --- a/public/javascripts/google_search_preview.js +++ /dev/null @@ -1,44 +0,0 @@ -$(function () { - var id = -1; - var start = function () { - id = setInterval(check, 1000); - }; - var old = ''; - var check = function () { - var q = $in.val().trim(); - if (q === '' || old === q) { - return; - } - old = q; - var url = 'http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=site:cnodejs.org+' + q + '&callback=?'; - $.getJSON(url, function (d) { - if (!(d.responseData && Array.isArray(d.responseData.results))) { - return; - } - var list = d.responseData.results; - showList(list); - }); - }; - var stop = function () { - clearInterval(id); - $list.slideUp(500); - }; - var $in = $('input#q'); - $in.focusin(start).focusout(stop); - $in.after('') - .after(''); - var $list = $('#__quick_search_list'); - var showList = function (list) { - var html = ''; - list.forEach(function (line) { - html += '
' + line.title + '' + - '' + line.content + '
'; - }); - if (!html) { - html = '暂时没有相关结果。'; - } - var o1 = $in.offset(); - var o2 = {top: o1.top + $in.height() + 10, left: o1.left}; - $list.offset(o2).html(html).show(); - }; -}); \ No newline at end of file diff --git a/public/javascripts/main.js b/public/javascripts/main.js index bf792d0a8f..6abe2fc380 100644 --- a/public/javascripts/main.js +++ b/public/javascripts/main.js @@ -1,27 +1,21 @@ $(document).ready(function () { - $('#search_form').submit(function (e) { - //e.preventDefault(); - search(); - }); - - function search() { - var q = document.getElementById('q'); - if (q.value) { - /* - var hostname = window.location.hostname; - var url = 'http://www.google.com/search?q=site:' + hostname + '%20'; - window.open(url + q.value, '_blank'); - */ - return true; - } else { - return false; - } - } - - var $wrapper = $('#wrapper'); + var windowHeight = $(window).height(); var $backtotop = $('#backtotop'); - var top = $(window).height() - $backtotop.height() - 90; - $backtotop.css({ top: top, right: 100 }); + var top = windowHeight - $backtotop.height() - 200; + + + function moveBacktotop() { + $backtotop.css({ top: top, right: 0}); + } + + function footerFixBottom() { + if($(document.body).height() < windowHeight){ + $("#footer").addClass('fix-bottom'); + }else{ + $("#footer").removeClass('fix-bottom'); + } + } + $backtotop.click(function () { $('html,body').animate({ scrollTop: 0 }); return false; @@ -35,5 +29,25 @@ $(document).ready(function () { } }); + moveBacktotop(); + footerFixBottom(); + $(window).resize(moveBacktotop); + $(window).resize(footerFixBottom); + $('.topic_content a,.reply_content a').attr('target', '_blank'); + + // pretty code + prettyPrint(); + + // data-loading-text="提交中" + $('.submit_btn').click(function () { + $(this).button('loading'); + }); + + // 广告的统计信息 + $('.sponsor_outlink').click(function () { + var $this = $(this); + var label = $this.data('label'); + ga('send', 'event', 'banner', 'click', label, 1.00, {'nonInteraction': 1}); + }); }); diff --git a/public/javascripts/responsive.js b/public/javascripts/responsive.js new file mode 100644 index 0000000000..a5f60ba61f --- /dev/null +++ b/public/javascripts/responsive.js @@ -0,0 +1,71 @@ +$(document).ready(function () { + var $responsiveBtn = $('#responsive-sidebar-trigger'), + $sidebarMask = $('#sidebar-mask'), + $sidebar = $('#sidebar'), + $main = $('#main'), + winWidth = $(window).width(), + startX = 0, + startY = 0, + delta = { + x: 0, + y: 0 + }, + swipeThreshold = winWidth / 3, + toggleSideBar = function () { + var isShow = $responsiveBtn.data('is-show'), + mainHeight = $main.height(), + sidebarHeight = $sidebar.outerHeight(); + $sidebar.css({right: isShow ? -300 : 0}); + $responsiveBtn.data('is-show', !isShow); + if (!isShow && mainHeight < sidebarHeight) { + $main.height(sidebarHeight); + } + $sidebarMask[isShow ? 'fadeOut' : 'fadeIn']().height($('body').height()); + $sidebar[isShow ? 'hide' : 'show']() + }, + touchstart = function (e) { + var touchs = e.targetTouches; + startX = +touchs[0].pageX; + startY = +touchs[0].pageY; + delta.x = delta.y = 0; + document.body.addEventListener('touchmove', touchmove, false); + document.body.addEventListener('touchend', touchend, false); + }, + touchmove = function (e) { + var touchs = e.changedTouches; + delta.x = +touchs[0].pageX - startX; + delta.y = +touchs[0].pageY - startY; + //当水平距离大于垂直距离时,才认为是用户想滑动打开右侧栏 + if (Math.abs(delta.x) > Math.abs(delta.y)) { + e.preventDefault(); + } + }, + touchend = function (e) { + var touchs = e.changedTouches, + isShow = $responsiveBtn.data('is-show'); + delta.x = +touchs[0].pageX - startX; + //右侧栏未显示&&用户touch点在屏幕右侧1/4区域内&&move距离大于阀值时,打开右侧栏 + if (!isShow && (startX > winWidth * 3 / 4) && Math.abs(delta.x) > swipeThreshold) { + $responsiveBtn.trigger('click'); + } + //右侧栏显示中&&用户touch点在屏幕左侧侧1/4区域内&&move距离大于阀值时,关闭右侧栏 + if (isShow && (startX < winWidth * 1 / 4) && Math.abs(delta.x) > swipeThreshold) { + $responsiveBtn.trigger('click'); + } + startX = startY = 0; + delta.x = delta.y = 0; + document.body.removeEventListener('touchmove', touchmove, false); + document.body.removeEventListener('touchend', touchend, false); + }; + + if (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) { + document.body.addEventListener('touchstart', touchstart); + } + + $responsiveBtn.on('click', toggleSideBar); + + $sidebarMask.on('click', function () { + $responsiveBtn.trigger('click'); + }); + +}); diff --git a/public/libs/ajax-upload/ajaxupload.js b/public/libs/ajax-upload/ajaxupload.js deleted file mode 100644 index c010a5e3f2..0000000000 --- a/public/libs/ajax-upload/ajaxupload.js +++ /dev/null @@ -1,682 +0,0 @@ -/** - * AJAX Upload ( http://valums.com/ajax-upload/ ) - * Copyright (c) Andrew Valums - * Licensed under the MIT license - */ -(function () { - /** - * Attaches event to a dom element. - * @param {Element} el - * @param type event name - * @param fn callback This refers to the passed element - */ - function addEvent(el, type, fn){ - if (el.addEventListener) { - el.addEventListener(type, fn, false); - } else if (el.attachEvent) { - el.attachEvent('on' + type, function(){ - fn.call(el); - }); - } else { - throw new Error('not supported or DOM not loaded'); - } - } - - /** - * Attaches resize event to a window, limiting - * number of event fired. Fires only when encounteres - * delay of 100 after series of events. - * - * Some browsers fire event multiple times when resizing - * http://www.quirksmode.org/dom/events/resize.html - * - * @param fn callback This refers to the passed element - */ - function addResizeEvent(fn){ - var timeout; - - addEvent(window, 'resize', function(){ - if (timeout){ - clearTimeout(timeout); - } - timeout = setTimeout(fn, 100); - }); - } - - // Needs more testing, will be rewriten for next version - // getOffset function copied from jQuery lib (http://jquery.com/) - if (document.documentElement.getBoundingClientRect){ - // Get Offset using getBoundingClientRect - // http://ejohn.org/blog/getboundingclientrect-is-awesome/ - var getOffset = function(el){ - var box = el.getBoundingClientRect(); - var doc = el.ownerDocument; - var body = doc.body; - var docElem = doc.documentElement; // for ie - var clientTop = docElem.clientTop || body.clientTop || 0; - var clientLeft = docElem.clientLeft || body.clientLeft || 0; - - // In Internet Explorer 7 getBoundingClientRect property is treated as physical, - // while others are logical. Make all logical, like in IE8. - var zoom = 1; - if (body.getBoundingClientRect) { - var bound = body.getBoundingClientRect(); - zoom = (bound.right - bound.left) / body.clientWidth; - } - - if (zoom > 1) { - clientTop = 0; - clientLeft = 0; - } - - var top = box.top / zoom + (window.pageYOffset || docElem && docElem.scrollTop / zoom || body.scrollTop / zoom) - clientTop, left = box.left / zoom + (window.pageXOffset || docElem && docElem.scrollLeft / zoom || body.scrollLeft / zoom) - clientLeft; - - return { - top: top, - left: left - }; - }; - } else { - // Get offset adding all offsets - var getOffset = function(el){ - var top = 0, left = 0; - do { - top += el.offsetTop || 0; - left += el.offsetLeft || 0; - el = el.offsetParent; - } while (el); - - return { - left: left, - top: top - }; - }; - } - - /** - * Returns left, top, right and bottom properties describing the border-box, - * in pixels, with the top-left relative to the body - * @param {Element} el - * @return {Object} Contains left, top, right,bottom - */ - function getBox(el){ - var left, right, top, bottom; - var offset = getOffset(el); - left = offset.left; - top = offset.top; - - right = left + el.offsetWidth; - bottom = top + el.offsetHeight; - - return { - left: left, - right: right, - top: top, - bottom: bottom - }; - } - - /** - * Helper that takes object literal - * and add all properties to element.style - * @param {Element} el - * @param {Object} styles - */ - function addStyles(el, styles){ - for (var name in styles) { - if (styles.hasOwnProperty(name)) { - el.style[name] = styles[name]; - } - } - } - - /** - * Function places an absolutely positioned - * element on top of the specified element - * copying position and dimentions. - * @param {Element} from - * @param {Element} to - */ - function copyLayout(from, to){ - var box = getBox(from); - - addStyles(to, { - position: 'absolute', - left : box.left + 'px', - top : box.top + 'px', - width : from.offsetWidth + 'px', - height : from.offsetHeight + 'px' - }); - } - - /** - * Creates and returns element from html chunk - * Uses innerHTML to create an element - */ - var toElement = (function(){ - var div = document.createElement('div'); - return function(html){ - div.innerHTML = html; - var el = div.firstChild; - return div.removeChild(el); - }; - })(); - - /** - * Function generates unique id - * @return unique id - */ - var getUID = (function(){ - var id = 0; - return function(){ - return 'ValumsAjaxUpload' + id++; - }; - })(); - - /** - * Get file name from path - * @param {String} file path to file - * @return filename - */ - function fileFromPath(file){ - return file.replace(/.*(\/|\\)/, ""); - } - - /** - * Get file extension lowercase - * @param {String} file name - * @return file extenstion - */ - function getExt(file){ - return (-1 !== file.indexOf('.')) ? file.replace(/.*[.]/, '') : ''; - } - - function hasClass(el, name){ - var re = new RegExp('\\b' + name + '\\b'); - return re.test(el.className); - } - function addClass(el, name){ - if ( ! hasClass(el, name)){ - el.className += ' ' + name; - } - } - function removeClass(el, name){ - var re = new RegExp('\\b' + name + '\\b'); - el.className = el.className.replace(re, ''); - } - - function removeNode(el){ - el.parentNode.removeChild(el); - } - - /** - * Easy styling and uploading - * @constructor - * @param button An element you want convert to - * upload button. Tested dimentions up to 500x500px - * @param {Object} options See defaults below. - */ - window.AjaxUpload = function(button, options){ - this._settings = { - // Location of the server-side upload script - action: 'upload.php', - // File upload name - name: 'userfile', - // Select & upload multiple files at once FF3.6+, Chrome 4+ - multiple: false, - // Additional data to send - data: {}, - // Submit file as soon as it's selected - autoSubmit: true, - // The type of data that you're expecting back from the server. - // html and xml are detected automatically. - // Only useful when you are using json data as a response. - // Set to "json" in that case. - responseType: false, - // Class applied to button when mouse is hovered - hoverClass: 'hover', - // Class applied to button when button is focused - focusClass: 'focus', - // Class applied to button when AU is disabled - disabledClass: 'disabled', - // When user selects a file, useful with autoSubmit disabled - // You can return false to cancel upload - onChange: function(file, extension){ - }, - // Callback to fire before file is uploaded - // You can return false to cancel upload - onSubmit: function(file, extension){ - }, - // Fired when file upload is completed - // WARNING! DO NOT USE "FALSE" STRING AS A RESPONSE! - onComplete: function(file, response){ - } - }; - - // Merge the users options with our defaults - for (var i in options) { - if (options.hasOwnProperty(i)){ - this._settings[i] = options[i]; - } - } - - // button isn't necessary a dom element - if (button.jquery){ - // jQuery object was passed - button = button[0]; - } else if (typeof button == "string") { - if (/^#.*/.test(button)){ - // If jQuery user passes #elementId don't break it - button = button.slice(1); - } - - button = document.getElementById(button); - } - - if ( ! button || button.nodeType !== 1){ - throw new Error("Please make sure that you're passing a valid element"); - } - - if ( button.nodeName.toUpperCase() == 'A'){ - // disable link - addEvent(button, 'click', function(e){ - if (e && e.preventDefault){ - e.preventDefault(); - } else if (window.event){ - window.event.returnValue = false; - } - }); - } - - // DOM element - this._button = button; - // DOM element - this._input = null; - // If disabled clicking on button won't do anything - this._disabled = false; - - // if the button was disabled before refresh if will remain - // disabled in FireFox, let's fix it - this.enable(); - - this._rerouteClicks(); - }; - - // assigning methods to our class - AjaxUpload.prototype = { - setData: function(data){ - this._settings.data = data; - }, - disable: function(){ - addClass(this._button, this._settings.disabledClass); - this._disabled = true; - - var nodeName = this._button.nodeName.toUpperCase(); - if (nodeName == 'INPUT' || nodeName == 'BUTTON'){ - this._button.setAttribute('disabled', 'disabled'); - } - - // hide input - if (this._input){ - if (this._input.parentNode) { - // We use visibility instead of display to fix problem with Safari 4 - // The problem is that the value of input doesn't change if it - // has display none when user selects a file - this._input.parentNode.style.visibility = 'hidden'; - } - } - }, - enable: function(){ - removeClass(this._button, this._settings.disabledClass); - this._button.removeAttribute('disabled'); - this._disabled = false; - - }, - /** - * Creates invisible file input - * that will hover above the button - *
- */ - _createInput: function(){ - var self = this; - - var input = document.createElement("input"); - input.setAttribute('type', 'file'); - input.setAttribute('name', this._settings.name); - if(this._settings.multiple) input.setAttribute('multiple', 'multiple'); - - addStyles(input, { - 'position' : 'absolute', - // in Opera only 'browse' button - // is clickable and it is located at - // the right side of the input - 'right' : 0, - 'margin' : 0, - 'padding' : 0, - 'fontSize' : '480px', - // in Firefox if font-family is set to - // 'inherit' the input doesn't work - 'fontFamily' : 'sans-serif', - 'cursor' : 'pointer' - }); - - var div = document.createElement("div"); - addStyles(div, { - 'display' : 'block', - 'position' : 'absolute', - 'overflow' : 'hidden', - 'margin' : 0, - 'padding' : 0, - 'opacity' : 0, - // Make sure browse button is in the right side - // in Internet Explorer - 'direction' : 'ltr', - //Max zIndex supported by Opera 9.0-9.2 - 'zIndex': 2147483583 - }); - - // Make sure that element opacity exists. - // Otherwise use IE filter - if ( div.style.opacity !== "0") { - if (typeof(div.filters) == 'undefined'){ - throw new Error('Opacity not supported by the browser'); - } - div.style.filter = "alpha(opacity=0)"; - } - - addEvent(input, 'change', function(){ - - if ( ! input || input.value === ''){ - return; - } - - // Get filename from input, required - // as some browsers have path instead of it - var file = fileFromPath(input.value); - - if (false === self._settings.onChange.call(self, file, getExt(file))){ - self._clearInput(); - return; - } - - // Submit form when value is changed - if (self._settings.autoSubmit) { - self.submit(); - } - }); - - addEvent(input, 'mouseover', function(){ - addClass(self._button, self._settings.hoverClass); - }); - - addEvent(input, 'mouseout', function(){ - removeClass(self._button, self._settings.hoverClass); - removeClass(self._button, self._settings.focusClass); - - if (input.parentNode) { - // We use visibility instead of display to fix problem with Safari 4 - // The problem is that the value of input doesn't change if it - // has display none when user selects a file - input.parentNode.style.visibility = 'hidden'; - } - }); - - addEvent(input, 'focus', function(){ - addClass(self._button, self._settings.focusClass); - }); - - addEvent(input, 'blur', function(){ - removeClass(self._button, self._settings.focusClass); - }); - - div.appendChild(input); - document.body.appendChild(div); - - this._input = input; - }, - _clearInput : function(){ - if (!this._input){ - return; - } - - // this._input.value = ''; Doesn't work in IE6 - removeNode(this._input.parentNode); - this._input = null; - this._createInput(); - - removeClass(this._button, this._settings.hoverClass); - removeClass(this._button, this._settings.focusClass); - }, - /** - * Function makes sure that when user clicks upload button, - * the this._input is clicked instead - */ - _rerouteClicks: function(){ - var self = this; - - // IE will later display 'access denied' error - // if you use using self._input.click() - // other browsers just ignore click() - - addEvent(self._button, 'mouseover', function(){ - if (self._disabled){ - return; - } - - if ( ! self._input){ - self._createInput(); - } - - var div = self._input.parentNode; - copyLayout(self._button, div); - div.style.visibility = 'visible'; - - }); - - - // commented because we now hide input on mouseleave - /** - * When the window is resized the elements - * can be misaligned if button position depends - * on window size - */ - //addResizeEvent(function(){ - // if (self._input){ - // copyLayout(self._button, self._input.parentNode); - // } - //}); - - }, - /** - * Creates iframe with unique name - * @return {Element} iframe - */ - _createIframe: function(){ - // We can't use getTime, because it sometimes return - // same value in safari :( - var id = getUID(); - - // We can't use following code as the name attribute - // won't be properly registered in IE6, and new window - // on form submit will open - // var iframe = document.createElement('iframe'); - // iframe.setAttribute('name', id); - - var iframe = toElement('').appendTo(content); - } - - wrap.show(); - - busy = false; - - $.fancybox.center(); - - currentOpts.onComplete(currentArray, currentIndex, currentOpts); - - _preload_images(); - }, - - _preload_images = function() { - var href, - objNext; - - if ((currentArray.length -1) > currentIndex) { - href = currentArray[ currentIndex + 1 ].href; - - if (typeof href !== 'undefined' && href.match(imgRegExp)) { - objNext = new Image(); - objNext.src = href; - } - } - - if (currentIndex > 0) { - href = currentArray[ currentIndex - 1 ].href; - - if (typeof href !== 'undefined' && href.match(imgRegExp)) { - objNext = new Image(); - objNext.src = href; - } - } - }, - - _draw = function(pos) { - var dim = { - width : parseInt(start_pos.width + (final_pos.width - start_pos.width) * pos, 10), - height : parseInt(start_pos.height + (final_pos.height - start_pos.height) * pos, 10), - - top : parseInt(start_pos.top + (final_pos.top - start_pos.top) * pos, 10), - left : parseInt(start_pos.left + (final_pos.left - start_pos.left) * pos, 10) - }; - - if (typeof final_pos.opacity !== 'undefined') { - dim.opacity = pos < 0.5 ? 0.5 : pos; - } - - wrap.css(dim); - - content.css({ - 'width' : dim.width - currentOpts.padding * 2, - 'height' : dim.height - (titleHeight * pos) - currentOpts.padding * 2 - }); - }, - - _get_viewport = function() { - return [ - $(window).width() - (currentOpts.margin * 2), - $(window).height() - (currentOpts.margin * 2), - $(document).scrollLeft() + currentOpts.margin, - $(document).scrollTop() + currentOpts.margin - ]; - }, - - _get_zoom_to = function () { - var view = _get_viewport(), - to = {}, - resize = currentOpts.autoScale, - double_padding = currentOpts.padding * 2, - ratio; - - if (currentOpts.width.toString().indexOf('%') > -1) { - to.width = parseInt((view[0] * parseFloat(currentOpts.width)) / 100, 10); - } else { - to.width = currentOpts.width + double_padding; - } - - if (currentOpts.height.toString().indexOf('%') > -1) { - to.height = parseInt((view[1] * parseFloat(currentOpts.height)) / 100, 10); - } else { - to.height = currentOpts.height + double_padding; - } - - if (resize && (to.width > view[0] || to.height > view[1])) { - if (selectedOpts.type == 'image' || selectedOpts.type == 'swf') { - ratio = (currentOpts.width ) / (currentOpts.height ); - - if ((to.width ) > view[0]) { - to.width = view[0]; - to.height = parseInt(((to.width - double_padding) / ratio) + double_padding, 10); - } - - if ((to.height) > view[1]) { - to.height = view[1]; - to.width = parseInt(((to.height - double_padding) * ratio) + double_padding, 10); - } - - } else { - to.width = Math.min(to.width, view[0]); - to.height = Math.min(to.height, view[1]); - } - } - - to.top = parseInt(Math.max(view[3] - 20, view[3] + ((view[1] - to.height - 40) * 0.5)), 10); - to.left = parseInt(Math.max(view[2] - 20, view[2] + ((view[0] - to.width - 40) * 0.5)), 10); - - return to; - }, - - _get_obj_pos = function(obj) { - var pos = obj.offset(); - - pos.top += parseInt( obj.css('paddingTop'), 10 ) || 0; - pos.left += parseInt( obj.css('paddingLeft'), 10 ) || 0; - - pos.top += parseInt( obj.css('border-top-width'), 10 ) || 0; - pos.left += parseInt( obj.css('border-left-width'), 10 ) || 0; - - pos.width = obj.width(); - pos.height = obj.height(); - - return pos; - }, - - _get_zoom_from = function() { - var orig = selectedOpts.orig ? $(selectedOpts.orig) : false, - from = {}, - pos, - view; - - if (orig && orig.length) { - pos = _get_obj_pos(orig); - - from = { - width : pos.width + (currentOpts.padding * 2), - height : pos.height + (currentOpts.padding * 2), - top : pos.top - currentOpts.padding - 20, - left : pos.left - currentOpts.padding - 20 - }; - - } else { - view = _get_viewport(); - - from = { - width : currentOpts.padding * 2, - height : currentOpts.padding * 2, - top : parseInt(view[3] + view[1] * 0.5, 10), - left : parseInt(view[2] + view[0] * 0.5, 10) - }; - } - - return from; - }, - - _animate_loading = function() { - if (!loading.is(':visible')){ - clearInterval(loadingTimer); - return; - } - - $('div', loading).css('top', (loadingFrame * -40) + 'px'); - - loadingFrame = (loadingFrame + 1) % 12; - }; - - /* - * Public methods - */ - - $.fn.fancybox = function(options) { - if (!$(this).length) { - return this; - } - - $(this) - .data('fancybox', $.extend({}, options, ($.metadata ? $(this).metadata() : {}))) - .unbind('click.fb') - .bind('click.fb', function(e) { - e.preventDefault(); - - if (busy) { - return; - } - - busy = true; - - $(this).blur(); - - selectedArray = []; - selectedIndex = 0; - - var rel = $(this).attr('rel') || ''; - - if (!rel || rel == '' || rel === 'nofollow') { - selectedArray.push(this); - - } else { - selectedArray = $("a[rel=" + rel + "], area[rel=" + rel + "]"); - selectedIndex = selectedArray.index( this ); - } - - _start(); - - return; - }); - - return this; - }; - - $.fancybox = function(obj) { - var opts; - - if (busy) { - return; - } - - busy = true; - opts = typeof arguments[1] !== 'undefined' ? arguments[1] : {}; - - selectedArray = []; - selectedIndex = parseInt(opts.index, 10) || 0; - - if ($.isArray(obj)) { - for (var i = 0, j = obj.length; i < j; i++) { - if (typeof obj[i] == 'object') { - $(obj[i]).data('fancybox', $.extend({}, opts, obj[i])); - } else { - obj[i] = $({}).data('fancybox', $.extend({content : obj[i]}, opts)); - } - } - - selectedArray = jQuery.merge(selectedArray, obj); - - } else { - if (typeof obj == 'object') { - $(obj).data('fancybox', $.extend({}, opts, obj)); - } else { - obj = $({}).data('fancybox', $.extend({content : obj}, opts)); - } - - selectedArray.push(obj); - } - - if (selectedIndex > selectedArray.length || selectedIndex < 0) { - selectedIndex = 0; - } - - _start(); - }; - - $.fancybox.showActivity = function() { - clearInterval(loadingTimer); - - loading.show(); - loadingTimer = setInterval(_animate_loading, 66); - }; - - $.fancybox.hideActivity = function() { - loading.hide(); - }; - - $.fancybox.next = function() { - return $.fancybox.pos( currentIndex + 1); - }; - - $.fancybox.prev = function() { - return $.fancybox.pos( currentIndex - 1); - }; - - $.fancybox.pos = function(pos) { - if (busy) { - return; - } - - pos = parseInt(pos); - - selectedArray = currentArray; - - if (pos > -1 && pos < currentArray.length) { - selectedIndex = pos; - _start(); - - } else if (currentOpts.cyclic && currentArray.length > 1) { - selectedIndex = pos >= currentArray.length ? 0 : currentArray.length - 1; - _start(); - } - - return; - }; - - $.fancybox.cancel = function() { - if (busy) { - return; - } - - busy = true; - - $.event.trigger('fancybox-cancel'); - - _abort(); - - selectedOpts.onCancel(selectedArray, selectedIndex, selectedOpts); - - busy = false; - }; - - // Note: within an iframe use - parent.$.fancybox.close(); - $.fancybox.close = function() { - if (busy || wrap.is(':hidden')) { - return; - } - - busy = true; - - if (currentOpts && false === currentOpts.onCleanup(currentArray, currentIndex, currentOpts)) { - busy = false; - return; - } - - _abort(); - - $(close.add( nav_left ).add( nav_right )).hide(); - - $(content.add( overlay )).unbind(); - - $(window).unbind("resize.fb scroll.fb"); - $(document).unbind('keydown.fb'); - - content.find('iframe').attr('src', isIE6 && /^https/i.test(window.location.href || '') ? 'javascript:void(false)' : 'about:blank'); - - if (currentOpts.titlePosition !== 'inside') { - title.empty(); - } - - wrap.stop(); - - function _cleanup() { - overlay.fadeOut('fast'); - - title.empty().hide(); - wrap.hide(); - - $.event.trigger('fancybox-cleanup'); - - content.empty(); - - currentOpts.onClosed(currentArray, currentIndex, currentOpts); - - currentArray = selectedOpts = []; - currentIndex = selectedIndex = 0; - currentOpts = selectedOpts = {}; - - busy = false; - } - - if (currentOpts.transitionOut == 'elastic') { - start_pos = _get_zoom_from(); - - var pos = wrap.position(); - - final_pos = { - top : pos.top , - left : pos.left, - width : wrap.width(), - height : wrap.height() - }; - - if (currentOpts.opacity) { - final_pos.opacity = 1; - } - - title.empty().hide(); - - fx.prop = 1; - - $(fx).animate({ prop: 0 }, { - duration : currentOpts.speedOut, - easing : currentOpts.easingOut, - step : _draw, - complete : _cleanup - }); - - } else { - wrap.fadeOut( currentOpts.transitionOut == 'none' ? 0 : currentOpts.speedOut, _cleanup); - } - }; - - $.fancybox.resize = function() { - if (overlay.is(':visible')) { - overlay.css('height', $(document).height()); - } - - $.fancybox.center(true); - }; - - $.fancybox.center = function() { - var view, align; - - if (busy) { - return; - } - - align = arguments[0] === true ? 1 : 0; - view = _get_viewport(); - - if (!align && (wrap.width() > view[0] || wrap.height() > view[1])) { - return; - } - - wrap - .stop() - .animate({ - 'top' : parseInt(Math.max(view[3] - 20, view[3] + ((view[1] - content.height() - 40) * 0.5) - currentOpts.padding)), - 'left' : parseInt(Math.max(view[2] - 20, view[2] + ((view[0] - content.width() - 40) * 0.5) - currentOpts.padding)) - }, typeof arguments[0] == 'number' ? arguments[0] : 200); - }; - - $.fancybox.init = function() { - if ($("#fancybox-wrap").length) { - return; - } - - $('body').append( - tmp = $('
'), - loading = $('
'), - overlay = $('
'), - wrap = $('
') - ); - - outer = $('
') - .append('
') - .appendTo( wrap ); - - outer.append( - content = $('
'), - close = $(''), - title = $('
'), - - nav_left = $(''), - nav_right = $('') - ); - - close.click($.fancybox.close); - loading.click($.fancybox.cancel); - - nav_left.click(function(e) { - e.preventDefault(); - $.fancybox.prev(); - }); - - nav_right.click(function(e) { - e.preventDefault(); - $.fancybox.next(); - }); - - if ($.fn.mousewheel) { - wrap.bind('mousewheel.fb', function(e, delta) { - if (busy) { - e.preventDefault(); - - } else if ($(e.target).get(0).clientHeight == 0 || $(e.target).get(0).scrollHeight === $(e.target).get(0).clientHeight) { - e.preventDefault(); - $.fancybox[ delta > 0 ? 'prev' : 'next'](); - } - }); - } - - if (!$.support.opacity) { - wrap.addClass('fancybox-ie'); - } - - if (isIE6) { - loading.addClass('fancybox-ie6'); - wrap.addClass('fancybox-ie6'); - - $('').prependTo(outer); - } - }; - - $.fn.fancybox.defaults = { - padding : 10, - margin : 40, - opacity : false, - modal : false, - cyclic : false, - scrolling : 'auto', // 'auto', 'yes' or 'no' - - width : 560, - height : 340, - - autoScale : true, - autoDimensions : true, - centerOnScroll : false, - - ajax : {}, - swf : { wmode: 'transparent' }, - - hideOnOverlayClick : true, - hideOnContentClick : false, - - overlayShow : true, - overlayOpacity : 0.7, - overlayColor : '#777', - - titleShow : true, - titlePosition : 'float', // 'float', 'outside', 'inside' or 'over' - titleFormat : null, - titleFromAlt : false, - - transitionIn : 'fade', // 'elastic', 'fade' or 'none' - transitionOut : 'fade', // 'elastic', 'fade' or 'none' - - speedIn : 300, - speedOut : 300, - - changeSpeed : 300, - changeFade : 'fast', - - easingIn : 'swing', - easingOut : 'swing', - - showCloseButton : true, - showNavArrows : true, - enableEscapeButton : true, - enableKeyboardNav : true, - - onStart : function(){}, - onCancel : function(){}, - onComplete : function(){}, - onCleanup : function(){}, - onClosed : function(){}, - onError : function(){} - }; - - $(document).ready(function() { - $.fancybox.init(); - }); - -})(jQuery); \ No newline at end of file diff --git a/public/libs/fancybox/jquery.fancybox-1.3.4.pack.js b/public/libs/fancybox/jquery.fancybox-1.3.4.pack.js deleted file mode 100644 index 1373ed0838..0000000000 --- a/public/libs/fancybox/jquery.fancybox-1.3.4.pack.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * FancyBox - jQuery Plugin - * Simple and fancy lightbox alternative - * - * Examples and documentation at: http://fancybox.net - * - * Copyright (c) 2008 - 2010 Janis Skarnelis - * That said, it is hardly a one-person project. Many people have submitted bugs, code, and offered their advice freely. Their support is greatly appreciated. - * - * Version: 1.3.4 (11/11/2010) - * Requires: jQuery v1.3+ - * - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - */ - -;(function(b){var m,t,u,f,D,j,E,n,z,A,q=0,e={},o=[],p=0,d={},l=[],G=null,v=new Image,J=/\.(jpg|gif|png|bmp|jpeg)(.*)?$/i,W=/[^\.]\.(swf)\s*$/i,K,L=1,y=0,s="",r,i,h=false,B=b.extend(b("
")[0],{prop:0}),M=b.browser.msie&&b.browser.version<7&&!window.XMLHttpRequest,N=function(){t.hide();v.onerror=v.onload=null;G&&G.abort();m.empty()},O=function(){if(false===e.onError(o,q,e)){t.hide();h=false}else{e.titleShow=false;e.width="auto";e.height="auto";m.html('

The requested content cannot be loaded.
Please try again later.

'); -F()}},I=function(){var a=o[q],c,g,k,C,P,w;N();e=b.extend({},b.fn.fancybox.defaults,typeof b(a).data("fancybox")=="undefined"?e:b(a).data("fancybox"));w=e.onStart(o,q,e);if(w===false)h=false;else{if(typeof w=="object")e=b.extend(e,w);k=e.title||(a.nodeName?b(a).attr("title"):a.title)||"";if(a.nodeName&&!e.orig)e.orig=b(a).children("img:first").length?b(a).children("img:first"):b(a);if(k===""&&e.orig&&e.titleFromAlt)k=e.orig.attr("alt");c=e.href||(a.nodeName?b(a).attr("href"):a.href)||null;if(/^(?:javascript)/i.test(c)|| -c=="#")c=null;if(e.type){g=e.type;if(!c)c=e.content}else if(e.content)g="html";else if(c)g=c.match(J)?"image":c.match(W)?"swf":b(a).hasClass("iframe")?"iframe":c.indexOf("#")===0?"inline":"ajax";if(g){if(g=="inline"){a=c.substr(c.indexOf("#"));g=b(a).length>0?"inline":"ajax"}e.type=g;e.href=c;e.title=k;if(e.autoDimensions)if(e.type=="html"||e.type=="inline"||e.type=="ajax"){e.width="auto";e.height="auto"}else e.autoDimensions=false;if(e.modal){e.overlayShow=true;e.hideOnOverlayClick=false;e.hideOnContentClick= -false;e.enableEscapeButton=false;e.showCloseButton=false}e.padding=parseInt(e.padding,10);e.margin=parseInt(e.margin,10);m.css("padding",e.padding+e.margin);b(".fancybox-inline-tmp").unbind("fancybox-cancel").bind("fancybox-change",function(){b(this).replaceWith(j.children())});switch(g){case "html":m.html(e.content);F();break;case "inline":if(b(a).parent().is("#fancybox-content")===true){h=false;break}b('
').hide().insertBefore(b(a)).bind("fancybox-cleanup",function(){b(this).replaceWith(j.children())}).bind("fancybox-cancel", -function(){b(this).replaceWith(m.children())});b(a).appendTo(m);F();break;case "image":h=false;b.fancybox.showActivity();v=new Image;v.onerror=function(){O()};v.onload=function(){h=true;v.onerror=v.onload=null;e.width=v.width;e.height=v.height;b("").attr({id:"fancybox-img",src:v.src,alt:e.title}).appendTo(m);Q()};v.src=c;break;case "swf":e.scrolling="no";C='';P="";b.each(e.swf,function(x,H){C+='';P+=" "+x+'="'+H+'"'});C+='";m.html(C);F();break;case "ajax":h=false;b.fancybox.showActivity();e.ajax.win=e.ajax.success;G=b.ajax(b.extend({},e.ajax,{url:c,data:e.ajax.data||{},error:function(x){x.status>0&&O()},success:function(x,H,R){if((typeof R=="object"?R:G).status==200){if(typeof e.ajax.win== -"function"){w=e.ajax.win(c,x,H,R);if(w===false){t.hide();return}else if(typeof w=="string"||typeof w=="object")x=w}m.html(x);F()}}}));break;case "iframe":Q()}}else O()}},F=function(){var a=e.width,c=e.height;a=a.toString().indexOf("%")>-1?parseInt((b(window).width()-e.margin*2)*parseFloat(a)/100,10)+"px":a=="auto"?"auto":a+"px";c=c.toString().indexOf("%")>-1?parseInt((b(window).height()-e.margin*2)*parseFloat(c)/100,10)+"px":c=="auto"?"auto":c+"px";m.wrapInner('
');e.width=m.width();e.height=m.height();Q()},Q=function(){var a,c;t.hide();if(f.is(":visible")&&false===d.onCleanup(l,p,d)){b.event.trigger("fancybox-cancel");h=false}else{h=true;b(j.add(u)).unbind();b(window).unbind("resize.fb scroll.fb");b(document).unbind("keydown.fb");f.is(":visible")&&d.titlePosition!=="outside"&&f.css("height",f.height());l=o;p=q;d=e;if(d.overlayShow){u.css({"background-color":d.overlayColor, -opacity:d.overlayOpacity,cursor:d.hideOnOverlayClick?"pointer":"auto",height:b(document).height()});if(!u.is(":visible")){M&&b("select:not(#fancybox-tmp select)").filter(function(){return this.style.visibility!=="hidden"}).css({visibility:"hidden"}).one("fancybox-cleanup",function(){this.style.visibility="inherit"});u.show()}}else u.hide();i=X();s=d.title||"";y=0;n.empty().removeAttr("style").removeClass();if(d.titleShow!==false){if(b.isFunction(d.titleFormat))a=d.titleFormat(s,l,p,d);else a=s&&s.length? -d.titlePosition=="float"?'
'+s+'
':'
'+s+"
":false;s=a;if(!(!s||s==="")){n.addClass("fancybox-title-"+d.titlePosition).html(s).appendTo("body").show();switch(d.titlePosition){case "inside":n.css({width:i.width-d.padding*2,marginLeft:d.padding,marginRight:d.padding}); -y=n.outerHeight(true);n.appendTo(D);i.height+=y;break;case "over":n.css({marginLeft:d.padding,width:i.width-d.padding*2,bottom:d.padding}).appendTo(D);break;case "float":n.css("left",parseInt((n.width()-i.width-40)/2,10)*-1).appendTo(f);break;default:n.css({width:i.width-d.padding*2,paddingLeft:d.padding,paddingRight:d.padding}).appendTo(f)}}}n.hide();if(f.is(":visible")){b(E.add(z).add(A)).hide();a=f.position();r={top:a.top,left:a.left,width:f.width(),height:f.height()};c=r.width==i.width&&r.height== -i.height;j.fadeTo(d.changeFade,0.3,function(){var g=function(){j.html(m.contents()).fadeTo(d.changeFade,1,S)};b.event.trigger("fancybox-change");j.empty().removeAttr("filter").css({"border-width":d.padding,width:i.width-d.padding*2,height:e.autoDimensions?"auto":i.height-y-d.padding*2});if(c)g();else{B.prop=0;b(B).animate({prop:1},{duration:d.changeSpeed,easing:d.easingChange,step:T,complete:g})}})}else{f.removeAttr("style");j.css("border-width",d.padding);if(d.transitionIn=="elastic"){r=V();j.html(m.contents()); -f.show();if(d.opacity)i.opacity=0;B.prop=0;b(B).animate({prop:1},{duration:d.speedIn,easing:d.easingIn,step:T,complete:S})}else{d.titlePosition=="inside"&&y>0&&n.show();j.css({width:i.width-d.padding*2,height:e.autoDimensions?"auto":i.height-y-d.padding*2}).html(m.contents());f.css(i).fadeIn(d.transitionIn=="none"?0:d.speedIn,S)}}}},Y=function(){if(d.enableEscapeButton||d.enableKeyboardNav)b(document).bind("keydown.fb",function(a){if(a.keyCode==27&&d.enableEscapeButton){a.preventDefault();b.fancybox.close()}else if((a.keyCode== -37||a.keyCode==39)&&d.enableKeyboardNav&&a.target.tagName!=="INPUT"&&a.target.tagName!=="TEXTAREA"&&a.target.tagName!=="SELECT"){a.preventDefault();b.fancybox[a.keyCode==37?"prev":"next"]()}});if(d.showNavArrows){if(d.cyclic&&l.length>1||p!==0)z.show();if(d.cyclic&&l.length>1||p!=l.length-1)A.show()}else{z.hide();A.hide()}},S=function(){if(!b.support.opacity){j.get(0).style.removeAttribute("filter");f.get(0).style.removeAttribute("filter")}e.autoDimensions&&j.css("height","auto");f.css("height","auto"); -s&&s.length&&n.show();d.showCloseButton&&E.show();Y();d.hideOnContentClick&&j.bind("click",b.fancybox.close);d.hideOnOverlayClick&&u.bind("click",b.fancybox.close);b(window).bind("resize.fb",b.fancybox.resize);d.centerOnScroll&&b(window).bind("scroll.fb",b.fancybox.center);if(d.type=="iframe")b('').appendTo(j); -f.show();h=false;b.fancybox.center();d.onComplete(l,p,d);var a,c;if(l.length-1>p){a=l[p+1].href;if(typeof a!=="undefined"&&a.match(J)){c=new Image;c.src=a}}if(p>0){a=l[p-1].href;if(typeof a!=="undefined"&&a.match(J)){c=new Image;c.src=a}}},T=function(a){var c={width:parseInt(r.width+(i.width-r.width)*a,10),height:parseInt(r.height+(i.height-r.height)*a,10),top:parseInt(r.top+(i.top-r.top)*a,10),left:parseInt(r.left+(i.left-r.left)*a,10)};if(typeof i.opacity!=="undefined")c.opacity=a<0.5?0.5:a;f.css(c); -j.css({width:c.width-d.padding*2,height:c.height-y*a-d.padding*2})},U=function(){return[b(window).width()-d.margin*2,b(window).height()-d.margin*2,b(document).scrollLeft()+d.margin,b(document).scrollTop()+d.margin]},X=function(){var a=U(),c={},g=d.autoScale,k=d.padding*2;c.width=d.width.toString().indexOf("%")>-1?parseInt(a[0]*parseFloat(d.width)/100,10):d.width+k;c.height=d.height.toString().indexOf("%")>-1?parseInt(a[1]*parseFloat(d.height)/100,10):d.height+k;if(g&&(c.width>a[0]||c.height>a[1]))if(e.type== -"image"||e.type=="swf"){g=d.width/d.height;if(c.width>a[0]){c.width=a[0];c.height=parseInt((c.width-k)/g+k,10)}if(c.height>a[1]){c.height=a[1];c.width=parseInt((c.height-k)*g+k,10)}}else{c.width=Math.min(c.width,a[0]);c.height=Math.min(c.height,a[1])}c.top=parseInt(Math.max(a[3]-20,a[3]+(a[1]-c.height-40)*0.5),10);c.left=parseInt(Math.max(a[2]-20,a[2]+(a[0]-c.width-40)*0.5),10);return c},V=function(){var a=e.orig?b(e.orig):false,c={};if(a&&a.length){c=a.offset();c.top+=parseInt(a.css("paddingTop"), -10)||0;c.left+=parseInt(a.css("paddingLeft"),10)||0;c.top+=parseInt(a.css("border-top-width"),10)||0;c.left+=parseInt(a.css("border-left-width"),10)||0;c.width=a.width();c.height=a.height();c={width:c.width+d.padding*2,height:c.height+d.padding*2,top:c.top-d.padding-20,left:c.left-d.padding-20}}else{a=U();c={width:d.padding*2,height:d.padding*2,top:parseInt(a[3]+a[1]*0.5,10),left:parseInt(a[2]+a[0]*0.5,10)}}return c},Z=function(){if(t.is(":visible")){b("div",t).css("top",L*-40+"px");L=(L+1)%12}else clearInterval(K)}; -b.fn.fancybox=function(a){if(!b(this).length)return this;b(this).data("fancybox",b.extend({},a,b.metadata?b(this).metadata():{})).unbind("click.fb").bind("click.fb",function(c){c.preventDefault();if(!h){h=true;b(this).blur();o=[];q=0;c=b(this).attr("rel")||"";if(!c||c==""||c==="nofollow")o.push(this);else{o=b("a[rel="+c+"], area[rel="+c+"]");q=o.index(this)}I()}});return this};b.fancybox=function(a,c){var g;if(!h){h=true;g=typeof c!=="undefined"?c:{};o=[];q=parseInt(g.index,10)||0;if(b.isArray(a)){for(var k= -0,C=a.length;ko.length||q<0)q=0;I()}};b.fancybox.showActivity=function(){clearInterval(K);t.show();K=setInterval(Z,66)};b.fancybox.hideActivity=function(){t.hide()};b.fancybox.next=function(){return b.fancybox.pos(p+ -1)};b.fancybox.prev=function(){return b.fancybox.pos(p-1)};b.fancybox.pos=function(a){if(!h){a=parseInt(a);o=l;if(a>-1&&a1){q=a>=l.length?0:l.length-1;I()}}};b.fancybox.cancel=function(){if(!h){h=true;b.event.trigger("fancybox-cancel");N();e.onCancel(o,q,e);h=false}};b.fancybox.close=function(){function a(){u.fadeOut("fast");n.empty().hide();f.hide();b.event.trigger("fancybox-cleanup");j.empty();d.onClosed(l,p,d);l=e=[];p=q=0;d=e={};h=false}if(!(h||f.is(":hidden"))){h= -true;if(d&&false===d.onCleanup(l,p,d))h=false;else{N();b(E.add(z).add(A)).hide();b(j.add(u)).unbind();b(window).unbind("resize.fb scroll.fb");b(document).unbind("keydown.fb");j.find("iframe").attr("src",M&&/^https/i.test(window.location.href||"")?"javascript:void(false)":"about:blank");d.titlePosition!=="inside"&&n.empty();f.stop();if(d.transitionOut=="elastic"){r=V();var c=f.position();i={top:c.top,left:c.left,width:f.width(),height:f.height()};if(d.opacity)i.opacity=1;n.empty().hide();B.prop=1; -b(B).animate({prop:0},{duration:d.speedOut,easing:d.easingOut,step:T,complete:a})}else f.fadeOut(d.transitionOut=="none"?0:d.speedOut,a)}}};b.fancybox.resize=function(){u.is(":visible")&&u.css("height",b(document).height());b.fancybox.center(true)};b.fancybox.center=function(a){var c,g;if(!h){g=a===true?1:0;c=U();!g&&(f.width()>c[0]||f.height()>c[1])||f.stop().animate({top:parseInt(Math.max(c[3]-20,c[3]+(c[1]-j.height()-40)*0.5-d.padding)),left:parseInt(Math.max(c[2]-20,c[2]+(c[0]-j.width()-40)*0.5- -d.padding))},typeof a=="number"?a:200)}};b.fancybox.init=function(){if(!b("#fancybox-wrap").length){b("body").append(m=b('
'),t=b('
'),u=b('
'),f=b('
'));D=b('
').append('
').appendTo(f); -D.append(j=b('
'),E=b(''),n=b('
'),z=b(''),A=b(''));E.click(b.fancybox.close);t.click(b.fancybox.cancel);z.click(function(a){a.preventDefault();b.fancybox.prev()});A.click(function(a){a.preventDefault();b.fancybox.next()}); -b.fn.mousewheel&&f.bind("mousewheel.fb",function(a,c){if(h)a.preventDefault();else if(b(a.target).get(0).clientHeight==0||b(a.target).get(0).scrollHeight===b(a.target).get(0).clientHeight){a.preventDefault();b.fancybox[c>0?"prev":"next"]()}});b.support.opacity||f.addClass("fancybox-ie");if(M){t.addClass("fancybox-ie6");f.addClass("fancybox-ie6");b('').prependTo(D)}}}; -b.fn.fancybox.defaults={padding:10,margin:40,opacity:false,modal:false,cyclic:false,scrolling:"auto",width:560,height:340,autoScale:true,autoDimensions:true,centerOnScroll:false,ajax:{},swf:{wmode:"transparent"},hideOnOverlayClick:true,hideOnContentClick:false,overlayShow:true,overlayOpacity:0.7,overlayColor:"#777",titleShow:true,titlePosition:"float",titleFormat:null,titleFromAlt:false,transitionIn:"fade",transitionOut:"fade",speedIn:300,speedOut:300,changeSpeed:300,changeFade:"fast",easingIn:"swing", -easingOut:"swing",showCloseButton:true,showNavArrows:true,enableEscapeButton:true,enableKeyboardNav:true,onStart:function(){},onCancel:function(){},onComplete:function(){},onCleanup:function(){},onClosed:function(){},onError:function(){}};b(document).ready(function(){b.fancybox.init()})})(jQuery); \ No newline at end of file diff --git a/public/libs/fancybox/jquery.mousewheel-3.0.4.pack.js b/public/libs/fancybox/jquery.mousewheel-3.0.4.pack.js deleted file mode 100644 index cb66588e29..0000000000 --- a/public/libs/fancybox/jquery.mousewheel-3.0.4.pack.js +++ /dev/null @@ -1,14 +0,0 @@ -/*! Copyright (c) 2010 Brandon Aaron (http://brandonaaron.net) -* Licensed under the MIT License (LICENSE.txt). -* -* Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers. -* Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix. -* Thanks to: Seamus Leahy for adding deltaX and deltaY -* -* Version: 3.0.4 -* -* Requires: 1.2.2+ -*/ - -(function(d){function g(a){var b=a||window.event,i=[].slice.call(arguments,1),c=0,h=0,e=0;a=d.event.fix(b);a.type="mousewheel";if(a.wheelDelta)c=a.wheelDelta/120;if(a.detail)c=-a.detail/3;e=c;if(b.axis!==undefined&&b.axis===b.HORIZONTAL_AXIS){e=0;h=-1*c}if(b.wheelDeltaY!==undefined)e=b.wheelDeltaY/120;if(b.wheelDeltaX!==undefined)h=-1*b.wheelDeltaX/120;i.unshift(a,c,h,e);return d.event.handle.apply(this,i)}var f=["DOMMouseScroll","mousewheel"];d.event.special.mousewheel={setup:function(){if(this.addEventListener)for(var a= -f.length;a;)this.addEventListener(f[--a],g,false);else this.onmousewheel=g},teardown:function(){if(this.removeEventListener)for(var a=f.length;a;)this.removeEventListener(f[--a],g,false);else this.onmousewheel=null}};d.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})})(jQuery); \ No newline at end of file diff --git a/public/libs/font-awesome/css/font-awesome.css b/public/libs/font-awesome/css/font-awesome.css new file mode 100644 index 0000000000..c4a53e5f08 --- /dev/null +++ b/public/libs/font-awesome/css/font-awesome.css @@ -0,0 +1,1672 @@ +/*! + * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url('/public/libs/font-awesome/fonts/fontawesome-webfont.eot?v=4.2.0'); + src: url('/public/libs/font-awesome/fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'), url('/public/libs/font-awesome/fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'), url('/public/libs/font-awesome/fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'), url('/public/libs/font-awesome/fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.28571429em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} +.fa-tty:before { + content: "\f1e4"; +} +.fa-binoculars:before { + content: "\f1e5"; +} +.fa-plug:before { + content: "\f1e6"; +} +.fa-slideshare:before { + content: "\f1e7"; +} +.fa-twitch:before { + content: "\f1e8"; +} +.fa-yelp:before { + content: "\f1e9"; +} +.fa-newspaper-o:before { + content: "\f1ea"; +} +.fa-wifi:before { + content: "\f1eb"; +} +.fa-calculator:before { + content: "\f1ec"; +} +.fa-paypal:before { + content: "\f1ed"; +} +.fa-google-wallet:before { + content: "\f1ee"; +} +.fa-cc-visa:before { + content: "\f1f0"; +} +.fa-cc-mastercard:before { + content: "\f1f1"; +} +.fa-cc-discover:before { + content: "\f1f2"; +} +.fa-cc-amex:before { + content: "\f1f3"; +} +.fa-cc-paypal:before { + content: "\f1f4"; +} +.fa-cc-stripe:before { + content: "\f1f5"; +} +.fa-bell-slash:before { + content: "\f1f6"; +} +.fa-bell-slash-o:before { + content: "\f1f7"; +} +.fa-trash:before { + content: "\f1f8"; +} +.fa-copyright:before { + content: "\f1f9"; +} +.fa-at:before { + content: "\f1fa"; +} +.fa-eyedropper:before { + content: "\f1fb"; +} +.fa-paint-brush:before { + content: "\f1fc"; +} +.fa-birthday-cake:before { + content: "\f1fd"; +} +.fa-area-chart:before { + content: "\f1fe"; +} +.fa-pie-chart:before { + content: "\f200"; +} +.fa-line-chart:before { + content: "\f201"; +} +.fa-lastfm:before { + content: "\f202"; +} +.fa-lastfm-square:before { + content: "\f203"; +} +.fa-toggle-off:before { + content: "\f204"; +} +.fa-toggle-on:before { + content: "\f205"; +} +.fa-bicycle:before { + content: "\f206"; +} +.fa-bus:before { + content: "\f207"; +} +.fa-ioxhost:before { + content: "\f208"; +} +.fa-angellist:before { + content: "\f209"; +} +.fa-cc:before { + content: "\f20a"; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} +.fa-meanpath:before { + content: "\f20c"; +} diff --git a/public/libs/font-awesome/fonts/FontAwesome.otf b/public/libs/font-awesome/fonts/FontAwesome.otf new file mode 100644 index 0000000000..81c9ad949b Binary files /dev/null and b/public/libs/font-awesome/fonts/FontAwesome.otf differ diff --git a/public/libs/font-awesome/fonts/fontawesome-webfont.eot b/public/libs/font-awesome/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000000..84677bc0c5 Binary files /dev/null and b/public/libs/font-awesome/fonts/fontawesome-webfont.eot differ diff --git a/public/libs/font-awesome/fonts/fontawesome-webfont.svg b/public/libs/font-awesome/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000000..d907b25ae6 --- /dev/null +++ b/public/libs/font-awesome/fonts/fontawesome-webfont.svg @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/libs/font-awesome/fonts/fontawesome-webfont.ttf b/public/libs/font-awesome/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000000..96a3639cdd Binary files /dev/null and b/public/libs/font-awesome/fonts/fontawesome-webfont.ttf differ diff --git a/public/libs/font-awesome/fonts/fontawesome-webfont.woff b/public/libs/font-awesome/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000000..628b6a52a8 Binary files /dev/null and b/public/libs/font-awesome/fonts/fontawesome-webfont.woff differ diff --git a/public/libs/jquery-2.1.0.js b/public/libs/jquery-2.1.0.js new file mode 100644 index 0000000000..f74c103fde --- /dev/null +++ b/public/libs/jquery-2.1.0.js @@ -0,0 +1,9083 @@ +/*! + * jQuery JavaScript Library v2.1.0 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2014-01-23T21:10Z + */ + +(function (global, factory) { + + if (typeof module === "object" && typeof module.exports === "object") { + // For CommonJS and CommonJS-like environments where a proper window is present, + // execute the factory and get jQuery + // For environments that do not inherently posses a window with a document + // (such as Node.js), expose a jQuery-making factory as module.exports + // This accentuates the need for the creation of a real window + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info + module.exports = global.document ? + factory(global, true) : + function (w) { + if (!w.document) { + throw new Error("jQuery requires a window with a document"); + } + return factory(w); + }; + } else { + factory(global); + } + +// Pass this if window is not defined yet +}(typeof window !== "undefined" ? window : this, function (window, noGlobal) { + +// Can't do this because several apps including ASP.NET trace +// the stack via arguments.caller.callee and Firefox dies if +// you try to trace through "use strict" call chains. (#13335) +// Support: Firefox 18+ +// + + var arr = []; + + var slice = arr.slice; + + var concat = arr.concat; + + var push = arr.push; + + var indexOf = arr.indexOf; + + var class2type = {}; + + var toString = class2type.toString; + + var hasOwn = class2type.hasOwnProperty; + + var trim = "".trim; + + var support = {}; + + + var + // Use the correct document accordingly with window argument (sandbox) + document = window.document, + + version = "2.1.0", + + // Define a local copy of jQuery + jQuery = function (selector, context) { + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init(selector, context); + }, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function (all, letter) { + return letter.toUpperCase(); + }; + + jQuery.fn = jQuery.prototype = { + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // Start with an empty selector + selector: "", + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function () { + return slice.call(this); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function (num) { + return num != null ? + + // Return a 'clean' array + ( num < 0 ? this[ num + this.length ] : this[ num ] ) : + + // Return just the object + slice.call(this); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function (elems) { + + // Build a new jQuery matched element set + var ret = jQuery.merge(this.constructor(), elems); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + ret.context = this.context; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function (callback, args) { + return jQuery.each(this, callback, args); + }, + + map: function (callback) { + return this.pushStack(jQuery.map(this, function (elem, i) { + return callback.call(elem, i, elem); + })); + }, + + slice: function () { + return this.pushStack(slice.apply(this, arguments)); + }, + + first: function () { + return this.eq(0); + }, + + last: function () { + return this.eq(-1); + }, + + eq: function (i) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack(j >= 0 && j < len ? [ this[j] ] : []); + }, + + end: function () { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice + }; + + jQuery.extend = jQuery.fn.extend = function () { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if (typeof target === "boolean") { + deep = target; + + // skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if (typeof target !== "object" && !jQuery.isFunction(target)) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if (i === length) { + target = this; + i--; + } + + for (; i < length; i++) { + // Only deal with non-null/undefined values + if ((options = arguments[ i ]) != null) { + // Extend the base object + for (name in options) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if (target === copy) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if (deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) )) { + if (copyIsArray) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend(deep, clone, copy); + + // Don't bring in undefined values + } else if (copy !== undefined) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; + }; + + jQuery.extend({ + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace(/\D/g, ""), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function (msg) { + throw new Error(msg); + }, + + noop: function () { + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function (obj) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray, + + isWindow: function (obj) { + return obj != null && obj === obj.window; + }, + + isNumeric: function (obj) { + // parseFloat NaNs numeric-cast false positives (null|true|false|"") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + return obj - parseFloat(obj) >= 0; + }, + + isPlainObject: function (obj) { + // Not plain objects: + // - Any object or value whose internal [[Class]] property is not "[object Object]" + // - DOM nodes + // - window + if (jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow(obj)) { + return false; + } + + // Support: Firefox <20 + // The try/catch suppresses exceptions thrown when attempting to access + // the "constructor" property of certain host objects, ie. |window.location| + // https://bugzilla.mozilla.org/show_bug.cgi?id=814622 + try { + if (obj.constructor && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) { + return false; + } + } catch (e) { + return false; + } + + // If the function hasn't returned already, we're confident that + // |obj| is a plain object, created by {} or constructed with new Object + return true; + }, + + isEmptyObject: function (obj) { + var name; + for (name in obj) { + return false; + } + return true; + }, + + type: function (obj) { + if (obj == null) { + return obj + ""; + } + // Support: Android < 4.0, iOS < 6 (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call(obj) ] || "object" : + typeof obj; + }, + + // Evaluates a script in a global context + globalEval: function (code) { + var script, + indirect = eval; + + code = jQuery.trim(code); + + if (code) { + // If the code includes a valid, prologue position + // strict mode pragma, execute code by injecting a + // script tag into the document. + if (code.indexOf("use strict") === 1) { + script = document.createElement("script"); + script.text = code; + document.head.appendChild(script).parentNode.removeChild(script); + } else { + // Otherwise, avoid the DOM node creation, insertion + // and removal by using an indirect global eval + indirect(code); + } + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function (string) { + return string.replace(rmsPrefix, "ms-").replace(rdashAlpha, fcamelCase); + }, + + nodeName: function (elem, name) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + // args is for internal usage only + each: function (obj, callback, args) { + var value, + i = 0, + length = obj.length, + isArray = isArraylike(obj); + + if (args) { + if (isArray) { + for (; i < length; i++) { + value = callback.apply(obj[ i ], args); + + if (value === false) { + break; + } + } + } else { + for (i in obj) { + value = callback.apply(obj[ i ], args); + + if (value === false) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if (isArray) { + for (; i < length; i++) { + value = callback.call(obj[ i ], i, obj[ i ]); + + if (value === false) { + break; + } + } + } else { + for (i in obj) { + value = callback.call(obj[ i ], i, obj[ i ]); + + if (value === false) { + break; + } + } + } + } + + return obj; + }, + + trim: function (text) { + return text == null ? "" : trim.call(text); + }, + + // results is for internal usage only + makeArray: function (arr, results) { + var ret = results || []; + + if (arr != null) { + if (isArraylike(Object(arr))) { + jQuery.merge(ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call(ret, arr); + } + } + + return ret; + }, + + inArray: function (elem, arr, i) { + return arr == null ? -1 : indexOf.call(arr, elem, i); + }, + + merge: function (first, second) { + var len = +second.length, + j = 0, + i = first.length; + + for (; j < len; j++) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function (elems, callback, invert) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for (; i < length; i++) { + callbackInverse = !callback(elems[ i ], i); + if (callbackInverse !== callbackExpect) { + matches.push(elems[ i ]); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function (elems, callback, arg) { + var value, + i = 0, + length = elems.length, + isArray = isArraylike(elems), + ret = []; + + // Go through the array, translating each of the items to their new values + if (isArray) { + for (; i < length; i++) { + value = callback(elems[ i ], i, arg); + + if (value != null) { + ret.push(value); + } + } + + // Go through every key on the object, + } else { + for (i in elems) { + value = callback(elems[ i ], i, arg); + + if (value != null) { + ret.push(value); + } + } + } + + // Flatten any nested arrays + return concat.apply([], ret); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function (fn, context) { + var tmp, args, proxy; + + if (typeof context === "string") { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if (!jQuery.isFunction(fn)) { + return undefined; + } + + // Simulated bind + args = slice.call(arguments, 2); + proxy = function () { + return fn.apply(context || this, args.concat(slice.call(arguments))); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + now: Date.now, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support + }); + +// Populate the class2type map + jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function (i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); + }); + + function isArraylike(obj) { + var length = obj.length, + type = jQuery.type(obj); + + if (type === "function" || jQuery.isWindow(obj)) { + return false; + } + + if (obj.nodeType === 1 && length) { + return true; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; + } + + var Sizzle = + /*! + * Sizzle CSS Selector Engine v1.10.16 + * http://sizzlejs.com/ + * + * Copyright 2013 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2014-01-13 + */ + (function (window) { + + var i, + support, + Expr, + getText, + isXML, + compile, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + -(new Date()), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + sortOrder = function (a, b) { + if (a === b) { + hasDuplicate = true; + } + return 0; + }, + + // General-purpose constants + strundefined = typeof undefined, + MAX_NEGATIVE = 1 << 31, + + // Instance methods + hasOwn = ({}).hasOwnProperty, + arr = [], + pop = arr.pop, + push_native = arr.push, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf if we can't use a native one + indexOf = arr.indexOf || function (elem) { + var i = 0, + len = this.length; + for (; i < len; i++) { + if (this[i] === elem) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + // http://www.w3.org/TR/css3-syntax/#characters + characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", + + // Loosely modeled on CSS identifier characters + // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors + // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = characterEncoding.replace("w", "w#"), + + // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + + "*(?:([*^$|!~]?=)" + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", + + // Prefer arguments quoted, + // then not containing pseudos/brackets, + // then attribute selectors/non-parenthetical expressions, + // then anything else + // These preferences are here to reduce the number of selectors + // needing tokenize in the PSEUDO preFilter + pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace(3, 8) + ")*)|.*)\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rtrim = new RegExp("^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g"), + + rcomma = new RegExp("^" + whitespace + "*," + whitespace + "*"), + rcombinators = new RegExp("^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*"), + + rattributeQuotes = new RegExp("=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g"), + + rpseudo = new RegExp(pseudos), + ridentifier = new RegExp("^" + identifier + "$"), + + matchExpr = { + "ID": new RegExp("^#(" + characterEncoding + ")"), + "CLASS": new RegExp("^\\.(" + characterEncoding + ")"), + "TAG": new RegExp("^(" + characterEncoding.replace("w", "w*") + ")"), + "ATTR": new RegExp("^" + attributes), + "PSEUDO": new RegExp("^" + pseudos), + "CHILD": new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i"), + "bool": new RegExp("^(?:" + booleans + ")$", "i"), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp("^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i") + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + rescape = /'|\\/g, + + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp("\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig"), + funescape = function (_, escaped, escapedWhitespace) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + // Support: Firefox + // Workaround erroneous numeric interpretation of +"0x" + return high !== high || escapedWhitespace ? + escaped : + high < 0 ? + // BMP codepoint + String.fromCharCode(high + 0x10000) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode(high >> 10 | 0xD800, high & 0x3FF | 0xDC00); + }; + +// Optimize for push.apply( _, NodeList ) + try { + push.apply( + (arr = slice.call(preferredDoc.childNodes)), + preferredDoc.childNodes + ); + // Support: Android<4.0 + // Detect silently failing push.apply + arr[ preferredDoc.childNodes.length ].nodeType; + } catch (e) { + push = { apply: arr.length ? + + // Leverage slice if possible + function (target, els) { + push_native.apply(target, slice.call(els)); + } : + + // Support: IE<9 + // Otherwise append directly + function (target, els) { + var j = target.length, + i = 0; + // Can't trust NodeList.length + while ((target[j++] = els[i++])) { + } + target.length = j - 1; + } + }; + } + + function Sizzle(selector, context, results, seed) { + var match, elem, m, nodeType, + // QSA vars + i, groups, old, nid, newContext, newSelector; + + if (( context ? context.ownerDocument || context : preferredDoc ) !== document) { + setDocument(context); + } + + context = context || document; + results = results || []; + + if (!selector || typeof selector !== "string") { + return results; + } + + if ((nodeType = context.nodeType) !== 1 && nodeType !== 9) { + return []; + } + + if (documentIsHTML && !seed) { + + // Shortcuts + if ((match = rquickExpr.exec(selector))) { + // Speed-up: Sizzle("#ID") + if ((m = match[1])) { + if (nodeType === 9) { + elem = context.getElementById(m); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document (jQuery #6963) + if (elem && elem.parentNode) { + // Handle the case where IE, Opera, and Webkit return items + // by name instead of ID + if (elem.id === m) { + results.push(elem); + return results; + } + } else { + return results; + } + } else { + // Context is not a document + if (context.ownerDocument && (elem = context.ownerDocument.getElementById(m)) && + contains(context, elem) && elem.id === m) { + results.push(elem); + return results; + } + } + + // Speed-up: Sizzle("TAG") + } else if (match[2]) { + push.apply(results, context.getElementsByTagName(selector)); + return results; + + // Speed-up: Sizzle(".CLASS") + } else if ((m = match[3]) && support.getElementsByClassName && context.getElementsByClassName) { + push.apply(results, context.getElementsByClassName(m)); + return results; + } + } + + // QSA path + if (support.qsa && (!rbuggyQSA || !rbuggyQSA.test(selector))) { + nid = old = expando; + newContext = context; + newSelector = nodeType === 9 && selector; + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + if (nodeType === 1 && context.nodeName.toLowerCase() !== "object") { + groups = tokenize(selector); + + if ((old = context.getAttribute("id"))) { + nid = old.replace(rescape, "\\$&"); + } else { + context.setAttribute("id", nid); + } + nid = "[id='" + nid + "'] "; + + i = groups.length; + while (i--) { + groups[i] = nid + toSelector(groups[i]); + } + newContext = rsibling.test(selector) && testContext(context.parentNode) || context; + newSelector = groups.join(","); + } + + if (newSelector) { + try { + push.apply(results, + newContext.querySelectorAll(newSelector) + ); + return results; + } catch (qsaError) { + } finally { + if (!old) { + context.removeAttribute("id"); + } + } + } + } + } + + // All others + return select(selector.replace(rtrim, "$1"), context, results, seed); + } + + /** + * Create key-value caches of limited size + * @returns {Function(string, Object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ + function createCache() { + var keys = []; + + function cache(key, value) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if (keys.push(key + " ") > Expr.cacheLength) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key + " " ] = value); + } + + return cache; + } + + /** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ + function markFunction(fn) { + fn[ expando ] = true; + return fn; + } + + /** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ + function assert(fn) { + var div = document.createElement("div"); + + try { + return !!fn(div); + } catch (e) { + return false; + } finally { + // Remove from its parent by default + if (div.parentNode) { + div.parentNode.removeChild(div); + } + // release memory in IE + div = null; + } + } + + /** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ + function addHandle(attrs, handler) { + var arr = attrs.split("|"), + i = attrs.length; + + while (i--) { + Expr.attrHandle[ arr[i] ] = handler; + } + } + + /** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ + function siblingCheck(a, b) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + ( ~b.sourceIndex || MAX_NEGATIVE ) - + ( ~a.sourceIndex || MAX_NEGATIVE ); + + // Use IE sourceIndex if available on both nodes + if (diff) { + return diff; + } + + // Check if b follows a + if (cur) { + while ((cur = cur.nextSibling)) { + if (cur === b) { + return -1; + } + } + } + + return a ? 1 : -1; + } + + /** + * Returns a function to use in pseudos for input types + * @param {String} type + */ + function createInputPseudo(type) { + return function (elem) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; + } + + /** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ + function createButtonPseudo(type) { + return function (elem) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; + } + + /** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ + function createPositionalPseudo(fn) { + return markFunction(function (argument) { + argument = +argument; + return markFunction(function (seed, matches) { + var j, + matchIndexes = fn([], seed.length, argument), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while (i--) { + if (seed[ (j = matchIndexes[i]) ]) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); + } + + /** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ + function testContext(context) { + return context && typeof context.getElementsByTagName !== strundefined && context; + } + +// Expose support vars for convenience + support = Sizzle.support = {}; + + /** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ + isXML = Sizzle.isXML = function (elem) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; + }; + + /** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ + setDocument = Sizzle.setDocument = function (node) { + var hasCompare, + doc = node ? node.ownerDocument || node : preferredDoc, + parent = doc.defaultView; + + // If no document and documentElement is available, return + if (doc === document || doc.nodeType !== 9 || !doc.documentElement) { + return document; + } + + // Set our document + document = doc; + docElem = doc.documentElement; + + // Support tests + documentIsHTML = !isXML(doc); + + // Support: IE>8 + // If iframe document is assigned to "document" variable and if iframe has been reloaded, + // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 + // IE6-8 do not support the defaultView property so parent will be undefined + if (parent && parent !== parent.top) { + // IE11 does not have attachEvent, so all must suffer + if (parent.addEventListener) { + parent.addEventListener("unload", function () { + setDocument(); + }, false); + } else if (parent.attachEvent) { + parent.attachEvent("onunload", function () { + setDocument(); + }); + } + } + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans) + support.attributes = assert(function (div) { + div.className = "i"; + return !div.getAttribute("className"); + }); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert(function (div) { + div.appendChild(doc.createComment("")); + return !div.getElementsByTagName("*").length; + }); + + // Check if getElementsByClassName can be trusted + support.getElementsByClassName = rnative.test(doc.getElementsByClassName) && assert(function (div) { + div.innerHTML = "
"; + + // Support: Safari<4 + // Catch class over-caching + div.firstChild.className = "i"; + // Support: Opera<10 + // Catch gEBCN failure to find non-leading classes + return div.getElementsByClassName("i").length === 2; + }); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert(function (div) { + docElem.appendChild(div).id = expando; + return !doc.getElementsByName || !doc.getElementsByName(expando).length; + }); + + // ID find and filter + if (support.getById) { + Expr.find["ID"] = function (id, context) { + if (typeof context.getElementById !== strundefined && documentIsHTML) { + var m = context.getElementById(id); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }; + Expr.filter["ID"] = function (id) { + var attrId = id.replace(runescape, funescape); + return function (elem) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + // Support: IE6/7 + // getElementById is not reliable as a find shortcut + delete Expr.find["ID"]; + + Expr.filter["ID"] = function (id) { + var attrId = id.replace(runescape, funescape); + return function (elem) { + var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } + + // Tag + Expr.find["TAG"] = support.getElementsByTagName ? + function (tag, context) { + if (typeof context.getElementsByTagName !== strundefined) { + return context.getElementsByTagName(tag); + } + } : + function (tag, context) { + var elem, + tmp = [], + i = 0, + results = context.getElementsByTagName(tag); + + // Filter out possible comments + if (tag === "*") { + while ((elem = results[i++])) { + if (elem.nodeType === 1) { + tmp.push(elem); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find["CLASS"] = support.getElementsByClassName && function (className, context) { + if (typeof context.getElementsByClassName !== strundefined && documentIsHTML) { + return context.getElementsByClassName(className); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See http://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ((support.qsa = rnative.test(doc.querySelectorAll))) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function (div) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + div.innerHTML = ""; + + // Support: IE8, Opera 10-12 + // Nothing should be selected when empty strings follow ^= or $= or *= + if (div.querySelectorAll("[t^='']").length) { + rbuggyQSA.push("[*^$]=" + whitespace + "*(?:''|\"\")"); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if (!div.querySelectorAll("[selected]").length) { + rbuggyQSA.push("\\[" + whitespace + "*(?:value|" + booleans + ")"); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if (!div.querySelectorAll(":checked").length) { + rbuggyQSA.push(":checked"); + } + }); + + assert(function (div) { + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = doc.createElement("input"); + input.setAttribute("type", "hidden"); + div.appendChild(input).setAttribute("name", "D"); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if (div.querySelectorAll("[name=d]").length) { + rbuggyQSA.push("name" + whitespace + "*[*^$|!~]?="); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if (!div.querySelectorAll(":enabled").length) { + rbuggyQSA.push(":enabled", ":disabled"); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ((support.matchesSelector = rnative.test((matches = docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector)))) { + + assert(function (div) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call(div, "div"); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call(div, "[s!='']:x"); + rbuggyMatches.push("!=", pseudos); + }); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp(rbuggyQSA.join("|")); + rbuggyMatches = rbuggyMatches.length && new RegExp(rbuggyMatches.join("|")); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test(docElem.compareDocumentPosition); + + // Element contains another + // Purposefully does not implement inclusive descendent + // As in, an element does not contain itself + contains = hasCompare || rnative.test(docElem.contains) ? + function (a, b) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains(bup) : + a.compareDocumentPosition && a.compareDocumentPosition(bup) & 16 + )); + } : + function (a, b) { + if (b) { + while ((b = b.parentNode)) { + if (b === a) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function (a, b) { + + // Flag for duplicate removal + if (a === b) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if (compare) { + return compare; + } + + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition(b) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if (compare & 1 || + (!support.sortDetached && b.compareDocumentPosition(a) === compare)) { + + // Choose the first element that is related to our preferred document + if (a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a)) { + return -1; + } + if (b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b)) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf.call(sortInput, a) - indexOf.call(sortInput, b) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function (a, b) { + // Exit early if the nodes are identical + if (a === b) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if (!aup || !bup) { + return a === doc ? -1 : + b === doc ? 1 : + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf.call(sortInput, a) - indexOf.call(sortInput, b) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if (aup === bup) { + return siblingCheck(a, b); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ((cur = cur.parentNode)) { + ap.unshift(cur); + } + cur = b; + while ((cur = cur.parentNode)) { + bp.unshift(cur); + } + + // Walk down the tree looking for a discrepancy + while (ap[i] === bp[i]) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck(ap[i], bp[i]) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + return doc; + }; + + Sizzle.matches = function (expr, elements) { + return Sizzle(expr, null, null, elements); + }; + + Sizzle.matchesSelector = function (elem, expr) { + // Set document vars if needed + if (( elem.ownerDocument || elem ) !== document) { + setDocument(elem); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace(rattributeQuotes, "='$1']"); + + if (support.matchesSelector && documentIsHTML && + ( !rbuggyMatches || !rbuggyMatches.test(expr) ) && + ( !rbuggyQSA || !rbuggyQSA.test(expr) )) { + + try { + var ret = matches.call(elem, expr); + + // IE 9's matchesSelector returns false on disconnected nodes + if (ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11) { + return ret; + } + } catch (e) { + } + } + + return Sizzle(expr, document, null, [elem]).length > 0; + }; + + Sizzle.contains = function (context, elem) { + // Set document vars if needed + if (( context.ownerDocument || context ) !== document) { + setDocument(context); + } + return contains(context, elem); + }; + + Sizzle.attr = function (elem, name) { + // Set document vars if needed + if (( elem.ownerDocument || elem ) !== document) { + setDocument(elem); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call(Expr.attrHandle, name.toLowerCase()) ? + fn(elem, name, !documentIsHTML) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute(name) : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null; + }; + + Sizzle.error = function (msg) { + throw new Error("Syntax error, unrecognized expression: " + msg); + }; + + /** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ + Sizzle.uniqueSort = function (results) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice(0); + results.sort(sortOrder); + + if (hasDuplicate) { + while ((elem = results[i++])) { + if (elem === results[ i ]) { + j = duplicates.push(i); + } + } + while (j--) { + results.splice(duplicates[ j ], 1); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; + }; + + /** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ + getText = Sizzle.getText = function (elem) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if (!nodeType) { + // If no nodeType, this is expected to be an array + while ((node = elem[i++])) { + // Do not traverse comment nodes + ret += getText(node); + } + } else if (nodeType === 1 || nodeType === 9 || nodeType === 11) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if (typeof elem.textContent === "string") { + return elem.textContent; + } else { + // Traverse its children + for (elem = elem.firstChild; elem; elem = elem.nextSibling) { + ret += getText(elem); + } + } + } else if (nodeType === 3 || nodeType === 4) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; + }; + + Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function (match) { + match[1] = match[1].replace(runescape, funescape); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[4] || match[5] || "" ).replace(runescape, funescape); + + if (match[2] === "~=") { + match[3] = " " + match[3] + " "; + } + + return match.slice(0, 4); + }, + + "CHILD": function (match) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if (match[1].slice(0, 3) === "nth") { + // nth-* requires argument + if (!match[3]) { + Sizzle.error(match[0]); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if (match[3]) { + Sizzle.error(match[0]); + } + + return match; + }, + + "PSEUDO": function (match) { + var excess, + unquoted = !match[5] && match[2]; + + if (matchExpr["CHILD"].test(match[0])) { + return null; + } + + // Accept quoted arguments as-is + if (match[3] && match[4] !== undefined) { + match[2] = match[4]; + + // Strip excess characters from unquoted arguments + } else if (unquoted && rpseudo.test(unquoted) && + // Get excess from tokenize (recursively) + (excess = tokenize(unquoted, true)) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf(")", unquoted.length - excess) - unquoted.length)) { + + // excess is a negative index + match[0] = match[0].slice(0, excess); + match[2] = unquoted.slice(0, excess); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice(0, 3); + } + }, + + filter: { + + "TAG": function (nodeNameSelector) { + var nodeName = nodeNameSelector.replace(runescape, funescape).toLowerCase(); + return nodeNameSelector === "*" ? + function () { + return true; + } : + function (elem) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function (className) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp("(^|" + whitespace + ")" + className + "(" + whitespace + "|$)")) && + classCache(className, function (elem) { + return pattern.test(typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || ""); + }); + }, + + "ATTR": function (name, operator, check) { + return function (elem) { + var result = Sizzle.attr(elem, name); + + if (result == null) { + return operator === "!="; + } + if (!operator) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf(check) === 0 : + operator === "*=" ? check && result.indexOf(check) > -1 : + operator === "$=" ? check && result.slice(-check.length) === check : + operator === "~=" ? ( " " + result + " " ).indexOf(check) > -1 : + operator === "|=" ? result === check || result.slice(0, check.length + 1) === check + "-" : + false; + }; + }, + + "CHILD": function (type, what, argument, first, last) { + var simple = type.slice(0, 3) !== "nth", + forward = type.slice(-4) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function (elem) { + return !!elem.parentNode; + } : + + function (elem, context, xml) { + var cache, outerCache, node, diff, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType; + + if (parent) { + + // :(first|last|only)-(child|of-type) + if (simple) { + while (dir) { + node = elem; + while ((node = node[ dir ])) { + if (ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1) { + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if (forward && useCache) { + // Seek `elem` from a previously-cached index + outerCache = parent[ expando ] || (parent[ expando ] = {}); + cache = outerCache[ type ] || []; + nodeIndex = cache[0] === dirruns && cache[1]; + diff = cache[0] === dirruns && cache[2]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ((node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop())) { + + // When found, cache indexes on `parent` and break + if (node.nodeType === 1 && ++diff && node === elem) { + outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + // Use previously-cached element index if available + } else if (useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns) { + diff = cache[1]; + + // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) + } else { + // Use the same loop as above to seek `elem` from the start + while ((node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop())) { + + if (( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff) { + // Cache the index of each encountered element + if (useCache) { + (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; + } + + if (node === elem) { + break; + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function (pseudo, argument) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error("unsupported pseudo: " + pseudo); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if (fn[ expando ]) { + return fn(argument); + } + + // But maintain support for old signatures + if (fn.length > 1) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty(pseudo.toLowerCase()) ? + markFunction(function (seed, matches) { + var idx, + matched = fn(seed, argument), + i = matched.length; + while (i--) { + idx = indexOf.call(seed, matched[i]); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function (elem) { + return fn(elem, 0, args); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function (selector) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile(selector.replace(rtrim, "$1")); + + return matcher[ expando ] ? + markFunction(function (seed, matches, context, xml) { + var elem, + unmatched = matcher(seed, null, xml, []), + i = seed.length; + + // Match elements unmatched by `matcher` + while (i--) { + if ((elem = unmatched[i])) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function (elem, context, xml) { + input[0] = elem; + matcher(input, null, xml, results); + return !results.pop(); + }; + }), + + "has": markFunction(function (selector) { + return function (elem) { + return Sizzle(selector, elem).length > 0; + }; + }), + + "contains": markFunction(function (text) { + return function (elem) { + return ( elem.textContent || elem.innerText || getText(elem) ).indexOf(text) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction(function (lang) { + // lang value must be a valid identifier + if (!ridentifier.test(lang || "")) { + Sizzle.error("unsupported lang: " + lang); + } + lang = lang.replace(runescape, funescape).toLowerCase(); + return function (elem) { + var elemLang; + do { + if ((elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute("xml:lang") || elem.getAttribute("lang"))) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf(lang + "-") === 0; + } + } while ((elem = elem.parentNode) && elem.nodeType === 1); + return false; + }; + }), + + // Miscellaneous + "target": function (elem) { + var hash = window.location && window.location.hash; + return hash && hash.slice(1) === elem.id; + }, + + "root": function (elem) { + return elem === docElem; + }, + + "focus": function (elem) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": function (elem) { + return elem.disabled === false; + }, + + "disabled": function (elem) { + return elem.disabled === true; + }, + + "checked": function (elem) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function (elem) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if (elem.parentNode) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function (elem) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for (elem = elem.firstChild; elem; elem = elem.nextSibling) { + if (elem.nodeType < 6) { + return false; + } + } + return true; + }, + + "parent": function (elem) { + return !Expr.pseudos["empty"](elem); + }, + + // Element/input types + "header": function (elem) { + return rheader.test(elem.nodeName); + }, + + "input": function (elem) { + return rinputs.test(elem.nodeName); + }, + + "button": function (elem) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function (elem) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function () { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function (matchIndexes, length) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function (matchIndexes, length, argument) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function (matchIndexes, length) { + var i = 0; + for (; i < length; i += 2) { + matchIndexes.push(i); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function (matchIndexes, length) { + var i = 1; + for (; i < length; i += 2) { + matchIndexes.push(i); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function (matchIndexes, length, argument) { + var i = argument < 0 ? argument + length : argument; + for (; --i >= 0;) { + matchIndexes.push(i); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function (matchIndexes, length, argument) { + var i = argument < 0 ? argument + length : argument; + for (; ++i < length;) { + matchIndexes.push(i); + } + return matchIndexes; + }) + } + }; + + Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Add button/input type pseudos + for (i in { radio: true, checkbox: true, file: true, password: true, image: true }) { + Expr.pseudos[ i ] = createInputPseudo(i); + } + for (i in { submit: true, reset: true }) { + Expr.pseudos[ i ] = createButtonPseudo(i); + } + +// Easy API for creating new setFilters + function setFilters() { + } + + setFilters.prototype = Expr.filters = Expr.pseudos; + Expr.setFilters = new setFilters(); + + function tokenize(selector, parseOnly) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if (cached) { + return parseOnly ? 0 : cached.slice(0); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while (soFar) { + + // Comma and first run + if (!matched || (match = rcomma.exec(soFar))) { + if (match) { + // Don't consume trailing commas as valid + soFar = soFar.slice(match[0].length) || soFar; + } + groups.push((tokens = [])); + } + + matched = false; + + // Combinators + if ((match = rcombinators.exec(soFar))) { + matched = match.shift(); + tokens.push({ + value: matched, + // Cast descendant combinators to space + type: match[0].replace(rtrim, " ") + }); + soFar = soFar.slice(matched.length); + } + + // Filters + for (type in Expr.filter) { + if ((match = matchExpr[ type ].exec(soFar)) && (!preFilters[ type ] || + (match = preFilters[ type ](match)))) { + matched = match.shift(); + tokens.push({ + value: matched, + type: type, + matches: match + }); + soFar = soFar.slice(matched.length); + } + } + + if (!matched) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error(selector) : + // Cache the tokens + tokenCache(selector, groups).slice(0); + } + + function toSelector(tokens) { + var i = 0, + len = tokens.length, + selector = ""; + for (; i < len; i++) { + selector += tokens[i].value; + } + return selector; + } + + function addCombinator(matcher, combinator, base) { + var dir = combinator.dir, + checkNonElements = base && dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function (elem, context, xml) { + while ((elem = elem[ dir ])) { + if (elem.nodeType === 1 || checkNonElements) { + return matcher(elem, context, xml); + } + } + } : + + // Check against all ancestor/preceding elements + function (elem, context, xml) { + var oldCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching + if (xml) { + while ((elem = elem[ dir ])) { + if (elem.nodeType === 1 || checkNonElements) { + if (matcher(elem, context, xml)) { + return true; + } + } + } + } else { + while ((elem = elem[ dir ])) { + if (elem.nodeType === 1 || checkNonElements) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + if ((oldCache = outerCache[ dir ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); + } else { + // Reuse newcache so results back-propagate to previous elements + outerCache[ dir ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ((newCache[ 2 ] = matcher(elem, context, xml))) { + return true; + } + } + } + } + } + }; + } + + function elementMatcher(matchers) { + return matchers.length > 1 ? + function (elem, context, xml) { + var i = matchers.length; + while (i--) { + if (!matchers[i](elem, context, xml)) { + return false; + } + } + return true; + } : + matchers[0]; + } + + function condense(unmatched, map, filter, context, xml) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for (; i < len; i++) { + if ((elem = unmatched[i])) { + if (!filter || filter(elem, context, xml)) { + newUnmatched.push(elem); + if (mapped) { + map.push(i); + } + } + } + } + + return newUnmatched; + } + + function setMatcher(preFilter, selector, matcher, postFilter, postFinder, postSelector) { + if (postFilter && !postFilter[ expando ]) { + postFilter = setMatcher(postFilter); + } + if (postFinder && !postFinder[ expando ]) { + postFinder = setMatcher(postFinder, postSelector); + } + return markFunction(function (seed, results, context, xml) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts(selector || "*", context.nodeType ? [ context ] : context, []), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense(elems, preMap, preFilter, context, xml) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if (matcher) { + matcher(matcherIn, matcherOut, context, xml); + } + + // Apply postFilter + if (postFilter) { + temp = condense(matcherOut, postMap); + postFilter(temp, [], context, xml); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while (i--) { + if ((elem = temp[i])) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if (seed) { + if (postFinder || preFilter) { + if (postFinder) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while (i--) { + if ((elem = matcherOut[i])) { + // Restore matcherIn since elem is not yet a final match + temp.push((matcherIn[i] = elem)); + } + } + postFinder(null, (matcherOut = []), temp, xml); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while (i--) { + if ((elem = matcherOut[i]) && + (temp = postFinder ? indexOf.call(seed, elem) : preMap[i]) > -1) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice(preexisting, matcherOut.length) : + matcherOut + ); + if (postFinder) { + postFinder(null, results, matcherOut, xml); + } else { + push.apply(results, matcherOut); + } + } + }); + } + + function matcherFromTokens(tokens) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator(function (elem) { + return elem === checkContext; + }, implicitRelative, true), + matchAnyContext = addCombinator(function (elem) { + return indexOf.call(checkContext, elem) > -1; + }, implicitRelative, true), + matchers = [ function (elem, context, xml) { + return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext(elem, context, xml) : + matchAnyContext(elem, context, xml) ); + } ]; + + for (; i < len; i++) { + if ((matcher = Expr.relative[ tokens[i].type ])) { + matchers = [ addCombinator(elementMatcher(matchers), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply(null, tokens[i].matches); + + // Return special upon seeing a positional matcher + if (matcher[ expando ]) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for (; j < len; j++) { + if (Expr.relative[ tokens[j].type ]) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher(matchers), + i > 1 && toSelector( + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice(0, i - 1).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) + ).replace(rtrim, "$1"), + matcher, + i < j && matcherFromTokens(tokens.slice(i, j)), + j < len && matcherFromTokens((tokens = tokens.slice(j))), + j < len && toSelector(tokens) + ); + } + matchers.push(matcher); + } + } + + return elementMatcher(matchers); + } + + function matcherFromGroupMatchers(elementMatchers, setMatchers) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function (seed, context, xml, results, outermost) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]("*", outermost), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; + + if (outermost) { + outermostContext = context !== document && context; + } + + // Add elements passing elementMatchers directly to results + // Keep `i` a string if there are no elements so `matchedCount` will be "00" below + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for (; i !== len && (elem = elems[i]) != null; i++) { + if (byElement && elem) { + j = 0; + while ((matcher = elementMatchers[j++])) { + if (matcher(elem, context, xml)) { + results.push(elem); + break; + } + } + if (outermost) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if (bySet) { + // They will have gone through all possible matchers + if ((elem = !matcher && elem)) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if (seed) { + unmatched.push(elem); + } + } + } + + // Apply set filters to unmatched elements + matchedCount += i; + if (bySet && i !== matchedCount) { + j = 0; + while ((matcher = setMatchers[j++])) { + matcher(unmatched, setMatched, context, xml); + } + + if (seed) { + // Reintegrate element matches to eliminate the need for sorting + if (matchedCount > 0) { + while (i--) { + if (!(unmatched[i] || setMatched[i])) { + setMatched[i] = pop.call(results); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense(setMatched); + } + + // Add matches to results + push.apply(results, setMatched); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if (outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1) { + + Sizzle.uniqueSort(results); + } + } + + // Override manipulation of globals by nested matchers + if (outermost) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction(superMatcher) : + superMatcher; + } + + compile = Sizzle.compile = function (selector, group /* Internal Use Only */) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if (!cached) { + // Generate a function of recursive functions that can be used to check each element + if (!group) { + group = tokenize(selector); + } + i = group.length; + while (i--) { + cached = matcherFromTokens(group[i]); + if (cached[ expando ]) { + setMatchers.push(cached); + } else { + elementMatchers.push(cached); + } + } + + // Cache the compiled function + cached = compilerCache(selector, matcherFromGroupMatchers(elementMatchers, setMatchers)); + } + return cached; + }; + + function multipleContexts(selector, contexts, results) { + var i = 0, + len = contexts.length; + for (; i < len; i++) { + Sizzle(selector, contexts[i], results); + } + return results; + } + + function select(selector, context, results, seed) { + var i, tokens, token, type, find, + match = tokenize(selector); + + if (!seed) { + // Try to minimize operations if there is only one group + if (match.length === 1) { + + // Take a shortcut and set the context if the root selector is an ID + tokens = match[0] = match[0].slice(0); + if (tokens.length > 2 && (token = tokens[0]).type === "ID" && + support.getById && context.nodeType === 9 && documentIsHTML && + Expr.relative[ tokens[1].type ]) { + + context = ( Expr.find["ID"](token.matches[0].replace(runescape, funescape), context) || [] )[0]; + if (!context) { + return results; + } + selector = selector.slice(tokens.shift().value.length); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test(selector) ? 0 : tokens.length; + while (i--) { + token = tokens[i]; + + // Abort if we hit a combinator + if (Expr.relative[ (type = token.type) ]) { + break; + } + if ((find = Expr.find[ type ])) { + // Search, expanding context for leading sibling combinators + if ((seed = find( + token.matches[0].replace(runescape, funescape), + rsibling.test(tokens[0].type) && testContext(context.parentNode) || context + ))) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice(i, 1); + selector = seed.length && toSelector(tokens); + if (!selector) { + push.apply(results, seed); + return results; + } + + break; + } + } + } + } + } + + // Compile and execute a filtering function + // Provide `match` to avoid retokenization if we modified the selector above + compile(selector, match)( + seed, + context, + !documentIsHTML, + results, + rsibling.test(selector) && testContext(context.parentNode) || context + ); + return results; + } + +// One-time assignments + +// Sort stability + support.sortStable = expando.split("").sort(sortOrder).join("") === expando; + +// Support: Chrome<14 +// Always assume duplicates if they aren't passed to the comparison function + support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document + setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* + support.sortDetached = assert(function (div1) { + // Should return 1, but returns 4 (following) + return div1.compareDocumentPosition(document.createElement("div")) & 1; + }); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx + if (!assert(function (div) { + div.innerHTML = ""; + return div.firstChild.getAttribute("href") === "#"; + })) { + addHandle("type|href|height|width", function (elem, name, isXML) { + if (!isXML) { + return elem.getAttribute(name, name.toLowerCase() === "type" ? 1 : 2); + } + }); + } + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") + if (!support.attributes || !assert(function (div) { + div.innerHTML = ""; + div.firstChild.setAttribute("value", ""); + return div.firstChild.getAttribute("value") === ""; + })) { + addHandle("value", function (elem, name, isXML) { + if (!isXML && elem.nodeName.toLowerCase() === "input") { + return elem.defaultValue; + } + }); + } + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies + if (!assert(function (div) { + return div.getAttribute("disabled") == null; + })) { + addHandle(booleans, function (elem, name, isXML) { + var val; + if (!isXML) { + return elem[ name ] === true ? name.toLowerCase() : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null; + } + }); + } + + return Sizzle; + + })(window); + + + jQuery.find = Sizzle; + jQuery.expr = Sizzle.selectors; + jQuery.expr[":"] = jQuery.expr.pseudos; + jQuery.unique = Sizzle.uniqueSort; + jQuery.text = Sizzle.getText; + jQuery.isXMLDoc = Sizzle.isXML; + jQuery.contains = Sizzle.contains; + + + var rneedsContext = jQuery.expr.match.needsContext; + + var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/); + + + var risSimple = /^.[^:#\[\.,]*$/; + +// Implement the identical functionality for filter and not + function winnow(elements, qualifier, not) { + if (jQuery.isFunction(qualifier)) { + return jQuery.grep(elements, function (elem, i) { + /* jshint -W018 */ + return !!qualifier.call(elem, i, elem) !== not; + }); + + } + + if (qualifier.nodeType) { + return jQuery.grep(elements, function (elem) { + return ( elem === qualifier ) !== not; + }); + + } + + if (typeof qualifier === "string") { + if (risSimple.test(qualifier)) { + return jQuery.filter(qualifier, elements, not); + } + + qualifier = jQuery.filter(qualifier, elements); + } + + return jQuery.grep(elements, function (elem) { + return ( indexOf.call(qualifier, elem) >= 0 ) !== not; + }); + } + + jQuery.filter = function (expr, elems, not) { + var elem = elems[ 0 ]; + + if (not) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector(elem, expr) ? [ elem ] : [] : + jQuery.find.matches(expr, jQuery.grep(elems, function (elem) { + return elem.nodeType === 1; + })); + }; + + jQuery.fn.extend({ + find: function (selector) { + var i, + len = this.length, + ret = [], + self = this; + + if (typeof selector !== "string") { + return this.pushStack(jQuery(selector).filter(function () { + for (i = 0; i < len; i++) { + if (jQuery.contains(self[ i ], this)) { + return true; + } + } + })); + } + + for (i = 0; i < len; i++) { + jQuery.find(selector, self[ i ], ret); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack(len > 1 ? jQuery.unique(ret) : ret); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + filter: function (selector) { + return this.pushStack(winnow(this, selector || [], false)); + }, + not: function (selector) { + return this.pushStack(winnow(this, selector || [], true)); + }, + is: function (selector) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test(selector) ? + jQuery(selector) : + selector || [], + false + ).length; + } + }); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) + var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + init = jQuery.fn.init = function (selector, context) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if (!selector) { + return this; + } + + // Handle HTML strings + if (typeof selector === "string") { + if (selector[0] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec(selector); + } + + // Match html or make sure no context is specified for #id + if (match && (match[1] || !context)) { + + // HANDLE: $(html) -> $(array) + if (match[1]) { + context = context instanceof jQuery ? context[0] : context; + + // scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge(this, jQuery.parseHTML( + match[1], + context && context.nodeType ? context.ownerDocument || context : document, + true + )); + + // HANDLE: $(html, props) + if (rsingleTag.test(match[1]) && jQuery.isPlainObject(context)) { + for (match in context) { + // Properties of context are called as methods if possible + if (jQuery.isFunction(this[ match ])) { + this[ match ](context[ match ]); + + // ...and otherwise set as attributes + } else { + this.attr(match, context[ match ]); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById(match[2]); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if (elem && elem.parentNode) { + // Inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if (!context || context.jquery) { + return ( context || rootjQuery ).find(selector); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor(context).find(selector); + } + + // HANDLE: $(DOMElement) + } else if (selector.nodeType) { + this.context = this[0] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if (jQuery.isFunction(selector)) { + return typeof rootjQuery.ready !== "undefined" ? + rootjQuery.ready(selector) : + // Execute immediately if ready is not present + selector(jQuery); + } + + if (selector.selector !== undefined) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray(selector, this); + }; + +// Give the init function the jQuery prototype for later instantiation + init.prototype = jQuery.fn; + +// Initialize central reference + rootjQuery = jQuery(document); + + + var rparentsprev = /^(?:parents|prev(?:Until|All))/, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + + jQuery.extend({ + dir: function (elem, dir, until) { + var matched = [], + truncate = until !== undefined; + + while ((elem = elem[ dir ]) && elem.nodeType !== 9) { + if (elem.nodeType === 1) { + if (truncate && jQuery(elem).is(until)) { + break; + } + matched.push(elem); + } + } + return matched; + }, + + sibling: function (n, elem) { + var matched = []; + + for (; n; n = n.nextSibling) { + if (n.nodeType === 1 && n !== elem) { + matched.push(n); + } + } + + return matched; + } + }); + + jQuery.fn.extend({ + has: function (target) { + var targets = jQuery(target, this), + l = targets.length; + + return this.filter(function () { + var i = 0; + for (; i < l; i++) { + if (jQuery.contains(this, targets[i])) { + return true; + } + } + }); + }, + + closest: function (selectors, context) { + var cur, + i = 0, + l = this.length, + matched = [], + pos = rneedsContext.test(selectors) || typeof selectors !== "string" ? + jQuery(selectors, context || this.context) : + 0; + + for (; i < l; i++) { + for (cur = this[i]; cur && cur !== context; cur = cur.parentNode) { + // Always skip document fragments + if (cur.nodeType < 11 && (pos ? + pos.index(cur) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector(cur, selectors))) { + + matched.push(cur); + break; + } + } + } + + return this.pushStack(matched.length > 1 ? jQuery.unique(matched) : matched); + }, + + // Determine the position of an element within + // the matched set of elements + index: function (elem) { + + // No argument, return index in parent + if (!elem) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // index in selector + if (typeof elem === "string") { + return indexOf.call(jQuery(elem), this[ 0 ]); + } + + // Locate the position of the desired element + return indexOf.call(this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function (selector, context) { + return this.pushStack( + jQuery.unique( + jQuery.merge(this.get(), jQuery(selector, context)) + ) + ); + }, + + addBack: function (selector) { + return this.add(selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } + }); + + function sibling(cur, dir) { + while ((cur = cur[dir]) && cur.nodeType !== 1) { + } + return cur; + } + + jQuery.each({ + parent: function (elem) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function (elem) { + return jQuery.dir(elem, "parentNode"); + }, + parentsUntil: function (elem, i, until) { + return jQuery.dir(elem, "parentNode", until); + }, + next: function (elem) { + return sibling(elem, "nextSibling"); + }, + prev: function (elem) { + return sibling(elem, "previousSibling"); + }, + nextAll: function (elem) { + return jQuery.dir(elem, "nextSibling"); + }, + prevAll: function (elem) { + return jQuery.dir(elem, "previousSibling"); + }, + nextUntil: function (elem, i, until) { + return jQuery.dir(elem, "nextSibling", until); + }, + prevUntil: function (elem, i, until) { + return jQuery.dir(elem, "previousSibling", until); + }, + siblings: function (elem) { + return jQuery.sibling(( elem.parentNode || {} ).firstChild, elem); + }, + children: function (elem) { + return jQuery.sibling(elem.firstChild); + }, + contents: function (elem) { + return elem.contentDocument || jQuery.merge([], elem.childNodes); + } + }, function (name, fn) { + jQuery.fn[ name ] = function (until, selector) { + var matched = jQuery.map(this, fn, until); + + if (name.slice(-5) !== "Until") { + selector = until; + } + + if (selector && typeof selector === "string") { + matched = jQuery.filter(selector, matched); + } + + if (this.length > 1) { + // Remove duplicates + if (!guaranteedUnique[ name ]) { + jQuery.unique(matched); + } + + // Reverse order for parents* and prev-derivatives + if (rparentsprev.test(name)) { + matched.reverse(); + } + } + + return this.pushStack(matched); + }; + }); + var rnotwhite = (/\S+/g); + + +// String to Object options format cache + var optionsCache = {}; + +// Convert String-formatted options into Object-formatted ones and store in cache + function createOptions(options) { + var object = optionsCache[ options ] = {}; + jQuery.each(options.match(rnotwhite) || [], function (_, flag) { + object[ flag ] = true; + }); + return object; + } + + /* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ + jQuery.Callbacks = function (options) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions(options) ) : + jQuery.extend({}, options); + + var // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks + fire = function (data) { + memory = options.memory && data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + for (; list && firingIndex < firingLength; firingIndex++) { + if (list[ firingIndex ].apply(data[ 0 ], data[ 1 ]) === false && options.stopOnFalse) { + memory = false; // To prevent further calls using add + break; + } + } + firing = false; + if (list) { + if (stack) { + if (stack.length) { + fire(stack.shift()); + } + } else if (memory) { + list = []; + } else { + self.disable(); + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function () { + if (list) { + // First, we save the current length + var start = list.length; + (function add(args) { + jQuery.each(args, function (_, arg) { + var type = jQuery.type(arg); + if (type === "function") { + if (!options.unique || !self.has(arg)) { + list.push(arg); + } + } else if (arg && arg.length && type !== "string") { + // Inspect recursively + add(arg); + } + }); + })(arguments); + // Do we need to add the callbacks to the + // current firing batch? + if (firing) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away + } else if (memory) { + firingStart = start; + fire(memory); + } + } + return this; + }, + // Remove a callback from the list + remove: function () { + if (list) { + jQuery.each(arguments, function (_, arg) { + var index; + while (( index = jQuery.inArray(arg, list, index) ) > -1) { + list.splice(index, 1); + // Handle firing indexes + if (firing) { + if (index <= firingLength) { + firingLength--; + } + if (index <= firingIndex) { + firingIndex--; + } + } + } + }); + } + return this; + }, + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function (fn) { + return fn ? jQuery.inArray(fn, list) > -1 : !!( list && list.length ); + }, + // Remove all callbacks from the list + empty: function () { + list = []; + firingLength = 0; + return this; + }, + // Have the list do nothing anymore + disable: function () { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function () { + return !list; + }, + // Lock the list in its current state + lock: function () { + stack = undefined; + if (!memory) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function () { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function (context, args) { + if (list && ( !fired || stack )) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if (firing) { + stack.push(args); + } else { + fire(args); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function () { + self.fireWith(this, arguments); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function () { + return !!fired; + } + }; + + return self; + }; + + + jQuery.extend({ + + Deferred: function (func) { + var tuples = [ + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory") ] + ], + state = "pending", + promise = { + state: function () { + return state; + }, + always: function () { + deferred.done(arguments).fail(arguments); + return this; + }, + then: function (/* fnDone, fnFail, fnProgress */) { + var fns = arguments; + return jQuery.Deferred(function (newDefer) { + jQuery.each(tuples, function (i, tuple) { + var fn = jQuery.isFunction(fns[ i ]) && fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[1] ](function () { + var returned = fn && fn.apply(this, arguments); + if (returned && jQuery.isFunction(returned.promise)) { + returned.promise() + .done(newDefer.resolve) + .fail(newDefer.reject) + .progress(newDefer.notify); + } else { + newDefer[ tuple[ 0 ] + "With" ](this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments); + } + }); + }); + fns = null; + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function (obj) { + return obj != null ? jQuery.extend(obj, promise) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each(tuples, function (i, tuple) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[1] ] = list.add; + + // Handle state + if (stateString) { + list.add(function () { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[0] ] = function () { + deferred[ tuple[0] + "With" ](this === deferred ? promise : this, arguments); + return this; + }; + deferred[ tuple[0] + "With" ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise(deferred); + + // Call given func if any + if (func) { + func.call(deferred, deferred); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function (subordinate /* , ..., subordinateN */) { + var i = 0, + resolveValues = slice.call(arguments), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || ( subordinate && jQuery.isFunction(subordinate.promise) ) ? length : 0, + + // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function (i, contexts, values) { + return function (value) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? slice.call(arguments) : value; + if (values === progressValues) { + deferred.notifyWith(contexts, values); + } else if (!( --remaining )) { + deferred.resolveWith(contexts, values); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if (length > 1) { + progressValues = new Array(length); + progressContexts = new Array(length); + resolveContexts = new Array(length); + for (; i < length; i++) { + if (resolveValues[ i ] && jQuery.isFunction(resolveValues[ i ].promise)) { + resolveValues[ i ].promise() + .done(updateFunc(i, resolveContexts, resolveValues)) + .fail(deferred.reject) + .progress(updateFunc(i, progressContexts, progressValues)); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if (!remaining) { + deferred.resolveWith(resolveContexts, resolveValues); + } + + return deferred.promise(); + } + }); + + +// The deferred used on DOM ready + var readyList; + + jQuery.fn.ready = function (fn) { + // Add the callback + jQuery.ready.promise().done(fn); + + return this; + }; + + jQuery.extend({ + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function (hold) { + if (hold) { + jQuery.readyWait++; + } else { + jQuery.ready(true); + } + }, + + // Handle when the DOM is ready + ready: function (wait) { + + // Abort if there are pending holds or we're already ready + if (wait === true ? --jQuery.readyWait : jQuery.isReady) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if (wait !== true && --jQuery.readyWait > 0) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith(document, [ jQuery ]); + + // Trigger any bound ready events + if (jQuery.fn.trigger) { + jQuery(document).trigger("ready").off("ready"); + } + } + }); + + /** + * The ready event handler and self cleanup method + */ + function completed() { + document.removeEventListener("DOMContentLoaded", completed, false); + window.removeEventListener("load", completed, false); + jQuery.ready(); + } + + jQuery.ready.promise = function (obj) { + if (!readyList) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called after the browser event has already occurred. + // we once tried to use readyState "interactive" here, but it caused issues like the one + // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 + if (document.readyState === "complete") { + // Handle it asynchronously to allow scripts the opportunity to delay ready + setTimeout(jQuery.ready); + + } else { + + // Use the handy event callback + document.addEventListener("DOMContentLoaded", completed, false); + + // A fallback to window.onload, that will always work + window.addEventListener("load", completed, false); + } + } + return readyList.promise(obj); + }; + +// Kick off the DOM ready check even if the user does not + jQuery.ready.promise(); + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function + var access = jQuery.access = function (elems, fn, key, value, chainable, emptyGet, raw) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if (jQuery.type(key) === "object") { + chainable = true; + for (i in key) { + jQuery.access(elems, fn, i, key[i], true, emptyGet, raw); + } + + // Sets one value + } else if (value !== undefined) { + chainable = true; + + if (!jQuery.isFunction(value)) { + raw = true; + } + + if (bulk) { + // Bulk operations run against the entire set + if (raw) { + fn.call(elems, value); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function (elem, key, value) { + return bulk.call(jQuery(elem), value); + }; + } + } + + if (fn) { + for (; i < len; i++) { + fn(elems[i], key, raw ? value : value.call(elems[i], i, fn(elems[i], key))); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call(elems) : + len ? fn(elems[0], key) : emptyGet; + }; + + + /** + * Determines whether an object can have data + */ + jQuery.acceptData = function (owner) { + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + /* jshint -W018 */ + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); + }; + + + function Data() { + // Support: Android < 4, + // Old WebKit does not have Object.preventExtensions/freeze method, + // return new empty object instead with no [[set]] accessor + Object.defineProperty(this.cache = {}, 0, { + get: function () { + return {}; + } + }); + + this.expando = jQuery.expando + Math.random(); + } + + Data.uid = 1; + Data.accepts = jQuery.acceptData; + + Data.prototype = { + key: function (owner) { + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return the key for a frozen object. + if (!Data.accepts(owner)) { + return 0; + } + + var descriptor = {}, + // Check if the owner object already has a cache key + unlock = owner[ this.expando ]; + + // If not, create one + if (!unlock) { + unlock = Data.uid++; + + // Secure it in a non-enumerable, non-writable property + try { + descriptor[ this.expando ] = { value: unlock }; + Object.defineProperties(owner, descriptor); + + // Support: Android < 4 + // Fallback to a less secure definition + } catch (e) { + descriptor[ this.expando ] = unlock; + jQuery.extend(owner, descriptor); + } + } + + // Ensure the cache object + if (!this.cache[ unlock ]) { + this.cache[ unlock ] = {}; + } + + return unlock; + }, + set: function (owner, data, value) { + var prop, + // There may be an unlock assigned to this node, + // if there is no entry for this "owner", create one inline + // and set the unlock as though an owner entry had always existed + unlock = this.key(owner), + cache = this.cache[ unlock ]; + + // Handle: [ owner, key, value ] args + if (typeof data === "string") { + cache[ data ] = value; + + // Handle: [ owner, { properties } ] args + } else { + // Fresh assignments by object are shallow copied + if (jQuery.isEmptyObject(cache)) { + jQuery.extend(this.cache[ unlock ], data); + // Otherwise, copy the properties one-by-one to the cache object + } else { + for (prop in data) { + cache[ prop ] = data[ prop ]; + } + } + } + return cache; + }, + get: function (owner, key) { + // Either a valid cache is found, or will be created. + // New caches will be created and the unlock returned, + // allowing direct access to the newly created + // empty data object. A valid owner object must be provided. + var cache = this.cache[ this.key(owner) ]; + + return key === undefined ? + cache : cache[ key ]; + }, + access: function (owner, key, value) { + var stored; + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if (key === undefined || + ((key && typeof key === "string") && value === undefined)) { + + stored = this.get(owner, key); + + return stored !== undefined ? + stored : this.get(owner, jQuery.camelCase(key)); + } + + // [*]When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set(owner, key, value); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function (owner, key) { + var i, name, camel, + unlock = this.key(owner), + cache = this.cache[ unlock ]; + + if (key === undefined) { + this.cache[ unlock ] = {}; + + } else { + // Support array or space separated string of keys + if (jQuery.isArray(key)) { + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = key.concat(key.map(jQuery.camelCase)); + } else { + camel = jQuery.camelCase(key); + // Try the string as a key before any manipulation + if (key in cache) { + name = [ key, camel ]; + } else { + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + name = camel; + name = name in cache ? + [ name ] : ( name.match(rnotwhite) || [] ); + } + } + + i = name.length; + while (i--) { + delete cache[ name[ i ] ]; + } + } + }, + hasData: function (owner) { + return !jQuery.isEmptyObject( + this.cache[ owner[ this.expando ] ] || {} + ); + }, + discard: function (owner) { + if (owner[ this.expando ]) { + delete this.cache[ owner[ this.expando ] ]; + } + } + }; + var data_priv = new Data(); + + var data_user = new Data(); + + + /* + Implementation Summary + + 1. Enforce API surface and semantic compatibility with 1.9.x branch + 2. Improve the module's maintainability by reducing the storage + paths to a single mechanism. + 3. Use the same single mechanism to support "private" and "user" data. + 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) + 5. Avoid exposing implementation details on user objects (eg. expando properties) + 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + */ + var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /([A-Z])/g; + + function dataAttr(elem, key, data) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if (data === undefined && elem.nodeType === 1) { + name = "data-" + key.replace(rmultiDash, "-$1").toLowerCase(); + data = elem.getAttribute(name); + + if (typeof data === "string") { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test(data) ? jQuery.parseJSON(data) : + data; + } catch (e) { + } + + // Make sure we set the data so it isn't changed later + data_user.set(elem, key, data); + } else { + data = undefined; + } + } + return data; + } + + jQuery.extend({ + hasData: function (elem) { + return data_user.hasData(elem) || data_priv.hasData(elem); + }, + + data: function (elem, name, data) { + return data_user.access(elem, name, data); + }, + + removeData: function (elem, name) { + data_user.remove(elem, name); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to data_priv methods, these can be deprecated. + _data: function (elem, name, data) { + return data_priv.access(elem, name, data); + }, + + _removeData: function (elem, name) { + data_priv.remove(elem, name); + } + }); + + jQuery.fn.extend({ + data: function (key, value) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if (key === undefined) { + if (this.length) { + data = data_user.get(elem); + + if (elem.nodeType === 1 && !data_priv.get(elem, "hasDataAttrs")) { + i = attrs.length; + while (i--) { + name = attrs[ i ].name; + + if (name.indexOf("data-") === 0) { + name = jQuery.camelCase(name.slice(5)); + dataAttr(elem, name, data[ name ]); + } + } + data_priv.set(elem, "hasDataAttrs", true); + } + } + + return data; + } + + // Sets multiple values + if (typeof key === "object") { + return this.each(function () { + data_user.set(this, key); + }); + } + + return access(this, function (value) { + var data, + camelKey = jQuery.camelCase(key); + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if (elem && value === undefined) { + // Attempt to get data from the cache + // with the key as-is + data = data_user.get(elem, key); + if (data !== undefined) { + return data; + } + + // Attempt to get data from the cache + // with the key camelized + data = data_user.get(elem, camelKey); + if (data !== undefined) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr(elem, camelKey, undefined); + if (data !== undefined) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + this.each(function () { + // First, attempt to store a copy or reference of any + // data that might've been store with a camelCased key. + var data = data_user.get(this, camelKey); + + // For HTML5 data-* attribute interop, we have to + // store property names with dashes in a camelCase form. + // This might not apply to all properties...* + data_user.set(this, camelKey, value); + + // *... In the case of properties that might _actually_ + // have dashes, we need to also store a copy of that + // unchanged property. + if (key.indexOf("-") !== -1 && data !== undefined) { + data_user.set(this, key, value); + } + }); + }, null, value, arguments.length > 1, null, true); + }, + + removeData: function (key) { + return this.each(function () { + data_user.remove(this, key); + }); + } + }); + + + jQuery.extend({ + queue: function (elem, type, data) { + var queue; + + if (elem) { + type = ( type || "fx" ) + "queue"; + queue = data_priv.get(elem, type); + + // Speed up dequeue by getting out quickly if this is just a lookup + if (data) { + if (!queue || jQuery.isArray(data)) { + queue = data_priv.access(elem, type, jQuery.makeArray(data)); + } else { + queue.push(data); + } + } + return queue || []; + } + }, + + dequeue: function (elem, type) { + type = type || "fx"; + + var queue = jQuery.queue(elem, type), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks(elem, type), + next = function () { + jQuery.dequeue(elem, type); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if (fn === "inprogress") { + fn = queue.shift(); + startLength--; + } + + if (fn) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if (type === "fx") { + queue.unshift("inprogress"); + } + + // clear up the last queue stop function + delete hooks.stop; + fn.call(elem, next, hooks); + } + + if (!startLength && hooks) { + hooks.empty.fire(); + } + }, + + // not intended for public consumption - generates a queueHooks object, or returns the current one + _queueHooks: function (elem, type) { + var key = type + "queueHooks"; + return data_priv.get(elem, key) || data_priv.access(elem, key, { + empty: jQuery.Callbacks("once memory").add(function () { + data_priv.remove(elem, [ type + "queue", key ]); + }) + }); + } + }); + + jQuery.fn.extend({ + queue: function (type, data) { + var setter = 2; + + if (typeof type !== "string") { + data = type; + type = "fx"; + setter--; + } + + if (arguments.length < setter) { + return jQuery.queue(this[0], type); + } + + return data === undefined ? + this : + this.each(function () { + var queue = jQuery.queue(this, type, data); + + // ensure a hooks for this queue + jQuery._queueHooks(this, type); + + if (type === "fx" && queue[0] !== "inprogress") { + jQuery.dequeue(this, type); + } + }); + }, + dequeue: function (type) { + return this.each(function () { + jQuery.dequeue(this, type); + }); + }, + clearQueue: function (type) { + return this.queue(type || "fx", []); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function (type, obj) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function () { + if (!( --count )) { + defer.resolveWith(elements, [ elements ]); + } + }; + + if (typeof type !== "string") { + obj = type; + type = undefined; + } + type = type || "fx"; + + while (i--) { + tmp = data_priv.get(elements[ i ], type + "queueHooks"); + if (tmp && tmp.empty) { + count++; + tmp.empty.add(resolve); + } + } + resolve(); + return defer.promise(obj); + } + }); + var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source; + + var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + + var isHidden = function (elem, el) { + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css(elem, "display") === "none" || !jQuery.contains(elem.ownerDocument, elem); + }; + + var rcheckableType = (/^(?:checkbox|radio)$/i); + + + (function () { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild(document.createElement("div")); + + // #11217 - WebKit loses check when the name is after the checked attribute + div.innerHTML = ""; + + // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3 + // old WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode(true).cloneNode(true).lastChild.checked; + + // Make sure textarea (and checkbox) defaultValue is properly cloned + // Support: IE9-IE11+ + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode(true).lastChild.defaultValue; + })(); + var strundefined = typeof undefined; + + + support.focusinBubbles = "onfocusin" in window; + + + var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; + + function returnTrue() { + return true; + } + + function returnFalse() { + return false; + } + + function safeActiveElement() { + try { + return document.activeElement; + } catch (err) { + } + } + + /* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ + jQuery.event = { + + global: {}, + + add: function (elem, types, handler, data, selector) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = data_priv.get(elem); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if (!elemData) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if (handler.handler) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if (!handler.guid) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if (!(events = elemData.events)) { + events = elemData.events = {}; + } + if (!(eventHandle = elemData.handle)) { + eventHandle = elemData.handle = function (e) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply(elem, arguments) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match(rnotwhite) || [ "" ]; + t = types.length; + while (t--) { + tmp = rtypenamespace.exec(types[t]) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split(".").sort(); + + // There *must* be a type, no attaching namespace-only handlers + if (!type) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test(selector), + namespace: namespaces.join(".") + }, handleObjIn); + + // Init the event handler queue if we're the first + if (!(handlers = events[ type ])) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if (!special.setup || special.setup.call(elem, data, namespaces, eventHandle) === false) { + if (elem.addEventListener) { + elem.addEventListener(type, eventHandle, false); + } + } + } + + if (special.add) { + special.add.call(elem, handleObj); + + if (!handleObj.handler.guid) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if (selector) { + handlers.splice(handlers.delegateCount++, 0, handleObj); + } else { + handlers.push(handleObj); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function (elem, types, handler, selector, mappedTypes) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = data_priv.hasData(elem) && data_priv.get(elem); + + if (!elemData || !(events = elemData.events)) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match(rnotwhite) || [ "" ]; + t = types.length; + while (t--) { + tmp = rtypenamespace.exec(types[t]) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split(".").sort(); + + // Unbind all events (on this namespace, if provided) for the element + if (!type) { + for (type in events) { + jQuery.event.remove(elem, type + types[ t ], handler, selector, true); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[2] && new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)"); + + // Remove matching events + origCount = j = handlers.length; + while (j--) { + handleObj = handlers[ j ]; + + if (( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test(handleObj.namespace) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector )) { + handlers.splice(j, 1); + + if (handleObj.selector) { + handlers.delegateCount--; + } + if (special.remove) { + special.remove.call(elem, handleObj); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if (origCount && !handlers.length) { + if (!special.teardown || special.teardown.call(elem, namespaces, elemData.handle) === false) { + jQuery.removeEvent(elem, type, elemData.handle); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if (jQuery.isEmptyObject(events)) { + delete elemData.handle; + data_priv.remove(elem, "events"); + } + }, + + trigger: function (event, data, elem, onlyHandlers) { + + var i, cur, tmp, bubbleType, ontype, handle, special, + eventPath = [ elem || document ], + type = hasOwn.call(event, "type") ? event.type : event, + namespaces = hasOwn.call(event, "namespace") ? event.namespace.split(".") : []; + + cur = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if (elem.nodeType === 3 || elem.nodeType === 8) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if (rfocusMorph.test(type + jQuery.event.triggered)) { + return; + } + + if (type.indexOf(".") >= 0) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf(":") < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event(type, typeof event === "object" && event); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join("."); + event.namespace_re = event.namespace ? + new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)") : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if (!event.target) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray(data, [ event ]); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if (!onlyHandlers && special.trigger && special.trigger.apply(elem, data) === false) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if (!onlyHandlers && !special.noBubble && !jQuery.isWindow(elem)) { + + bubbleType = special.delegateType || type; + if (!rfocusMorph.test(bubbleType + type)) { + cur = cur.parentNode; + } + for (; cur; cur = cur.parentNode) { + eventPath.push(cur); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if (tmp === (elem.ownerDocument || document)) { + eventPath.push(tmp.defaultView || tmp.parentWindow || window); + } + } + + // Fire handlers on the event path + i = 0; + while ((cur = eventPath[i++]) && !event.isPropagationStopped()) { + + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( data_priv.get(cur, "events") || {} )[ event.type ] && data_priv.get(cur, "handle"); + if (handle) { + handle.apply(cur, data); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if (handle && handle.apply && jQuery.acceptData(cur)) { + event.result = handle.apply(cur, data); + if (event.result === false) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if (!onlyHandlers && !event.isDefaultPrevented()) { + + if ((!special._default || special._default.apply(eventPath.pop(), data) === false) && + jQuery.acceptData(elem)) { + + // Call a native DOM method on the target with the same name name as the event. + // Don't do default actions on window, that's where global variables be (#6170) + if (ontype && jQuery.isFunction(elem[ type ]) && !jQuery.isWindow(elem)) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if (tmp) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if (tmp) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + dispatch: function (event) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix(event); + + var i, j, ret, matched, handleObj, + handlerQueue = [], + args = slice.call(arguments), + handlers = ( data_priv.get(this, "events") || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if (special.preDispatch && special.preDispatch.call(this, event) === false) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call(this, event, handlers); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ((matched = handlerQueue[ i++ ]) && !event.isPropagationStopped()) { + event.currentTarget = matched.elem; + + j = 0; + while ((handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped()) { + + // Triggered event must either 1) have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if (!event.namespace_re || event.namespace_re.test(handleObj.namespace)) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply(matched.elem, args); + + if (ret !== undefined) { + if ((event.result = ret) === false) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if (special.postDispatch) { + special.postDispatch.call(this, event); + } + + return event.result; + }, + + handlers: function (event, handlers) { + var i, matches, sel, handleObj, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // Avoid non-left-click bubbling in Firefox (#3861) + if (delegateCount && cur.nodeType && (!event.button || event.type !== "click")) { + + for (; cur !== this; cur = cur.parentNode || this) { + + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if (cur.disabled !== true || event.type !== "click") { + matches = []; + for (i = 0; i < delegateCount; i++) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if (matches[ sel ] === undefined) { + matches[ sel ] = handleObj.needsContext ? + jQuery(sel, this).index(cur) >= 0 : + jQuery.find(sel, this, null, [ cur ]).length; + } + if (matches[ sel ]) { + matches.push(handleObj); + } + } + if (matches.length) { + handlerQueue.push({ elem: cur, handlers: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if (delegateCount < handlers.length) { + handlerQueue.push({ elem: this, handlers: handlers.slice(delegateCount) }); + } + + return handlerQueue; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function (event, original) { + + // Add which for key events + if (event.which == null) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function (event, original) { + var eventDoc, doc, body, + button = original.button; + + // Calculate pageX/Y if missing and clientX/Y available + if (event.pageX == null && original.clientX != null) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if (!event.which && button !== undefined) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function (event) { + if (event[ jQuery.expando ]) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if (!fixHook) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test(type) ? this.mouseHooks : + rkeyEvent.test(type) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat(fixHook.props) : this.props; + + event = new jQuery.Event(originalEvent); + + i = copy.length; + while (i--) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: Cordova 2.5 (WebKit) (#13255) + // All events should have a target; Cordova deviceready doesn't + if (!event.target) { + event.target = document; + } + + // Support: Safari 6.0+, Chrome < 28 + // Target should not be a text node (#504, #13143) + if (event.target.nodeType === 3) { + event.target = event.target.parentNode; + } + + return fixHook.filter ? fixHook.filter(event, originalEvent) : event; + }, + + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + // Fire native event if possible so blur/focus sequence is correct + trigger: function () { + if (this !== safeActiveElement() && this.focus) { + this.focus(); + return false; + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function () { + if (this === safeActiveElement() && this.blur) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + // For checkbox, fire native event so checked state will be right + trigger: function () { + if (this.type === "checkbox" && this.click && jQuery.nodeName(this, "input")) { + this.click(); + return false; + } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function (event) { + return jQuery.nodeName(event.target, "a"); + } + }, + + beforeunload: { + postDispatch: function (event) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if (event.result !== undefined) { + event.originalEvent.returnValue = event.result; + } + } + } + }, + + simulate: function (type, elem, event, bubble) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true, + originalEvent: {} + } + ); + if (bubble) { + jQuery.event.trigger(e, null, elem); + } else { + jQuery.event.dispatch.call(elem, e); + } + if (e.isDefaultPrevented()) { + event.preventDefault(); + } + } + }; + + jQuery.removeEvent = function (elem, type, handle) { + if (elem.removeEventListener) { + elem.removeEventListener(type, handle, false); + } + }; + + jQuery.Event = function (src, props) { + // Allow instantiation without the 'new' keyword + if (!(this instanceof jQuery.Event)) { + return new jQuery.Event(src, props); + } + + // Event object + if (src && src.type) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + // Support: Android < 4.0 + src.defaultPrevented === undefined && + src.getPreventDefault && src.getPreventDefault() ? + returnTrue : + returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if (props) { + jQuery.extend(this, props); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; + }; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html + jQuery.Event.prototype = { + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + + preventDefault: function () { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if (e && e.preventDefault) { + e.preventDefault(); + } + }, + stopPropagation: function () { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if (e && e.stopPropagation) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function () { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + } + }; + +// Create mouseenter/leave events using mouseover/out and event-time checks +// Support: Chrome 15+ + jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" + }, function (orig, fix) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function (event) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if (!related || (related !== target && !jQuery.contains(target, related))) { + event.type = handleObj.origType; + ret = handleObj.handler.apply(this, arguments); + event.type = fix; + } + return ret; + } + }; + }); + +// Create "bubbling" focus and blur events +// Support: Firefox, Chrome, Safari + if (!support.focusinBubbles) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function (orig, fix) { + + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function (event) { + jQuery.event.simulate(fix, event.target, jQuery.event.fix(event), true); + }; + + jQuery.event.special[ fix ] = { + setup: function () { + var doc = this.ownerDocument || this, + attaches = data_priv.access(doc, fix); + + if (!attaches) { + doc.addEventListener(orig, handler, true); + } + data_priv.access(doc, fix, ( attaches || 0 ) + 1); + }, + teardown: function () { + var doc = this.ownerDocument || this, + attaches = data_priv.access(doc, fix) - 1; + + if (!attaches) { + doc.removeEventListener(orig, handler, true); + data_priv.remove(doc, fix); + + } else { + data_priv.access(doc, fix, attaches); + } + } + }; + }); + } + + jQuery.fn.extend({ + + on: function (types, selector, data, fn, /*INTERNAL*/ one) { + var origFn, type; + + // Types can be a map of types/handlers + if (typeof types === "object") { + // ( types-Object, selector, data ) + if (typeof selector !== "string") { + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for (type in types) { + this.on(type, selector, data, types[ type ], one); + } + return this; + } + + if (data == null && fn == null) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if (fn == null) { + if (typeof selector === "string") { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if (fn === false) { + fn = returnFalse; + } else if (!fn) { + return this; + } + + if (one === 1) { + origFn = fn; + fn = function (event) { + // Can use an empty set, since event contains the info + jQuery().off(event); + return origFn.apply(this, arguments); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each(function () { + jQuery.event.add(this, types, fn, data, selector); + }); + }, + one: function (types, selector, data, fn) { + return this.on(types, selector, data, fn, 1); + }, + off: function (types, selector, fn) { + var handleObj, type; + if (types && types.preventDefault && types.handleObj) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery(types.delegateTarget).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if (typeof types === "object") { + // ( types-object [, selector] ) + for (type in types) { + this.off(type, selector, types[ type ]); + } + return this; + } + if (selector === false || typeof selector === "function") { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if (fn === false) { + fn = returnFalse; + } + return this.each(function () { + jQuery.event.remove(this, types, fn, selector); + }); + }, + + trigger: function (type, data) { + return this.each(function () { + jQuery.event.trigger(type, data, this); + }); + }, + triggerHandler: function (type, data) { + var elem = this[0]; + if (elem) { + return jQuery.event.trigger(type, data, elem, true); + } + } + }); + + + var + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rhtml = /<|&#?\w+;/, + rnoInnerhtml = /<(?:script|style|link)/i, + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /^$|\/(?:java|ecma)script/i, + rscriptTypeMasked = /^true\/(.*)/, + rcleanScript = /^\s*\s*$/g, + + // We have to close these tags to support XHTML (#13200) + wrapMap = { + + // Support: IE 9 + option: [ 1, "" ], + + thead: [ 1, "", "
" ], + col: [ 2, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + _default: [ 0, "", "" ] + }; + +// Support: IE 9 + wrapMap.optgroup = wrapMap.option; + + wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; + wrapMap.th = wrapMap.td; + +// Support: 1.x compatibility +// Manipulating tables requires a tbody + function manipulationTarget(elem, content) { + return jQuery.nodeName(elem, "table") && + jQuery.nodeName(content.nodeType !== 11 ? content : content.firstChild, "tr") ? + + elem.getElementsByTagName("tbody")[0] || + elem.appendChild(elem.ownerDocument.createElement("tbody")) : + elem; + } + +// Replace/restore the type attribute of script elements for safe DOM manipulation + function disableScript(elem) { + elem.type = (elem.getAttribute("type") !== null) + "/" + elem.type; + return elem; + } + + function restoreScript(elem) { + var match = rscriptTypeMasked.exec(elem.type); + + if (match) { + elem.type = match[ 1 ]; + } else { + elem.removeAttribute("type"); + } + + return elem; + } + +// Mark scripts as having already been evaluated + function setGlobalEval(elems, refElements) { + var i = 0, + l = elems.length; + + for (; i < l; i++) { + data_priv.set( + elems[ i ], "globalEval", !refElements || data_priv.get(refElements[ i ], "globalEval") + ); + } + } + + function cloneCopyEvent(src, dest) { + var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; + + if (dest.nodeType !== 1) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if (data_priv.hasData(src)) { + pdataOld = data_priv.access(src); + pdataCur = data_priv.set(dest, pdataOld); + events = pdataOld.events; + + if (events) { + delete pdataCur.handle; + pdataCur.events = {}; + + for (type in events) { + for (i = 0, l = events[ type ].length; i < l; i++) { + jQuery.event.add(dest, type, events[ type ][ i ]); + } + } + } + } + + // 2. Copy user data + if (data_user.hasData(src)) { + udataOld = data_user.access(src); + udataCur = jQuery.extend({}, udataOld); + + data_user.set(dest, udataCur); + } + } + + function getAll(context, tag) { + var ret = context.getElementsByTagName ? context.getElementsByTagName(tag || "*") : + context.querySelectorAll ? context.querySelectorAll(tag || "*") : + []; + + return tag === undefined || tag && jQuery.nodeName(context, tag) ? + jQuery.merge([ context ], ret) : + ret; + } + +// Support: IE >= 9 + function fixInput(src, dest) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if (nodeName === "input" && rcheckableType.test(src.type)) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if (nodeName === "input" || nodeName === "textarea") { + dest.defaultValue = src.defaultValue; + } + } + + jQuery.extend({ + clone: function (elem, dataAndEvents, deepDataAndEvents) { + var i, l, srcElements, destElements, + clone = elem.cloneNode(true), + inPage = jQuery.contains(elem.ownerDocument, elem); + + // Support: IE >= 9 + // Fix Cloning issues + if (!support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && !jQuery.isXMLDoc(elem)) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll(clone); + srcElements = getAll(elem); + + for (i = 0, l = srcElements.length; i < l; i++) { + fixInput(srcElements[ i ], destElements[ i ]); + } + } + + // Copy the events from the original to the clone + if (dataAndEvents) { + if (deepDataAndEvents) { + srcElements = srcElements || getAll(elem); + destElements = destElements || getAll(clone); + + for (i = 0, l = srcElements.length; i < l; i++) { + cloneCopyEvent(srcElements[ i ], destElements[ i ]); + } + } else { + cloneCopyEvent(elem, clone); + } + } + + // Preserve script evaluation history + destElements = getAll(clone, "script"); + if (destElements.length > 0) { + setGlobalEval(destElements, !inPage && getAll(elem, "script")); + } + + // Return the cloned set + return clone; + }, + + buildFragment: function (elems, context, scripts, selection) { + var elem, tmp, tag, wrap, contains, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for (; i < l; i++) { + elem = elems[ i ]; + + if (elem || elem === 0) { + + // Add nodes directly + if (jQuery.type(elem) === "object") { + // Support: QtWebKit + // jQuery.merge because push.apply(_, arraylike) throws + jQuery.merge(nodes, elem.nodeType ? [ elem ] : elem); + + // Convert non-html into a text node + } else if (!rhtml.test(elem)) { + nodes.push(context.createTextNode(elem)); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild(context.createElement("div")); + + // Deserialize a standard representation + tag = ( rtagName.exec(elem) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + elem.replace(rxhtmlTag, "<$1>") + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while (j--) { + tmp = tmp.lastChild; + } + + // Support: QtWebKit + // jQuery.merge because push.apply(_, arraylike) throws + jQuery.merge(nodes, tmp.childNodes); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Fixes #12346 + // Support: Webkit, IE + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ((elem = nodes[ i++ ])) { + + // #4087 - If origin and destination elements are the same, and this is + // that element, do not do anything + if (selection && jQuery.inArray(elem, selection) !== -1) { + continue; + } + + contains = jQuery.contains(elem.ownerDocument, elem); + + // Append to fragment + tmp = getAll(fragment.appendChild(elem), "script"); + + // Preserve script evaluation history + if (contains) { + setGlobalEval(tmp); + } + + // Capture executables + if (scripts) { + j = 0; + while ((elem = tmp[ j++ ])) { + if (rscriptType.test(elem.type || "")) { + scripts.push(elem); + } + } + } + } + + return fragment; + }, + + cleanData: function (elems) { + var data, elem, events, type, key, j, + special = jQuery.event.special, + i = 0; + + for (; (elem = elems[ i ]) !== undefined; i++) { + if (jQuery.acceptData(elem)) { + key = elem[ data_priv.expando ]; + + if (key && (data = data_priv.cache[ key ])) { + events = Object.keys(data.events || {}); + if (events.length) { + for (j = 0; (type = events[j]) !== undefined; j++) { + if (special[ type ]) { + jQuery.event.remove(elem, type); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent(elem, type, data.handle); + } + } + } + if (data_priv.cache[ key ]) { + // Discard any remaining `private` data + delete data_priv.cache[ key ]; + } + } + } + // Discard any remaining `user` data + delete data_user.cache[ elem[ data_user.expando ] ]; + } + } + }); + + jQuery.fn.extend({ + text: function (value) { + return access(this, function (value) { + return value === undefined ? + jQuery.text(this) : + this.empty().each(function () { + if (this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9) { + this.textContent = value; + } + }); + }, null, value, arguments.length); + }, + + append: function () { + return this.domManip(arguments, function (elem) { + if (this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9) { + var target = manipulationTarget(this, elem); + target.appendChild(elem); + } + }); + }, + + prepend: function () { + return this.domManip(arguments, function (elem) { + if (this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9) { + var target = manipulationTarget(this, elem); + target.insertBefore(elem, target.firstChild); + } + }); + }, + + before: function () { + return this.domManip(arguments, function (elem) { + if (this.parentNode) { + this.parentNode.insertBefore(elem, this); + } + }); + }, + + after: function () { + return this.domManip(arguments, function (elem) { + if (this.parentNode) { + this.parentNode.insertBefore(elem, this.nextSibling); + } + }); + }, + + remove: function (selector, keepData /* Internal Use Only */) { + var elem, + elems = selector ? jQuery.filter(selector, this) : this, + i = 0; + + for (; (elem = elems[i]) != null; i++) { + if (!keepData && elem.nodeType === 1) { + jQuery.cleanData(getAll(elem)); + } + + if (elem.parentNode) { + if (keepData && jQuery.contains(elem.ownerDocument, elem)) { + setGlobalEval(getAll(elem, "script")); + } + elem.parentNode.removeChild(elem); + } + } + + return this; + }, + + empty: function () { + var elem, + i = 0; + + for (; (elem = this[i]) != null; i++) { + if (elem.nodeType === 1) { + + // Prevent memory leaks + jQuery.cleanData(getAll(elem, false)); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function (dataAndEvents, deepDataAndEvents) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map(function () { + return jQuery.clone(this, dataAndEvents, deepDataAndEvents); + }); + }, + + html: function (value) { + return access(this, function (value) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if (value === undefined && elem.nodeType === 1) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if (typeof value === "string" && !rnoInnerhtml.test(value) && !wrapMap[ ( rtagName.exec(value) || [ "", "" ] )[ 1 ].toLowerCase() ]) { + + value = value.replace(rxhtmlTag, "<$1>"); + + try { + for (; i < l; i++) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if (elem.nodeType === 1) { + jQuery.cleanData(getAll(elem, false)); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch (e) { + } + } + + if (elem) { + this.empty().append(value); + } + }, null, value, arguments.length); + }, + + replaceWith: function () { + var arg = arguments[ 0 ]; + + // Make the changes, replacing each context element with the new content + this.domManip(arguments, function (elem) { + arg = this.parentNode; + + jQuery.cleanData(getAll(this)); + + if (arg) { + arg.replaceChild(elem, this); + } + }); + + // Force removal if there was no new content (e.g., from empty arguments) + return arg && (arg.length || arg.nodeType) ? this : this.remove(); + }, + + detach: function (selector) { + return this.remove(selector, true); + }, + + domManip: function (args, callback) { + + // Flatten any nested arrays + args = concat.apply([], args); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = this.length, + set = this, + iNoClone = l - 1, + value = args[ 0 ], + isFunction = jQuery.isFunction(value); + + // We can't cloneNode fragments that contain checked, in WebKit + if (isFunction || + ( l > 1 && typeof value === "string" && !support.checkClone && rchecked.test(value) )) { + return this.each(function (index) { + var self = set.eq(index); + if (isFunction) { + args[ 0 ] = value.call(this, index, self.html()); + } + self.domManip(args, callback); + }); + } + + if (l) { + fragment = jQuery.buildFragment(args, this[ 0 ].ownerDocument, false, this); + first = fragment.firstChild; + + if (fragment.childNodes.length === 1) { + fragment = first; + } + + if (first) { + scripts = jQuery.map(getAll(fragment, "script"), disableScript); + hasScripts = scripts.length; + + // Use the original fragment for the last item instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for (; i < l; i++) { + node = fragment; + + if (i !== iNoClone) { + node = jQuery.clone(node, true, true); + + // Keep references to cloned scripts for later restoration + if (hasScripts) { + // Support: QtWebKit + // jQuery.merge because push.apply(_, arraylike) throws + jQuery.merge(scripts, getAll(node, "script")); + } + } + + callback.call(this[ i ], node, i); + } + + if (hasScripts) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map(scripts, restoreScript); + + // Evaluate executable scripts on first document insertion + for (i = 0; i < hasScripts; i++) { + node = scripts[ i ]; + if (rscriptType.test(node.type || "") && !data_priv.access(node, "globalEval") && jQuery.contains(doc, node)) { + + if (node.src) { + // Optional AJAX dependency, but won't run scripts if not present + if (jQuery._evalUrl) { + jQuery._evalUrl(node.src); + } + } else { + jQuery.globalEval(node.textContent.replace(rcleanScript, "")); + } + } + } + } + } + } + + return this; + } + }); + + jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" + }, function (name, original) { + jQuery.fn[ name ] = function (selector) { + var elems, + ret = [], + insert = jQuery(selector), + last = insert.length - 1, + i = 0; + + for (; i <= last; i++) { + elems = i === last ? this : this.clone(true); + jQuery(insert[ i ])[ original ](elems); + + // Support: QtWebKit + // .get() because push.apply(_, arraylike) throws + push.apply(ret, elems.get()); + } + + return this.pushStack(ret); + }; + }); + + + var iframe, + elemdisplay = {}; + + /** + * Retrieve the actual display of a element + * @param {String} name nodeName of the element + * @param {Object} doc Document object + */ +// Called only from within defaultDisplay + function actualDisplay(name, doc) { + var elem = jQuery(doc.createElement(name)).appendTo(doc.body), + + // getDefaultComputedStyle might be reliably used only on attached element + display = window.getDefaultComputedStyle ? + + // Use of this method is a temporary fix (more like optmization) until something better comes along, + // since it was removed from specification and supported only in FF + window.getDefaultComputedStyle(elem[ 0 ]).display : jQuery.css(elem[ 0 ], "display"); + + // We don't have any data stored on the element, + // so use "detach" method as fast way to get rid of the element + elem.detach(); + + return display; + } + + /** + * Try to determine the default display value of an element + * @param {String} nodeName + */ + function defaultDisplay(nodeName) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if (!display) { + display = actualDisplay(nodeName, doc); + + // If the simple way fails, read from inside an iframe + if (display === "none" || !display) { + + // Use the already-created iframe if possible + iframe = (iframe || jQuery("