├── .gitignore ├── README.md ├── app.js ├── config.js.tpl ├── controllers ├── blog.js ├── comment.js ├── post.js ├── tag.js └── user.js ├── db.js ├── history.md ├── lib ├── combo.js ├── cookie.js ├── eventproxy.js ├── session_store.js ├── showdown.js └── utils.js ├── models ├── comment.js ├── index.js ├── post.js ├── tag.js └── user.js ├── package.json ├── public ├── css │ └── prettify.css ├── favicon.ico ├── image │ ├── 16x16.png │ ├── 80x80.png │ ├── forkme-grey.png │ ├── forkme-white.png │ ├── forkme.png │ └── nbe_logo.jpg ├── js │ ├── date.format.js │ ├── jquery-1.5.2.min.js │ ├── modernizr-1.7.min.js │ └── prettify.js └── simple │ └── css │ └── main.css ├── test ├── session_store.test.js ├── showdown.test.code.md ├── showdown.test.js └── showdown.test.md └── views ├── readme.md └── simple ├── 404.html ├── archive.html ├── index.html ├── layout.html ├── post ├── edit.html └── item.html ├── rss.xml ├── tag.html └── user ├── item.html └── login.html /.gitignore: -------------------------------------------------------------------------------- 1 | .settings 2 | data/ 3 | .project 4 | .nodeblog.sessions 5 | *.log 6 | config.js 7 | node_modules/ 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A blog base on nodejs. Demo: http://nodeblog.org, http://nodeblog.cnodejs.net 2 | 3 | ## Features 4 | 5 | * Write, Read, List, Search blog. 6 | * Comments 7 | * Support image and file upload. 8 | * DIY template 9 | * Support nodester.com, no.de... nodejs host services 10 | * Simple to install 11 | * Support post to twitter, facebook, weibo, tqq and so on. 12 | * Speed 13 | * Support MetaWeblog API Sync 14 | * Support 42qu.com 15 | 16 | ## Requirements 17 | 18 | * [node.js](http://nodejs.org/) 19 | * [mongodb](http://www.mongodb.org/) 20 | 21 | ### Ubuntu 22 | 23 | $ sudo apt-get install mongodb 24 | 25 | ### CentOS 26 | 27 | # install mongodb 28 | # see: http://www.mongodb.org/display/DOCS/CentOS+and+Fedora+Packages 29 | 30 | ## Node Modules Install 31 | 32 | $ sudo npm install connect ejs weibo metaweblog mongoskin github-flavored-markdown 33 | 34 | ## Install NodeBlog 35 | 36 | $ git clone git://github.com/fengmk2/nodeblog.git 37 | $ cd nodeblog 38 | $ cp config.js.tpl config.js 39 | $ node server.js 40 | 41 | ## DIY template 42 | 43 | * http://www.csstemplatesfree.org/templates/a_bit_boxy/index.html 44 | * http://www.dcarter.co.uk/templates.html 45 | * http://www.csstemplatesfree.org/templates/simple_square/index.html 46 | * http://www.instapaper.com/extras 47 | 48 | ## Snapshot 49 | 50 | * Index Page 51 | ![Index](http://ww1.sinaimg.cn/large/6cfc7910jw1dn1p7j7demj.jpg) 52 | * Settings Page 53 | ![Settings](http://ww1.sinaimg.cn/large/6cfc7910jw1dn1p8enjrmj.jpg) 54 | * New Post Page 55 | ![new post](http://ww3.sinaimg.cn/large/6cfc7910jw1dn1p9wmumkj.jpg) 56 | * Comments of post 57 | ![comments](http://ww2.sinaimg.cn/large/6cfc7910jw1dn1pbjnfeij.jpg) 58 | 59 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NodeBlog APP 3 | */ 4 | 5 | require('./public/js/date.format'); 6 | 7 | var connect = require('connect'); 8 | var render = require('connect-render'); 9 | var blog = require('./controllers/blog'); 10 | var user = require('./controllers/user'); 11 | var post = require('./controllers/post'); 12 | var comment = require('./controllers/comment'); 13 | var tag = require('./controllers/tag'); 14 | var config = require('./config'); 15 | var utils = require('./lib/utils'); 16 | var Store = require('./lib/session_store'); 17 | var db = require('./db'); 18 | 19 | if (!config.view_theme) { 20 | config.view_theme = 'simple'; 21 | } 22 | var qsOptions = { limit: 100 }; 23 | var app = connect( 24 | connect.static(__dirname + '/public'), 25 | connect.logger(), 26 | connect.cookieParser(), 27 | connect.bodyParser(qsOptions), 28 | connect.session({ 29 | secret: config.session_secret, 30 | cookie:{ path: '/', httpOnly: true, maxAge: 24 * 3600000 * 3650 }, 31 | store: new Store(config.db_options) 32 | }), 33 | connect.query(qsOptions), 34 | user.oauth_handle, 35 | render({ 36 | root: __dirname + '/views/'+config.view_theme, 37 | cache: config.view_cache || false, 38 | helpers: { 39 | config: config, 40 | markdown: utils.markdown, 41 | first_paragraph_markdown: utils.first_paragraph_markdown 42 | } 43 | }) 44 | ); 45 | app.use('/', connect.router(blog)); 46 | app.use('/user', connect.router(user)); 47 | app.use('/post', connect.router(post)); 48 | app.use('/comment', connect.router(comment)); 49 | app.use('/tag', connect.router(tag)); 50 | app.use(function(req, res, next) { 51 | res.statusCode = 404; 52 | res.render('404.html'); 53 | }); 54 | 55 | app.listen(config.PORT); 56 | console.log("nodeblog started: http://localhost:" + config.PORT); -------------------------------------------------------------------------------- /config.js.tpl: -------------------------------------------------------------------------------- 1 | // config for blog settings 2 | 3 | var path = require('path'); 4 | 5 | // for support vhost 6 | //exports.subdomain = '*.nodeblogtest.org'; 7 | 8 | exports.DATA_DIR = path.join(__dirname, 'data'); 9 | 10 | exports.PORT = process.env.PORT || 3000; 11 | 12 | exports.db_options = { 13 | host: 'localhost', 14 | port: 27017, 15 | user: 'nodeblog', 16 | password: '123456', 17 | database: 'nodeblog' 18 | }; 19 | 20 | exports.session_secret = 'weorulx123dfwlfjlsjdfppqlasAWDOfjnjxjclvjlsdfjoasdufowefjljdf'; 21 | exports.weibo_appkeys = { 22 | tsina: ['808860407', 'ef895e3caba5dc4c1cb43ca33987e50d', '新浪微博'] 23 | , tqq: ['b6d893a83bd54e598b5a7c359599190a', '34ad78be42426de26e5c4b445843bb78', '腾讯微博'] 24 | , tsohu: ['geDQ7cFZ7iruNPHm3lZk', 'iQ%mtL!eh%xVl!SjQN^($Efdw41!#Ytt*r8SMtw8', '搜狐微博'] 25 | //, twitter: ['i1aAkHo2GkZRWbUOQe8zA', 'MCskw4dW5dhWAYKGl3laRVTLzT8jTonOIOpmzEY', 'Twitter'] 26 | }; 27 | 28 | exports.admins = ['tsina:1640328892']; 29 | 30 | exports.view_theme = 'simple'; 31 | exports.view_cache = false; 32 | exports.site_name = 'Node Blog Engine'; 33 | exports.site_url = 'http://nodeblog.org'; 34 | exports.site_description = 'Nodeblog is a simple blog system base on Nodejs.'; 35 | 36 | -------------------------------------------------------------------------------- /controllers/blog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var db = require('../db'); 6 | 7 | module.exports = function(app) { 8 | app.get(/^\/(rss)?$/, function(req, res, next) { 9 | var count = 20, page = parseInt(req.query.page || 1); 10 | if (isNaN(page)) { 11 | page = 1; 12 | } 13 | var options = { sort: { _id: -1 }, limit: count }; 14 | if (page > 1) { 15 | options.skip = (page - 1) * count; 16 | } 17 | db.posts.list({}, options, function(err, posts) { 18 | posts = posts || []; 19 | if (req.params[0] === 'rss') { 20 | return res.render('rss.xml', { posts: posts, layout: false }); 21 | } 22 | res.render('index.html', { posts: posts, page: page }); 23 | }); 24 | }); 25 | }; -------------------------------------------------------------------------------- /controllers/comment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var utils = require('../lib/utils') 6 | , db = require('../db'); 7 | 8 | 9 | module.exports = function(app) { 10 | app.post('/', function(req, res, next) { 11 | var comment = { 12 | parent_id: req.body.parent_id 13 | , content: req.body.content 14 | , create_at: new Date() 15 | }; 16 | var user = req.session.user; 17 | if(user) { 18 | comment.user_info = { 19 | name: user.screen_name 20 | , site: user.t_url 21 | , profile_image_url: user.profile_image_url 22 | }; 23 | } else { 24 | var email = req.body.email; 25 | comment.user_info = { 26 | name: req.body.name 27 | , email: email 28 | , site: req.body.site 29 | }; 30 | if(email) { 31 | comment.user_info.profile_image_url = utils.gravatar(email); 32 | } 33 | req.session.comment_user = comment.user_info; 34 | } 35 | db.comments.insert(comment, function(err) { 36 | if(err) return next(err); 37 | res.writeHead(302, {Location: '/post/' + comment.parent_id + '#comment_' + comment._id}); 38 | res.end(); 39 | }); 40 | }); 41 | 42 | app.post('/:id/delete', function(req, res, next) { 43 | if(!utils.check_admin(req)) { 44 | return res.end(JSON.stringify({success: false, error: 'No permissions.'})); 45 | } 46 | db.comments.remove({_id: db.ObjectID(req.params.id)}, function(err) { 47 | var success = true; 48 | if(err) { 49 | err = err.message || err; 50 | success = false; 51 | } 52 | res.end(JSON.stringify({success: success, error: err})); 53 | }); 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /controllers/post.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var db = require('../db') 6 | , EventProxy = require('../lib/eventproxy').EventProxy 7 | , utils = require('../lib/utils') 8 | , MetaWeblog = require('metaweblog').MetaWeblog; 9 | 10 | 11 | module.exports = function(app) { 12 | app.get('/new', function(req, res, next) { 13 | if(!req.session.user || !req.session.user.is_admin) { 14 | res.writeHead(302, { Location: '/' }); 15 | return res.end(); 16 | } 17 | res.render('post/edit.html'); 18 | }); 19 | 20 | app.get('/:id', function(req, res, next) { 21 | var ep = new EventProxy(); 22 | ep.assign('post', 'comments', function(post, comments) { 23 | res.render('post/item.html', {post: post, comments: comments}); 24 | }); 25 | ep.on('error', function(err) { 26 | ep.unbind(); 27 | next(err); 28 | }); 29 | db.posts.findById(req.params.id, function(err, post) { 30 | if(err) { 31 | return ep.emit('error', err); 32 | } 33 | if (!post) { 34 | return ep.emit('error'); 35 | } 36 | if(typeof post.author_id !== 'string') { 37 | post.author_id = post.author_id.toString(); 38 | } 39 | if (!post.author_id) { 40 | return ep.emit('error'); 41 | } 42 | db.users.findById(post.author_id, function(err, user) { 43 | if(err) { 44 | return ep.emit('error', err); 45 | } 46 | post.author = user; 47 | ep.emit('post', post); 48 | }); 49 | }); 50 | db.comments.findItems({parent_id: {$in: [req.params.id, db.ObjectID(req.params.id)]}}, 51 | {sort: {_id: -1}}, function(err, comments) { 52 | if(err) { 53 | return ep.emit('error', err); 54 | } 55 | ep.emit('comments', comments); 56 | }); 57 | }); 58 | 59 | app.get('/:id/edit', function(req, res, next) { 60 | _get_post_and_check_author(req, res, next, function(post) { 61 | res.render('post/edit.html', {post: post}); 62 | }); 63 | }); 64 | 65 | // new post 66 | app.post('/', function(req, res, next) { 67 | if(!utils.check_admin(req)) { 68 | res.writeHead(302, { Location: '/' }); 69 | return res.end(); 70 | } 71 | 72 | var post = { 73 | title: req.body.title 74 | , content: req.body.content 75 | }; 76 | if(post.content) { 77 | post.content = post.content.trim(); 78 | } 79 | post.tags = parse_tags(req.body.tags); 80 | var user = req.session.user; 81 | post.weblog_sync = req.body.sync_cb === 'on'; 82 | post.is_markdown = req.body.markdown_cb === 'on'; 83 | post.public = req.body.public_cb === 'on'; 84 | post.author_id = user._id; 85 | post.update_at = post.create_at = new Date(); 86 | var metaweblog = req.session.user.metaweblog; 87 | sync_weblog(metaweblog, post, function(err) { 88 | if(err) { 89 | return next(err); 90 | } 91 | db.posts.insert(post, function(err) { 92 | res.writeHead(302, { 93 | Location: '/post/' + post._id 94 | }); 95 | res.end(); 96 | }); 97 | }); 98 | }); 99 | 100 | // update post 101 | app.post('/:id', function(req, res, next) { 102 | _get_post_and_check_author(req, res, next, function(post) { 103 | post.title = req.body.title; 104 | post.content = req.body.content; 105 | if(post.content) { 106 | post.content = post.content.trim(); 107 | } 108 | post.tags = parse_tags(req.body.tags); 109 | post.weblog_sync = req.body.sync_cb === 'on'; 110 | post.is_markdown = req.body.markdown_cb === 'on'; 111 | post.public = req.body.public_cb === 'on'; 112 | post.update_at = new Date(); 113 | if(!post.create_at) { 114 | post.create_at = new Date(); 115 | } 116 | var metaweblog = req.session.user.metaweblog; 117 | sync_weblog(metaweblog, post, function(err) { 118 | if(err) { 119 | return next(err); 120 | } 121 | db.posts.update({_id: post._id}, post, function(err) { 122 | res.writeHead(302, { Location: '/post/' + post._id }); 123 | res.end(); 124 | }); 125 | }); 126 | }); 127 | }); 128 | 129 | app.post('/:id/delete', function(req, res, next) { 130 | _get_post_and_check_author(req, res, next, function(post) { 131 | var metaweblog = req.session.user.metaweblog; 132 | remove_weblog_post(metaweblog, post, function(err) { 133 | if(err) { 134 | if(utils.isxhr(req)) { 135 | return res.end(JSON.stringify({ error: err.message })); 136 | } 137 | return next(err); 138 | } 139 | db.posts.remove({_id: post._id}, function(err) { 140 | if(utils.isxhr(req)) { 141 | return res.end(JSON.stringify({ error: err && err.message })); 142 | } 143 | if(err) { 144 | return next(err); 145 | } 146 | res.writeHead(302, { Location: '/' }); 147 | res.end(); 148 | }); 149 | }); 150 | }); 151 | }); 152 | }; 153 | 154 | /** 155 | * 分割标签字符串(,) 156 | */ 157 | function parse_tags(tags) { 158 | tags = tags.split(','), needs = []; 159 | for(var i = 0, l = tags.length; i < l; i++) { 160 | var tag = tags[i].trim(); 161 | if(tag) { 162 | needs.push(tag); 163 | } 164 | } 165 | return needs.length > 0 ? needs : null; 166 | } 167 | 168 | /** 169 | * Get post by req.params.id and check if current use is author. 170 | * 171 | * @param req 172 | * @param res 173 | * @param next 174 | * @param callback 175 | * @api private 176 | */ 177 | function _get_post_and_check_author(req, res, next, callback) { 178 | db.posts.findById(req.params.id, function(err, post) { 179 | if(err || !post) { 180 | return next(err); 181 | } 182 | if(!utils.check_author(req, post)) { 183 | res.writeHead(302, { Location: '/' }); 184 | return res.end(); 185 | } 186 | callback(post); 187 | }); 188 | }; 189 | 190 | /** 191 | * Sync to your blog using MetaWeblog API 192 | * 193 | * @param {Object} metaweblog 194 | * @param {Object} post 195 | * @param {Function} callback(err) 196 | * @api private 197 | */ 198 | function sync_weblog(metaweblog, post, callback) { 199 | if(post.weblog_sync && metaweblog && metaweblog.bloginfo) { 200 | var weblog = new MetaWeblog(metaweblog.url); 201 | var weblog_post = { 202 | dateCreated: post.create_at 203 | , title: post.title 204 | , description: post.is_markdown ? utils.markdown(post.content) : post.content 205 | }; 206 | if(post.weblog_post) { // update 207 | weblog.editPost(post.weblog_post, metaweblog.username, metaweblog.password, weblog_post, post.public, callback); 208 | } else { 209 | weblog.newPost(metaweblog.bloginfo.blogid, metaweblog.username, metaweblog.password, weblog_post, post.public, 210 | function(err, weblog_post_id) { 211 | if(weblog_post_id) { 212 | post.weblog_post = weblog_post_id; 213 | } 214 | callback(err, weblog_post_id); 215 | }); 216 | } 217 | } else { 218 | callback(); 219 | } 220 | } 221 | 222 | function remove_weblog_post(metaweblog, post, callback) { 223 | if(post.weblog_sync && post.weblog_post && metaweblog && metaweblog.bloginfo) { 224 | var weblog = new MetaWeblog(metaweblog.url); 225 | weblog.deletePost('nodeblog', post.weblog_post, metaweblog.username, metaweblog.password, true, callback); 226 | } else { 227 | callback(); 228 | } 229 | }; 230 | 231 | 232 | -------------------------------------------------------------------------------- /controllers/tag.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | 6 | var db = require('../db') 7 | , EventProxy = require('../lib/eventproxy').EventProxy 8 | , utils = require('../lib/utils') 9 | , MetaWeblog = require('metaweblog').MetaWeblog; 10 | 11 | module.exports = function(app) { 12 | app.get('/:tagName', function(req, res, next) { 13 | var count = 20, page = parseInt(req.query.page || 1); 14 | if(isNaN(page)) { 15 | page = 1; 16 | } 17 | var options = {sort: {_id: -1}, limit: count}; 18 | if(page > 1) { 19 | options.skip = (page - 1) * count; 20 | } 21 | db.posts.list({tags: req.params.tagName}, options, function(err, posts) { 22 | posts= posts || []; 23 | res.render('archive.html', { 24 | posts: posts 25 | , page: page 26 | }); 27 | }); 28 | }); 29 | } 30 | /* 31 | exports.index = function(req, res, next) { 32 | Tag.where().find(function(err, tags) { 33 | if (err) return next(err); 34 | res.render('tag', { 35 | tags: tags 36 | , layout: true 37 | }); 38 | }) 39 | }; 40 | 41 | exports.show= function(req, res, next) { 42 | var tag= req.params.tag; 43 | // Post.where().limit(20).sort('create_at', -1).findBytag(tag, function(err, posts) { 44 | Post.findByTag(tag, function(err, posts) { 45 | if (err) return next(err); 46 | detailPosts(posts, function(err, posts) { 47 | var tpl="archive" 48 | , layout= true; 49 | if (req.query.format==="rss") { 50 | tpl= "rss", layout= false; 51 | } 52 | res.render(tpl, {posts: posts, layout: layout}); 53 | }); 54 | }); 55 | } 56 | */ -------------------------------------------------------------------------------- /controllers/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var MetaWeblog = require('metaweblog').MetaWeblog; 6 | 7 | var db = require('../db') 8 | , weibo = require('weibo') 9 | , config = require('../config'); 10 | 11 | module.exports = function(app) { 12 | app.get('/login', function(req, res, next) { 13 | var t_user = req.session.user; 14 | if(t_user) { 15 | res.writeHead(302, { 'Location': '/user/' + t_user.uid }); 16 | res.end(); 17 | } else { 18 | res.render('user/login.html'); 19 | } 20 | }); 21 | 22 | app.get('/logout', function(req, res, next) { 23 | req.session.user = null; 24 | res.writeHead(302, { 'Location': '/' }); 25 | res.end(); 26 | }); 27 | 28 | app.get('/:id', function(req, res, next) { 29 | _get_and_check_user(req, res, next, function(user) { 30 | var metaweblog = user.metaweblog || {}; 31 | res.render('user/item.html', {user: user, metaweblog: metaweblog}); 32 | }); 33 | }); 34 | 35 | app.post('/:id', function(req, res, next) { 36 | _get_and_check_user(req, res, next, function(user) { 37 | var metaweblog = user.metaweblog || {}; 38 | metaweblog.username = req.body.metaweblog_username; 39 | metaweblog.password = req.body.metaweblog_password; 40 | metaweblog.url = req.body.metaweblog_url; 41 | metaweblog.error = null; 42 | _check_metaweblog(metaweblog, function(err, bloginfo) { 43 | if(err) { 44 | metaweblog.error = err.message; 45 | } 46 | metaweblog.bloginfo = bloginfo; 47 | user.metaweblog = metaweblog; 48 | db.users.update({_id: user._id}, user, function(err) { 49 | if(err) { 50 | return next(err); 51 | } 52 | req.session.user = user; 53 | res.writeHead(302, { Location: '/user/' + user.uid }); 54 | res.end(); 55 | }); 56 | }); 57 | }); 58 | }); 59 | }; 60 | 61 | for(var type in config.weibo_appkeys) { 62 | var appkeys = config.weibo_appkeys[type]; 63 | if(appkeys[0]) { 64 | weibo.init(type, appkeys[0], appkeys[1]); 65 | } 66 | } 67 | 68 | module.exports.oauth_handle = weibo.oauth_middleware(function(oauth_user, referer, req, res, callback) { 69 | oauth_user.uid = oauth_user.blogtype + ':' + oauth_user.id; 70 | db.users.findOne({uid: oauth_user.uid}, function(err, user) { 71 | if(err) { 72 | return callback(err); 73 | } 74 | user = user || {}; 75 | user.uid = oauth_user.uid; 76 | user.screen_name = oauth_user.screen_name; 77 | user.t_url = oauth_user.t_url; 78 | user.profile_image_url = oauth_user.profile_image_url; 79 | user.info = oauth_user; 80 | user.is_admin = config.admins.indexOf(user.uid) >= 0; 81 | db.users.update({uid: user.uid}, user, {upsert: true}, function(err) { 82 | if(err) { 83 | return callback(err); 84 | } 85 | req.session.user = user; 86 | callback(); 87 | }); 88 | }); 89 | }); 90 | 91 | function _get_and_check_user(req, res, next, callback) { 92 | if(!req.session.user || req.session.user.uid !== req.params.id) { 93 | res.writeHead(302, { 'Location': '/' }); 94 | return res.end(); 95 | } 96 | db.users.findOne({uid: req.params.id}, function(err, user) { 97 | if(err) { 98 | return next(err); 99 | } 100 | callback(user); 101 | }); 102 | }; 103 | 104 | function _check_metaweblog(metaweblog, callback) { 105 | if(metaweblog.url && metaweblog.username && metaweblog.password && MetaWeblog) { 106 | var weblog = new MetaWeblog(metaweblog.url); 107 | weblog.getUsersBlogs('nodeblog', metaweblog.username, metaweblog.password, function(err, bloginfos) { 108 | if(err) { 109 | return callback(err); 110 | } 111 | var bloginfo = null; 112 | if(bloginfos && bloginfos.length > 0) { 113 | bloginfo = bloginfos[0]; 114 | } 115 | callback(null, bloginfo); 116 | }); 117 | } else { 118 | callback(); 119 | } 120 | }; 121 | 122 | //exports.show = function(req, res, next) { 123 | // var t_user = req.session.user; 124 | // if(!t_user) { 125 | // return res.redirect('/'); 126 | // } 127 | // User.findOne({uid: t_user.uid}, function(err, user) { 128 | // if(err) return next(err); 129 | // var setting = user.setting || {}; 130 | // var metaweblog = user.metaweblog || {}; 131 | // res.render('user', {user: user, setting: setting, metaweblog: metaweblog, support_metaweblog: support_metaweblog}); 132 | // }); 133 | //}; 134 | // 135 | //function save_metaweblog(params, user, callback) { 136 | // var metaweblog = user.metaweblog || {}; 137 | // metaweblog.username = params.metaweblog_username; 138 | // metaweblog.password = params.metaweblog_password; 139 | // metaweblog.url = params.metaweblog_url; 140 | // user.metaweblog = metaweblog; 141 | // if(metaweblog.url && metaweblog.username && metaweblog.password && MetaWeblog) { 142 | // var weblog = new MetaWeblog(metaweblog.url); 143 | // weblog.getUsersBlogs('nodeblog', metaweblog.username, metaweblog.password, function(err, bloginfos) { 144 | // metaweblog.error = (err && err.message) || null; 145 | // if(bloginfos && bloginfos.length > 0) { 146 | // metaweblog.bloginfo = bloginfos[0]; 147 | // } else { 148 | // metaweblog.bloginfo = null; 149 | // } 150 | // callback(); 151 | // }); 152 | // } else { 153 | // metaweblog.bloginfo = null; 154 | // callback(); 155 | // } 156 | //}; 157 | // 158 | //exports.save = function(req, res, next) { 159 | // if(!req.session.user) { 160 | // return next(); 161 | // } 162 | // User.findOne({uid: req.session.user.uid}, function(err, user) { 163 | // save_metaweblog(req.body, user, function() { 164 | // req.session.user = user; 165 | // user.save(function(err) { 166 | // if(err) return next(err); 167 | // res.redirect('/user/' + user.id); 168 | // }); 169 | // }); 170 | // }); 171 | //}; -------------------------------------------------------------------------------- /db.js: -------------------------------------------------------------------------------- 1 | var config = require('./config'); 2 | var mongo = require('mongoskin'); 3 | 4 | var dburl = ''; 5 | var uri = 'mongodb://'; 6 | if (config.db_options.user && config.db_options.password) { 7 | dburl += config.db_options.user + ':' + config.db_options.password + '@'; 8 | } 9 | dburl += config.db_options.host + ':' + config.db_options.port 10 | + '/' + config.db_options.database; 11 | var db = module.exports = mongo.db(dburl); 12 | db.ObjectID = db.db.bson_serializer.ObjectID; 13 | 14 | db.bind('posts', { 15 | list: function(query, options, callback) { 16 | return this.findItems(query, options, function(err, posts) { 17 | if(posts && posts.length > 0) { 18 | var ids = []; 19 | for(var i = 0, l = posts.length; i < l; i++) { 20 | var post = posts[i]; 21 | var author_id = typeof post.author_id === 'string' ? db.ObjectID(post.author_id) : post.author_id; 22 | ids.push(author_id); 23 | } 24 | 25 | db.users.findItems({_id: {$in: ids}}, function(err, users) { 26 | if(users) { 27 | var map = {}; 28 | for(var i = 0, l = users.length; i < l; i++) { 29 | var user = users[i]; 30 | map[user._id] = user; 31 | } 32 | for(var i = 0, l = posts.length; i < l; i++) { 33 | var post = posts[i]; 34 | post.author = map[post.author_id]; 35 | } 36 | } 37 | callback(err, posts); 38 | }); 39 | } else { 40 | callback(err, posts); 41 | } 42 | }); 43 | } 44 | }); 45 | 46 | db.bind('users'); 47 | db.users.ensureIndex({uid: 1}, {unique: true}, function() {}); 48 | //tag用 49 | db.posts.ensureIndex({tags: 1}, {}, function() {}); 50 | 51 | db.bind('comments'); 52 | -------------------------------------------------------------------------------- /history.md: -------------------------------------------------------------------------------- 1 | # Update history 2 | 3 | ## v0.2.1 4 | 5 | * 首页默认显示markdown的第一个段落 6 | * 发布markdown格式文章时,自动将第一行# xxx 作为标题 7 | * 默认选中markdown格式 8 | * 首页增加分页功能 9 | 10 | ## v0.2.0 11 | 12 | * 完全基于Connect,提高整体响应性能; 13 | * 移除对express, libxml2的依赖; 14 | * Session使用Mongodb存储; 15 | 16 | -------------------------------------------------------------------------------- /lib/combo.js: -------------------------------------------------------------------------------- 1 | 2 | function Combo(callback) { 3 | this.callback = callback; 4 | this.items = 0; 5 | this.results = []; 6 | } 7 | 8 | Combo.prototype = { 9 | add: function () { 10 | var self = this, 11 | id = this.items; 12 | this.items++; 13 | return function () { 14 | self.check(id, arguments); 15 | }; 16 | }, 17 | check: function (id, arguments) { 18 | this.results[id] = Array.prototype.slice.call(arguments); 19 | this.items--; 20 | if (this.items == 0) { 21 | this.callback.apply(this, this.results); 22 | } 23 | } 24 | }; 25 | 26 | exports.Combo = Combo; 27 | 28 | exports.combo = function(callback) { 29 | return new Combo(callback); 30 | }; -------------------------------------------------------------------------------- /lib/cookie.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Response Cookie utils for Connect 3 | */ 4 | 5 | var utils = require('connect').utils; 6 | 7 | module.exports = function() { 8 | return function(req, res, next) { 9 | res.clearCookie = clearCookie; 10 | res.cookie = cookie; 11 | next(); 12 | }; 13 | }; 14 | 15 | /** 16 | * Clear cookie `name`. 17 | * 18 | * @param {String} name 19 | * @param {Object} options 20 | * @param {ServerResponse} for chaining 21 | * @api public 22 | */ 23 | function clearCookie(name, options){ 24 | options = options || {}; 25 | options.expires = new Date(1); 26 | return this.cookie(name, '', options); 27 | }; 28 | 29 | /** 30 | * Set cookie `name` to `val`, with the given `options`. 31 | * 32 | * Options: 33 | * 34 | * - `maxAge` max-age in milliseconds, converted to `expires` 35 | * - `path` defaults is "/" 36 | * 37 | * Examples: 38 | * 39 | * // "Remember Me" for 15 minutes 40 | * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true }); 41 | * 42 | * // save as above 43 | * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }) 44 | * 45 | * @param {String} name 46 | * @param {String} val 47 | * @param {Options} options 48 | * @api public 49 | */ 50 | function cookie(name, val, options){ 51 | options = options || {}; 52 | if ('maxAge' in options) { 53 | options.expires = new Date(Date.now() + options.maxAge); 54 | } 55 | options.path = options.path || '/'; 56 | var cookie = utils.serializeCookie(name, val, options); 57 | this.writeHead('Set-Cookie', cookie); 58 | return this; 59 | }; -------------------------------------------------------------------------------- /lib/eventproxy.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // EventProxy 3 | // ----------------- 4 | 5 | // A module that can be mixed in to *any object* in order to provide it with 6 | // custom events. You may `bind` or `unbind` a callback function to an event; 7 | // `trigger`-ing an event fires all callbacks in succession. 8 | // 9 | // 10 | 11 | /** 12 | * @description Event Proxy. 13 | * @constructor EventProxy. 14 | * @example 15 | * var render = function (template, resources) {}; 16 | * var proxy = new EventProxy(); 17 | * proxy.assign("template", "l10n", render); 18 | * proxy.trigger("template", template); 19 | * proxy.trigger("l10n", resources); 20 | */ 21 | var EventProxy = function () { 22 | this._callbacks = {}; 23 | this._fired = {}; 24 | }; 25 | /** 26 | * @description Bind an event, specified by a string name, `ev`, to a `callback` function. 27 | * Passing `"all"` will bind the callback to all events fired. 28 | * @param {string} eventName Event name. 29 | * @param {function} callback Callback. 30 | */ 31 | EventProxy.prototype.bind = EventProxy.prototype.on = EventProxy.prototype.addListener = function(ev, callback) { 32 | this._callbacks = this._callbacks || {}; 33 | var list = this._callbacks[ev] || (this._callbacks[ev] = []); 34 | list.push(callback); 35 | return this; 36 | }; 37 | 38 | /** 39 | * @description Remove one or many callbacks. If `callback` is null, removes all 40 | * callbacks for the event. If `ev` is null, removes all bound callbacks 41 | * for all events. 42 | * @param {string} eventName Event name. 43 | * @param {function} callback Callback. 44 | */ 45 | EventProxy.prototype.unbind = EventProxy.prototype.removeListener = function(ev, callback) { 46 | var calls; 47 | if (!ev) { 48 | this._callbacks = {}; 49 | } else if (calls = this._callbacks) { 50 | if (!callback) { 51 | calls[ev] = []; 52 | } else { 53 | var list = calls[ev]; 54 | if (!list) return this; 55 | for (var i = 0, l = list.length; i < l; i++) { 56 | if (callback === list[i]) { 57 | list[i] = null; 58 | break; 59 | } 60 | } 61 | } 62 | } 63 | return this; 64 | }; 65 | 66 | /** 67 | * @description Remove all listeners. 68 | * It equals proxy.unbind(); Just add this API for as same as Event.Emitter. 69 | * @param {string} event Event name. 70 | */ 71 | EventProxy.prototype.removeAllListeners = function (event) { 72 | return this.unbind(event); 73 | }; 74 | 75 | /** 76 | * @description Trigger an event, firing all bound callbacks. Callbacks are passed the 77 | * same arguments as `trigger` is, apart from the event name. 78 | * Listening for `"all"` passes the true event name as the first argument. 79 | * @param {string} eventName Event name. 80 | * @param {mix} data Pass in data. 81 | */ 82 | EventProxy.prototype.emit = EventProxy.prototype.fire = EventProxy.prototype.trigger = function(eventName, data, data2) { 83 | var list, calls, ev, callback, args, i, l; 84 | var both = 2; 85 | if (!(calls = this._callbacks)) return this; 86 | while (both--) { 87 | ev = both ? eventName : 'all'; 88 | if (list = calls[ev]) { 89 | for (i = 0, l = list.length; i < l; i++) { 90 | if (!(callback = list[i])) { 91 | list.splice(i, 1); i--; l--; 92 | } else { 93 | args = both ? Array.prototype.slice.call(arguments, 1) : arguments; 94 | callback.apply(this, args); 95 | } 96 | } 97 | } 98 | } 99 | return this; 100 | }; 101 | 102 | /** 103 | * @description Bind an event like the bind method, but will remove the listener after it was fired. 104 | * @param {string} ev Event name. 105 | * @param {function} callback Callback. 106 | */ 107 | EventProxy.prototype.once = function (ev, callback) { 108 | var self = this; 109 | this.bind(ev, function () { 110 | callback.apply(self, arguments); 111 | self.unbind(ev, arguments.callee); 112 | }); 113 | return this; 114 | }; 115 | var _assign = function (eventname1, eventname2, cb, once) { 116 | var proxy = this, length, index = 0, argsLength = arguments.length, 117 | callback, events, isOnce, times = 0, flag = {}; 118 | 119 | // Check the arguments length. 120 | if (argsLength < 3) { 121 | return this; 122 | } 123 | events = [].slice.apply(arguments, [0, argsLength - 2]); 124 | callback = arguments[argsLength - 2]; 125 | isOnce = arguments[argsLength - 1]; 126 | // Check the callback type. 127 | if (typeof callback !== "function") { 128 | return this; 129 | } 130 | length = events.length; 131 | 132 | var bind = function (key) { 133 | var method = isOnce ? "once" : "bind"; 134 | proxy[method](key, function (data) { 135 | proxy._fired[key] = proxy._fired[key] || {}; 136 | proxy._fired[key].data = data; 137 | if (!flag[key]) { 138 | flag[key] = true; 139 | times++; 140 | } 141 | }); 142 | }; 143 | for (index = 0; index < length; index++) { 144 | bind(events[index]); 145 | } 146 | var all = function () { 147 | if (times < length) { 148 | return; 149 | } 150 | var data = []; 151 | for (index = 0; index < length; index++) { 152 | data.push(proxy._fired[events[index]].data); 153 | } 154 | if (isOnce) { 155 | proxy.unbind("all", all); 156 | } 157 | callback.apply(null, data); 158 | }; 159 | proxy.bind("all", all); 160 | }; 161 | /** 162 | * @description Assign some events, after all events were fired, the callback will be executed once. 163 | * @param {string} eventname1 First event name. 164 | * @param {string} eventname2 Second event name. 165 | * @param {function} cb Callback, that will be called after predefined events were fired. 166 | */ 167 | EventProxy.prototype.assign = function (eventname1, eventname2, cb) { 168 | var args = [].slice.call(arguments); 169 | args.push(true); 170 | _assign.apply(this, args); 171 | return this; 172 | }; 173 | /** 174 | * @description Assign some events, after all events were fired, the callback will be executed first time. 175 | * then any event that predefined be fired again, the callback will executed with the newest data. 176 | * @param {string} eventname1 First event name. 177 | * @param {string} eventname2 Second event name. 178 | * @param {function} cb Callback, that will be called after predefined events were fired. 179 | */ 180 | EventProxy.prototype.assignAll = EventProxy.prototype.assignAlways = function () { 181 | var args = [].slice.call(arguments); 182 | args.push(false); 183 | _assign.apply(this, args); 184 | return this; 185 | }; 186 | 187 | /** 188 | * @description The callback will be executed after the event be fired N times. 189 | * @param {string} eventName Event name. 190 | * @param {number} times N times. 191 | * @param {function} callback Callback, that will be called after event was fired N times. 192 | */ 193 | EventProxy.prototype.after = function (eventName, times, callback) { 194 | var proxy = this, 195 | firedData = [], 196 | all = function (name, fired){ 197 | if (name === eventName) { 198 | times--; 199 | //firedData.push(data); 200 | if (times < 1) { 201 | proxy.unbind("all", all); 202 | callback.apply(null, [firedData]); 203 | } 204 | } 205 | }; 206 | proxy.bind("all", all); 207 | return this; 208 | }; 209 | 210 | // Event proxy can be used in browser and Nodejs both. 211 | if (typeof exports !== "undefined") { 212 | exports.EventProxy = EventProxy; 213 | } else { 214 | window.EventProxy = EventProxy; 215 | } 216 | 217 | }()); -------------------------------------------------------------------------------- /lib/session_store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Session Store for Connect, base on mongoskin 3 | * 4 | * Session Store Implementation 5 | 6 | Every session store must implement the following methods 7 | 8 | .get(sid, callback) 9 | .set(sid, session, callback) 10 | .destroy(sid, callback) 11 | Recommended methods include, but are not limited to: 12 | 13 | .length(callback) 14 | .clear(callback) 15 | For an example implementation view the connect-redis repo. 16 | 17 | * 18 | * Use case: 19 | * 20 | * var Store = require('./lib/session_store'); 21 | * 22 | * var store = new Store({ 23 | * host: 'localhost' 24 | * , port: 27017 25 | * , database: 'connect_session' 26 | * , user: 'foo' // optional 27 | * , password: 'bar' // optional 28 | * }); 29 | * 30 | * connect( 31 | * connect.sesstion({ 32 | * store: store 33 | * }); 34 | * ); 35 | * 36 | */ 37 | 38 | var mongo = require('mongoskin') 39 | , Store = require('connect').session.Store; 40 | 41 | var MongoStore = function(options) { 42 | var dburl = 'mongodb://'; 43 | if(options.user && options.password) { 44 | dburl = options.user + ':' + options.password + '@'; 45 | } 46 | dburl += options.host + ':' + options.port + '/' + options.database; 47 | this.db = mongo.db(dburl); 48 | var name = options.collection || 'session'; 49 | this.db.bind(name); 50 | this.collection = this.db[name]; 51 | }; 52 | 53 | MongoStore.prototype.__proto__ = Store.prototype; 54 | 55 | MongoStore.prototype.get = function(sid, callback) { 56 | this.collection.findOne({_id: sid}, function(err, data) { 57 | if(data) { 58 | var sess = typeof data.session === 'string' ? JSON.parse(data.session) : data.session; 59 | return callback(null, sess); 60 | } 61 | callback(err); 62 | }); 63 | }; 64 | 65 | MongoStore.prototype.set = function(sid, session, callback) { 66 | var update = {_id: sid, session: JSON.stringify(session)}; 67 | if (session && session.cookie && session.cookie.expires) { 68 | update.expires = Date.parse(session.cookie.expires); 69 | } 70 | this.collection.update({_id: sid}, update, {upsert: true}, callback); 71 | }; 72 | 73 | MongoStore.prototype.destroy = function(sid, callback) { 74 | this.collection.remove({_id: sid}, callback); 75 | }; 76 | 77 | MongoStore.prototype.length = function(callback) { 78 | this.collection.count({}, callback); 79 | }; 80 | 81 | MongoStore.prototype.clear = function(callback) { 82 | this.db.dropCollection(this.collection.collectionName, callback); 83 | }; 84 | 85 | module.exports = MongoStore; -------------------------------------------------------------------------------- /lib/showdown.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * nodeblog - showdown 3 | * port from https://raw.github.com/github/github-flavored-markdown/gh-pages/scripts/showdown.js 4 | * 5 | * Copyright(c) 2012 fengmk2 6 | * MIT Licensed 7 | */ 8 | 9 | // 10 | // showdown.js -- A javascript port of Markdown. 11 | // 12 | // Copyright (c) 2007 John Fraser. 13 | // 14 | // Original Markdown Copyright (c) 2004-2005 John Gruber 15 | // 16 | // 17 | // Redistributable under a BSD-style open source license. 18 | // See license.txt for more information. 19 | // 20 | // The full source distribution is at: 21 | // 22 | // A A L 23 | // T C A 24 | // T K B 25 | // 26 | // 27 | // 28 | 29 | // 30 | // Wherever possible, Showdown is a straight, line-by-line port 31 | // of the Perl version of Markdown. 32 | // 33 | // This is not a normal parser design; it's basically just a 34 | // series of string substitutions. It's hard to read and 35 | // maintain this way, but keeping Showdown close to the original 36 | // design makes it easier to port new features. 37 | // 38 | // More importantly, Showdown behaves like markdown.pl in most 39 | // edge cases. So web applications can do client-side preview 40 | // in Javascript, and then build identical HTML on the server. 41 | // 42 | // This port needs the new RegExp functionality of ECMA 262, 43 | // 3rd Edition (i.e. Javascript 1.5). Most modern web browsers 44 | // should do fine. Even with the new regular expression features, 45 | // We do a lot of work to emulate Perl's regex functionality. 46 | // The tricky changes in this file mostly have the "attacklab:" 47 | // label. Major or self-explanatory changes don't. 48 | // 49 | // Smart diff tools like Araxis Merge will be able to match up 50 | // this file with markdown.pl in a useful way. A little tweaking 51 | // helps: in a copy of markdown.pl, replace "#" with "//" and 52 | // replace "$text" with "text". Be sure to ignore whitespace 53 | // and line endings. 54 | // 55 | 56 | 57 | // 58 | // Showdown usage: 59 | // 60 | // var text = "Markdown *rocks*."; 61 | // 62 | // var converter = new Showdown.converter(); 63 | // var html = converter.makeHtml(text); 64 | // 65 | // alert(html); 66 | // 67 | // Note: move the sample code to the bottom of this 68 | // file before uncommenting it. 69 | // 70 | 71 | 72 | // ************************************************** 73 | // GitHub Flavored Markdown modifications by Tekkub 74 | // http://github.github.com/github-flavored-markdown/ 75 | // 76 | // Modifications are tagged with "GFM" 77 | // ************************************************** 78 | 79 | // ************************************************** 80 | // Node.JS port by fengmk2 81 | // 82 | // Modifications are tagged with "mk2" 83 | // ************************************************** 84 | 85 | // 86 | // Showdown namespace 87 | // 88 | var Showdown = {}; 89 | 90 | // 91 | // mk2: export the Showdown object 92 | // 93 | if (typeof exports !== "undefined") { 94 | Showdown = exports; 95 | Showdown.parse = function (md) { 96 | return new Showdown.converter().makeHtml(md); 97 | }; 98 | } 99 | 100 | 101 | // 102 | // converter 103 | // 104 | // Wraps all "globals" so that the only thing 105 | // exposed is makeHtml(). 106 | // 107 | Showdown.converter = function() { 108 | 109 | // 110 | // Globals: 111 | // 112 | 113 | // Global hashes, used by various utility routines 114 | var g_urls; 115 | var g_titles; 116 | var g_html_blocks; 117 | 118 | // Used to track when we're inside an ordered or unordered list 119 | // (see _ProcessListItems() for details): 120 | var g_list_level = 0; 121 | 122 | 123 | this.makeHtml = function(text) { 124 | // 125 | // Main function. The order in which other subs are called here is 126 | // essential. Link and image substitutions need to happen before 127 | // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the 128 | // and tags get encoded. 129 | // 130 | 131 | // Clear the global hashes. If we don't clear these, you get conflicts 132 | // from other articles when generating a page which contains more than 133 | // one article (e.g. an index page that shows the N most recent 134 | // articles): 135 | g_urls = new Array(); 136 | g_titles = new Array(); 137 | g_html_blocks = new Array(); 138 | 139 | // attacklab: Replace ~ with ~T 140 | // This lets us use tilde as an escape char to avoid md5 hashes 141 | // The choice of character is arbitray; anything that isn't 142 | // magic in Markdown will work. 143 | text = text.replace(/~/g,"~T"); 144 | 145 | // attacklab: Replace $ with ~D 146 | // RegExp interprets $ as a special character 147 | // when it's in a replacement string 148 | text = text.replace(/\$/g,"~D"); 149 | 150 | // Standardize line endings 151 | text = text.replace(/\r\n/g,"\n"); // DOS to Unix 152 | text = text.replace(/\r/g,"\n"); // Mac to Unix 153 | 154 | // Make sure text begins and ends with a couple of newlines: 155 | text = "\n\n" + text + "\n\n"; 156 | 157 | // Convert all tabs to spaces. 158 | text = _Detab(text); 159 | 160 | // Strip any lines consisting only of spaces and tabs. 161 | // This makes subsequent regexen easier to write, because we can 162 | // match consecutive blank lines with /\n+/ instead of something 163 | // contorted like /[ \t]*\n+/ . 164 | text = text.replace(/^[ \t]+$/mg,""); 165 | 166 | // Turn block-level HTML blocks into hash entries 167 | text = _HashHTMLBlocks(text); 168 | 169 | // Strip link definitions, store in hashes. 170 | text = _StripLinkDefinitions(text); 171 | 172 | text = _RunBlockGamut(text); 173 | 174 | text = _UnescapeSpecialChars(text); 175 | 176 | // attacklab: Restore dollar signs 177 | text = text.replace(/~D/g,"$$"); 178 | 179 | // attacklab: Restore tildes 180 | text = text.replace(/~T/g,"~"); 181 | 182 | // ** GFM ** Auto-link URLs and emails 183 | text = text.replace(/https?\:\/\/[^"\s\<\>]*[^.,;'">\:\s\<\>\)\]\!]/g, function(wholeMatch,matchIndex){ 184 | var left = text.slice(0, matchIndex), right = text.slice(matchIndex) 185 | if (left.match(/<[^>]+$/) && right.match(/^[^>]*>/)) {return wholeMatch} 186 | href = wholeMatch.replace(/^http:\/\/github.com\//, "https://github.com/") 187 | return "" + wholeMatch + ""; 188 | }); 189 | text = text.replace(/[a-z0-9_\-+=.]+@[a-z0-9\-]+(\.[a-z0-9-]+)+/ig, function(wholeMatch){return "" + wholeMatch + "";}); 190 | 191 | // ** GFM ** Auto-link sha1 if GitHub.nameWithOwner is defined 192 | text = text.replace(/[a-f0-9]{40}/ig, function(wholeMatch,matchIndex){ 193 | if (typeof(GitHub) == "undefined" || typeof(GitHub.nameWithOwner) == "undefined") {return wholeMatch;} 194 | var left = text.slice(0, matchIndex), right = text.slice(matchIndex) 195 | if (left.match(/@$/) || (left.match(/<[^>]+$/) && right.match(/^[^>]*>/))) {return wholeMatch;} 196 | return "" + wholeMatch.substring(0,7) + ""; 197 | }); 198 | 199 | // ** GFM ** Auto-link user@sha1 if GitHub.nameWithOwner is defined 200 | text = text.replace(/([a-z0-9_\-+=.]+)@([a-f0-9]{40})/ig, function(wholeMatch,username,sha,matchIndex){ 201 | if (typeof(GitHub) == "undefined" || typeof(GitHub.nameWithOwner) == "undefined") {return wholeMatch;} 202 | GitHub.repoName = GitHub.repoName || _GetRepoName() 203 | var left = text.slice(0, matchIndex), right = text.slice(matchIndex) 204 | if (left.match(/\/$/) || (left.match(/<[^>]+$/) && right.match(/^[^>]*>/))) {return wholeMatch;} 205 | return "" + username + "@" + sha.substring(0,7) + ""; 206 | }); 207 | 208 | // ** GFM ** Auto-link user/repo@sha1 209 | text = text.replace(/([a-z0-9_\-+=.]+\/[a-z0-9_\-+=.]+)@([a-f0-9]{40})/ig, function(wholeMatch,repo,sha){ 210 | return "" + repo + "@" + sha.substring(0,7) + ""; 211 | }); 212 | 213 | // ** GFM ** Auto-link #issue if GitHub.nameWithOwner is defined 214 | text = text.replace(/#([0-9]+)/ig, function(wholeMatch,issue,matchIndex){ 215 | if (typeof(GitHub) == "undefined" || typeof(GitHub.nameWithOwner) == "undefined") {return wholeMatch;} 216 | var left = text.slice(0, matchIndex), right = text.slice(matchIndex) 217 | if (left == "" || left.match(/[a-z0-9_\-+=.]$/) || (left.match(/<[^>]+$/) && right.match(/^[^>]*>/))) {return wholeMatch;} 218 | return "" + wholeMatch + ""; 219 | }); 220 | 221 | // ** GFM ** Auto-link user#issue if GitHub.nameWithOwner is defined 222 | text = text.replace(/([a-z0-9_\-+=.]+)#([0-9]+)/ig, function(wholeMatch,username,issue,matchIndex){ 223 | if (typeof(GitHub) == "undefined" || typeof(GitHub.nameWithOwner) == "undefined") {return wholeMatch;} 224 | GitHub.repoName = GitHub.repoName || _GetRepoName() 225 | var left = text.slice(0, matchIndex), right = text.slice(matchIndex) 226 | if (left.match(/\/$/) || (left.match(/<[^>]+$/) && right.match(/^[^>]*>/))) {return wholeMatch;} 227 | return "" + wholeMatch + ""; 228 | }); 229 | 230 | // ** GFM ** Auto-link user/repo#issue 231 | text = text.replace(/([a-z0-9_\-+=.]+\/[a-z0-9_\-+=.]+)#([0-9]+)/ig, function(wholeMatch,repo,issue){ 232 | return "" + wholeMatch + ""; 233 | }); 234 | 235 | return text; 236 | } 237 | 238 | 239 | var _GetRepoName = function() { 240 | return GitHub.nameWithOwner.match(/^.+\/(.+)$/)[1] 241 | } 242 | 243 | var _StripLinkDefinitions = function(text) { 244 | // 245 | // Strips link definitions from text, stores the URLs and titles in 246 | // hash references. 247 | // 248 | 249 | // Link defs are in the form: ^[id]: url "optional title" 250 | 251 | /* 252 | var text = text.replace(/ 253 | ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1 254 | [ \t]* 255 | \n? // maybe *one* newline 256 | [ \t]* 257 | ? // url = $2 258 | [ \t]* 259 | \n? // maybe one newline 260 | [ \t]* 261 | (?: 262 | (\n*) // any lines skipped = $3 attacklab: lookbehind removed 263 | ["(] 264 | (.+?) // title = $4 265 | [")] 266 | [ \t]* 267 | )? // title is optional 268 | (?:\n+|$) 269 | /gm, 270 | function(){...}); 271 | */ 272 | var text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|\Z)/gm, 273 | function (wholeMatch,m1,m2,m3,m4) { 274 | m1 = m1.toLowerCase(); 275 | g_urls[m1] = _EncodeAmpsAndAngles(m2); // Link IDs are case-insensitive 276 | if (m3) { 277 | // Oops, found blank lines, so it's not a title. 278 | // Put back the parenthetical statement we stole. 279 | return m3+m4; 280 | } else if (m4) { 281 | g_titles[m1] = m4.replace(/"/g,"""); 282 | } 283 | 284 | // Completely remove the definition from the text 285 | return ""; 286 | } 287 | ); 288 | 289 | return text; 290 | } 291 | 292 | 293 | var _HashHTMLBlocks = function(text) { 294 | // attacklab: Double up blank lines to reduce lookaround 295 | text = text.replace(/\n/g,"\n\n"); 296 | 297 | // Hashify HTML blocks: 298 | // We only want to do this for block-level HTML tags, such as headers, 299 | // lists, and tables. That's because we still want to wrap

s around 300 | // "paragraphs" that are wrapped in non-block-level tags, such as anchors, 301 | // phrase emphasis, and spans. The list of tags we're looking for is 302 | // hard-coded: 303 | var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del" 304 | var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math" 305 | 306 | // First, look for nested blocks, e.g.: 307 | //

308 | //
309 | // tags for inner block must be indented. 310 | //
311 | //
312 | // 313 | // The outermost tags must start at the left margin for this to match, and 314 | // the inner nested divs must be indented. 315 | // We need to do this before the next, more liberal match, because the next 316 | // match will start at the first `
` and stop at the first `
`. 317 | 318 | // attacklab: This regex can be expensive when it fails. 319 | /* 320 | var text = text.replace(/ 321 | ( // save in $1 322 | ^ // start of line (with /m) 323 | <($block_tags_a) // start tag = $2 324 | \b // word break 325 | // attacklab: hack around khtml/pcre bug... 326 | [^\r]*?\n // any number of lines, minimally matching 327 | // the matching end tag 328 | [ \t]* // trailing spaces/tabs 329 | (?=\n+) // followed by a newline 330 | ) // attacklab: there are sentinel newlines at end of document 331 | /gm,function(){...}}; 332 | */ 333 | text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm,hashElement); 334 | 335 | // 336 | // Now match more liberally, simply from `\n` to `\n` 337 | // 338 | 339 | /* 340 | var text = text.replace(/ 341 | ( // save in $1 342 | ^ // start of line (with /m) 343 | <($block_tags_b) // start tag = $2 344 | \b // word break 345 | // attacklab: hack around khtml/pcre bug... 346 | [^\r]*? // any number of lines, minimally matching 347 | .* // the matching end tag 348 | [ \t]* // trailing spaces/tabs 349 | (?=\n+) // followed by a newline 350 | ) // attacklab: there are sentinel newlines at end of document 351 | /gm,function(){...}}; 352 | */ 353 | text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm,hashElement); 354 | 355 | // Special case just for
. It was easier to make a special case than 356 | // to make the other regex more complicated. 357 | 358 | /* 359 | text = text.replace(/ 360 | ( // save in $1 361 | \n\n // Starting after a blank line 362 | [ ]{0,3} 363 | (<(hr) // start tag = $2 364 | \b // word break 365 | ([^<>])*? // 366 | \/?>) // the matching end tag 367 | [ \t]* 368 | (?=\n{2,}) // followed by a blank line 369 | ) 370 | /g,hashElement); 371 | */ 372 | text = text.replace(/(\n[ ]{0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,hashElement); 373 | 374 | // Special case for standalone HTML comments: 375 | 376 | /* 377 | text = text.replace(/ 378 | ( // save in $1 379 | \n\n // Starting after a blank line 380 | [ ]{0,3} // attacklab: g_tab_width - 1 381 | 384 | [ \t]* 385 | (?=\n{2,}) // followed by a blank line 386 | ) 387 | /g,hashElement); 388 | */ 389 | text = text.replace(/(\n\n[ ]{0,3}[ \t]*(?=\n{2,}))/g,hashElement); 390 | 391 | // PHP and ASP-style processor instructions ( and <%...%>) 392 | 393 | /* 394 | text = text.replace(/ 395 | (?: 396 | \n\n // Starting after a blank line 397 | ) 398 | ( // save in $1 399 | [ ]{0,3} // attacklab: g_tab_width - 1 400 | (?: 401 | <([?%]) // $2 402 | [^\r]*? 403 | \2> 404 | ) 405 | [ \t]* 406 | (?=\n{2,}) // followed by a blank line 407 | ) 408 | /g,hashElement); 409 | */ 410 | text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,hashElement); 411 | 412 | // attacklab: Undo double lines (see comment at top of this function) 413 | text = text.replace(/\n\n/g,"\n"); 414 | return text; 415 | } 416 | 417 | var hashElement = function(wholeMatch,m1) { 418 | var blockText = m1; 419 | 420 | // Undo double lines 421 | blockText = blockText.replace(/\n\n/g,"\n"); 422 | blockText = blockText.replace(/^\n/,""); 423 | 424 | // strip trailing blank lines 425 | blockText = blockText.replace(/\n+$/g,""); 426 | 427 | // Replace the element text with a marker ("~KxK" where x is its key) 428 | blockText = "\n\n~K" + (g_html_blocks.push(blockText)-1) + "K\n\n"; 429 | 430 | return blockText; 431 | }; 432 | 433 | var _RunBlockGamut = function(text) { 434 | // 435 | // These are all the transformations that form block-level 436 | // tags like paragraphs, headers, and list items. 437 | // 438 | text = _DoHeaders(text); 439 | 440 | // Do Horizontal Rules: 441 | var key = hashBlock("
"); 442 | text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm,key); 443 | text = text.replace(/^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$/gm,key); 444 | text = text.replace(/^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$/gm,key); 445 | 446 | text = _DoLists(text); 447 | text = _DoCodeBlocks(text); 448 | text = _DoBlockQuotes(text); 449 | 450 | // We already ran _HashHTMLBlocks() before, in Markdown(), but that 451 | // was to escape raw HTML in the original Markdown source. This time, 452 | // we're escaping the markup we've just created, so that we don't wrap 453 | //

tags around block-level tags. 454 | text = _HashHTMLBlocks(text); 455 | text = _FormParagraphs(text); 456 | 457 | return text; 458 | } 459 | 460 | 461 | var _RunSpanGamut = function(text) { 462 | // 463 | // These are all the transformations that occur *within* block-level 464 | // tags like paragraphs, headers, and list items. 465 | // 466 | 467 | text = _DoCodeSpans(text); 468 | text = _EscapeSpecialCharsWithinTagAttributes(text); 469 | text = _EncodeBackslashEscapes(text); 470 | 471 | // Process anchor and image tags. Images must come first, 472 | // because ![foo][f] looks like an anchor. 473 | text = _DoImages(text); 474 | text = _DoAnchors(text); 475 | 476 | // Make links out of things like `` 477 | // Must come after _DoAnchors(), because you can use < and > 478 | // delimiters in inline links like [this](). 479 | text = _DoAutoLinks(text); 480 | text = _EncodeAmpsAndAngles(text); 481 | text = _DoItalicsAndBold(text); 482 | 483 | // Do hard breaks: 484 | text = text.replace(/ +\n/g,"
\n"); 485 | 486 | return text; 487 | } 488 | 489 | var _EscapeSpecialCharsWithinTagAttributes = function(text) { 490 | // 491 | // Within tags -- meaning between < and > -- encode [\ ` * _] so they 492 | // don't conflict with their use in Markdown for code, italics and strong. 493 | // 494 | 495 | // Build a regex to find HTML tags and comments. See Friedl's 496 | // "Mastering Regular Expressions", 2nd Ed., pp. 200-201. 497 | var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|)/gi; 498 | 499 | text = text.replace(regex, function(wholeMatch) { 500 | var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g,"$1`"); 501 | tag = escapeCharacters(tag,"\\`*_"); 502 | return tag; 503 | }); 504 | 505 | return text; 506 | } 507 | 508 | var _DoAnchors = function(text) { 509 | // 510 | // Turn Markdown link shortcuts into XHTML tags. 511 | // 512 | // 513 | // First, handle reference-style links: [link text] [id] 514 | // 515 | 516 | /* 517 | text = text.replace(/ 518 | ( // wrap whole match in $1 519 | \[ 520 | ( 521 | (?: 522 | \[[^\]]*\] // allow brackets nested one level 523 | | 524 | [^\[] // or anything else 525 | )* 526 | ) 527 | \] 528 | 529 | [ ]? // one optional space 530 | (?:\n[ ]*)? // one optional newline followed by spaces 531 | 532 | \[ 533 | (.*?) // id = $3 534 | \] 535 | )()()()() // pad remaining backreferences 536 | /g,_DoAnchors_callback); 537 | */ 538 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeAnchorTag); 539 | 540 | // 541 | // Next, inline-style links: [link text](url "optional title") 542 | // 543 | 544 | /* 545 | text = text.replace(/ 546 | ( // wrap whole match in $1 547 | \[ 548 | ( 549 | (?: 550 | \[[^\]]*\] // allow brackets nested one level 551 | | 552 | [^\[\]] // or anything else 553 | ) 554 | ) 555 | \] 556 | \( // literal paren 557 | [ \t]* 558 | () // no id, so leave $3 empty 559 | ? // href = $4 560 | [ \t]* 561 | ( // $5 562 | (['"]) // quote char = $6 563 | (.*?) // Title = $7 564 | \6 // matching quote 565 | [ \t]* // ignore any spaces/tabs between closing quote and ) 566 | )? // title is optional 567 | \) 568 | ) 569 | /g,writeAnchorTag); 570 | */ 571 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeAnchorTag); 572 | 573 | // 574 | // Last, handle reference-style shortcuts: [link text] 575 | // These must come last in case you've also got [link test][1] 576 | // or [link test](/foo) 577 | // 578 | 579 | /* 580 | text = text.replace(/ 581 | ( // wrap whole match in $1 582 | \[ 583 | ([^\[\]]+) // link text = $2; can't contain '[' or ']' 584 | \] 585 | )()()()()() // pad rest of backreferences 586 | /g, writeAnchorTag); 587 | */ 588 | text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag); 589 | 590 | return text; 591 | } 592 | 593 | var writeAnchorTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) { 594 | if (m7 == undefined) m7 = ""; 595 | var whole_match = m1; 596 | var link_text = m2; 597 | var link_id = m3.toLowerCase(); 598 | var url = m4; 599 | var title = m7; 600 | 601 | if (url == "") { 602 | if (link_id == "") { 603 | // lower-case and turn embedded newlines into spaces 604 | link_id = link_text.toLowerCase().replace(/ ?\n/g," "); 605 | } 606 | url = "#"+link_id; 607 | 608 | if (g_urls[link_id] != undefined) { 609 | url = g_urls[link_id]; 610 | if (g_titles[link_id] != undefined) { 611 | title = g_titles[link_id]; 612 | } 613 | } 614 | else { 615 | if (whole_match.search(/\(\s*\)$/m)>-1) { 616 | // Special case for explicit empty url 617 | url = ""; 618 | } else { 619 | return whole_match; 620 | } 621 | } 622 | } 623 | 624 | url = escapeCharacters(url,"*_"); 625 | var result = ""; 634 | 635 | return result; 636 | } 637 | 638 | 639 | var _DoImages = function(text) { 640 | // 641 | // Turn Markdown image shortcuts into tags. 642 | // 643 | 644 | // 645 | // First, handle reference-style labeled images: ![alt text][id] 646 | // 647 | 648 | /* 649 | text = text.replace(/ 650 | ( // wrap whole match in $1 651 | !\[ 652 | (.*?) // alt text = $2 653 | \] 654 | 655 | [ ]? // one optional space 656 | (?:\n[ ]*)? // one optional newline followed by spaces 657 | 658 | \[ 659 | (.*?) // id = $3 660 | \] 661 | )()()()() // pad rest of backreferences 662 | /g,writeImageTag); 663 | */ 664 | text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g,writeImageTag); 665 | 666 | // 667 | // Next, handle inline images: ![alt text](url "optional title") 668 | // Don't forget: encode * and _ 669 | 670 | /* 671 | text = text.replace(/ 672 | ( // wrap whole match in $1 673 | !\[ 674 | (.*?) // alt text = $2 675 | \] 676 | \s? // One optional whitespace character 677 | \( // literal paren 678 | [ \t]* 679 | () // no id, so leave $3 empty 680 | ? // src url = $4 681 | [ \t]* 682 | ( // $5 683 | (['"]) // quote char = $6 684 | (.*?) // title = $7 685 | \6 // matching quote 686 | [ \t]* 687 | )? // title is optional 688 | \) 689 | ) 690 | /g,writeImageTag); 691 | */ 692 | text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeImageTag); 693 | 694 | return text; 695 | } 696 | 697 | var writeImageTag = function(wholeMatch,m1,m2,m3,m4,m5,m6,m7) { 698 | var whole_match = m1; 699 | var alt_text = m2; 700 | var link_id = m3.toLowerCase(); 701 | var url = m4; 702 | var title = m7; 703 | 704 | if (!title) title = ""; 705 | 706 | if (url == "") { 707 | if (link_id == "") { 708 | // lower-case and turn embedded newlines into spaces 709 | link_id = alt_text.toLowerCase().replace(/ ?\n/g," "); 710 | } 711 | url = "#"+link_id; 712 | 713 | if (g_urls[link_id] != undefined) { 714 | url = g_urls[link_id]; 715 | if (g_titles[link_id] != undefined) { 716 | title = g_titles[link_id]; 717 | } 718 | } 719 | else { 720 | return whole_match; 721 | } 722 | } 723 | 724 | alt_text = alt_text.replace(/"/g,"""); 725 | url = escapeCharacters(url,"*_"); 726 | var result = "\""" + _RunSpanGamut(m1) + "");}); 754 | 755 | text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, 756 | function(matchFound,m1){return hashBlock("

" + _RunSpanGamut(m1) + "

");}); 757 | 758 | // atx-style headers: 759 | // # Header 1 760 | // ## Header 2 761 | // ## Header 2 with closing hashes ## 762 | // ... 763 | // ###### Header 6 764 | // 765 | 766 | /* 767 | text = text.replace(/ 768 | ^(\#{1,6}) // $1 = string of #'s 769 | [ \t]* 770 | (.+?) // $2 = Header text 771 | [ \t]* 772 | \#* // optional closing #'s (not counted) 773 | \n+ 774 | /gm, function() {...}); 775 | */ 776 | 777 | text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm, 778 | function(wholeMatch,m1,m2) { 779 | var h_level = m1.length; 780 | return hashBlock("" + _RunSpanGamut(m2) + ""); 781 | }); 782 | 783 | return text; 784 | } 785 | 786 | // This declaration keeps Dojo compressor from outputting garbage: 787 | var _ProcessListItems; 788 | 789 | var _DoLists = function(text) { 790 | // 791 | // Form HTML ordered (numbered) and unordered (bulleted) lists. 792 | // 793 | 794 | // attacklab: add sentinel to hack around khtml/safari bug: 795 | // http://bugs.webkit.org/show_bug.cgi?id=11231 796 | text += "~0"; 797 | 798 | // Re-usable pattern to match any entirel ul or ol list: 799 | 800 | /* 801 | var whole_list = / 802 | ( // $1 = whole list 803 | ( // $2 804 | [ ]{0,3} // attacklab: g_tab_width - 1 805 | ([*+-]|\d+[.]) // $3 = first list item marker 806 | [ \t]+ 807 | ) 808 | [^\r]+? 809 | ( // $4 810 | ~0 // sentinel for workaround; should be $ 811 | | 812 | \n{2,} 813 | (?=\S) 814 | (?! // Negative lookahead for another list item marker 815 | [ \t]* 816 | (?:[*+-]|\d+[.])[ \t]+ 817 | ) 818 | ) 819 | )/g 820 | */ 821 | var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; 822 | 823 | if (g_list_level) { 824 | text = text.replace(whole_list,function(wholeMatch,m1,m2) { 825 | var list = m1; 826 | var list_type = (m2.search(/[*+-]/g)>-1) ? "ul" : "ol"; 827 | 828 | // Turn double returns into triple returns, so that we can make a 829 | // paragraph for the last item in a list, if necessary: 830 | list = list.replace(/\n{2,}/g,"\n\n\n");; 831 | var result = _ProcessListItems(list); 832 | 833 | // Trim any trailing whitespace, to put the closing `` 834 | // up on the preceding line, to get it past the current stupid 835 | // HTML block parser. This is a hack to work around the terrible 836 | // hack that is the HTML block parser. 837 | result = result.replace(/\s+$/,""); 838 | result = "<"+list_type+">" + result + "\n"; 839 | return result; 840 | }); 841 | } else { 842 | whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g; 843 | text = text.replace(whole_list,function(wholeMatch,m1,m2,m3) { 844 | var runup = m1; 845 | var list = m2; 846 | 847 | var list_type = (m3.search(/[*+-]/g)>-1) ? "ul" : "ol"; 848 | // Turn double returns into triple returns, so that we can make a 849 | // paragraph for the last item in a list, if necessary: 850 | var list = list.replace(/\n{2,}/g,"\n\n\n");; 851 | var result = _ProcessListItems(list); 852 | result = runup + "<"+list_type+">\n" + result + "\n"; 853 | return result; 854 | }); 855 | } 856 | 857 | // attacklab: strip sentinel 858 | text = text.replace(/~0/,""); 859 | 860 | return text; 861 | } 862 | 863 | _ProcessListItems = function(list_str) { 864 | // 865 | // Process the contents of a single ordered or unordered list, splitting it 866 | // into individual list items. 867 | // 868 | // The $g_list_level global keeps track of when we're inside a list. 869 | // Each time we enter a list, we increment it; when we leave a list, 870 | // we decrement. If it's zero, we're not in a list anymore. 871 | // 872 | // We do this because when we're not inside a list, we want to treat 873 | // something like this: 874 | // 875 | // I recommend upgrading to version 876 | // 8. Oops, now this line is treated 877 | // as a sub-list. 878 | // 879 | // As a single paragraph, despite the fact that the second line starts 880 | // with a digit-period-space sequence. 881 | // 882 | // Whereas when we're inside a list (or sub-list), that line will be 883 | // treated as the start of a sub-list. What a kludge, huh? This is 884 | // an aspect of Markdown's syntax that's hard to parse perfectly 885 | // without resorting to mind-reading. Perhaps the solution is to 886 | // change the syntax rules such that sub-lists must start with a 887 | // starting cardinal number; e.g. "1." or "a.". 888 | 889 | g_list_level++; 890 | 891 | // trim trailing blank lines: 892 | list_str = list_str.replace(/\n{2,}$/,"\n"); 893 | 894 | // attacklab: add sentinel to emulate \z 895 | list_str += "~0"; 896 | 897 | /* 898 | list_str = list_str.replace(/ 899 | (\n)? // leading line = $1 900 | (^[ \t]*) // leading whitespace = $2 901 | ([*+-]|\d+[.]) [ \t]+ // list marker = $3 902 | ([^\r]+? // list item text = $4 903 | (\n{1,2})) 904 | (?= \n* (~0 | \2 ([*+-]|\d+[.]) [ \t]+)) 905 | /gm, function(){...}); 906 | */ 907 | list_str = list_str.replace(/(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+([^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm, 908 | function(wholeMatch,m1,m2,m3,m4){ 909 | var item = m4; 910 | var leading_line = m1; 911 | var leading_space = m2; 912 | 913 | if (leading_line || (item.search(/\n{2,}/)>-1)) { 914 | item = _RunBlockGamut(_Outdent(item)); 915 | } 916 | else { 917 | // Recursion for sub-lists: 918 | item = _DoLists(_Outdent(item)); 919 | item = item.replace(/\n$/,""); // chomp(item) 920 | item = _RunSpanGamut(item); 921 | } 922 | 923 | return "
  • " + item + "
  • \n"; 924 | } 925 | ); 926 | 927 | // attacklab: strip sentinel 928 | list_str = list_str.replace(/~0/g,""); 929 | 930 | g_list_level--; 931 | return list_str; 932 | } 933 | 934 | 935 | var _DoCodeBlocks = function(text) { 936 | // 937 | // Process Markdown `
    ` blocks.
     938 | //
     939 | 
     940 |   //
     941 |   // mk2: support code block
     942 |   // ```
     943 |   // console.log('markdown'); 
     944 |   // ```
     945 |   text = text.replace(/```\n*((.|\n)*?)```/g,
     946 |     function(wholeMatch, m1) {
     947 |       var codeblock = m1;
     948 |       codeblock = "
    " + codeblock + "
    "; 949 | 950 | return hashBlock(codeblock); 951 | } 952 | ); 953 | 954 | /* 955 | text = text.replace(text, 956 | /(?:\n\n|^) 957 | ( // $1 = the code block -- one or more lines, starting with a space/tab 958 | (?: 959 | (?:[ ]{4}|\t) // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width 960 | .*\n+ 961 | )+ 962 | ) 963 | (\n*[ ]{0,3}[^ \t\n]|(?=~0)) // attacklab: g_tab_width 964 | /g,function(){...}); 965 | */ 966 | 967 | // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug 968 | text += "~0"; 969 | 970 | text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g, 971 | function(wholeMatch,m1,m2) { 972 | var codeblock = m1; 973 | var nextChar = m2; 974 | 975 | codeblock = _EncodeCode( _Outdent(codeblock)); 976 | codeblock = _Detab(codeblock); 977 | codeblock = codeblock.replace(/^\n+/g,""); // trim leading newlines 978 | codeblock = codeblock.replace(/\n+$/g,""); // trim trailing whitespace 979 | 980 | codeblock = "
    " + codeblock + "\n
    "; 981 | 982 | return hashBlock(codeblock) + nextChar; 983 | } 984 | ); 985 | 986 | // attacklab: strip sentinel 987 | text = text.replace(/~0/,""); 988 | 989 | return text; 990 | } 991 | 992 | var hashBlock = function(text) { 993 | text = text.replace(/(^\n+|\n+$)/g,""); 994 | return "\n\n~K" + (g_html_blocks.push(text)-1) + "K\n\n"; 995 | } 996 | 997 | 998 | var _DoCodeSpans = function(text) { 999 | // 1000 | // * Backtick quotes are used for spans. 1001 | // 1002 | // * You can use multiple backticks as the delimiters if you want to 1003 | // include literal backticks in the code span. So, this input: 1004 | // 1005 | // Just type ``foo `bar` baz`` at the prompt. 1006 | // 1007 | // Will translate to: 1008 | // 1009 | //

    Just type foo `bar` baz at the prompt.

    1010 | // 1011 | // There's no arbitrary limit to the number of backticks you 1012 | // can use as delimters. If you need three consecutive backticks 1013 | // in your code, use four for delimiters, etc. 1014 | // 1015 | // * You can use spaces to get literal backticks at the edges: 1016 | // 1017 | // ... type `` `bar` `` ... 1018 | // 1019 | // Turns to: 1020 | // 1021 | // ... type `bar` ... 1022 | // 1023 | 1024 | /* 1025 | text = text.replace(/ 1026 | (^|[^\\]) // Character before opening ` can't be a backslash 1027 | (`+) // $2 = Opening run of ` 1028 | ( // $3 = The code block 1029 | [^\r]*? 1030 | [^`] // attacklab: work around lack of lookbehind 1031 | ) 1032 | \2 // Matching closer 1033 | (?!`) 1034 | /gm, function(){...}); 1035 | */ 1036 | 1037 | text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, 1038 | function(wholeMatch,m1,m2,m3,m4) { 1039 | var c = m3; 1040 | c = c.replace(/^([ \t]*)/g,""); // leading whitespace 1041 | c = c.replace(/[ \t]*$/g,""); // trailing whitespace 1042 | c = _EncodeCode(c); 1043 | return m1+""+c+""; 1044 | }); 1045 | 1046 | return text; 1047 | } 1048 | 1049 | 1050 | var _EncodeCode = function(text) { 1051 | // 1052 | // Encode/escape certain characters inside Markdown code runs. 1053 | // The point is that in code, these characters are literals, 1054 | // and lose their special Markdown meanings. 1055 | // 1056 | // Encode all ampersands; HTML entities are not 1057 | // entities within a Markdown code span. 1058 | text = text.replace(/&/g,"&"); 1059 | 1060 | // Do the angle bracket song and dance: 1061 | text = text.replace(//g,">"); 1063 | 1064 | // Now, escape characters that are magic in Markdown: 1065 | text = escapeCharacters(text,"\*_{}[]\\",false); 1066 | 1067 | // jj the line above breaks this: 1068 | //--- 1069 | 1070 | //* Item 1071 | 1072 | // 1. Subitem 1073 | 1074 | // special char: * 1075 | //--- 1076 | 1077 | return text; 1078 | } 1079 | 1080 | 1081 | var _DoItalicsAndBold = function(text) { 1082 | 1083 | // must go first: 1084 | text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g, 1085 | "$2"); 1086 | 1087 | text = text.replace(/(\w)_(\w)/g, "$1~E95E$2") // ** GFM ** "~E95E" == escaped "_" 1088 | text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g, 1089 | "$2"); 1090 | 1091 | return text; 1092 | } 1093 | 1094 | 1095 | var _DoBlockQuotes = function(text) { 1096 | 1097 | /* 1098 | text = text.replace(/ 1099 | ( // Wrap whole match in $1 1100 | ( 1101 | ^[ \t]*>[ \t]? // '>' at the start of a line 1102 | .+\n // rest of the first line 1103 | (.+\n)* // subsequent consecutive lines 1104 | \n* // blanks 1105 | )+ 1106 | ) 1107 | /gm, function(){...}); 1108 | */ 1109 | 1110 | text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, 1111 | function(wholeMatch,m1) { 1112 | var bq = m1; 1113 | 1114 | // attacklab: hack around Konqueror 3.5.4 bug: 1115 | // "----------bug".replace(/^-/g,"") == "bug" 1116 | 1117 | bq = bq.replace(/^[ \t]*>[ \t]?/gm,"~0"); // trim one level of quoting 1118 | 1119 | // attacklab: clean up hack 1120 | bq = bq.replace(/~0/g,""); 1121 | 1122 | bq = bq.replace(/^[ \t]+$/gm,""); // trim whitespace-only lines 1123 | bq = _RunBlockGamut(bq); // recurse 1124 | 1125 | bq = bq.replace(/(^|\n)/g,"$1 "); 1126 | // These leading spaces screw with
     content, so we need to fix that:
    1127 |       bq = bq.replace(
    1128 |           /(\s*
    [^\r]+?<\/pre>)/gm,
    1129 |         function(wholeMatch,m1) {
    1130 |           var pre = m1;
    1131 |           // attacklab: hack around Konqueror 3.5.4 bug:
    1132 |           pre = pre.replace(/^  /mg,"~0");
    1133 |           pre = pre.replace(/~0/g,"");
    1134 |           return pre;
    1135 |         });
    1136 | 
    1137 |       return hashBlock("
    \n" + bq + "\n
    "); 1138 | }); 1139 | return text; 1140 | } 1141 | 1142 | 1143 | var _FormParagraphs = function(text) { 1144 | // 1145 | // Params: 1146 | // $text - string to process with html

    tags 1147 | // 1148 | 1149 | // Strip leading and trailing lines: 1150 | text = text.replace(/^\n+/g,""); 1151 | text = text.replace(/\n+$/g,""); 1152 | 1153 | var grafs = text.split(/\n{2,}/g); 1154 | var grafsOut = new Array(); 1155 | 1156 | // 1157 | // Wrap

    tags. 1158 | // 1159 | var end = grafs.length; 1160 | for (var i=0; i= 0) { 1165 | grafsOut.push(str); 1166 | } 1167 | else if (str.search(/\S/) >= 0) { 1168 | str = _RunSpanGamut(str); 1169 | str = str.replace(/\n/g,"
    "); // ** GFM ** 1170 | str = str.replace(/^([ \t]*)/g,"

    "); 1171 | str += "

    " 1172 | grafsOut.push(str); 1173 | } 1174 | 1175 | } 1176 | 1177 | // 1178 | // Unhashify HTML blocks 1179 | // 1180 | end = grafsOut.length; 1181 | for (var i=0; i= 0) { 1184 | var blockText = g_html_blocks[RegExp.$1]; 1185 | blockText = blockText.replace(/\$/g,"$$$$"); // Escape any dollar signs 1186 | grafsOut[i] = grafsOut[i].replace(/~K\d+K/,blockText); 1187 | } 1188 | } 1189 | 1190 | return grafsOut.join("\n\n"); 1191 | } 1192 | 1193 | 1194 | var _EncodeAmpsAndAngles = function(text) { 1195 | // Smart processing for ampersands and angle brackets that need to be encoded. 1196 | 1197 | // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: 1198 | // http://bumppo.net/projects/amputator/ 1199 | text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g,"&"); 1200 | 1201 | // Encode naked <'s 1202 | text = text.replace(/<(?![a-z\/?\$!])/gi,"<"); 1203 | 1204 | return text; 1205 | } 1206 | 1207 | 1208 | var _EncodeBackslashEscapes = function(text) { 1209 | // 1210 | // Parameter: String. 1211 | // Returns: The string, with after processing the following backslash 1212 | // escape sequences. 1213 | // 1214 | 1215 | // attacklab: The polite way to do this is with the new 1216 | // escapeCharacters() function: 1217 | // 1218 | // text = escapeCharacters(text,"\\",true); 1219 | // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); 1220 | // 1221 | // ...but we're sidestepping its use of the (slow) RegExp constructor 1222 | // as an optimization for Firefox. This function gets called a LOT. 1223 | 1224 | text = text.replace(/\\(\\)/g,escapeCharacters_callback); 1225 | text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g,escapeCharacters_callback); 1226 | return text; 1227 | } 1228 | 1229 | 1230 | var _DoAutoLinks = function(text) { 1231 | 1232 | text = text.replace(/<((https?|ftp|dict):[^'">\s]+)>/gi,"
    $1"); 1233 | 1234 | // Email addresses: 1235 | 1236 | /* 1237 | text = text.replace(/ 1238 | < 1239 | (?:mailto:)? 1240 | ( 1241 | [-.\w]+ 1242 | \@ 1243 | [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ 1244 | ) 1245 | > 1246 | /gi, _DoAutoLinks_callback()); 1247 | */ 1248 | text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, 1249 | function(wholeMatch,m1) { 1250 | return _EncodeEmailAddress( _UnescapeSpecialChars(m1) ); 1251 | } 1252 | ); 1253 | 1254 | return text; 1255 | } 1256 | 1257 | 1258 | var _EncodeEmailAddress = function(addr) { 1259 | // 1260 | // Input: an email address, e.g. "foo@example.com" 1261 | // 1262 | // Output: the email address as a mailto link, with each character 1263 | // of the address encoded as either a decimal or hex entity, in 1264 | // the hopes of foiling most address harvesting spam bots. E.g.: 1265 | // 1266 | // foo 1268 | // @example.com 1269 | // 1270 | // Based on a filter by Matthew Wickline, posted to the BBEdit-Talk 1271 | // mailing list: 1272 | // 1273 | 1274 | // attacklab: why can't javascript speak hex? 1275 | function char2hex(ch) { 1276 | var hexDigits = '0123456789ABCDEF'; 1277 | var dec = ch.charCodeAt(0); 1278 | return(hexDigits.charAt(dec>>4) + hexDigits.charAt(dec&15)); 1279 | } 1280 | 1281 | var encode = [ 1282 | function(ch){return "&#"+ch.charCodeAt(0)+";";}, 1283 | function(ch){return "&#x"+char2hex(ch)+";";}, 1284 | function(ch){return ch;} 1285 | ]; 1286 | 1287 | addr = "mailto:" + addr; 1288 | 1289 | addr = addr.replace(/./g, function(ch) { 1290 | if (ch == "@") { 1291 | // this *must* be encoded. I insist. 1292 | ch = encode[Math.floor(Math.random()*2)](ch); 1293 | } else if (ch !=":") { 1294 | // leave ':' alone (to spot mailto: later) 1295 | var r = Math.random(); 1296 | // roughly 10% raw, 45% hex, 45% dec 1297 | ch = ( 1298 | r > .9 ? encode[2](ch) : 1299 | r > .45 ? encode[1](ch) : 1300 | encode[0](ch) 1301 | ); 1302 | } 1303 | return ch; 1304 | }); 1305 | 1306 | addr = "" + addr + ""; 1307 | addr = addr.replace(/">.+:/g,"\">"); // strip the mailto: from the visible part 1308 | 1309 | return addr; 1310 | } 1311 | 1312 | 1313 | var _UnescapeSpecialChars = function(text) { 1314 | // 1315 | // Swap back in all the special characters we've hidden. 1316 | // 1317 | text = text.replace(/~E(\d+)E/g, 1318 | function(wholeMatch,m1) { 1319 | var charCodeToReplace = parseInt(m1); 1320 | return String.fromCharCode(charCodeToReplace); 1321 | } 1322 | ); 1323 | return text; 1324 | } 1325 | 1326 | 1327 | var _Outdent = function(text) { 1328 | // 1329 | // Remove one level of line-leading tabs or spaces 1330 | // 1331 | 1332 | // attacklab: hack around Konqueror 3.5.4 bug: 1333 | // "----------bug".replace(/^-/g,"") == "bug" 1334 | 1335 | text = text.replace(/^(\t|[ ]{1,4})/gm,"~0"); // attacklab: g_tab_width 1336 | 1337 | // attacklab: clean up hack 1338 | text = text.replace(/~0/g,"") 1339 | 1340 | return text; 1341 | } 1342 | 1343 | var _Detab = function(text) { 1344 | // attacklab: Detab's completely rewritten for speed. 1345 | // In perl we could fix it by anchoring the regexp with \G. 1346 | // In javascript we're less fortunate. 1347 | 1348 | // expand first n-1 tabs 1349 | text = text.replace(/\t(?=\t)/g," "); // attacklab: g_tab_width 1350 | 1351 | // replace the nth with two sentinels 1352 | text = text.replace(/\t/g,"~A~B"); 1353 | 1354 | // use the sentinel to anchor our regex so it doesn't explode 1355 | text = text.replace(/~B(.+?)~A/g, 1356 | function(wholeMatch,m1,m2) { 1357 | var leadingText = m1; 1358 | var numSpaces = 4 - leadingText.length % 4; // attacklab: g_tab_width 1359 | 1360 | // there *must* be a better way to do this: 1361 | for (var i=0; i= 0) { 63 | var index = markdown.indexOf('##', first + 2); 64 | if(index > 0) { 65 | markdown = markdown.substring(0, index) + '......'; 66 | } 67 | } 68 | return markdown_parse(markdown); 69 | }; 70 | 71 | exports.truncatechars = function(text, max) { 72 | if(!text) return ''; 73 | text = text.trim(); 74 | var len = Math.round(text.replace(/[^\x00-\xff]/g, "qq").length / 2); 75 | if(len > max) { 76 | for(var index = 0, j = 0, l = max - 2; j < l; index++) { 77 | if(/[^\x00-\xff]/.test(text[index])) { 78 | j += 1; 79 | } else { 80 | j += 0.5; 81 | } 82 | } 83 | text = text.substring(0, index) + '…'; 84 | } 85 | return text; 86 | }; -------------------------------------------------------------------------------- /models/comment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var mongoose = require('mongoose') 6 | , Schema = mongoose.Schema 7 | , ObjectId = Schema.ObjectId 8 | , markdown = require('github-flavored-markdown').parse; 9 | 10 | var Comment = new Schema({ 11 | author_id: ObjectId 12 | , user_info: {type: {}} 13 | , content: String 14 | , parent_id: {type: ObjectId, index: true} 15 | , is_markdown: {type: Boolean, default: true} 16 | , create_at: {type: Date, default: Date.now, index: true} 17 | , update_at: {type: Date, default: Date.now} 18 | }); 19 | Comment.virtual('html').get(function() { 20 | return this.is_markdown && this.content ? markdown(this.content) : this.content; 21 | }); 22 | mongoose.model('Comment', Comment); -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var mongoose = require('mongoose') 6 | , db = require('../config').db_options; 7 | require('./post'), require('./user'), require('./comment') 8 | , require('./tag'); 9 | 10 | var uri = 'mongodb://'; 11 | if(db.user && db.password) { 12 | uri += db.user + ':' + db.password + '@'; 13 | } 14 | db.host = db.host || 'localhost'; 15 | uri += db.host; 16 | if(db.port) { 17 | uri += ':' + db.port; 18 | } 19 | uri += '/' + db.database; 20 | 21 | mongoose.connect(uri, function(err) { 22 | if(err) { 23 | console.error(uri + ' error: ' + err.message); 24 | console.error(err); 25 | process.exit(1); 26 | } else { 27 | console.log(uri + ' success.'); 28 | } 29 | }); 30 | 31 | exports.Post = mongoose.model('Post'); 32 | exports.Tag = mongoose.model('Tag'); 33 | exports.User = mongoose.model('User'); 34 | exports.Comment = mongoose.model('Comment'); -------------------------------------------------------------------------------- /models/post.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var mongoose = require('mongoose') 6 | , Schema = mongoose.Schema 7 | , ObjectId = Schema.ObjectId 8 | , markdown = require('github-flavored-markdown').parse; 9 | 10 | var Post = new Schema({ 11 | title: String 12 | , author_id: ObjectId 13 | , content: String 14 | , is_markdown: {type: Boolean, default: true} 15 | , tags: [String] 16 | , public: {type: Boolean, default: true} 17 | , weblog_post: String 18 | , weblog_sync: {type: Boolean, default: true} 19 | , create_at: {type: Date, default: Date.now, index: true} 20 | , update_at: {type: Date, default: Date.now} 21 | }); 22 | Post.virtual('html').get(function() { 23 | return this.is_markdown && this.content ? markdown(this.content) : this.content; 24 | }); 25 | /** 26 | * find by tag 27 | */ 28 | Post.statics.findByTag= function(tagName, callback) { 29 | //先通过collection Tag定位 30 | var Tag=mongoose.model('Tag') 31 | , self= this; 32 | 33 | var afterTags= function(err, tags) { 34 | //根据检索出的post id列表查找实际post 35 | if (err) return callback(err); 36 | var posts_id=[]; 37 | if (Array.isArray(tags)) { 38 | for (var i=0, len=tags.length; i++; i (http://fengmk2.cnblogs.com)", 8 | "@zolunx10 (https://github.com/zolunx10)" 9 | ], 10 | "main": "app.js", 11 | "dependencies": { 12 | "connect": ">= 1.5.2", 13 | "connect-render": ">=0.1.0", 14 | "ejs": ">=0.6.1", 15 | "weibo": ">=0.3.0", 16 | "metaweblog": ">=0.2.0", 17 | "mongoskin": ">=0.2.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/css/prettify.css: -------------------------------------------------------------------------------- 1 | /* Pretty printing styles. Used with prettify.js. */ 2 | 3 | /* SPAN elements with the classes below are added by prettyprint. */ 4 | .pln { color: #000 } /* plain text */ 5 | 6 | @media screen { 7 | .str { color: #080 } /* string content */ 8 | .kwd { color: #008 } /* a keyword */ 9 | .com { color: #800 } /* a comment */ 10 | .typ { color: #606 } /* a type name */ 11 | .lit { color: #066 } /* a literal value */ 12 | /* punctuation, lisp open bracket, lisp close bracket */ 13 | .pun, .opn, .clo { color: #660 } 14 | .tag { color: #008 } /* a markup tag name */ 15 | .atn { color: #606 } /* a markup attribute name */ 16 | .atv { color: #080 } /* a markup attribute value */ 17 | .dec, .var { color: #606 } /* a declaration; a variable name */ 18 | .fun { color: red } /* a function name */ 19 | } 20 | 21 | /* Use higher contrast and text-weight for printable form. */ 22 | @media print, projection { 23 | .str { color: #060 } 24 | .kwd { color: #006; font-weight: bold } 25 | .com { color: #600; font-style: italic } 26 | .typ { color: #404; font-weight: bold } 27 | .lit { color: #044 } 28 | .pun, .opn, .clo { color: #440 } 29 | .tag { color: #006; font-weight: bold } 30 | .atn { color: #404 } 31 | .atv { color: #060 } 32 | } 33 | 34 | pre { 35 | 36 | } 37 | 38 | /* Put a border around prettyprinted code snippets. */ 39 | pre.prettyprint { 40 | font-family: 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', 'Monaco', Courier, monospace !important; 41 | font-size: 14px; 42 | display: block; 43 | padding: 10px; 44 | background-color: #fff; 45 | /*border: 2px dashed #f4f4f4;*/ 46 | border-radius: 10px; 47 | } 48 | 49 | /* Specify class=linenums on a pre to get line numbering */ 50 | ol.linenums { margin-top: 0; margin-bottom: 0 } /* IE indents via margin-left */ 51 | li.L0, 52 | li.L1, 53 | li.L2, 54 | li.L3, 55 | li.L5, 56 | li.L6, 57 | li.L7, 58 | li.L8 { list-style-type: none } 59 | /* Alternate shading for lines */ 60 | li.L1, 61 | li.L3, 62 | li.L5, 63 | li.L7, 64 | li.L9 { background: #eee } 65 | 66 | /* desert scheme ported from vim to google prettify */ 67 | pre.prettyprint { display: block; background-color: #333; padding: 5px 10px; } 68 | pre .nocode { background-color: none; color: #000 } 69 | pre .str { color: #ffa0a0 } /* string - pink */ 70 | pre .kwd { color: #f0e68c; font-weight: bold } 71 | pre .com { color: #87ceeb } /* comment - skyblue */ 72 | pre .typ { color: #98fb98 } /* type - lightgreen */ 73 | pre .lit { color: #cd5c5c } /* literal - darkred */ 74 | pre .pun { color: #fff } /* punctuation */ 75 | pre .pln { color: #fff } /* plaintext */ 76 | pre .tag { color: #f0e68c; font-weight: bold } /* html/xml tag - lightyellow */ 77 | pre .atn { color: #bdb76b; font-weight: bold } /* attribute name - khaki */ 78 | pre .atv { color: #ffa0a0 } /* attribute value - pink */ 79 | pre .dec { color: #98fb98 } /* decimal - lightgreen */ -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCena/nodeblog/1150320c9c8bf0031e8cbfc663aa894cce59369c/public/favicon.ico -------------------------------------------------------------------------------- /public/image/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCena/nodeblog/1150320c9c8bf0031e8cbfc663aa894cce59369c/public/image/16x16.png -------------------------------------------------------------------------------- /public/image/80x80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCena/nodeblog/1150320c9c8bf0031e8cbfc663aa894cce59369c/public/image/80x80.png -------------------------------------------------------------------------------- /public/image/forkme-grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCena/nodeblog/1150320c9c8bf0031e8cbfc663aa894cce59369c/public/image/forkme-grey.png -------------------------------------------------------------------------------- /public/image/forkme-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCena/nodeblog/1150320c9c8bf0031e8cbfc663aa894cce59369c/public/image/forkme-white.png -------------------------------------------------------------------------------- /public/image/forkme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCena/nodeblog/1150320c9c8bf0031e8cbfc663aa894cce59369c/public/image/forkme.png -------------------------------------------------------------------------------- /public/image/nbe_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/freeCena/nodeblog/1150320c9c8bf0031e8cbfc663aa894cce59369c/public/image/nbe_logo.jpg -------------------------------------------------------------------------------- /public/js/date.format.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Date Format 1.2.3 3 | * (c) 2007-2009 Steven Levithan 4 | * MIT license 5 | * 6 | * Includes enhancements by Scott Trenda 7 | * and Kris Kowal 8 | * 9 | * Accepts a date, a mask, or a date and a mask. 10 | * Returns a formatted version of the given date. 11 | * The date defaults to the current date/time. 12 | * The mask defaults to dateFormat.masks.default. 13 | */ 14 | // http://stevenlevithan.com/assets/misc/date.format.js 15 | // http://blog.stevenlevithan.com/archives/date-time-format 16 | var dateFormat = function () { 17 | var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, 18 | timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, 19 | timezoneClip = /[^-+\dA-Z]/g, 20 | pad = function (val, len) { 21 | val = String(val); 22 | len = len || 2; 23 | while (val.length < len) val = "0" + val; 24 | return val; 25 | }; 26 | 27 | // Regexes and supporting functions are cached through closure 28 | return function (date, mask, utc) { 29 | var dF = dateFormat; 30 | 31 | // You can't provide utc if you skip other args (use the "UTC:" mask prefix) 32 | if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { 33 | mask = date; 34 | date = undefined; 35 | } 36 | 37 | // Passing date through Date applies Date.parse, if necessary 38 | date = date ? new Date(date) : new Date; 39 | if (isNaN(date)) throw SyntaxError("invalid date"); 40 | 41 | mask = String(dF.masks[mask] || mask || dF.masks["default"]); 42 | 43 | // Allow setting the utc argument via the mask 44 | if (mask.slice(0, 4) == "UTC:") { 45 | mask = mask.slice(4); 46 | utc = true; 47 | } 48 | 49 | var _ = utc ? "getUTC" : "get", 50 | d = date[_ + "Date"](), 51 | D = date[_ + "Day"](), 52 | m = date[_ + "Month"](), 53 | y = date[_ + "FullYear"](), 54 | H = date[_ + "Hours"](), 55 | M = date[_ + "Minutes"](), 56 | s = date[_ + "Seconds"](), 57 | L = date[_ + "Milliseconds"](), 58 | o = utc ? 0 : date.getTimezoneOffset(), 59 | flags = { 60 | d: d, 61 | dd: pad(d), 62 | ddd: dF.i18n.dayNames[D], 63 | dddd: dF.i18n.dayNames[D + 7], 64 | m: m + 1, 65 | mm: pad(m + 1), 66 | mmm: dF.i18n.monthNames[m], 67 | mmmm: dF.i18n.monthNames[m + 12], 68 | yy: String(y).slice(2), 69 | yyyy: y, 70 | h: H % 12 || 12, 71 | hh: pad(H % 12 || 12), 72 | H: H, 73 | HH: pad(H), 74 | M: M, 75 | MM: pad(M), 76 | s: s, 77 | ss: pad(s), 78 | l: pad(L, 3), 79 | L: pad(L > 99 ? Math.round(L / 10) : L), 80 | t: H < 12 ? "a" : "p", 81 | tt: H < 12 ? "am" : "pm", 82 | T: H < 12 ? "A" : "P", 83 | TT: H < 12 ? "AM" : "PM", 84 | Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), 85 | o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), 86 | S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] 87 | }; 88 | 89 | return mask.replace(token, function ($0) { 90 | return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); 91 | }); 92 | }; 93 | }(); 94 | 95 | // Some common format strings 96 | dateFormat.masks = { 97 | //"default": "ddd mmm dd yyyy HH:MM:ss", 98 | 'default': "yyyy-mm-dd HH:MM:ss", 99 | shortDate: "m/d/yy", 100 | mediumDate: "mmm d, yyyy", 101 | longDate: "mmmm d, yyyy", 102 | fullDate: "dddd, mmmm d, yyyy", 103 | shortTime: "h:MM TT", 104 | mediumTime: "h:MM:ss TT", 105 | longTime: "h:MM:ss TT Z", 106 | isoDate: "yyyy-mm-dd", 107 | isoTime: "HH:MM:ss", 108 | isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", 109 | isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'", 110 | // Thu, 07 Jul 2011 10:04:06 GMT 111 | rssDateTime: 'UTC:ddd, dd mmm yyyy HH:MM:ss "GMT"' 112 | }; 113 | 114 | // Internationalization strings 115 | dateFormat.i18n = { 116 | dayNames: [ 117 | "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", 118 | "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" 119 | ], 120 | monthNames: [ 121 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 122 | "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" 123 | ] 124 | }; 125 | 126 | // For convenience... 127 | Date.prototype.format = function (mask, utc) { 128 | return dateFormat(this, mask, utc); 129 | }; 130 | -------------------------------------------------------------------------------- /public/js/modernizr-1.7.min.js: -------------------------------------------------------------------------------- 1 | // Modernizr v1.7 www.modernizr.com 2 | window.Modernizr=function(a,b,c){function G(){e.input=function(a){for(var b=0,c=a.length;b7)},r.history=function(){return !!(a.history&&history.pushState)},r.draganddrop=function(){return x("dragstart")&&x("drop")},r.websockets=function(){return"WebSocket"in a},r.rgba=function(){A("background-color:rgba(150,255,150,.5)");return D(k.backgroundColor,"rgba")},r.hsla=function(){A("background-color:hsla(120,40%,100%,.5)");return D(k.backgroundColor,"rgba")||D(k.backgroundColor,"hsla")},r.multiplebgs=function(){A("background:url(//:),url(//:),red url(//:)");return(new RegExp("(url\\s*\\(.*?){3}")).test(k.background)},r.backgroundsize=function(){return F("backgroundSize")},r.borderimage=function(){return F("borderImage")},r.borderradius=function(){return F("borderRadius","",function(a){return D(a,"orderRadius")})},r.boxshadow=function(){return F("boxShadow")},r.textshadow=function(){return b.createElement("div").style.textShadow===""},r.opacity=function(){B("opacity:.55");return/^0.55$/.test(k.opacity)},r.cssanimations=function(){return F("animationName")},r.csscolumns=function(){return F("columnCount")},r.cssgradients=function(){var a="background-image:",b="gradient(linear,left top,right bottom,from(#9f9),to(white));",c="linear-gradient(left top,#9f9, white);";A((a+o.join(b+a)+o.join(c+a)).slice(0,-a.length));return D(k.backgroundImage,"gradient")},r.cssreflections=function(){return F("boxReflect")},r.csstransforms=function(){return!!E(["transformProperty","WebkitTransform","MozTransform","OTransform","msTransform"])},r.csstransforms3d=function(){var a=!!E(["perspectiveProperty","WebkitPerspective","MozPerspective","OPerspective","msPerspective"]);a&&"webkitPerspective"in g.style&&(a=w("@media ("+o.join("transform-3d),(")+"modernizr)"));return a},r.csstransitions=function(){return F("transitionProperty")},r.fontface=function(){var a,c,d=h||g,e=b.createElement("style"),f=b.implementation||{hasFeature:function(){return!1}};e.type="text/css",d.insertBefore(e,d.firstChild),a=e.sheet||e.styleSheet;var i=f.hasFeature("CSS2","")?function(b){if(!a||!b)return!1;var c=!1;try{a.insertRule(b,0),c=/src/i.test(a.cssRules[0].cssText),a.deleteRule(a.cssRules.length-1)}catch(d){}return c}:function(b){if(!a||!b)return!1;a.cssText=b;return a.cssText.length!==0&&/src/i.test(a.cssText)&&a.cssText.replace(/\r+|\n+/g,"").indexOf(b.split(" ")[0])===0};c=i('@font-face { font-family: "font"; src: url(data:,); }'),d.removeChild(e);return c},r.video=function(){var a=b.createElement("video"),c=!!a.canPlayType;if(c){c=new Boolean(c),c.ogg=a.canPlayType('video/ogg; codecs="theora"');var d='video/mp4; codecs="avc1.42E01E';c.h264=a.canPlayType(d+'"')||a.canPlayType(d+', mp4a.40.2"'),c.webm=a.canPlayType('video/webm; codecs="vp8, vorbis"')}return c},r.audio=function(){var a=b.createElement("audio"),c=!!a.canPlayType;c&&(c=new Boolean(c),c.ogg=a.canPlayType('audio/ogg; codecs="vorbis"'),c.mp3=a.canPlayType("audio/mpeg;"),c.wav=a.canPlayType('audio/wav; codecs="1"'),c.m4a=a.canPlayType("audio/x-m4a;")||a.canPlayType("audio/aac;"));return c},r.localstorage=function(){try{return!!localStorage.getItem}catch(a){return!1}},r.sessionstorage=function(){try{return!!sessionStorage.getItem}catch(a){return!1}},r.webWorkers=function(){return!!a.Worker},r.applicationcache=function(){return!!a.applicationCache},r.svg=function(){return!!b.createElementNS&&!!b.createElementNS(q.svg,"svg").createSVGRect},r.inlinesvg=function(){var a=b.createElement("div");a.innerHTML="";return(a.firstChild&&a.firstChild.namespaceURI)==q.svg},r.smil=function(){return!!b.createElementNS&&/SVG/.test(n.call(b.createElementNS(q.svg,"animate")))},r.svgclippaths=function(){return!!b.createElementNS&&/SVG/.test(n.call(b.createElementNS(q.svg,"clipPath")))};for(var H in r)z(r,H)&&(v=H.toLowerCase(),e[v]=r[H](),u.push((e[v]?"":"no-")+v));e.input||G(),e.crosswindowmessaging=e.postmessage,e.historymanagement=e.history,e.addTest=function(a,b){a=a.toLowerCase();if(!e[a]){b=!!b(),g.className+=" "+(b?"":"no-")+a,e[a]=b;return e}},A(""),j=l=null,f&&a.attachEvent&&function(){var a=b.createElement("div");a.innerHTML="";return a.childNodes.length!==1}()&&function(a,b){function p(a,b){var c=-1,d=a.length,e,f=[];while(++c 20 | * 21 | * For a fairly comprehensive set of languages see the 22 | * README 23 | * file that came with this source. At a minimum, the lexer should work on a 24 | * number of languages including C and friends, Java, Python, Bash, SQL, HTML, 25 | * XML, CSS, Javascript, and Makefiles. It works passably on Ruby, PHP and Awk 26 | * and a subset of Perl, but, because of commenting conventions, doesn't work on 27 | * Smalltalk, Lisp-like, or CAML-like languages without an explicit lang class. 28 | *

    29 | * Usage:

      30 | *
    1. include this source file in an html page via 31 | * {@code } 32 | *
    2. define style rules. See the example page for examples. 33 | *
    3. mark the {@code
      } and {@code } tags in your source with
        34 |  *    {@code class=prettyprint.}
        35 |  *    You can also use the (html deprecated) {@code } tag, but the pretty
        36 |  *    printer needs to do more substantial DOM manipulations to support that, so
        37 |  *    some css styles may not be preserved.
        38 |  * </ol>
        39 |  * That's it.  I wanted to keep the API as simple as possible, so there's no
        40 |  * need to specify which language the code is in, but if you wish, you can add
        41 |  * another class to the {@code <pre>} or {@code <code>} element to specify the
        42 |  * language, as in {@code <pre class="prettyprint lang-java">}.  Any class that
        43 |  * starts with "lang-" followed by a file extension, specifies the file type.
        44 |  * See the "lang-*.js" files in this directory for code that implements
        45 |  * per-language file handlers.
        46 |  * <p>
        47 |  * Change log:<br>
        48 |  * cbeust, 2006/08/22
        49 |  * <blockquote>
        50 |  *   Java annotations (start with "@") are now captured as literals ("lit")
        51 |  * </blockquote>
        52 |  * @requires console
        53 |  */
        54 | 
        55 | // JSLint declarations
        56 | /*global console, document, navigator, setTimeout, window */
        57 | 
        58 | /**
        59 |  * Split {@code prettyPrint} into multiple timeouts so as not to interfere with
        60 |  * UI events.
        61 |  * If set to {@code false}, {@code prettyPrint()} is synchronous.
        62 |  */
        63 | window['PR_SHOULD_USE_CONTINUATION'] = true;
        64 | 
        65 | /** the number of characters between tab columns */
        66 | window['PR_TAB_WIDTH'] = 8;
        67 | 
        68 | /** Walks the DOM returning a properly escaped version of innerHTML.
        69 |   * @param {Node} node
        70 |   * @param {Array.<string>} out output buffer that receives chunks of HTML.
        71 |   */
        72 | window['PR_normalizedHtml']
        73 | 
        74 | /** Contains functions for creating and registering new language handlers.
        75 |   * @type {Object}
        76 |   */
        77 |   = window['PR']
        78 | 
        79 | /** Pretty print a chunk of code.
        80 |   *
        81 |   * @param {string} sourceCodeHtml code as html
        82 |   * @return {string} code as html, but prettier
        83 |   */
        84 |   = window['prettyPrintOne']
        85 | /** Find all the {@code <pre>} and {@code <code>} tags in the DOM with
        86 |   * {@code class=prettyprint} and prettify them.
        87 |   * @param {Function?} opt_whenDone if specified, called when the last entry
        88 |   *     has been finished.
        89 |   */
        90 |   = window['prettyPrint'] = void 0;
        91 | 
        92 | /** browser detection. @extern @returns false if not IE, otherwise the major version. */
        93 | window['_pr_isIE6'] = function () {
        94 |   var ieVersion = navigator && navigator.userAgent &&
        95 |       navigator.userAgent.match(/\bMSIE ([678])\./);
        96 |   ieVersion = ieVersion ? +ieVersion[1] : false;
        97 |   window['_pr_isIE6'] = function () { return ieVersion; };
        98 |   return ieVersion;
        99 | };
       100 | 
       101 | 
       102 | (function () {
       103 |   // Keyword lists for various languages.
       104 |   var FLOW_CONTROL_KEYWORDS =
       105 |       "break continue do else for if return while ";
       106 |   var C_KEYWORDS = FLOW_CONTROL_KEYWORDS + "auto case char const default " +
       107 |       "double enum extern float goto int long register short signed sizeof " +
       108 |       "static struct switch typedef union unsigned void volatile ";
       109 |   var COMMON_KEYWORDS = C_KEYWORDS + "catch class delete false import " +
       110 |       "new operator private protected public this throw true try typeof ";
       111 |   var CPP_KEYWORDS = COMMON_KEYWORDS + "alignof align_union asm axiom bool " +
       112 |       "concept concept_map const_cast constexpr decltype " +
       113 |       "dynamic_cast explicit export friend inline late_check " +
       114 |       "mutable namespace nullptr reinterpret_cast static_assert static_cast " +
       115 |       "template typeid typename using virtual wchar_t where ";
       116 |   var JAVA_KEYWORDS = COMMON_KEYWORDS +
       117 |       "abstract boolean byte extends final finally implements import " +
       118 |       "instanceof null native package strictfp super synchronized throws " +
       119 |       "transient ";
       120 |   var CSHARP_KEYWORDS = JAVA_KEYWORDS +
       121 |       "as base by checked decimal delegate descending dynamic event " +
       122 |       "fixed foreach from group implicit in interface internal into is lock " +
       123 |       "object out override orderby params partial readonly ref sbyte sealed " +
       124 |       "stackalloc string select uint ulong unchecked unsafe ushort var ";
       125 |   var COFFEE_KEYWORDS = "all and by catch class else extends false finally " +
       126 |       "for if in is isnt loop new no not null of off on or return super then " +
       127 |       "true try unless until when while yes ";
       128 |   var JSCRIPT_KEYWORDS = COMMON_KEYWORDS +
       129 |       "debugger eval export function get null set undefined var with " +
       130 |       "Infinity NaN ";
       131 |   var PERL_KEYWORDS = "caller delete die do dump elsif eval exit foreach for " +
       132 |       "goto if import last local my next no our print package redo require " +
       133 |       "sub undef unless until use wantarray while BEGIN END ";
       134 |   var PYTHON_KEYWORDS = FLOW_CONTROL_KEYWORDS + "and as assert class def del " +
       135 |       "elif except exec finally from global import in is lambda " +
       136 |       "nonlocal not or pass print raise try with yield " +
       137 |       "False True None ";
       138 |   var RUBY_KEYWORDS = FLOW_CONTROL_KEYWORDS + "alias and begin case class def" +
       139 |       " defined elsif end ensure false in module next nil not or redo rescue " +
       140 |       "retry self super then true undef unless until when yield BEGIN END ";
       141 |   var SH_KEYWORDS = FLOW_CONTROL_KEYWORDS + "case done elif esac eval fi " +
       142 |       "function in local set then until ";
       143 |   var ALL_KEYWORDS = (
       144 |       CPP_KEYWORDS + CSHARP_KEYWORDS + JSCRIPT_KEYWORDS + PERL_KEYWORDS +
       145 |       PYTHON_KEYWORDS + RUBY_KEYWORDS + SH_KEYWORDS);
       146 | 
       147 |   // token style names.  correspond to css classes
       148 |   /** token style for a string literal */
       149 |   var PR_STRING = 'str';
       150 |   /** token style for a keyword */
       151 |   var PR_KEYWORD = 'kwd';
       152 |   /** token style for a comment */
       153 |   var PR_COMMENT = 'com';
       154 |   /** token style for a type */
       155 |   var PR_TYPE = 'typ';
       156 |   /** token style for a literal value.  e.g. 1, null, true. */
       157 |   var PR_LITERAL = 'lit';
       158 |   /** token style for a punctuation string. */
       159 |   var PR_PUNCTUATION = 'pun';
       160 |   /** token style for a punctuation string. */
       161 |   var PR_PLAIN = 'pln';
       162 | 
       163 |   /** token style for an sgml tag. */
       164 |   var PR_TAG = 'tag';
       165 |   /** token style for a markup declaration such as a DOCTYPE. */
       166 |   var PR_DECLARATION = 'dec';
       167 |   /** token style for embedded source. */
       168 |   var PR_SOURCE = 'src';
       169 |   /** token style for an sgml attribute name. */
       170 |   var PR_ATTRIB_NAME = 'atn';
       171 |   /** token style for an sgml attribute value. */
       172 |   var PR_ATTRIB_VALUE = 'atv';
       173 | 
       174 |   /**
       175 |    * A class that indicates a section of markup that is not code, e.g. to allow
       176 |    * embedding of line numbers within code listings.
       177 |    */
       178 |   var PR_NOCODE = 'nocode';
       179 | 
       180 |   /** A set of tokens that can precede a regular expression literal in
       181 |     * javascript.
       182 |     * http://www.mozilla.org/js/language/js20/rationale/syntax.html has the full
       183 |     * list, but I've removed ones that might be problematic when seen in
       184 |     * languages that don't support regular expression literals.
       185 |     *
       186 |     * <p>Specifically, I've removed any keywords that can't precede a regexp
       187 |     * literal in a syntactically legal javascript program, and I've removed the
       188 |     * "in" keyword since it's not a keyword in many languages, and might be used
       189 |     * as a count of inches.
       190 |     *
       191 |     * <p>The link a above does not accurately describe EcmaScript rules since
       192 |     * it fails to distinguish between (a=++/b/i) and (a++/b/i) but it works
       193 |     * very well in practice.
       194 |     *
       195 |     * @private
       196 |     */
       197 |   var REGEXP_PRECEDER_PATTERN = function () {
       198 |       var preceders = [
       199 |           "!", "!=", "!==", "#", "%", "%=", "&", "&&", "&&=",
       200 |           "&=", "(", "*", "*=", /* "+", */ "+=", ",", /* "-", */ "-=",
       201 |           "->", /*".", "..", "...", handled below */ "/", "/=", ":", "::", ";",
       202 |           "<", "<<", "<<=", "<=", "=", "==", "===", ">",
       203 |           ">=", ">>", ">>=", ">>>", ">>>=", "?", "@", "[",
       204 |           "^", "^=", "^^", "^^=", "{", "|", "|=", "||",
       205 |           "||=", "~" /* handles =~ and !~ */,
       206 |           "break", "case", "continue", "delete",
       207 |           "do", "else", "finally", "instanceof",
       208 |           "return", "throw", "try", "typeof"
       209 |           ];
       210 |       var pattern = '(?:^^|[+-]';
       211 |       for (var i = 0; i < preceders.length; ++i) {
       212 |         pattern += '|' + preceders[i].replace(/([^=<>:&a-z])/g, '\\$1');
       213 |       }
       214 |       pattern += ')\\s*';  // matches at end, and matches empty string
       215 |       return pattern;
       216 |       // CAVEAT: this does not properly handle the case where a regular
       217 |       // expression immediately follows another since a regular expression may
       218 |       // have flags for case-sensitivity and the like.  Having regexp tokens
       219 |       // adjacent is not valid in any language I'm aware of, so I'm punting.
       220 |       // TODO: maybe style special characters inside a regexp as punctuation.
       221 |     }();
       222 | 
       223 |   // Define regexps here so that the interpreter doesn't have to create an
       224 |   // object each time the function containing them is called.
       225 |   // The language spec requires a new object created even if you don't access
       226 |   // the $1 members.
       227 |   var pr_amp = /&/g;
       228 |   var pr_lt = /</g;
       229 |   var pr_gt = />/g;
       230 |   var pr_quot = /\"/g;
       231 |   /** like textToHtml but escapes double quotes to be attribute safe. */
       232 |   function attribToHtml(str) {
       233 |     return str.replace(pr_amp, '&amp;')
       234 |         .replace(pr_lt, '&lt;')
       235 |         .replace(pr_gt, '&gt;')
       236 |         .replace(pr_quot, '&quot;');
       237 |   }
       238 | 
       239 |   /** escapest html special characters to html. */
       240 |   function textToHtml(str) {
       241 |     return str.replace(pr_amp, '&amp;')
       242 |         .replace(pr_lt, '&lt;')
       243 |         .replace(pr_gt, '&gt;');
       244 |   }
       245 | 
       246 | 
       247 |   var pr_ltEnt = /&lt;/g;
       248 |   var pr_gtEnt = /&gt;/g;
       249 |   var pr_aposEnt = /&apos;/g;
       250 |   var pr_quotEnt = /&quot;/g;
       251 |   var pr_ampEnt = /&amp;/g;
       252 |   var pr_nbspEnt = /&nbsp;/g;
       253 |   /** unescapes html to plain text. */
       254 |   function htmlToText(html) {
       255 |     var pos = html.indexOf('&');
       256 |     if (pos < 0) { return html; }
       257 |     // Handle numeric entities specially.  We can't use functional substitution
       258 |     // since that doesn't work in older versions of Safari.
       259 |     // These should be rare since most browsers convert them to normal chars.
       260 |     for (--pos; (pos = html.indexOf('&#', pos + 1)) >= 0;) {
       261 |       var end = html.indexOf(';', pos);
       262 |       if (end >= 0) {
       263 |         var num = html.substring(pos + 3, end);
       264 |         var radix = 10;
       265 |         if (num && num.charAt(0) === 'x') {
       266 |           num = num.substring(1);
       267 |           radix = 16;
       268 |         }
       269 |         var codePoint = parseInt(num, radix);
       270 |         if (!isNaN(codePoint)) {
       271 |           html = (html.substring(0, pos) + String.fromCharCode(codePoint) +
       272 |                   html.substring(end + 1));
       273 |         }
       274 |       }
       275 |     }
       276 | 
       277 |     return html.replace(pr_ltEnt, '<')
       278 |         .replace(pr_gtEnt, '>')
       279 |         .replace(pr_aposEnt, "'")
       280 |         .replace(pr_quotEnt, '"')
       281 |         .replace(pr_nbspEnt, ' ')
       282 |         .replace(pr_ampEnt, '&');
       283 |   }
       284 | 
       285 |   /** is the given node's innerHTML normally unescaped? */
       286 |   function isRawContent(node) {
       287 |     return 'XMP' === node.tagName;
       288 |   }
       289 | 
       290 |   var newlineRe = /[\r\n]/g;
       291 |   /**
       292 |    * Are newlines and adjacent spaces significant in the given node's innerHTML?
       293 |    */
       294 |   function isPreformatted(node, content) {
       295 |     // PRE means preformatted, and is a very common case, so don't create
       296 |     // unnecessary computed style objects.
       297 |     if ('PRE' === node.tagName) { return true; }
       298 |     if (!newlineRe.test(content)) { return true; }  // Don't care
       299 |     var whitespace = '';
       300 |     // For disconnected nodes, IE has no currentStyle.
       301 |     if (node.currentStyle) {
       302 |       whitespace = node.currentStyle.whiteSpace;
       303 |     } else if (window.getComputedStyle) {
       304 |       // Firefox makes a best guess if node is disconnected whereas Safari
       305 |       // returns the empty string.
       306 |       whitespace = window.getComputedStyle(node, null).whiteSpace;
       307 |     }
       308 |     return !whitespace || whitespace === 'pre';
       309 |   }
       310 | 
       311 |   function normalizedHtml(node, out, opt_sortAttrs) {
       312 |     switch (node.nodeType) {
       313 |       case 1:  // an element
       314 |         var name = node.tagName.toLowerCase();
       315 | 
       316 |         out.push('<', name);
       317 |         var attrs = node.attributes;
       318 |         var n = attrs.length;
       319 |         if (n) {
       320 |           if (opt_sortAttrs) {
       321 |             var sortedAttrs = [];
       322 |             for (var i = n; --i >= 0;) { sortedAttrs[i] = attrs[i]; }
       323 |             sortedAttrs.sort(function (a, b) {
       324 |                 return (a.name < b.name) ? -1 : a.name === b.name ? 0 : 1;
       325 |               });
       326 |             attrs = sortedAttrs;
       327 |           }
       328 |           for (var i = 0; i < n; ++i) {
       329 |             var attr = attrs[i];
       330 |             if (!attr.specified) { continue; }
       331 |             out.push(' ', attr.name.toLowerCase(),
       332 |                      '="', attribToHtml(attr.value), '"');
       333 |           }
       334 |         }
       335 |         out.push('>');
       336 |         for (var child = node.firstChild; child; child = child.nextSibling) {
       337 |           normalizedHtml(child, out, opt_sortAttrs);
       338 |         }
       339 |         if (node.firstChild || !/^(?:br|link|img)$/.test(name)) {
       340 |           out.push('<\/', name, '>');
       341 |         }
       342 |         break;
       343 |       case 3: case 4: // text
       344 |         out.push(textToHtml(node.nodeValue));
       345 |         break;
       346 |     }
       347 |   }
       348 | 
       349 |   /**
       350 |    * Given a group of {@link RegExp}s, returns a {@code RegExp} that globally
       351 |    * matches the union o the sets o strings matched d by the input RegExp.
       352 |    * Since it matches globally, if the input strings have a start-of-input
       353 |    * anchor (/^.../), it is ignored for the purposes of unioning.
       354 |    * @param {Array.<RegExp>} regexs non multiline, non-global regexs.
       355 |    * @return {RegExp} a global regex.
       356 |    */
       357 |   function combinePrefixPatterns(regexs) {
       358 |     var capturedGroupIndex = 0;
       359 | 
       360 |     var needToFoldCase = false;
       361 |     var ignoreCase = false;
       362 |     for (var i = 0, n = regexs.length; i < n; ++i) {
       363 |       var regex = regexs[i];
       364 |       if (regex.ignoreCase) {
       365 |         ignoreCase = true;
       366 |       } else if (/[a-z]/i.test(regex.source.replace(
       367 |                      /\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi, ''))) {
       368 |         needToFoldCase = true;
       369 |         ignoreCase = false;
       370 |         break;
       371 |       }
       372 |     }
       373 | 
       374 |     function decodeEscape(charsetPart) {
       375 |       if (charsetPart.charAt(0) !== '\\') { return charsetPart.charCodeAt(0); }
       376 |       switch (charsetPart.charAt(1)) {
       377 |         case 'b': return 8;
       378 |         case 't': return 9;
       379 |         case 'n': return 0xa;
       380 |         case 'v': return 0xb;
       381 |         case 'f': return 0xc;
       382 |         case 'r': return 0xd;
       383 |         case 'u': case 'x':
       384 |           return parseInt(charsetPart.substring(2), 16)
       385 |               || charsetPart.charCodeAt(1);
       386 |         case '0': case '1': case '2': case '3': case '4':
       387 |         case '5': case '6': case '7':
       388 |           return parseInt(charsetPart.substring(1), 8);
       389 |         default: return charsetPart.charCodeAt(1);
       390 |       }
       391 |     }
       392 | 
       393 |     function encodeEscape(charCode) {
       394 |       if (charCode < 0x20) {
       395 |         return (charCode < 0x10 ? '\\x0' : '\\x') + charCode.toString(16);
       396 |       }
       397 |       var ch = String.fromCharCode(charCode);
       398 |       if (ch === '\\' || ch === '-' || ch === '[' || ch === ']') {
       399 |         ch = '\\' + ch;
       400 |       }
       401 |       return ch;
       402 |     }
       403 | 
       404 |     function caseFoldCharset(charSet) {
       405 |       var charsetParts = charSet.substring(1, charSet.length - 1).match(
       406 |           new RegExp(
       407 |               '\\\\u[0-9A-Fa-f]{4}'
       408 |               + '|\\\\x[0-9A-Fa-f]{2}'
       409 |               + '|\\\\[0-3][0-7]{0,2}'
       410 |               + '|\\\\[0-7]{1,2}'
       411 |               + '|\\\\[\\s\\S]'
       412 |               + '|-'
       413 |               + '|[^-\\\\]',
       414 |               'g'));
       415 |       var groups = [];
       416 |       var ranges = [];
       417 |       var inverse = charsetParts[0] === '^';
       418 |       for (var i = inverse ? 1 : 0, n = charsetParts.length; i < n; ++i) {
       419 |         var p = charsetParts[i];
       420 |         switch (p) {
       421 |           case '\\B': case '\\b':
       422 |           case '\\D': case '\\d':
       423 |           case '\\S': case '\\s':
       424 |           case '\\W': case '\\w':
       425 |             groups.push(p);
       426 |             continue;
       427 |         }
       428 |         var start = decodeEscape(p);
       429 |         var end;
       430 |         if (i + 2 < n && '-' === charsetParts[i + 1]) {
       431 |           end = decodeEscape(charsetParts[i + 2]);
       432 |           i += 2;
       433 |         } else {
       434 |           end = start;
       435 |         }
       436 |         ranges.push([start, end]);
       437 |         // If the range might intersect letters, then expand it.
       438 |         if (!(end < 65 || start > 122)) {
       439 |           if (!(end < 65 || start > 90)) {
       440 |             ranges.push([Math.max(65, start) | 32, Math.min(end, 90) | 32]);
       441 |           }
       442 |           if (!(end < 97 || start > 122)) {
       443 |             ranges.push([Math.max(97, start) & ~32, Math.min(end, 122) & ~32]);
       444 |           }
       445 |         }
       446 |       }
       447 | 
       448 |       // [[1, 10], [3, 4], [8, 12], [14, 14], [16, 16], [17, 17]]
       449 |       // -> [[1, 12], [14, 14], [16, 17]]
       450 |       ranges.sort(function (a, b) { return (a[0] - b[0]) || (b[1]  - a[1]); });
       451 |       var consolidatedRanges = [];
       452 |       var lastRange = [NaN, NaN];
       453 |       for (var i = 0; i < ranges.length; ++i) {
       454 |         var range = ranges[i];
       455 |         if (range[0] <= lastRange[1] + 1) {
       456 |           lastRange[1] = Math.max(lastRange[1], range[1]);
       457 |         } else {
       458 |           consolidatedRanges.push(lastRange = range);
       459 |         }
       460 |       }
       461 | 
       462 |       var out = ['['];
       463 |       if (inverse) { out.push('^'); }
       464 |       out.push.apply(out, groups);
       465 |       for (var i = 0; i < consolidatedRanges.length; ++i) {
       466 |         var range = consolidatedRanges[i];
       467 |         out.push(encodeEscape(range[0]));
       468 |         if (range[1] > range[0]) {
       469 |           if (range[1] + 1 > range[0]) { out.push('-'); }
       470 |           out.push(encodeEscape(range[1]));
       471 |         }
       472 |       }
       473 |       out.push(']');
       474 |       return out.join('');
       475 |     }
       476 | 
       477 |     function allowAnywhereFoldCaseAndRenumberGroups(regex) {
       478 |       // Split into character sets, escape sequences, punctuation strings
       479 |       // like ('(', '(?:', ')', '^'), and runs of characters that do not
       480 |       // include any of the above.
       481 |       var parts = regex.source.match(
       482 |           new RegExp(
       483 |               '(?:'
       484 |               + '\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]'  // a character set
       485 |               + '|\\\\u[A-Fa-f0-9]{4}'  // a unicode escape
       486 |               + '|\\\\x[A-Fa-f0-9]{2}'  // a hex escape
       487 |               + '|\\\\[0-9]+'  // a back-reference or octal escape
       488 |               + '|\\\\[^ux0-9]'  // other escape sequence
       489 |               + '|\\(\\?[:!=]'  // start of a non-capturing group
       490 |               + '|[\\(\\)\\^]'  // start/emd of a group, or line start
       491 |               + '|[^\\x5B\\x5C\\(\\)\\^]+'  // run of other characters
       492 |               + ')',
       493 |               'g'));
       494 |       var n = parts.length;
       495 | 
       496 |       // Maps captured group numbers to the number they will occupy in
       497 |       // the output or to -1 if that has not been determined, or to
       498 |       // undefined if they need not be capturing in the output.
       499 |       var capturedGroups = [];
       500 | 
       501 |       // Walk over and identify back references to build the capturedGroups
       502 |       // mapping.
       503 |       for (var i = 0, groupIndex = 0; i < n; ++i) {
       504 |         var p = parts[i];
       505 |         if (p === '(') {
       506 |           // groups are 1-indexed, so max group index is count of '('
       507 |           ++groupIndex;
       508 |         } else if ('\\' === p.charAt(0)) {
       509 |           var decimalValue = +p.substring(1);
       510 |           if (decimalValue && decimalValue <= groupIndex) {
       511 |             capturedGroups[decimalValue] = -1;
       512 |           }
       513 |         }
       514 |       }
       515 | 
       516 |       // Renumber groups and reduce capturing groups to non-capturing groups
       517 |       // where possible.
       518 |       for (var i = 1; i < capturedGroups.length; ++i) {
       519 |         if (-1 === capturedGroups[i]) {
       520 |           capturedGroups[i] = ++capturedGroupIndex;
       521 |         }
       522 |       }
       523 |       for (var i = 0, groupIndex = 0; i < n; ++i) {
       524 |         var p = parts[i];
       525 |         if (p === '(') {
       526 |           ++groupIndex;
       527 |           if (capturedGroups[groupIndex] === undefined) {
       528 |             parts[i] = '(?:';
       529 |           }
       530 |         } else if ('\\' === p.charAt(0)) {
       531 |           var decimalValue = +p.substring(1);
       532 |           if (decimalValue && decimalValue <= groupIndex) {
       533 |             parts[i] = '\\' + capturedGroups[groupIndex];
       534 |           }
       535 |         }
       536 |       }
       537 | 
       538 |       // Remove any prefix anchors so that the output will match anywhere.
       539 |       // ^^ really does mean an anchored match though.
       540 |       for (var i = 0, groupIndex = 0; i < n; ++i) {
       541 |         if ('^' === parts[i] && '^' !== parts[i + 1]) { parts[i] = ''; }
       542 |       }
       543 | 
       544 |       // Expand letters to groupts to handle mixing of case-sensitive and
       545 |       // case-insensitive patterns if necessary.
       546 |       if (regex.ignoreCase && needToFoldCase) {
       547 |         for (var i = 0; i < n; ++i) {
       548 |           var p = parts[i];
       549 |           var ch0 = p.charAt(0);
       550 |           if (p.length >= 2 && ch0 === '[') {
       551 |             parts[i] = caseFoldCharset(p);
       552 |           } else if (ch0 !== '\\') {
       553 |             // TODO: handle letters in numeric escapes.
       554 |             parts[i] = p.replace(
       555 |                 /[a-zA-Z]/g,
       556 |                 function (ch) {
       557 |                   var cc = ch.charCodeAt(0);
       558 |                   return '[' + String.fromCharCode(cc & ~32, cc | 32) + ']';
       559 |                 });
       560 |           }
       561 |         }
       562 |       }
       563 | 
       564 |       return parts.join('');
       565 |     }
       566 | 
       567 |     var rewritten = [];
       568 |     for (var i = 0, n = regexs.length; i < n; ++i) {
       569 |       var regex = regexs[i];
       570 |       if (regex.global || regex.multiline) { throw new Error('' + regex); }
       571 |       rewritten.push(
       572 |           '(?:' + allowAnywhereFoldCaseAndRenumberGroups(regex) + ')');
       573 |     }
       574 | 
       575 |     return new RegExp(rewritten.join('|'), ignoreCase ? 'gi' : 'g');
       576 |   }
       577 | 
       578 |   var PR_innerHtmlWorks = null;
       579 |   function getInnerHtml(node) {
       580 |     // inner html is hopelessly broken in Safari 2.0.4 when the content is
       581 |     // an html description of well formed XML and the containing tag is a PRE
       582 |     // tag, so we detect that case and emulate innerHTML.
       583 |     if (null === PR_innerHtmlWorks) {
       584 |       var testNode = document.createElement('PRE');
       585 |       testNode.appendChild(
       586 |           document.createTextNode('<!DOCTYPE foo PUBLIC "foo bar">\n<foo />'));
       587 |       PR_innerHtmlWorks = !/</.test(testNode.innerHTML);
       588 |     }
       589 | 
       590 |     if (PR_innerHtmlWorks) {
       591 |       var content = node.innerHTML;
       592 |       // XMP tags contain unescaped entities so require special handling.
       593 |       if (isRawContent(node)) {
       594 |         content = textToHtml(content);
       595 |       } else if (!isPreformatted(node, content)) {
       596 |         content = content.replace(/(<br\s*\/?>)[\r\n]+/g, '$1')
       597 |             .replace(/(?:[\r\n]+[ \t]*)+/g, ' ');
       598 |       }
       599 |       return content;
       600 |     }
       601 | 
       602 |     var out = [];
       603 |     for (var child = node.firstChild; child; child = child.nextSibling) {
       604 |       normalizedHtml(child, out);
       605 |     }
       606 |     return out.join('');
       607 |   }
       608 | 
       609 |   /** returns a function that expand tabs to spaces.  This function can be fed
       610 |     * successive chunks of text, and will maintain its own internal state to
       611 |     * keep track of how tabs are expanded.
       612 |     * @return {function (string) : string} a function that takes
       613 |     *   plain text and return the text with tabs expanded.
       614 |     * @private
       615 |     */
       616 |   function makeTabExpander(tabWidth) {
       617 |     var SPACES = '                ';
       618 |     var charInLine = 0;
       619 | 
       620 |     return function (plainText) {
       621 |       // walk over each character looking for tabs and newlines.
       622 |       // On tabs, expand them.  On newlines, reset charInLine.
       623 |       // Otherwise increment charInLine
       624 |       var out = null;
       625 |       var pos = 0;
       626 |       for (var i = 0, n = plainText.length; i < n; ++i) {
       627 |         var ch = plainText.charAt(i);
       628 | 
       629 |         switch (ch) {
       630 |           case '\t':
       631 |             if (!out) { out = []; }
       632 |             out.push(plainText.substring(pos, i));
       633 |             // calculate how much space we need in front of this part
       634 |             // nSpaces is the amount of padding -- the number of spaces needed
       635 |             // to move us to the next column, where columns occur at factors of
       636 |             // tabWidth.
       637 |             var nSpaces = tabWidth - (charInLine % tabWidth);
       638 |             charInLine += nSpaces;
       639 |             for (; nSpaces >= 0; nSpaces -= SPACES.length) {
       640 |               out.push(SPACES.substring(0, nSpaces));
       641 |             }
       642 |             pos = i + 1;
       643 |             break;
       644 |           case '\n':
       645 |             charInLine = 0;
       646 |             break;
       647 |           default:
       648 |             ++charInLine;
       649 |         }
       650 |       }
       651 |       if (!out) { return plainText; }
       652 |       out.push(plainText.substring(pos));
       653 |       return out.join('');
       654 |     };
       655 |   }
       656 | 
       657 |   var pr_chunkPattern = new RegExp(
       658 |       '[^<]+'  // A run of characters other than '<'
       659 |       + '|<\!--[\\s\\S]*?--\>'  // an HTML comment
       660 |       + '|<!\\[CDATA\\[[\\s\\S]*?\\]\\]>'  // a CDATA section
       661 |       // a probable tag that should not be highlighted
       662 |       + '|<\/?[a-zA-Z](?:[^>\"\']|\'[^\']*\'|\"[^\"]*\")*>'
       663 |       + '|<',  // A '<' that does not begin a larger chunk
       664 |       'g');
       665 |   var pr_commentPrefix = /^<\!--/;
       666 |   var pr_cdataPrefix = /^<!\[CDATA\[/;
       667 |   var pr_brPrefix = /^<br\b/i;
       668 |   var pr_tagNameRe = /^<(\/?)([a-zA-Z][a-zA-Z0-9]*)/;
       669 | 
       670 |   /** split markup into chunks of html tags (style null) and
       671 |     * plain text (style {@link #PR_PLAIN}), converting tags which are
       672 |     * significant for tokenization (<br>) into their textual equivalent.
       673 |     *
       674 |     * @param {string} s html where whitespace is considered significant.
       675 |     * @return {Object} source code and extracted tags.
       676 |     * @private
       677 |     */
       678 |   function extractTags(s) {
       679 |     // since the pattern has the 'g' modifier and defines no capturing groups,
       680 |     // this will return a list of all chunks which we then classify and wrap as
       681 |     // PR_Tokens
       682 |     var matches = s.match(pr_chunkPattern);
       683 |     var sourceBuf = [];
       684 |     var sourceBufLen = 0;
       685 |     var extractedTags = [];
       686 |     if (matches) {
       687 |       for (var i = 0, n = matches.length; i < n; ++i) {
       688 |         var match = matches[i];
       689 |         if (match.length > 1 && match.charAt(0) === '<') {
       690 |           if (pr_commentPrefix.test(match)) { continue; }
       691 |           if (pr_cdataPrefix.test(match)) {
       692 |             // strip CDATA prefix and suffix.  Don't unescape since it's CDATA
       693 |             sourceBuf.push(match.substring(9, match.length - 3));
       694 |             sourceBufLen += match.length - 12;
       695 |           } else if (pr_brPrefix.test(match)) {
       696 |             // <br> tags are lexically significant so convert them to text.
       697 |             // This is undone later.
       698 |             sourceBuf.push('\n');
       699 |             ++sourceBufLen;
       700 |           } else {
       701 |             if (match.indexOf(PR_NOCODE) >= 0 && isNoCodeTag(match)) {
       702 |               // A <span class="nocode"> will start a section that should be
       703 |               // ignored.  Continue walking the list until we see a matching end
       704 |               // tag.
       705 |               var name = match.match(pr_tagNameRe)[2];
       706 |               var depth = 1;
       707 |               var j;
       708 |               end_tag_loop:
       709 |               for (j = i + 1; j < n; ++j) {
       710 |                 var name2 = matches[j].match(pr_tagNameRe);
       711 |                 if (name2 && name2[2] === name) {
       712 |                   if (name2[1] === '/') {
       713 |                     if (--depth === 0) { break end_tag_loop; }
       714 |                   } else {
       715 |                     ++depth;
       716 |                   }
       717 |                 }
       718 |               }
       719 |               if (j < n) {
       720 |                 extractedTags.push(
       721 |                     sourceBufLen, matches.slice(i, j + 1).join(''));
       722 |                 i = j;
       723 |               } else {  // Ignore unclosed sections.
       724 |                 extractedTags.push(sourceBufLen, match);
       725 |               }
       726 |             } else {
       727 |               extractedTags.push(sourceBufLen, match);
       728 |             }
       729 |           }
       730 |         } else {
       731 |           var literalText = htmlToText(match);
       732 |           sourceBuf.push(literalText);
       733 |           sourceBufLen += literalText.length;
       734 |         }
       735 |       }
       736 |     }
       737 |     return { source: sourceBuf.join(''), tags: extractedTags };
       738 |   }
       739 | 
       740 |   /** True if the given tag contains a class attribute with the nocode class. */
       741 |   function isNoCodeTag(tag) {
       742 |     return !!tag
       743 |         // First canonicalize the representation of attributes
       744 |         .replace(/\s(\w+)\s*=\s*(?:\"([^\"]*)\"|'([^\']*)'|(\S+))/g,
       745 |                  ' $1="$2$3$4"')
       746 |         // Then look for the attribute we want.
       747 |         .match(/[cC][lL][aA][sS][sS]=\"[^\"]*\bnocode\b/);
       748 |   }
       749 | 
       750 |   /**
       751 |    * Apply the given language handler to sourceCode and add the resulting
       752 |    * decorations to out.
       753 |    * @param {number} basePos the index of sourceCode within the chunk of source
       754 |    *    whose decorations are already present on out.
       755 |    */
       756 |   function appendDecorations(basePos, sourceCode, langHandler, out) {
       757 |     if (!sourceCode) { return; }
       758 |     var job = {
       759 |       source: sourceCode,
       760 |       basePos: basePos
       761 |     };
       762 |     langHandler(job);
       763 |     out.push.apply(out, job.decorations);
       764 |   }
       765 | 
       766 |   /** Given triples of [style, pattern, context] returns a lexing function,
       767 |     * The lexing function interprets the patterns to find token boundaries and
       768 |     * returns a decoration list of the form
       769 |     * [index_0, style_0, index_1, style_1, ..., index_n, style_n]
       770 |     * where index_n is an index into the sourceCode, and style_n is a style
       771 |     * constant like PR_PLAIN.  index_n-1 <= index_n, and style_n-1 applies to
       772 |     * all characters in sourceCode[index_n-1:index_n].
       773 |     *
       774 |     * The stylePatterns is a list whose elements have the form
       775 |     * [style : string, pattern : RegExp, DEPRECATED, shortcut : string].
       776 |     *
       777 |     * Style is a style constant like PR_PLAIN, or can be a string of the
       778 |     * form 'lang-FOO', where FOO is a language extension describing the
       779 |     * language of the portion of the token in $1 after pattern executes.
       780 |     * E.g., if style is 'lang-lisp', and group 1 contains the text
       781 |     * '(hello (world))', then that portion of the token will be passed to the
       782 |     * registered lisp handler for formatting.
       783 |     * The text before and after group 1 will be restyled using this decorator
       784 |     * so decorators should take care that this doesn't result in infinite
       785 |     * recursion.  For example, the HTML lexer rule for SCRIPT elements looks
       786 |     * something like ['lang-js', /<[s]cript>(.+?)<\/script>/].  This may match
       787 |     * '<script>foo()<\/script>', which would cause the current decorator to
       788 |     * be called with '<script>' which would not match the same rule since
       789 |     * group 1 must not be empty, so it would be instead styled as PR_TAG by
       790 |     * the generic tag rule.  The handler registered for the 'js' extension would
       791 |     * then be called with 'foo()', and finally, the current decorator would
       792 |     * be called with '<\/script>' which would not match the original rule and
       793 |     * so the generic tag rule would identify it as a tag.
       794 |     *
       795 |     * Pattern must only match prefixes, and if it matches a prefix, then that
       796 |     * match is considered a token with the same style.
       797 |     *
       798 |     * Context is applied to the last non-whitespace, non-comment token
       799 |     * recognized.
       800 |     *
       801 |     * Shortcut is an optional string of characters, any of which, if the first
       802 |     * character, gurantee that this pattern and only this pattern matches.
       803 |     *
       804 |     * @param {Array} shortcutStylePatterns patterns that always start with
       805 |     *   a known character.  Must have a shortcut string.
       806 |     * @param {Array} fallthroughStylePatterns patterns that will be tried in
       807 |     *   order if the shortcut ones fail.  May have shortcuts.
       808 |     *
       809 |     * @return {function (Object)} a
       810 |     *   function that takes source code and returns a list of decorations.
       811 |     */
       812 |   function createSimpleLexer(shortcutStylePatterns, fallthroughStylePatterns) {
       813 |     var shortcuts = {};
       814 |     var tokenizer;
       815 |     (function () {
       816 |       var allPatterns = shortcutStylePatterns.concat(fallthroughStylePatterns);
       817 |       var allRegexs = [];
       818 |       var regexKeys = {};
       819 |       for (var i = 0, n = allPatterns.length; i < n; ++i) {
       820 |         var patternParts = allPatterns[i];
       821 |         var shortcutChars = patternParts[3];
       822 |         if (shortcutChars) {
       823 |           for (var c = shortcutChars.length; --c >= 0;) {
       824 |             shortcuts[shortcutChars.charAt(c)] = patternParts;
       825 |           }
       826 |         }
       827 |         var regex = patternParts[1];
       828 |         var k = '' + regex;
       829 |         if (!regexKeys.hasOwnProperty(k)) {
       830 |           allRegexs.push(regex);
       831 |           regexKeys[k] = null;
       832 |         }
       833 |       }
       834 |       allRegexs.push(/[\0-\uffff]/);
       835 |       tokenizer = combinePrefixPatterns(allRegexs);
       836 |     })();
       837 | 
       838 |     var nPatterns = fallthroughStylePatterns.length;
       839 |     var notWs = /\S/;
       840 | 
       841 |     /**
       842 |      * Lexes job.source and produces an output array job.decorations of style
       843 |      * classes preceded by the position at which they start in job.source in
       844 |      * order.
       845 |      *
       846 |      * @param {Object} job an object like {@code
       847 |      *    source: {string} sourceText plain text,
       848 |      *    basePos: {int} position of job.source in the larger chunk of
       849 |      *        sourceCode.
       850 |      * }
       851 |      */
       852 |     var decorate = function (job) {
       853 |       var sourceCode = job.source, basePos = job.basePos;
       854 |       /** Even entries are positions in source in ascending order.  Odd enties
       855 |         * are style markers (e.g., PR_COMMENT) that run from that position until
       856 |         * the end.
       857 |         * @type {Array.<number|string>}
       858 |         */
       859 |       var decorations = [basePos, PR_PLAIN];
       860 |       var pos = 0;  // index into sourceCode
       861 |       var tokens = sourceCode.match(tokenizer) || [];
       862 |       var styleCache = {};
       863 | 
       864 |       for (var ti = 0, nTokens = tokens.length; ti < nTokens; ++ti) {
       865 |         var token = tokens[ti];
       866 |         var style = styleCache[token];
       867 |         var match = void 0;
       868 | 
       869 |         var isEmbedded;
       870 |         if (typeof style === 'string') {
       871 |           isEmbedded = false;
       872 |         } else {
       873 |           var patternParts = shortcuts[token.charAt(0)];
       874 |           if (patternParts) {
       875 |             match = token.match(patternParts[1]);
       876 |             style = patternParts[0];
       877 |           } else {
       878 |             for (var i = 0; i < nPatterns; ++i) {
       879 |               patternParts = fallthroughStylePatterns[i];
       880 |               match = token.match(patternParts[1]);
       881 |               if (match) {
       882 |                 style = patternParts[0];
       883 |                 break;
       884 |               }
       885 |             }
       886 | 
       887 |             if (!match) {  // make sure that we make progress
       888 |               style = PR_PLAIN;
       889 |             }
       890 |           }
       891 | 
       892 |           isEmbedded = style.length >= 5 && 'lang-' === style.substring(0, 5);
       893 |           if (isEmbedded && !(match && typeof match[1] === 'string')) {
       894 |             isEmbedded = false;
       895 |             style = PR_SOURCE;
       896 |           }
       897 | 
       898 |           if (!isEmbedded) { styleCache[token] = style; }
       899 |         }
       900 | 
       901 |         var tokenStart = pos;
       902 |         pos += token.length;
       903 | 
       904 |         if (!isEmbedded) {
       905 |           decorations.push(basePos + tokenStart, style);
       906 |         } else {  // Treat group 1 as an embedded block of source code.
       907 |           var embeddedSource = match[1];
       908 |           var embeddedSourceStart = token.indexOf(embeddedSource);
       909 |           var embeddedSourceEnd = embeddedSourceStart + embeddedSource.length;
       910 |           if (match[2]) {
       911 |             // If embeddedSource can be blank, then it would match at the
       912 |             // beginning which would cause us to infinitely recurse on the
       913 |             // entire token, so we catch the right context in match[2].
       914 |             embeddedSourceEnd = token.length - match[2].length;
       915 |             embeddedSourceStart = embeddedSourceEnd - embeddedSource.length;
       916 |           }
       917 |           var lang = style.substring(5);
       918 |           // Decorate the left of the embedded source
       919 |           appendDecorations(
       920 |               basePos + tokenStart,
       921 |               token.substring(0, embeddedSourceStart),
       922 |               decorate, decorations);
       923 |           // Decorate the embedded source
       924 |           appendDecorations(
       925 |               basePos + tokenStart + embeddedSourceStart,
       926 |               embeddedSource,
       927 |               langHandlerForExtension(lang, embeddedSource),
       928 |               decorations);
       929 |           // Decorate the right of the embedded section
       930 |           appendDecorations(
       931 |               basePos + tokenStart + embeddedSourceEnd,
       932 |               token.substring(embeddedSourceEnd),
       933 |               decorate, decorations);
       934 |         }
       935 |       }
       936 |       job.decorations = decorations;
       937 |     };
       938 |     return decorate;
       939 |   }
       940 | 
       941 |   /** returns a function that produces a list of decorations from source text.
       942 |     *
       943 |     * This code treats ", ', and ` as string delimiters, and \ as a string
       944 |     * escape.  It does not recognize perl's qq() style strings.
       945 |     * It has no special handling for double delimiter escapes as in basic, or
       946 |     * the tripled delimiters used in python, but should work on those regardless
       947 |     * although in those cases a single string literal may be broken up into
       948 |     * multiple adjacent string literals.
       949 |     *
       950 |     * It recognizes C, C++, and shell style comments.
       951 |     *
       952 |     * @param {Object} options a set of optional parameters.
       953 |     * @return {function (Object)} a function that examines the source code
       954 |     *     in the input job and builds the decoration list.
       955 |     */
       956 |   function sourceDecorator(options) {
       957 |     var shortcutStylePatterns = [], fallthroughStylePatterns = [];
       958 |     if (options['tripleQuotedStrings']) {
       959 |       // '''multi-line-string''', 'single-line-string', and double-quoted
       960 |       shortcutStylePatterns.push(
       961 |           [PR_STRING,  /^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,
       962 |            null, '\'"']);
       963 |     } else if (options['multiLineStrings']) {
       964 |       // 'multi-line-string', "multi-line-string"
       965 |       shortcutStylePatterns.push(
       966 |           [PR_STRING,  /^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,
       967 |            null, '\'"`']);
       968 |     } else {
       969 |       // 'single-line-string', "single-line-string"
       970 |       shortcutStylePatterns.push(
       971 |           [PR_STRING,
       972 |            /^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,
       973 |            null, '"\'']);
       974 |     }
       975 |     if (options['verbatimStrings']) {
       976 |       // verbatim-string-literal production from the C# grammar.  See issue 93.
       977 |       fallthroughStylePatterns.push(
       978 |           [PR_STRING, /^@\"(?:[^\"]|\"\")*(?:\"|$)/, null]);
       979 |     }
       980 |     var hc = options['hashComments'];
       981 |     if (hc) {
       982 |       if (options['cStyleComments']) {
       983 |         if (hc > 1) {  // multiline hash comments
       984 |           shortcutStylePatterns.push(
       985 |               [PR_COMMENT, /^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/, null, '#']);
       986 |         } else {
       987 |           // Stop C preprocessor declarations at an unclosed open comment
       988 |           shortcutStylePatterns.push(
       989 |               [PR_COMMENT, /^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,
       990 |                null, '#']);
       991 |         }
       992 |         fallthroughStylePatterns.push(
       993 |             [PR_STRING,
       994 |              /^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,
       995 |              null]);
       996 |       } else {
       997 |         shortcutStylePatterns.push([PR_COMMENT, /^#[^\r\n]*/, null, '#']);
       998 |       }
       999 |     }
      1000 |     if (options['cStyleComments']) {
      1001 |       fallthroughStylePatterns.push([PR_COMMENT, /^\/\/[^\r\n]*/, null]);
      1002 |       fallthroughStylePatterns.push(
      1003 |           [PR_COMMENT, /^\/\*[\s\S]*?(?:\*\/|$)/, null]);
      1004 |     }
      1005 |     if (options['regexLiterals']) {
      1006 |       var REGEX_LITERAL = (
      1007 |           // A regular expression literal starts with a slash that is
      1008 |           // not followed by * or / so that it is not confused with
      1009 |           // comments.
      1010 |           '/(?=[^/*])'
      1011 |           // and then contains any number of raw characters,
      1012 |           + '(?:[^/\\x5B\\x5C]'
      1013 |           // escape sequences (\x5C),
      1014 |           +    '|\\x5C[\\s\\S]'
      1015 |           // or non-nesting character sets (\x5B\x5D);
      1016 |           +    '|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+'
      1017 |           // finally closed by a /.
      1018 |           + '/');
      1019 |       fallthroughStylePatterns.push(
      1020 |           ['lang-regex',
      1021 |            new RegExp('^' + REGEXP_PRECEDER_PATTERN + '(' + REGEX_LITERAL + ')')
      1022 |            ]);
      1023 |     }
      1024 | 
      1025 |     var keywords = options['keywords'].replace(/^\s+|\s+$/g, '');
      1026 |     if (keywords.length) {
      1027 |       fallthroughStylePatterns.push(
      1028 |           [PR_KEYWORD,
      1029 |            new RegExp('^(?:' + keywords.replace(/\s+/g, '|') + ')\\b'), null]);
      1030 |     }
      1031 | 
      1032 |     shortcutStylePatterns.push([PR_PLAIN,       /^\s+/, null, ' \r\n\t\xA0']);
      1033 |     fallthroughStylePatterns.push(
      1034 |         // TODO(mikesamuel): recognize non-latin letters and numerals in idents
      1035 |         [PR_LITERAL,     /^@[a-z_$][a-z_$@0-9]*/i, null],
      1036 |         [PR_TYPE,        /^@?[A-Z]+[a-z][A-Za-z_$@0-9]*/, null],
      1037 |         [PR_PLAIN,       /^[a-z_$][a-z_$@0-9]*/i, null],
      1038 |         [PR_LITERAL,
      1039 |          new RegExp(
      1040 |              '^(?:'
      1041 |              // A hex number
      1042 |              + '0x[a-f0-9]+'
      1043 |              // or an octal or decimal number,
      1044 |              + '|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)'
      1045 |              // possibly in scientific notation
      1046 |              + '(?:e[+\\-]?\\d+)?'
      1047 |              + ')'
      1048 |              // with an optional modifier like UL for unsigned long
      1049 |              + '[a-z]*', 'i'),
      1050 |          null, '0123456789'],
      1051 |         [PR_PUNCTUATION, /^.[^\s\w\.$@\'\"\`\/\#]*/, null]);
      1052 | 
      1053 |     return createSimpleLexer(shortcutStylePatterns, fallthroughStylePatterns);
      1054 |   }
      1055 | 
      1056 |   var decorateSource = sourceDecorator({
      1057 |         'keywords': ALL_KEYWORDS,
      1058 |         'hashComments': true,
      1059 |         'cStyleComments': true,
      1060 |         'multiLineStrings': true,
      1061 |         'regexLiterals': true
      1062 |       });
      1063 | 
      1064 |   /** Breaks {@code job.source} around style boundaries in
      1065 |     * {@code job.decorations} while re-interleaving {@code job.extractedTags},
      1066 |     * and leaves the result in {@code job.prettyPrintedHtml}.
      1067 |     * @param {Object} job like {
      1068 |     *    source: {string} source as plain text,
      1069 |     *    extractedTags: {Array.<number|string>} extractedTags chunks of raw
      1070 |     *                   html preceded by their position in {@code job.source}
      1071 |     *                   in order
      1072 |     *    decorations: {Array.<number|string} an array of style classes preceded
      1073 |     *                 by the position at which they start in job.source in order
      1074 |     * }
      1075 |     * @private
      1076 |     */
      1077 |   function recombineTagsAndDecorations(job) {
      1078 |     var sourceText = job.source;
      1079 |     var extractedTags = job.extractedTags;
      1080 |     var decorations = job.decorations;
      1081 |     var numberLines = job.numberLines;
      1082 |     var sourceNode = job.sourceNode;
      1083 | 
      1084 |     var html = [];
      1085 |     // index past the last char in sourceText written to html
      1086 |     var outputIdx = 0;
      1087 | 
      1088 |     var openDecoration = null;
      1089 |     var currentDecoration = null;
      1090 |     var tagPos = 0;  // index into extractedTags
      1091 |     var decPos = 0;  // index into decorations
      1092 |     var tabExpander = makeTabExpander(window['PR_TAB_WIDTH']);
      1093 | 
      1094 |     var adjacentSpaceRe = /([\r\n ]) /g;
      1095 |     var startOrSpaceRe = /(^| ) /gm;
      1096 |     var newlineRe = /\r\n?|\n/g;
      1097 |     var trailingSpaceRe = /[ \r\n]$/;
      1098 |     var lastWasSpace = true;  // the last text chunk emitted ended with a space.
      1099 | 
      1100 |     // See bug 71 and http://stackoverflow.com/questions/136443/why-doesnt-ie7-
      1101 |     var isIE678 = window['_pr_isIE6']();
      1102 |     var lineBreakHtml = (
      1103 |         isIE678
      1104 |         ? (sourceNode && sourceNode.tagName === 'PRE'
      1105 |            // Use line feeds instead of <br>s so that copying and pasting works
      1106 |            // on IE.
      1107 |            // See Issue 104 for the derivation of this mess.
      1108 |            ? (isIE678 === 6 ? '&#160;\r\n' :
      1109 |               isIE678 === 7 ? '&#160;<br />\r' :
      1110 |               isIE678 === 8 ? '&#160;<br />' : '&#160;\r')
      1111 |            // IE collapses multiple adjacent <br>s into 1 line break.
      1112 |            // Prefix every newline with '&#160;' to prevent such behavior.
      1113 |            // &nbsp; is the same as &#160; but works in XML as well as HTML.
      1114 |            : '&#160;<br />')
      1115 |         : '<br />');
      1116 | 
      1117 |     var lineBreaker;
      1118 |     if (numberLines) {
      1119 |       var lineBreaks = [];
      1120 |       for (var i = 0; i < 10; ++i) {
      1121 |         lineBreaks[i] = lineBreakHtml + '</li><li class="L' + i + '">';
      1122 |       }
      1123 |       var lineNum = typeof numberLines === 'number'
      1124 |           ? numberLines - 1 /* number lines are 1 indexed */ : 0;
      1125 |       html.push('<ol class="linenums"><li class="L', (lineNum) % 10, '"');
      1126 |       if (lineNum) {
      1127 |         html.push(' value="', lineNum + 1, '"');
      1128 |       }
      1129 |       html.push('>');
      1130 |       lineBreaker = function () {
      1131 |         var lb = lineBreaks[++lineNum % 10];
      1132 |         // If a decoration is open, we need to close it before closing a list-item
      1133 |         // and reopen it on the other side of the list item.
      1134 |         return openDecoration
      1135 |             ? ('</span>' + lb + '<span class="' + openDecoration + '">') : lb;
      1136 |       };
      1137 |     } else {
      1138 |       lineBreaker = lineBreakHtml;
      1139 |     }
      1140 | 
      1141 |     // A helper function that is responsible for opening sections of decoration
      1142 |     // and outputing properly escaped chunks of source
      1143 |     function emitTextUpTo(sourceIdx) {
      1144 |       if (sourceIdx > outputIdx) {
      1145 |         if (openDecoration && openDecoration !== currentDecoration) {
      1146 |           // Close the current decoration
      1147 |           html.push('</span>');
      1148 |           openDecoration = null;
      1149 |         }
      1150 |         if (!openDecoration && currentDecoration) {
      1151 |           openDecoration = currentDecoration;
      1152 |           html.push('<span class="', openDecoration, '">');
      1153 |         }
      1154 |         // This interacts badly with some wikis which introduces paragraph tags
      1155 |         // into pre blocks for some strange reason.
      1156 |         // It's necessary for IE though which seems to lose the preformattedness
      1157 |         // of <pre> tags when their innerHTML is assigned.
      1158 |         // http://stud3.tuwien.ac.at/~e0226430/innerHtmlQuirk.html
      1159 |         // and it serves to undo the conversion of <br>s to newlines done in
      1160 |         // chunkify.
      1161 |         var htmlChunk = textToHtml(
      1162 |             tabExpander(sourceText.substring(outputIdx, sourceIdx)))
      1163 |             .replace(lastWasSpace
      1164 |                      ? startOrSpaceRe
      1165 |                      : adjacentSpaceRe, '$1&#160;');
      1166 |         // Keep track of whether we need to escape space at the beginning of the
      1167 |         // next chunk.
      1168 |         lastWasSpace = trailingSpaceRe.test(htmlChunk);
      1169 |         html.push(htmlChunk.replace(newlineRe, lineBreaker));
      1170 |         outputIdx = sourceIdx;
      1171 |       }
      1172 |     }
      1173 | 
      1174 |     while (true) {
      1175 |       // Determine if we're going to consume a tag this time around.  Otherwise
      1176 |       // we consume a decoration or exit.
      1177 |       var outputTag;
      1178 |       if (tagPos < extractedTags.length) {
      1179 |         if (decPos < decorations.length) {
      1180 |           // Pick one giving preference to extractedTags since we shouldn't open
      1181 |           // a new style that we're going to have to immediately close in order
      1182 |           // to output a tag.
      1183 |           outputTag = extractedTags[tagPos] <= decorations[decPos];
      1184 |         } else {
      1185 |           outputTag = true;
      1186 |         }
      1187 |       } else {
      1188 |         outputTag = false;
      1189 |       }
      1190 |       // Consume either a decoration or a tag or exit.
      1191 |       if (outputTag) {
      1192 |         emitTextUpTo(extractedTags[tagPos]);
      1193 |         if (openDecoration) {
      1194 |           // Close the current decoration
      1195 |           html.push('</span>');
      1196 |           openDecoration = null;
      1197 |         }
      1198 |         html.push(extractedTags[tagPos + 1]);
      1199 |         tagPos += 2;
      1200 |       } else if (decPos < decorations.length) {
      1201 |         emitTextUpTo(decorations[decPos]);
      1202 |         currentDecoration = decorations[decPos + 1];
      1203 |         decPos += 2;
      1204 |       } else {
      1205 |         break;
      1206 |       }
      1207 |     }
      1208 |     emitTextUpTo(sourceText.length);
      1209 |     if (openDecoration) {
      1210 |       html.push('</span>');
      1211 |     }
      1212 |     if (numberLines) { html.push('</li></ol>'); }
      1213 |     job.prettyPrintedHtml = html.join('');
      1214 |   }
      1215 | 
      1216 |   /** Maps language-specific file extensions to handlers. */
      1217 |   var langHandlerRegistry = {};
      1218 |   /** Register a language handler for the given file extensions.
      1219 |     * @param {function (Object)} handler a function from source code to a list
      1220 |     *      of decorations.  Takes a single argument job which describes the
      1221 |     *      state of the computation.   The single parameter has the form
      1222 |     *      {@code {
      1223 |     *        source: {string} as plain text.
      1224 |     *        decorations: {Array.<number|string>} an array of style classes
      1225 |     *                     preceded by the position at which they start in
      1226 |     *                     job.source in order.
      1227 |     *                     The language handler should assigned this field.
      1228 |     *        basePos: {int} the position of source in the larger source chunk.
      1229 |     *                 All positions in the output decorations array are relative
      1230 |     *                 to the larger source chunk.
      1231 |     *      } }
      1232 |     * @param {Array.<string>} fileExtensions
      1233 |     */
      1234 |   function registerLangHandler(handler, fileExtensions) {
      1235 |     for (var i = fileExtensions.length; --i >= 0;) {
      1236 |       var ext = fileExtensions[i];
      1237 |       if (!langHandlerRegistry.hasOwnProperty(ext)) {
      1238 |         langHandlerRegistry[ext] = handler;
      1239 |       } else if ('console' in window) {
      1240 |         console['warn']('cannot override language handler %s', ext);
      1241 |       }
      1242 |     }
      1243 |   }
      1244 |   function langHandlerForExtension(extension, source) {
      1245 |     if (!(extension && langHandlerRegistry.hasOwnProperty(extension))) {
      1246 |       // Treat it as markup if the first non whitespace character is a < and
      1247 |       // the last non-whitespace character is a >.
      1248 |       extension = /^\s*</.test(source)
      1249 |           ? 'default-markup'
      1250 |           : 'default-code';
      1251 |     }
      1252 |     return langHandlerRegistry[extension];
      1253 |   }
      1254 |   registerLangHandler(decorateSource, ['default-code']);
      1255 |   registerLangHandler(
      1256 |       createSimpleLexer(
      1257 |           [],
      1258 |           [
      1259 |            [PR_PLAIN,       /^[^<?]+/],
      1260 |            [PR_DECLARATION, /^<!\w[^>]*(?:>|$)/],
      1261 |            [PR_COMMENT,     /^<\!--[\s\S]*?(?:-\->|$)/],
      1262 |            // Unescaped content in an unknown language
      1263 |            ['lang-',        /^<\?([\s\S]+?)(?:\?>|$)/],
      1264 |            ['lang-',        /^<%([\s\S]+?)(?:%>|$)/],
      1265 |            [PR_PUNCTUATION, /^(?:<[%?]|[%?]>)/],
      1266 |            ['lang-',        /^<xmp\b[^>]*>([\s\S]+?)<\/xmp\b[^>]*>/i],
      1267 |            // Unescaped content in javascript.  (Or possibly vbscript).
      1268 |            ['lang-js',      /^<script\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],
      1269 |            // Contains unescaped stylesheet content
      1270 |            ['lang-css',     /^<style\b[^>]*>([\s\S]*?)(<\/style\b[^>]*>)/i],
      1271 |            ['lang-in.tag',  /^(<\/?[a-z][^<>]*>)/i]
      1272 |           ]),
      1273 |       ['default-markup', 'htm', 'html', 'mxml', 'xhtml', 'xml', 'xsl']);
      1274 |   registerLangHandler(
      1275 |       createSimpleLexer(
      1276 |           [
      1277 |            [PR_PLAIN,        /^[\s]+/, null, ' \t\r\n'],
      1278 |            [PR_ATTRIB_VALUE, /^(?:\"[^\"]*\"?|\'[^\']*\'?)/, null, '\"\'']
      1279 |            ],
      1280 |           [
      1281 |            [PR_TAG,          /^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],
      1282 |            [PR_ATTRIB_NAME,  /^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],
      1283 |            ['lang-uq.val',   /^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],
      1284 |            [PR_PUNCTUATION,  /^[=<>\/]+/],
      1285 |            ['lang-js',       /^on\w+\s*=\s*\"([^\"]+)\"/i],
      1286 |            ['lang-js',       /^on\w+\s*=\s*\'([^\']+)\'/i],
      1287 |            ['lang-js',       /^on\w+\s*=\s*([^\"\'>\s]+)/i],
      1288 |            ['lang-css',      /^style\s*=\s*\"([^\"]+)\"/i],
      1289 |            ['lang-css',      /^style\s*=\s*\'([^\']+)\'/i],
      1290 |            ['lang-css',      /^style\s*=\s*([^\"\'>\s]+)/i]
      1291 |            ]),
      1292 |       ['in.tag']);
      1293 |   registerLangHandler(
      1294 |       createSimpleLexer([], [[PR_ATTRIB_VALUE, /^[\s\S]+/]]), ['uq.val']);
      1295 |   registerLangHandler(sourceDecorator({
      1296 |           'keywords': CPP_KEYWORDS,
      1297 |           'hashComments': true,
      1298 |           'cStyleComments': true
      1299 |         }), ['c', 'cc', 'cpp', 'cxx', 'cyc', 'm']);
      1300 |   registerLangHandler(sourceDecorator({
      1301 |           'keywords': 'null true false'
      1302 |         }), ['json']);
      1303 |   registerLangHandler(sourceDecorator({
      1304 |           'keywords': CSHARP_KEYWORDS,
      1305 |           'hashComments': true,
      1306 |           'cStyleComments': true,
      1307 |           'verbatimStrings': true
      1308 |         }), ['cs']);
      1309 |   registerLangHandler(sourceDecorator({
      1310 |           'keywords': JAVA_KEYWORDS,
      1311 |           'cStyleComments': true
      1312 |         }), ['java']);
      1313 |   registerLangHandler(sourceDecorator({
      1314 |           'keywords': SH_KEYWORDS,
      1315 |           'hashComments': true,
      1316 |           'multiLineStrings': true
      1317 |         }), ['bsh', 'csh', 'sh']);
      1318 |   registerLangHandler(sourceDecorator({
      1319 |           'keywords': PYTHON_KEYWORDS,
      1320 |           'hashComments': true,
      1321 |           'multiLineStrings': true,
      1322 |           'tripleQuotedStrings': true
      1323 |         }), ['cv', 'py']);
      1324 |   registerLangHandler(sourceDecorator({
      1325 |           'keywords': PERL_KEYWORDS,
      1326 |           'hashComments': true,
      1327 |           'multiLineStrings': true,
      1328 |           'regexLiterals': true
      1329 |         }), ['perl', 'pl', 'pm']);
      1330 |   registerLangHandler(sourceDecorator({
      1331 |           'keywords': RUBY_KEYWORDS,
      1332 |           'hashComments': true,
      1333 |           'multiLineStrings': true,
      1334 |           'regexLiterals': true
      1335 |         }), ['rb']);
      1336 |   registerLangHandler(sourceDecorator({
      1337 |           'keywords': JSCRIPT_KEYWORDS,
      1338 |           'cStyleComments': true,
      1339 |           'regexLiterals': true
      1340 |         }), ['js']);
      1341 |   registerLangHandler(sourceDecorator({
      1342 |           'keywords': COFFEE_KEYWORDS,
      1343 |           'hashComments': 3,  // ### style block comments
      1344 |           'cStyleComments': true,
      1345 |           'multilineStrings': true,
      1346 |           'tripleQuotedStrings': true,
      1347 |           'regexLiterals': true
      1348 |         }), ['coffee']);
      1349 |   registerLangHandler(createSimpleLexer([], [[PR_STRING, /^[\s\S]+/]]), ['regex']);
      1350 | 
      1351 |   function applyDecorator(job) {
      1352 |     var sourceCodeHtml = job.sourceCodeHtml;
      1353 |     var opt_langExtension = job.langExtension;
      1354 | 
      1355 |     // Prepopulate output in case processing fails with an exception.
      1356 |     job.prettyPrintedHtml = sourceCodeHtml;
      1357 | 
      1358 |     try {
      1359 |       // Extract tags, and convert the source code to plain text.
      1360 |       var sourceAndExtractedTags = extractTags(sourceCodeHtml);
      1361 |       /** Plain text. @type {string} */
      1362 |       var source = sourceAndExtractedTags.source;
      1363 |       job.source = source;
      1364 |       job.basePos = 0;
      1365 | 
      1366 |       /** Even entries are positions in source in ascending order.  Odd entries
      1367 |         * are tags that were extracted at that position.
      1368 |         * @type {Array.<number|string>}
      1369 |         */
      1370 |       job.extractedTags = sourceAndExtractedTags.tags;
      1371 | 
      1372 |       // Apply the appropriate language handler
      1373 |       langHandlerForExtension(opt_langExtension, source)(job);
      1374 |       // Integrate the decorations and tags back into the source code to produce
      1375 |       // a decorated html string which is left in job.prettyPrintedHtml.
      1376 |       recombineTagsAndDecorations(job);
      1377 |     } catch (e) {
      1378 |       if ('console' in window) {
      1379 |         console['log'](e && e['stack'] ? e['stack'] : e);
      1380 |       }
      1381 |     }
      1382 |   }
      1383 | 
      1384 |   /**
      1385 |    * @param sourceCodeHtml {string} The HTML to pretty print.
      1386 |    * @param opt_langExtension {string} The language name to use.
      1387 |    *     Typically, a filename extension like 'cpp' or 'java'.
      1388 |    * @param opt_numberLines {number|boolean} True to number lines,
      1389 |    *     or the 1-indexed number of the first line in sourceCodeHtml.
      1390 |    */
      1391 |   function prettyPrintOne(sourceCodeHtml, opt_langExtension, opt_numberLines) {
      1392 |     var job = {
      1393 |       sourceCodeHtml: sourceCodeHtml,
      1394 |       langExtension: opt_langExtension,
      1395 |       numberLines: opt_numberLines
      1396 |     };
      1397 |     applyDecorator(job);
      1398 |     return job.prettyPrintedHtml;
      1399 |   }
      1400 | 
      1401 |   function prettyPrint(opt_whenDone) {
      1402 |     function byTagName(tn) { return document.getElementsByTagName(tn); }
      1403 |     // fetch a list of nodes to rewrite
      1404 |     var codeSegments = [byTagName('pre'), byTagName('code'), byTagName('xmp')];
      1405 |     var elements = [];
      1406 |     for (var i = 0; i < codeSegments.length; ++i) {
      1407 |       for (var j = 0, n = codeSegments[i].length; j < n; ++j) {
      1408 |         elements.push(codeSegments[i][j]);
      1409 |       }
      1410 |     }
      1411 |     codeSegments = null;
      1412 | 
      1413 |     var clock = Date;
      1414 |     if (!clock['now']) {
      1415 |       clock = { 'now': function () { return (new Date).getTime(); } };
      1416 |     }
      1417 | 
      1418 |     // The loop is broken into a series of continuations to make sure that we
      1419 |     // don't make the browser unresponsive when rewriting a large page.
      1420 |     var k = 0;
      1421 |     var prettyPrintingJob;
      1422 | 
      1423 |     function doWork() {
      1424 |       var endTime = (window['PR_SHOULD_USE_CONTINUATION'] ?
      1425 |                      clock.now() + 250 /* ms */ :
      1426 |                      Infinity);
      1427 |       for (; k < elements.length && clock.now() < endTime; k++) {
      1428 |         var cs = elements[k];
      1429 |         if (cs.className && cs.className.indexOf('prettyprint') >= 0) {
      1430 |           // If the classes includes a language extensions, use it.
      1431 |           // Language extensions can be specified like
      1432 |           //     <pre class="prettyprint lang-cpp">
      1433 |           // the language extension "cpp" is used to find a language handler as
      1434 |           // passed to PR.registerLangHandler.
      1435 |           var langExtension = cs.className.match(/\blang-(\w+)\b/);
      1436 |           if (langExtension) { langExtension = langExtension[1]; }
      1437 | 
      1438 |           // make sure this is not nested in an already prettified element
      1439 |           var nested = false;
      1440 |           for (var p = cs.parentNode; p; p = p.parentNode) {
      1441 |             if ((p.tagName === 'pre' || p.tagName === 'code' ||
      1442 |                  p.tagName === 'xmp') &&
      1443 |                 p.className && p.className.indexOf('prettyprint') >= 0) {
      1444 |               nested = true;
      1445 |               break;
      1446 |             }
      1447 |           }
      1448 |           if (!nested) {
      1449 |             // fetch the content as a snippet of properly escaped HTML.
      1450 |             // Firefox adds newlines at the end.
      1451 |             var content = getInnerHtml(cs);
      1452 |             content = content.replace(/(?:\r\n?|\n)$/, '');
      1453 | 
      1454 |             // Look for a class like linenums or linenums:<n> where <n> is the
      1455 |             // 1-indexed number of the first line.
      1456 |             var numberLines = cs.className.match(/\blinenums\b(?::(\d+))?/);
      1457 | 
      1458 |             // do the pretty printing
      1459 |             prettyPrintingJob = {
      1460 |               sourceCodeHtml: content,
      1461 |               langExtension: langExtension,
      1462 |               sourceNode: cs,
      1463 |               numberLines: numberLines
      1464 |                   ? numberLines[1] && numberLines[1].length ? +numberLines[1] : true
      1465 |                   : false
      1466 |             };
      1467 |             applyDecorator(prettyPrintingJob);
      1468 |             replaceWithPrettyPrintedHtml();
      1469 |           }
      1470 |         }
      1471 |       }
      1472 |       if (k < elements.length) {
      1473 |         // finish up in a continuation
      1474 |         setTimeout(doWork, 250);
      1475 |       } else if (opt_whenDone) {
      1476 |         opt_whenDone();
      1477 |       }
      1478 |     }
      1479 | 
      1480 |     function replaceWithPrettyPrintedHtml() {
      1481 |       var newContent = prettyPrintingJob.prettyPrintedHtml;
      1482 |       if (!newContent) { return; }
      1483 |       var cs = prettyPrintingJob.sourceNode;
      1484 | 
      1485 |       // push the prettified html back into the tag.
      1486 |       if (!isRawContent(cs)) {
      1487 |         // just replace the old html with the new
      1488 |         cs.innerHTML = newContent;
      1489 |       } else {
      1490 |         // we need to change the tag to a <pre> since <xmp>s do not allow
      1491 |         // embedded tags such as the span tags used to attach styles to
      1492 |         // sections of source code.
      1493 |         var pre = document.createElement('PRE');
      1494 |         for (var i = 0; i < cs.attributes.length; ++i) {
      1495 |           var a = cs.attributes[i];
      1496 |           if (a.specified) {
      1497 |             var aname = a.name.toLowerCase();
      1498 |             if (aname === 'class') {
      1499 |               pre.className = a.value;  // For IE 6
      1500 |             } else {
      1501 |               pre.setAttribute(a.name, a.value);
      1502 |             }
      1503 |           }
      1504 |         }
      1505 |         pre.innerHTML = newContent;
      1506 | 
      1507 |         // remove the old
      1508 |         cs.parentNode.replaceChild(pre, cs);
      1509 |         cs = pre;
      1510 |       }
      1511 |     }
      1512 | 
      1513 |     doWork();
      1514 |   }
      1515 | 
      1516 |   window['PR_normalizedHtml'] = normalizedHtml;
      1517 |   window['prettyPrintOne'] = prettyPrintOne;
      1518 |   window['prettyPrint'] = prettyPrint;
      1519 |   window['PR'] = {
      1520 |         'combinePrefixPatterns': combinePrefixPatterns,
      1521 |         'createSimpleLexer': createSimpleLexer,
      1522 |         'registerLangHandler': registerLangHandler,
      1523 |         'sourceDecorator': sourceDecorator,
      1524 |         'PR_ATTRIB_NAME': PR_ATTRIB_NAME,
      1525 |         'PR_ATTRIB_VALUE': PR_ATTRIB_VALUE,
      1526 |         'PR_COMMENT': PR_COMMENT,
      1527 |         'PR_DECLARATION': PR_DECLARATION,
      1528 |         'PR_KEYWORD': PR_KEYWORD,
      1529 |         'PR_LITERAL': PR_LITERAL,
      1530 |         'PR_NOCODE': PR_NOCODE,
      1531 |         'PR_PLAIN': PR_PLAIN,
      1532 |         'PR_PUNCTUATION': PR_PUNCTUATION,
      1533 |         'PR_SOURCE': PR_SOURCE,
      1534 |         'PR_STRING': PR_STRING,
      1535 |         'PR_TAG': PR_TAG,
      1536 |         'PR_TYPE': PR_TYPE
      1537 |       };
      1538 | })();
      1539 | 
      
      
      --------------------------------------------------------------------------------
      /public/simple/css/main.css:
      --------------------------------------------------------------------------------
        1 | a { color: #00c; text-decoration: none; }
        2 | a:visited { color: #00c; }
        3 | a:hover { text-decoration: underline; }
        4 | 
        5 | img { border: 0; }
        6 | 
        7 | table {
        8 |   border-collapse: collapse;
        9 | }
       10 | 
       11 | td, th {
       12 |     border: 1px solid #ccc;
       13 |     padding: 5px;
       14 | }
       15 | 
       16 | .clear { clear: both; line-height: 1px; font-size: 1px; margin: 1px; }
       17 | 
       18 | a.article {
       19 |     display: block;
       20 |     padding-top: 0.25em;
       21 | }
       22 | 
       23 | #rss {
       24 |     margin-bottom: 1em;
       25 | }
       26 | 
       27 | #rss img {
       28 |     width: 14px;
       29 |     height: 14px;
       30 |     border: 0px;
       31 |     margin-bottom: -3px;
       32 | }
       33 |     
       34 | #registration_explanation {
       35 |     font-size: 13px;
       36 |     text-align: left; 
       37 |     margin-top: 30px;
       38 | }
       39 | 
       40 | #password_explanation {
       41 |     font-size: 11px;
       42 |     text-align: center; 
       43 |     color: #777;
       44 | }
       45 | 
       46 | #description {
       47 |     font-size: 14px; margin-top: 8px;
       48 | }
       49 | 
       50 | .errors {
       51 |     color: #800;
       52 |     margin-bottom: 20px;
       53 |     font-family: Helvetica, Verdana, Arial, sans-serif; 
       54 |     padding: 5px;
       55 |     background-color: #fee;
       56 |     text-align: center;
       57 |     -moz-border-radius: 8px;
       58 |     -webkit-border-radius: 8px;
       59 |     border-radius: 8px;
       60 | }
       61 | 
       62 | .host {
       63 |     margin-top: 10px; 
       64 |     font-family: Helvetica, Verdana, Arial, sans-serif; 
       65 |     font-size: 11px; 
       66 |     color: #555;
       67 | }
       68 | 
       69 | .article_bar {
       70 |     clear: both;
       71 |     padding-bottom: 10px;
       72 |     border-top: 1px solid #777;
       73 | }
       74 | 
       75 | .article_render {
       76 |     float: left; 
       77 |     width: 300px;
       78 |     font-size: 15px;
       79 | }
       80 | 
       81 | /* --------------------------- Buttons on Unread ------------- */
       82 | 
       83 | a.button {
       84 |     font-family: Helvetica, Verdana, Arial, sans-serif;
       85 |     display: block;
       86 |     border-width: 1px;
       87 |     border-style: solid;
       88 |     -moz-border-radius: 8px;
       89 |     -webkit-border-radius: 8px;
       90 |     border-radius: 8px;
       91 |     text-align: center;
       92 |     text-decoration: none;
       93 |     font-size: 12px;
       94 | }
       95 | a.button:visited { color: #fff; }
       96 |    
       97 | .skip_button {
       98 |     float: left;
       99 |     width: 4em;
      100 |     background-color: #f77;
      101 |     border-color: #f77;
      102 |     color: #fff;
      103 |     margin: 0.5em 2em 0.5em 0em;
      104 |     padding: 1.2em 0em;
      105 | }
      106 | .skip_button:hover { background-color: #d22; border-color: #d22; }
      107 | 
      108 | .unread_button {
      109 |     float: left;
      110 |     border-color: #3c3;
      111 |     background-color: #3c3;
      112 |     color: #fff;
      113 |     width: 4em;
      114 |     margin: 0.5em 1em 0.5em 0em;
      115 |     padding: 0.75em 0em;
      116 | }
      117 | .unread_button:hover { background-color: #0a0; border-color: #0a0; }
      118 | 
      119 | .text_button {
      120 |     float: right; 
      121 |     width: 4em;
      122 |     background-color: #88b; 
      123 |     color: #fff; 
      124 |     border-color: #88b;
      125 |     margin: 0.5em 0em 0.5em 10px;
      126 |     padding: 1.2em 0em;
      127 | }
      128 | .text_button:hover { background-color: #448; border-color: #448; }
      129 | .text_button:visited { color: #448; }
      130 | 
      131 | .delete_button {
      132 |     float: right;
      133 |     width: 3.5em;
      134 |     background-color: #f77;    
      135 |     border-color: #f77;
      136 |     color: #fff;
      137 |     margin: 0.5em 0em 0.5em 10px;
      138 |     padding: 1.25em 0em;
      139 | }
      140 | .delete_button:hover { background-color: #d22; border-color: #d22; }
      141 | .delete_button:visited { color: #c00; }
      142 | 
      143 | .foldermenu {
      144 |     display: none;
      145 |     position: absolute;
      146 |     right: -11px;
      147 |     top: -15px;
      148 |     min-width: 150px;
      149 |     text-align: left;
      150 |     font-size: 13px;
      151 |     border: 2px solid #555;
      152 |     background-color: #fff;
      153 |     padding: 10px 10px 20px 20px;
      154 |     -moz-border-radius: 5px;
      155 |     -webkit-border-radius: 5px;
      156 |     border-radius: 5px;
      157 |     z-index: 1337;
      158 | }
      159 | 
      160 |     .foldermenu a:hover { text-decoration: none; }
      161 |     .foldermenu img { border: 0; margin-right: 2px; }
      162 |     .movemenu img { width: 12px; height: 9px; cursor: pointer; }
      163 |     .sharemenu img { margin-bottom: -3px; }
      164 |     .sharemenu { 
      165 |         width: 150px; 
      166 |         -webkit-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.4), 0 0 5px #888;
      167 |     }
      168 | 
      169 |     .sharemenu textarea {
      170 |         width: 385px;
      171 |         height: 110px;
      172 |         border: 1px solid #444;
      173 |     }
      174 |     
      175 |     .sharemenu input.text {
      176 |         width: 385px;
      177 |         border: 1px solid #444;
      178 |     }
      179 |     
      180 |     .sharemenu label {
      181 |         display: block;
      182 |         margin-top: 1em;
      183 |     }
      184 |     
      185 |     #facebooksharemenu {
      186 |         width: 400px;
      187 |         height: 300px;
      188 |     }
      189 |     
      190 |     #pinboardsharemenu {
      191 |         width: 400px;
      192 |         height: 300px;
      193 |     }
      194 |     
      195 |     #tumblrsharemenu {
      196 |         width: 400px;
      197 |         height: 250px;        
      198 |     }
      199 | 
      200 |     #twittersharemenu {
      201 |         width: 400px;
      202 |         height: 140px;
      203 |     }
      204 |     
      205 |         #twittersharemenu textarea {
      206 |             height: 40px;
      207 |         }
      208 |         
      209 |         #twitter_counter {
      210 |             padding-top: 4px;
      211 |             text-align: right;
      212 |             padding-right: 10px;
      213 |             font-size: 11px;
      214 |         }
      215 | 
      216 |     #evernotesharemenu {
      217 |         width: 400px;
      218 |         height: 200px;
      219 |     }
      220 | 
      221 |     
      222 | /* ------------------ The Deck --------------------------------- */
      223 | #adlabel {
      224 |     margin-top: 20px; 
      225 |     margin-bottom: 0;
      226 |     text-transform: uppercase;
      227 |     font-size: 10px;
      228 |     font-family: Helvetica, Verdana, Arial, sans-serif;
      229 |     color: #ccc;
      230 | }
      231 | #ad { 
      232 |     margin-top: 0;
      233 |     border-top: 1px solid #ccc;
      234 | }
      235 | #deckad img, #deckad iframe { border: 0; float: right; padding-left: 5px; }
      236 | #deckad { font-size: 12px; }
      237 | #deckad .ads { display: block; margin-top: 7px; }
      238 | #adclear { 
      239 |     font-size: 1px; height: 1px; line-height: 1px; clear: both; 
      240 |     margin-bottom: 30px; 
      241 |     border-bottom: 1px solid #ccc;
      242 | }
      243 | #deckpromo {
      244 |     display: block;
      245 |     margin-top: -10px;
      246 | }
      247 | #deckpromo img { border: 0; }
      248 | 
      249 | #bookmarkListDeckAd #deckad img, #bookmarkListDeckAd #deckad iframe {
      250 |     display: block;
      251 |     float: none;
      252 |     padding: 5px 0;
      253 | }
      254 | 
      255 | #bookmarkListDeckAd #ad {
      256 |     border: 0;
      257 | }
      258 | 
      259 | #bookmarkListDeckAd #adclear {
      260 |     border: 0;
      261 | }
      262 | 
      263 | /* ------------------ Tableview styles ------------------------ */
      264 | 
      265 | #left_column {
      266 |     width: 550px;
      267 |     padding-bottom: 20px;
      268 |     float: left;
      269 | }
      270 | 
      271 | #right_column {
      272 |     float: right;
      273 |     width: 170px;
      274 | }
      275 | 
      276 | #categoryHeader {
      277 |     float: left;
      278 |     margin: 0 0 20px 0;
      279 | }
      280 | 
      281 | #bookmark_list {
      282 |     width: 550px;
      283 |     clear: both;
      284 | }
      285 | 
      286 | #folder_container {
      287 |     font-size: 13px; 
      288 |     width: 200px;
      289 | }
      290 | 
      291 | #right_column {
      292 |     height: 100%;
      293 | }
      294 | 
      295 | #bookmark_list .story {
      296 |     border-top: 1px solid #ccc;
      297 |     padding-top: 16px;
      298 |     margin-bottom: 0;
      299 | }
      300 | 
      301 | #bookmark_list .headline {
      302 |     font-weight: normal;
      303 |     font-size: 16px;
      304 | }
      305 | 
      306 | .tableViewCell {
      307 |     font-family: Helvetica, Arial, sans-serif;
      308 |     border: 1px solid #ddd;
      309 |     border-bottom-width: 0;
      310 |     padding: 10px;
      311 |     background-color: #f8f8f8;
      312 | }
      313 | .tableViewCellFirst, #bookmark_list > div:first-child {
      314 |     -moz-border-radius-topleft: 15px;
      315 |     -webkit-border-top-left-radius: 15px;
      316 |     border-top-left-radius: 15px;
      317 |     -moz-border-radius-topright: 15px;
      318 |     -webkit-border-top-right-radius: 15px;
      319 |     border-top-right-radius: 15px;
      320 | }
      321 | .tableViewCellLast, #bookmark_list > div:last-child {
      322 |     border-bottom-width: 1px;
      323 |     -moz-border-radius-bottomleft: 15px;
      324 |     -webkit-border-bottom-left-radius: 15px;
      325 |     border-bottom-left-radius: 15px;
      326 |     -moz-border-radius-bottomright: 15px;
      327 |     -webkit-border-bottom-right-radius: 15px;
      328 |     border-bottom-right-radius: 15px;
      329 | }
      330 | 
      331 | a.tableViewCellTitleLink,
      332 | a.tableViewCellTitleLink:visited {
      333 |     font-family: Georgia;
      334 |     color: #000;
      335 |     font-size: 16px;
      336 | }
      337 | 
      338 |     a.tableViewCellTitleLink:hover {
      339 |         color: #00c;
      340 |     }
      341 |     
      342 | .tableViewCell a.starToggleStarred,
      343 | .tableViewCell a.starToggleUnstarred,
      344 | .tableViewCell a.starToggleProgress {
      345 | }                    
      346 |     .tableViewCell a.starToggleProgress img {
      347 |         border: 0;
      348 |     }
      349 |     
      350 |     .tableViewCell a.starToggleUnstarred img,
      351 |     .tableViewCell a.starToggleStarred img {
      352 |         border: 0;
      353 |     }
      354 | 
      355 | .tableViewCell .titleRow {
      356 |     float: left;
      357 |     width: 375px;
      358 |     padding-top: 3px;
      359 |     overflow: hidden;
      360 |     word-wrap: break-word;
      361 | }
      362 | 
      363 | .tableViewCell .summary {
      364 |     font-size: 13px;
      365 |     color: #555;
      366 |     margin-top: 10px;
      367 |     overflow: hidden;
      368 |     word-wrap: break-word;
      369 | }
      370 | 
      371 | .tableViewCell .likeBox {
      372 |     float: left;
      373 |     width: 26px;
      374 |     padding-top: 4px;
      375 | }
      376 | 
      377 | .tableViewCell .starBox {
      378 |     float: left;
      379 |     width: 26px;
      380 |     padding-top: 2px;
      381 | }
      382 | 
      383 | .tableViewCell .controls {
      384 |     text-align: right;
      385 | }
      386 | 
      387 | .tableViewCell .cornerControls {
      388 |     float: right;
      389 | /*    width: 54px;*/
      390 |     width: 120px;
      391 |     text-align: right;
      392 | }
      393 | 
      394 | .secondaryControls {
      395 |     position: relative;
      396 |     clear: both;
      397 |     text-align: right;
      398 |     padding-right: 5px;
      399 |     padding-top: 10px;
      400 | }
      401 | 
      402 |     .secondaryControls .separator {
      403 |         color: #777;
      404 |         font-size: 9px;
      405 |     }
      406 | 
      407 |     .actionLink {
      408 |         font-size: 11px;
      409 |     }
      410 | 
      411 |     .actionButton,
      412 |     .actionButton:visited,
      413 |     .actionButton:active {
      414 |         font-family: 'Lucida Grande', Verdana, Helvetica, Arial, sans-serif;
      415 |         display: inline-block;
      416 |         padding: 5px 5px;
      417 |         margin: 2px;
      418 |         font-size: 11px;
      419 |         text-align: center;
      420 |         text-decoration: none;
      421 |         color: #555;
      422 |         background-color: #f8f8f8;
      423 |         border: 1px solid #ccc;
      424 |         -moz-border-radius: 5px;
      425 |         -webkit-border-radius: 5px;
      426 |         border-radius: 5px;
      427 |     }
      428 |     
      429 |     .actionButton:hover {
      430 |         text-decoration: none;
      431 |     }
      432 | 
      433 |     .archiveButton,
      434 |     .archiveButton:hover,
      435 |     .archiveButton:visited,
      436 |     .archiveButton:active {
      437 |         background-color: #f8f8f8;
      438 |         border-color: #a44;
      439 |         color: #933;
      440 |     }
      441 | 
      442 |     .restoreButton,
      443 |     .restoreButton:hover,
      444 |     .restoreButton:visited,
      445 |     .restoreButton:active {
      446 |         background-color: #f8f8f8;
      447 |         border-color: #4a4;
      448 |         color: #393;
      449 |     }
      450 |     
      451 |     .actionButton img { vertical-align: -5px; }
      452 |         
      453 |     .textButton, .textButton:visited, .textButton:active, .textButton:hover,
      454 |     .archiveButton, .archiveButton:visited, .archiveButton:active, .archiveButton:hover {
      455 |         width: 40px;
      456 | /*        float: right;*/
      457 |     }
      458 | 
      459 | 
      460 |     .tableViewCellAjaxReplacement {
      461 |         padding: 0;
      462 |         text-align: center;
      463 |         background-color: #f0f0f0;
      464 |     }
      465 |     
      466 |     .ajaxActivityButton {
      467 |     }
      468 | 
      469 | 
      470 | #download_as .outputdevice {
      471 |     display: inline-block;
      472 |     text-align: center;
      473 |     width: 50px;
      474 |     margin: 0 5px;
      475 | }
      476 | #download_as .outputdevicefirst { margin-left: 0; }
      477 | #download_as .outputdevicelast { margin-right: 0; }
      478 | 
      479 | #download_as {
      480 |     font-size: 11px;
      481 | }
      482 | 
      483 | #tools_container {
      484 |     font-size: 11px;
      485 |     margin-bottom: 40px;
      486 | }
      487 | 
      488 | #download_as a, 
      489 | #download_as a:hover,
      490 | #download_as a:visited,
      491 | #download_as a:active {
      492 |     text-decoration: none;
      493 | }
      494 | 
      495 | #download_as img {
      496 |     padding-bottom: 5px;
      497 | }
      498 | 
      499 | .pagination {
      500 |     text-align: center; 
      501 |     margin: 30px;
      502 | }
      503 | 
      504 | #paginationTop {
      505 |     text-align: right;
      506 |     float: right;
      507 |     margin: 15px 0 0 0;
      508 | }
      509 | 
      510 | /* GMSTR on frontpage */
      511 | 
      512 | .html_summary {
      513 |     font-style: italic;
      514 | }
      515 | .html_summary blockquote {
      516 |     font-style: normal;
      517 |     margin: 0;
      518 | }
      519 | 
      520 | .html_summary p {
      521 |     margin-top: 0;
      522 |     padding-top: 0;
      523 | }
      524 | 
      525 | /* -------------- Frontpage, main ------------------------------------------ */
      526 | 
      527 | body {
      528 |      font-family: 'Helvetica', 'Arial', sans-serif;
      529 |      font-size: 100%;
      530 |      background-color: #f4f4f4;
      531 | }
      532 | 
      533 | div.body {
      534 |      width: 800px;
      535 |      margin: 0px auto 0px auto;
      536 |      position: relative;
      537 | }
      538 | 
      539 | #logo {
      540 |     font-family: Georgia, 'Times New Roman', Times, serif;
      541 |     font-size: 40px;
      542 |     line-height: 1.4;
      543 |     border-bottom: 2px solid #444;
      544 | }
      545 | #logo a { color: #000; text-decoration: none; }
      546 | h1#logo { font-size: 40px; font-weight: normal; margin: 0; }
      547 | 
      548 | #navigation { float: left; }
      549 | 
      550 | #userpanel {
      551 |     float: right;
      552 |     margin-top: 20px;
      553 |     font-size: 18px;
      554 |     text-align: right;
      555 | }
      556 | 
      557 |     #userpanel a { text-decoration: underline; }
      558 | 
      559 | a { color: #00c; text-decoration: none; }
      560 | a:visited { color: #00c; }
      561 | a:hover { text-decoration: underline; }
      562 | 
      563 | .clear { clear: both; line-height: 1px; font-size: 1px; margin: 1px; }
      564 | #header { margin-bottom: 30px; }
      565 | 
      566 | #content { 
      567 |     clear: both; 
      568 |     font-size: 14px;
      569 | }
      570 | 
      571 | #footer {
      572 |     clear: both;
      573 |     margin: 80px auto 20px auto;
      574 |     text-align: center;
      575 |     font-size: 12px;
      576 |     color: #555;
      577 |     border-top: 1px solid #ccc;
      578 |     padding-top: 10px;
      579 |     padding-bottom: 20px;
      580 | }
      581 | 
      582 | #feature_column {
      583 |     width: 420px;
      584 |     float: left;
      585 | }
      586 | 
      587 | #side_column {
      588 |     width: 270px;
      589 |     padding-left: 60px;
      590 |     float: left;
      591 | }
      592 | 
      593 | #stories_from_friends {
      594 |     clear: both;
      595 |     margin-bottom: 40px;
      596 | }
      597 | 
      598 | .story {
      599 |     margin-bottom: 30px;
      600 | }
      601 | 
      602 | .story .headline {
      603 |     font-family: Georgia;
      604 |     font-size: 20px;
      605 |     font-weight: normal;
      606 |     margin: 0;
      607 | }
      608 |     .story .headline a { color: #111; }
      609 |     
      610 |     #side_column .story .headline {
      611 |         font-size: 16px;
      612 |     }
      613 | 
      614 | .story .byline {
      615 |     font-size: 12px;
      616 |     color: #777;
      617 |     margin-top: 2px;
      618 | }
      619 |     .story .bookmarklet {
      620 |         margin-top: -1px;
      621 |     }
      622 |     
      623 | .story .summary {
      624 |     font-size: 13px;
      625 |     color: #222;
      626 | }
      627 |     .story .summary p:first-child { margin-top: 0.5em; }
      628 |     
      629 | .section_header2 {
      630 |     background-color: #444;
      631 |     color: #ddd;
      632 |     text-transform: uppercase;
      633 |     padding: 4px;
      634 |     margin: 0 0 20px 0;
      635 |     font-weight: normal;
      636 |     font-size: 14px;
      637 | }
      638 | 
      639 | .section_header {
      640 |     color: #444;
      641 |     text-transform: uppercase;
      642 |     padding: 4px 0;
      643 |     margin: 0 0 20px 0;
      644 |     font-weight: normal;
      645 |     font-size: 14px;
      646 |     border-bottom: 1px solid #ccc;
      647 | }
      648 | 
      649 | #explanation {
      650 |     height: 280px;
      651 | }
      652 | 
      653 | .bookmarklet {
      654 |     display: inline-block;
      655 |     font-family: 'Lucida Grande', Verdana, sans-serif;
      656 |     font-weight: bold;
      657 |     font-size: 11px;
      658 |     -webkit-border-radius: 8px;
      659 |     -moz-border-radius: 8px;
      660 |     border-radius: 8px;
      661 |     color: #fff;
      662 |     background-color: #626262;
      663 |     border: 1px solid #626262;
      664 |     padding: 0px 7px 1px 7px;
      665 |     text-shadow: #3b3b3b 1px 1px 0px;
      666 |     min-width: 62px;
      667 |     text-align: center;
      668 |     vertical-align: 2px;
      669 | }
      670 | a.bookmarket, a.bookmarklet:hover, a.bookmarklet:active, a.bookmarklet:visited { 
      671 |     color: #fff; text-decoration: none; outline: none;
      672 | }
      673 | .bookmarklet:focus { outline: none; }
      674 | 
      675 | a.inactive_bookmarklet, a.inactive_bookmarklet:hover, a.inactive_bookmarklet:active, a.inactive_bookmarklet:visited {
      676 |     background-color: #f8f8f8;
      677 |     border-color: #aaa;
      678 |     color: #444;
      679 |     text-shadow: none;
      680 | }
      681 | 
      682 | .bookmarklet:hover, .active_bookmarklet, a.inactive_bookmarklet:hover, a.active_bookmarklet:hover, a.active_bookmarklet:active, a.active_bookmarklet:visited {
      683 |     background-color: #626262;                
      684 |     border-color: #626262;
      685 |     text-shadow: #3b3b3b 1px 1px 0px;
      686 |     color: #fff;
      687 | }
      688 | 
      689 | #howitworks {
      690 |     float: left; 
      691 |     width: 350px;
      692 |     padding-left: 50px;
      693 |     padding-top: 20px;
      694 | }
      695 | 
      696 |     #howitworks li.first { padding-bottom: 14px; }
      697 |     #howitworks li .bookmarklet { margin-top: 2px; }
      698 | 
      699 | #outputdevices {
      700 |     float: right;
      701 |     width: 270px;
      702 | }
      703 | 
      704 | .outputdevice {
      705 |     float: left;
      706 |     width: 135px;
      707 |     text-align: center;
      708 |     padding-bottom: 10px;
      709 | }
      710 | 
      711 | 
      712 | /* --------------- LEGACY --------------------------- */
      713 | 
      714 | .errors {
      715 |     color: #c00;
      716 |     font-weight: bold;
      717 |     text-align: center;
      718 | }
      719 | 
      720 | .extras_section {
      721 |     clear: both;
      722 |     border-top: 1px solid #ccc;
      723 |     padding-top: 10px;
      724 |     padding-bottom: 10px;
      725 |     word-break: break-all;
      726 |     word-wrap: break-word;
      727 | }
      728 | 
      729 | img.extras_big {
      730 |     float: right;
      731 |     border: 4px solid #ccc;
      732 |     margin: 20px 0 30px 10px;
      733 | }
      734 | 
      735 | img.extras_icon {
      736 |     max-width: 80px;
      737 |     margin-top: 10px;
      738 |     margin-right: 10px;
      739 | }
      740 | 
      741 | .app_list {
      742 |     float: left;
      743 |     width: 179px;
      744 |     border-right: 1px solid #ccc;
      745 |     margin-right: 10px;
      746 |     margin-top: 20px;
      747 |     margin-bottom: 20px;
      748 |     font-size: 13px;
      749 | }
      750 | 
      751 | .app_list ul {
      752 |     list-style-type: none;
      753 |     padding: 0;
      754 |     margin: 0;
      755 | }
      756 | 
      757 | .app_list li {
      758 |     padding-bottom: 5px;
      759 |     height: 44px;
      760 |     clear: both;
      761 | }
      762 | 
      763 | img.iphone_app_icon {
      764 |     margin-right: 10px;
      765 |     vertical-align: middle;
      766 |     float: left;
      767 | }
      768 | 
      769 | .iphone_app_name {
      770 |     float: left;
      771 |     vertical-align: middle;
      772 |     width: 120px;
      773 |     margin-top: 8px;
      774 | }
      775 | 
      776 | .app_list h3 {
      777 |     margin-top: 0;
      778 | }
      779 | 
      780 | .app_list a:hover { text-decoration: none; }
      781 | 
      782 | a.reveal {
      783 |     display: block;
      784 |     width: 160px;
      785 |     text-align: center;
      786 |     background-color: #446;
      787 |     border-radius: 3px;
      788 |     margin-bottom: 10px;
      789 |     color: #eee;
      790 |     font-weight: bold;
      791 |     text-decoration: none;
      792 |     padding: 5px;
      793 | }
      794 | 
      795 | .dd_disclosure {
      796 |     float: right;
      797 | }
      798 | 
      799 | h2.post_title {
      800 |   font-size: 150%!important;
      801 |   border: 0!important;
      802 | }
      803 | 
      804 | .post_content h2 {
      805 |   font-size: 120%!important;
      806 |   border: 0!important;
      807 |   border-top: 4px solid #eee!important;
      808 |   padding-top: .5em!important;
      809 | }
      810 | 
      811 | blockquote {
      812 |   margin: 1em 0!important;
      813 |   border-left: 5px solid #DDD!important;
      814 |   padding-left: .6em!important;
      815 | }
      816 | 
      817 | p code {
      818 |   font-size: 12px!important;
      819 |   background-color: ghostWhite!important;
      820 |   color: #444!important;
      821 |   padding: 0 .2em!important;
      822 |   border: 1px solid #DEDEDE!important;
      823 | }
      
      
      --------------------------------------------------------------------------------
      /test/session_store.test.js:
      --------------------------------------------------------------------------------
       1 | 
       2 | var Store = require('../lib/session_store')
       3 |   , config = require('../config');
       4 | 
       5 | var store = new Store(config.db_options);
       6 | 
       7 | module.exports = {
       8 |     setUp: function (callback) {
       9 |         this.existsKey = 'key1';
      10 |         this.notExistsKey = 'key2';
      11 |         this.session = {foo: 'bar'};
      12 |         store.set(this.existsKey, this.session, function() {
      13 |             callback();
      14 |         });
      15 |     },
      16 |     tearDown: function (callback) {
      17 |         // clean up
      18 |         store.destroy(this.existsKey, function() {
      19 |             callback();
      20 |         });
      21 |     },
      22 |     test_get: function(test) {
      23 |         store.get(this.existsKey, function(err, session) {
      24 |             test.ok(!err);
      25 |             test.ok(session);
      26 |             for(var k in this.session) {
      27 |                 test.strictEqual(session[k], this.session[k]);
      28 |             }
      29 |             test.done();
      30 |         });
      31 |     },
      32 |     test_length: function(test) {
      33 |         store.length(function(err, count) {
      34 |             test.ok(!err);
      35 |             test.ok(count > 0);
      36 |             test.done();
      37 |         });
      38 |     },
      39 |     test_set: function(test) {
      40 |         var key = new Date().toString();
      41 |         var session = {key: key, now: new Date().getTime()};
      42 |         store.set(key, session, function(err) {
      43 |             test.ok(!err);
      44 |             store.get(key, function(err, sess) {
      45 |                 test.ok(!err);
      46 |                 for(var k in session) {
      47 |                     test.strictEqual(sess[k], session[k]);
      48 |                 }
      49 |                 test.done();
      50 |             });
      51 |         });
      52 |     },
      53 |     test_clear: function(test) {
      54 |         store.clear(function(err) {
      55 |             test.ok(!err);
      56 |             store.length(function(err, count) {
      57 |                 test.ok(!err);
      58 |                 test.equal(0, count);
      59 |                 test.done();
      60 |             });
      61 |         });
      62 |     },
      63 | };
      
      
      --------------------------------------------------------------------------------
      /test/showdown.test.code.md:
      --------------------------------------------------------------------------------
       1 | <div class="slide">
       2 | # h1
       3 | </div>
       4 | 
       5 | 
       6 | 
       7 | ## h2
       8 | 
       9 | 代码啊
      10 | ```
      11 | var showdown = require('../lib/showdown');
      12 | var fs = require('fs');
      13 | 
      14 | var html = showdown.parse(fs.readFileSync(__dirname + '/showdown.test.md'));
      15 | 
      16 | console.log(html);
      17 | ```
      18 | 
      19 | ```
      20 | code 2
      21 | 
      22 | ddd
      23 | ```
      24 | 
      25 | PS: 提了pull request,但是估计在没有真实攻击示例放出来之前,是不会被接受的。
      26 | 
      27 | ```
      28 | /**
      29 |  * Parse the given str.
      30 |  */
      31 | `abc`
      32 | exports.parse = function(str, options) {
      33 |   if (null == str || '' == str) return {};
      34 |   return 'object' == typeof str
      35 |     ? parseObject(str)
      36 |     : parseString(str, options);
      37 | };
      38 | ```
      39 | 
      
      
      --------------------------------------------------------------------------------
      /test/showdown.test.js:
      --------------------------------------------------------------------------------
       1 | /*!
       2 |  * nodeblog - showdown.test.js
       3 |  * Copyright(c) 2012 fengmk2 <fengmk2@gmail.com>
       4 |  * MIT Licensed
       5 |  */
       6 | 
       7 | /**
       8 |  * Module dependencies.
       9 |  */
      10 | 
      11 | var showdown = require('../lib/showdown');
      12 | var fs = require('fs');
      13 | 
      14 | var md = fs.readFileSync(__dirname + '/showdown.test.code.md').toString();
      15 | var html = showdown.parse(md);
      16 | 
      17 | console.log(md);
      18 | 
      19 | console.log('\n\n-------- ==============> ---------------\n\n');
      20 | 
      21 | console.log(html);
      
      
      --------------------------------------------------------------------------------
      /test/showdown.test.md:
      --------------------------------------------------------------------------------
       1 | # H1
       2 | 
       3 | Below is a code
       4 | 
       5 |     var showdown = require('../lib/showdown');
       6 |     var fs = require('fs');
       7 | 
       8 |     var html = showdown.parse(fs.readFileSync(__dirname + '/showdown.test.md'));
       9 | 
      10 |     console.log(html);
      11 | 
      12 | also code too.
      13 | 
      14 | ```
      15 | var showdown = require('../lib/showdown');
      16 | var fs = require('fs');
      17 | 
      18 | var html = showdown.parse(fs.readFileSync(__dirname + '/showdown.test.md'));
      19 | 
      20 | console.log(html);
      21 | ```
      22 | 
      23 | 
      24 | ## H2
      25 | 
      26 | > 哈哈
      27 | 
      28 | ### 3
      29 | 
      30 | * item 1
      31 | * item 2
      32 | * item 3
      33 | 
      34 | 1. sorted item 1
      35 | 2. sorted item 2
      36 | 3. sorted item 3
      37 | 
      38 | `END` over.
      
      
      --------------------------------------------------------------------------------
      /views/readme.md:
      --------------------------------------------------------------------------------
       1 | # View Theme
       2 | 
       3 | ## Howto use a View Theme
       4 | 
       5 | 1. add view templates under /views/{your theme}
       6 | 
       7 | 2. add corresponding css\js\img files under /public/{your theme}
       8 | 
       9 | 3. set the 'view_theme' option in /config.js
      10 | 
      11 | 
      12 | ## Structure of View Theme templates
      13 | 
      14 | normally a View Theme should contain following templates: 
      15 | 
      16 | * index
      17 | 
      18 | * 404
      19 | error page for 404
      20 | 
      21 | * archive 
      22 | for posts list. usually used to display searching\catgorizing results
      23 | 
      24 | * post/item
      25 | for a single post
      26 | 
      27 | * page
      28 | for static pages
      29 | 
      30 | 
      31 | ## P.S.
      32 | 
      33 | You can use <%- partial('inclued_template') %> in template file to include an another file.
      
      
      --------------------------------------------------------------------------------
      /views/simple/404.html:
      --------------------------------------------------------------------------------
      1 | <h2>error 404</h2>
      2 | <p> T.T</p>
      
      
      --------------------------------------------------------------------------------
      /views/simple/archive.html:
      --------------------------------------------------------------------------------
       1 | <% 
       2 | var current_user = request.session.user;
       3 | if (posts.length <=0) { %>
       4 |     <div class="extras_section">
       5 |       no related post T.T
       6 |     </div>
       7 | <%
       8 | } else {
       9 | for(var i = 0, len = posts.length; i < len; i++) { 
      10 |     var post = posts[i];
      11 | %>
      12 | <div class="extras_section"> 
      13 |     <h2><a href="/post/<%- post._id %>"><%= post.title %></a></h2> 
      14 |     <div class="post_content">
      15 |     <%- post.is_markdown ? first_paragraph_markdown(post.content) : post.content %>
      16 |     <a href="/post/<%- post._id %>">查看全文</a>
      17 |     </div>
      18 |     <% if(post.tags && post.tags.length > 0) { %>
      19 |     <div class="tag"><span class="name">标签: </span>
      20 |         <%
      21 |         var tags = post.tags;
      22 |         for(var j = 0, jl = tags.length; j < jl; j++) {
      23 |         %>
      24 |         <a href="/tag/<%= tags[j] %>"><%= tags[j] %></a>
      25 |         <% } %>
      26 |     </div>
      27 |     <% } %>
      28 |     <p>
      29 |         <% if(post.author) { %>
      30 |         <a href="<%- post.author.t_url %>">
      31 |            <img style="width: 25px; height: 25px;" src="<%- post.author.profile_image_url %>" />
      32 |            @<%= post.author.screen_name %>
      33 |         </a>
      34 |         <% } %>
      35 |         <% if(post.create_at) { %>
      36 |         Post at <%- post.create_at.format() %>
      37 |         <% } %>
      38 |         <span style="color: #ccc;">&bull;</span>
      39 |         <% if(current_user && (current_user.is_admin || current_user.uid === post.author.uid)) { %>
      40 |         <a href="/post/<%- post._id %>/edit">编辑</a>
      41 |         <span style="color: #ccc;">&bull;</span>
      42 |         <a href="javascript:delete_post('<%- post._id %>');">删除</a>
      43 |         <% } %>
      44 |     </p>
      45 | </div> 
      46 | <% } %>
      47 | <% if(page > 1) { %>
      48 | <a href="/?page=<%- page - 1 %>">上一页</a>
      49 | <% } %>
      50 | <% if(posts.length > 0) { %>
      51 | <a href="/?page=<%- page + 1 %>">下一页</a>
      52 | <% } //end for
      53 | } %>
      54 | 
      
      
      --------------------------------------------------------------------------------
      /views/simple/index.html:
      --------------------------------------------------------------------------------
       1 | <% 
       2 | var current_user = request.session.user;
       3 | for(var i = 0, len = posts.length; i < len; i++) { 
       4 |     var post = posts[i];
       5 | %>
       6 | <div class="extras_section"> 
       7 |     <h2><a href="/post/<%- post._id %>"><%= post.title %></a></h2> 
       8 |     <div class="post_content">
       9 |     <%- post.is_markdown ? first_paragraph_markdown(post.content) : post.content %>
      10 |     <a href="/post/<%- post._id %>">查看全文</a>
      11 |     </div>
      12 |     <% if(post.tags && post.tags.length > 0) { %>
      13 |     <div class="tag"><span class="name">标签: </span>
      14 |         <%
      15 |         var tags = post.tags;
      16 |         for(var j = 0, jl = tags.length; j < jl; j++) {
      17 |         %>
      18 |         <a href="/tag/<%= tags[j] %>"><%= tags[j] %></a>
      19 |         <% } %>
      20 |     </div>
      21 |     <% } %>
      22 |     <p>
      23 |         <% if(post.author) { %>
      24 |         <a href="<%- post.author.t_url %>">
      25 |            <img style="width: 25px; height: 25px;" src="<%- post.author.profile_image_url %>" />
      26 |            @<%= post.author.screen_name %>
      27 |         </a>
      28 |         <% } %>
      29 |         <% if(post.create_at) { %>
      30 |         Post at <%- post.create_at.format() %>
      31 |         <% } %>
      32 |         <span style="color: #ccc;">&bull;</span>
      33 |         <% if(current_user && (current_user.is_admin || current_user.uid === post.author.uid)) { %>
      34 |         <a href="/post/<%- post._id %>/edit">编辑</a>
      35 |         <span style="color: #ccc;">&bull;</span>
      36 |         <a href="javascript:delete_post('<%- post._id %>');">删除</a>
      37 |         <% } %>
      38 |     </p>
      39 | </div> 
      40 | <% } %>
      41 | <% if(page > 1) { %>
      42 | <a href="/?page=<%- page - 1 %>">上一页</a>
      43 | <% } %>
      44 | <% if(posts.length > 0) { %>
      45 | <a href="/?page=<%- page + 1 %>">下一页</a>
      46 | <% } %>
      47 | 
      
      
      --------------------------------------------------------------------------------
      /views/simple/layout.html:
      --------------------------------------------------------------------------------
       1 | <!DOCTYPE html>
       2 | <html> 
       3 | <head>
       4 |     <meta charset="utf-8" />
       5 |     <meta http-equiv="X-UA-Compatible" content="chrome=1" />
       6 |     <meta name="description" content="A simple blog base on nodejs." /> 
       7 |     <meta name="viewport" content="width=770" /> 
       8 |     <title><% if(locals.post) { %> <%= post.title %> - <% } %><%- config.site_name %></title> 
       9 |     <link rel="icon" href="/favicon.ico" /> 
      10 |     <link rel="stylesheet" href="/simple/css/main.css" />
      11 |     <link rel="stylesheet" href="/css/prettify.css" /> 
      12 |     <script src="/js/jquery-1.5.2.min.js"></script>
      13 |     <script src="/js/prettify.js"></script> 
      14 | <script>
      15 | function delete_post(id) {
      16 |     if(window.confirm('确定要删除此文章吗?')) {
      17 |         $.post('/post/' + id + '/delete', function(result) {
      18 |             if(result.error) {
      19 |                 alert(result.error);
      20 |             } else {
      21 |                 window.location = '/';
      22 |             }
      23 |         });
      24 |     }
      25 | };
      26 | $(document).ready(function(){
      27 | 	$('pre code').parent().addClass('prettyprint');
      28 | 	prettyPrint();
      29 | });
      30 | </script> 
      31 | </head>
      32 | <body>
      33 | <a href="http://github.com/fengmk2/nodeblog"><img style="position: absolute; top: 0; right: 0; border: 0;" src="/image/forkme.png" alt="Fork me on GitHub" /></a>
      34 | <div class="body">
      35 |    <div id="header"> 
      36 |         <div id="userpanel">
      37 | 		     <a href="/">首页</a>
      38 | 		     <span style="color: #ccc;">&bull;</span> 
      39 | 		     <a href="/rss">RSS</a>
      40 | 		     <% if(request.session.user) { 
      41 | 		         var user = request.session.user;
      42 | 		     %>
      43 | 			     <% if(user.is_admin) { %>
      44 | 			     <span style="color: #ccc;">&bull;</span> 
      45 | 	             <a href="/post/new">添加文章</a> 
      46 | 	             <% } %>
      47 | 		     <span style="color: #ccc;">&bull;</span> 
      48 | 		     <a href="/user/<%- user.uid %>">
      49 |                 <img style="width: 25px; height: 25px;" src="<%- user.profile_image_url %>" />
      50 |                 @<%- user.screen_name %>
      51 |              </a>
      52 | 		     <span style="color: #ccc;">&bull;</span> 
      53 | 		     <a href="/user/logout">退出</a>
      54 | 		     <% } else { %>
      55 | 		     <span style="color: #ccc;">&bull;</span>
      56 | 		     <a href="/user/login">登录</a>
      57 | 		     <% } %>
      58 |         </div> 
      59 |         <h1 id="logo"> 
      60 |             <a href="/" class="logo" title="<%- config.site_name %>"><%- config.site_name %></a> 
      61 |         </h1> 
      62 |         <div id="description"><%- config.site_description %></div> 
      63 |     </div> 
      64 |     <div id="content"><%- body %></div> 
      65 |     <div id="footer"> 
      66 |         <a href="/">Home</a> &bull;
      67 |         <a href="/about">About</a> &bull;
      68 |         <a href="/m">Mobile</a> &bull;
      69 |         <a href="/api">API</a> 
      70 |         |
      71 |         Power by
      72 |         <a href="http://www.mongodb.org" target="_blank">Mongodb</a> &bull;
      73 |         <a href="http://nodejs.org" target="_blank">Nodejs</a> &bull;
      74 |         <a href="http://senchalabs.github.com/connect/" target="_blank">Connect</a> &bull;
      75 |         <a href="https://github.com/guileen/node-mongoskin" target="_blank">Mongoskin</a> &bull;
      76 |         <a href="https://github.com/visionmedia/ejs" target="_blank">ejs</a> &bull;
      77 |         <a href="http://jquery.com" target="_blank">jQuery</a>
      78 |         <div style="margin-top: 1em;"> 
      79 | 	        &copy;&nbsp;2011 Net4Team.
      80 | 	        Help and Contact :
      81 | 	        [Weibo]<a href="http://t.sina.com.cn/imk2" target="_blank">@Python发烧友</a> &bull;
      82 | 	        [Twitter]<a href="http://twitter.com/fengmk2" target="_blank">@fengmk2</a>.
      83 |         </div> 
      84 |     </div>
      85 | </div>
      86 | </body> 
      87 | </html> 
      88 | 
      
      
      --------------------------------------------------------------------------------
      /views/simple/post/edit.html:
      --------------------------------------------------------------------------------
       1 | <%  
       2 | var post = locals.post || {}, current_user = request.session.user;
       3 | %>
       4 | <form action="/post<%- (post._id ? '/' + post._id : '') %>" method="post" style="width: 98%; margin: 0px auto 0px auto;"> 
       5 |     <label for="bookmark_title" style="float: left;">Title:</label> 
       6 |     <div style="font-size: 0.8em; font-style: italic; float: right; padding-top: 4px;"> 
       7 |         (Title for this post)
       8 |     </div> 
       9 |     <input style="display: block; clear: both; width: 100%; margin-bottom: 20px;" 
      10 |         id="title" name="title" type="text" value="<%= post.title || '' %>" /> 
      11 |     <label for="bookmark_selection" style="float: left;">正文: </label> 
      12 |     <input type="checkbox" <% if(post.is_markdown !== false) { %>checked="checked"<% } %> 
      13 |         id="markdown_cb" name="markdown_cb" /><label for="markdown_cb">Markdown</label> 
      14 |     |
      15 |     <input type="checkbox" <% if(post.public) { %>checked="checked"<% } %> 
      16 |         id="public_cb" name="public_cb" /><label for="public_cb">Public</label> 
      17 |     <% if(current_user.metaweblog && current_user.metaweblog.bloginfo) { %>
      18 |     |
      19 |     <input type="checkbox" <% if(post.weblog_sync) { %>checked="checked"<% } %> 
      20 |         id="sync_cb" name="sync_cb" /><label for="sync_cb">Sync to Blog</label> 
      21 |     <% } %>
      22 |     <div style="font-size: 0.8em; font-style: italic; float: right; padding-top: 4px;"> 
      23 |         (<a href="https://github.com/github/github-flavored-markdown/blob/gh-pages/index.md" target="_blank">Markdown</a> or HTML)
      24 |     </div> 
      25 |     <textarea style="display: block; clear: both; width: 100%; height: 500px; margin-bottom: 20px;" 
      26 |         id="postcontent" name="content"><%- post.content || '' %></textarea> 
      27 |     <label for="tags">标签: </label>
      28 |     <input name="tags" id="tags" type="text" value="<%= post.tags ? post.tags.join(',') : '' %>" style="width: 50%;" />
      29 |     <div style="margin-top: 20px;"> 
      30 |         <input id="submit" type="submit" value="提 交" style="width: 100px; height: 40px; font-size: 18px;" /> 
      31 |     </div> 
      32 | </form>
      33 | <script>
      34 | $(function() {
      35 |     $('#postcontent').change(function() {
      36 |         if($('#markdown_cb').attr('checked')) {
      37 |             // check the first line if # xxx for title
      38 |             var content = $(this).val();
      39 |             var title = content.split('\n', 1)[0];
      40 |             if(title && title.substring(0, 2) === '# ') {
      41 |                 $('#title').val(title.substring(2));
      42 |                 $(this).val(content.substring(title.length));
      43 |             }
      44 |         }
      45 |     });
      46 | });
      47 | </script>
      
      
      --------------------------------------------------------------------------------
      /views/simple/post/item.html:
      --------------------------------------------------------------------------------
       1 | <% var current_user = request.session.user; %>
       2 | 
       3 | <div class="extras_section">
       4 |     <h2 class="post_title"><a href="/post/<%- post._id %>"><%= post.title %></a></h2>
       5 |     <div class="post_content">
       6 |     <%- post.is_markdown ? markdown(post.content) : post.content %>
       7 |     </div>
       8 |     <% if(post.tags && post.tags.length > 0) { %>
       9 |     <div class="tag"><span class="name">标签: </span>
      10 |         <%
      11 |         var tags = post.tags;
      12 |         for(var j = 0, jl = tags.length; j < jl; j++) {
      13 |         %>
      14 |         <a href="/tag/<%= tags[j] %>"><%= tags[j] %></a>
      15 |         <% } %>
      16 |     </div>
      17 |     <% } %>
      18 |     <p class="post_info">
      19 |         <% if(post.author) { %>
      20 |         <a href="<%- post.author.t_url %>">
      21 |            <img style="width: 25px; height: 25px;" src="<%- post.author.profile_image_url %>" />
      22 |            @<%= post.author.screen_name %>
      23 |         </a>
      24 |         <% } %>
      25 |         <% if(post.create_at) { %>
      26 |         Post at <%- post.create_at.format() %>
      27 |         <% } %>
      28 |         <span style="color: #ccc;">&bull;</span>
      29 |         <% if(current_user && (current_user.is_admin || current_user.uid === post.author.uid)) { %>
      30 |         <a href="/post/<%- post._id %>/edit">编辑</a>
      31 |         <span style="color: #ccc;">&bull;</span>
      32 |         <a href="javascript:delete_post('<%- post._id %>');">删除</a>
      33 |         <% } %>
      34 |     </p>
      35 |     
      36 |     <hr/>
      37 | 
      38 |     <div id="disqus_thread"></div>
      39 |     <script type="text/javascript">
      40 |         /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
      41 |         var disqus_shortname = 'nodeblog'; // required: replace example with your forum shortname
      42 | 
      43 |         /* * * DON'T EDIT BELOW THIS LINE * * */
      44 |         (function() {
      45 |             var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
      46 |             dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
      47 |             (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
      48 |         })();
      49 |     </script>
      50 |     <noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
      51 |     <a href="http://disqus.com" class="dsq-brlink">blog comments powered by <span class="logo-disqus">Disqus</span></a>
      52 |     
      53 | </div>
      54 | 
      55 | <script>
      56 | $(function(){
      57 |     var c_url = 'http://api.yongwo.de/api/s?u=';
      58 |     c_url += document.location.href;
      59 |     c_url += '&cb=?'
      60 |     $.getJSON(c_url, function(data){
      61 |         var html = '<span>Shorten URL <input class="short_url" type="text" value="' + data + '" /></span><br/>';
      62 |         $('.post_info').prepend(html).find('.short_url').click(function() {
      63 |             $(this).select();
      64 |         });
      65 |     });
      66 | });
      67 | </script> 
      
      
      --------------------------------------------------------------------------------
      /views/simple/rss.xml:
      --------------------------------------------------------------------------------
       1 | <?xml version="1.0"?>
       2 | <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/">
       3 |   <channel>
       4 |     <title><%= config.site_name %></title>
       5 |     <link><%- config.site_url %></link>
       6 |     <description><![CDATA[<%= config.site_description %>]]></description>
       7 |     <pubDate><%- new Date().format('rssDateTime') %></pubDate>
       8 |     <% for(var i = 0, len = posts.length; i < len; i++) {
       9 |     	var post = posts[i];
      10 |     	post.rss_link = config.site_url + '/post/' + post._id;
      11 |     %>
      12 |     <item>
      13 |        <title><%= post.title %></title>
      14 |        <link><%- post.rss_link %></link>
      15 |        <% if(post.author) { %>
      16 |        <dc:creator>@<%= post.author.screen_name %></dc:creator>
      17 |        <author>@$<%= post.author.screen_name %></author>
      18 |        <% } %>
      19 |        <pubDate><%- post.create_at.format('rssDateTime') %></pubDate>
      20 |        <guid><%- post.rss_link %></guid>
      21 |        <description><![CDATA[<%- post.is_markdown ? markdown(post.content) : post.content %>]]></description>
      22 |     </item>
      23 |     <% } %>
      24 |   </channel>
      25 | </rss>
      
      
      --------------------------------------------------------------------------------
      /views/simple/tag.html:
      --------------------------------------------------------------------------------
      1 | <ul>
      2 | <?js for (var i=0, len= it.tags.length; i<len; i++) {
      3 |   var tag= it.tags[i];
      4 | ?>
      5 |   <li>
      6 |     <a href="/tag/#{tag.name}">${tag.name}</a>
      7 |   </li>
      8 | <?js } ?>
      9 | </ul>
      
      
      --------------------------------------------------------------------------------
      /views/simple/user/item.html:
      --------------------------------------------------------------------------------
       1 | <div class="extras_section">
       2 | <%
       3 | var current_user = request.session.user;
       4 | %>
       5 |     <h2>关联博客设置</h2>
       6 |     <form action="/user/<%- current_user.uid %>" method="post">
       7 |         <h3>MetaWeblog API</h3>
       8 |         <label for="metaweblog_url">URL:</label>
       9 |         <input type="text" id="metaweblog_url" name="metaweblog_url" value="<%- metaweblog.url || '' %>"
      10 |            style="display: block; clear: both; width: 100%; margin-bottom: 20px;" />
      11 |         <label for="metaweblog_username">UserName:</label>
      12 |         <input type="text" id="metaweblog_username" name="metaweblog_username" value="<%- metaweblog.username || '' %>" />
      13 |         <label for="metaweblog_password">Password:</label>
      14 |         <input type="password" id="metaweblog_password" name="metaweblog_password" value="<%- metaweblog.password || '' %>" />
      15 |         <% if(metaweblog.bloginfo) { %>
      16 |         <div style="margin-top:20px;">
      17 |         <label for="">BlogName:</label>
      18 |         <a href="<%- metaweblog.bloginfo.url %>" target="_blank"><%= metaweblog.bloginfo.blogName %></a>
      19 |         </div>
      20 |         <% } 
      21 |         if(metaweblog.error) { %>
      22 |         <div style="margin-top:20px;">
      23 |         <label for="">Error:</label><%= metaweblog.error %>
      24 |         </div>
      25 |         <% } %>
      26 |         <div style="text-align: center; margin-top:20px;"> 
      27 |             <input id="submit" type="submit" style="width: 100px; height: 40px; font-size: 18px;" value="保 存" /> 
      28 |         </div> 
      29 |     </form>
      30 | </div> 
      
      
      --------------------------------------------------------------------------------
      /views/simple/user/login.html:
      --------------------------------------------------------------------------------
      1 | <div class="extras_section">
      2 | <h2>微博登录</h2> 
      3 | <% 
      4 | for(var type in config.weibo_appkeys) { 
      5 |     var name = config.weibo_appkeys[type][2];
      6 | %>
      7 | <a title="<%- name %>" href="/oauth?blogtype=<%- type %>"><%- name %></a>
      8 | <% } %>
      9 | </div> 
      
      
      --------------------------------------------------------------------------------