├── .gitignore ├── README.md ├── __init__.py ├── config.py ├── core.py ├── handlers ├── __init__.py ├── account.py ├── admin.py ├── blog.py └── mixin.py ├── lib ├── __init__.py ├── database.py ├── filters.py ├── helpers.py ├── mail │ ├── __init__.py │ ├── encoding.py │ └── message.py ├── markdown.py ├── pagination.py └── session.py ├── manager.py ├── models.py ├── requirements.txt ├── static ├── default │ ├── images │ │ ├── app-systempref.png │ │ ├── arrow2-left.png │ │ ├── arrow2-right.png │ │ ├── calendar2.png │ │ ├── chat_512.png │ │ ├── comment_white.png │ │ ├── folder-black.png │ │ ├── folder-open.png │ │ ├── folder.png │ │ ├── link.png │ │ ├── tag.png │ │ ├── text-x-generic.png │ │ └── text_list_bullets.png │ └── style.css ├── favicon.ico ├── fluid │ ├── print.css │ └── style.css ├── robots.txt └── sitemap.xsl ├── templates ├── admin │ ├── base.html │ ├── category │ │ └── index.html │ ├── index.html │ ├── link │ │ └── index.html │ ├── login.html │ ├── post │ │ ├── add.html │ │ ├── index.html │ │ └── update.html │ └── user │ │ └── index.html ├── baidu.xml ├── comment_feed.xml ├── default │ ├── archive.html │ ├── base.html │ ├── comment.html │ ├── index.html │ ├── post.html │ └── sidebar.html ├── errors │ ├── 404.html │ └── exception.html ├── feed.xml ├── fluid-blue │ ├── archive.html │ ├── base.html │ ├── comment.html │ ├── index.html │ ├── post.html │ └── sidebar.html ├── macros │ └── pagination.html ├── mail │ ├── new_comment.html │ └── reply_comment.html └── sitemap.xml └── urls.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.so 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | logpress-tornado 2 | ================ 3 | 4 | 使用[tornado][tornado],[jinja2][jinja2],[peewee][peewee]开发的基于markdown写作的博客 [站点][demo] 5 | 6 | 环境配置 7 | 8 | virtualenv pyenv 9 | 10 | source pyenv/bin/activate 11 | 12 | pip install -r requirements.txt 13 | 14 | 1. **创建数据库** 15 | 16 | ``` 17 | python manager.py --cmd=syncdb 18 | ``` 19 | 20 | 默认采用Sqlite3数据库,如需使用mysql 请修改config.py 21 | 22 | DB_ENGINE = 'peewee.SqliteDatabase' 23 | 24 | 修改成 25 | 26 | DB_ENGINE = 'peewee.MySQLDatabase' 27 | 28 | 2. **创建用户** 29 | 30 | ``` 31 | python manager.py --cmd=createuser 32 | ``` 33 | 34 | 3. **运行** 35 | 36 | ``` 37 | python manager.py 38 | ``` 39 | 40 | ***指定端口*** 41 | 42 | ``` 43 | python manager.py --port=8080 44 | ``` 45 | 46 | 4. **配置** 47 | 48 | [tornado]:http://www.tornadoweb.org/ 49 | [jinja2]:http://jinja.pocoo.org/ 50 | [peewee]:http://peewee.readthedocs.org/en/latest/index.html 51 | [demo]:http://blog.szgeist.com 52 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/__init__.py -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding=utf-8 3 | import os 4 | 5 | DEBUG = True 6 | 7 | SITE_NAME = u'Logpress' 8 | SITE_KEYWORDS = """""" 9 | SITE_DESC = """blog powered by tornado,jinja2,peewee""" 10 | DOMAIN = 'http://0.0.0.0:9000' 11 | 12 | THEME_NAME = 'fluid-blue' 13 | 14 | DB_ENGINE = 'peewee.SqliteDatabase' # peewee.SqliteDatabase,peewee.MySQLDatabase 15 | DB_HOST = '0.0.0.0' 16 | DB_USER = 'root' 17 | DB_PASSWD = 'root' 18 | # db file if DB_ENGINE is SqliteDatabase 19 | DB_NAME = os.path.join(os.path.dirname(__file__), 'blog.db') 20 | 21 | ADMIN_EMAIL = '594611460@qq.com' 22 | SMTP_SERVER = 'smtp.qq.com' 23 | SMTP_PORT = 587 24 | SMTP_USER = 'noreply@szgeist.com' 25 | SMTP_PASSWORD = 'xxxxxx' 26 | SMTP_USETLS = True 27 | -------------------------------------------------------------------------------- /core.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf8 3 | 4 | import os 5 | from jinja2 import Environment, FileSystemLoader 6 | from lib.helpers import setting_from_object 7 | from lib.database import Database 8 | from lib.mail import EmailBackend 9 | import config 10 | import redis 11 | 12 | redis_server = redis.StrictRedis() 13 | 14 | settings = setting_from_object(config) 15 | 16 | settings.update({ 17 | 'template_path': os.path.join(os.path.dirname(__file__), 'templates'), 18 | 'static_path': os.path.join(os.path.join(os.path.dirname(__file__), 'static')), 19 | 'cookie_secret': "NjAzZWY2ZTk1YWY5NGE5NmIyYWM0ZDAzOWZjMTg3YTU=|1355811811|3245286b611f74805b195a8fec1beea7234d79d6", 20 | 'login_url': '/account/login', 21 | "xsrf_cookies": True, 22 | 'autoescape': None 23 | }) 24 | 25 | jinja_environment = Environment( 26 | loader=FileSystemLoader(settings['template_path']), 27 | auto_reload=settings['debug'], 28 | autoescape=False) 29 | 30 | db = Database({'db': settings['db_name'], 'engine': settings['db_engine']}) 31 | 32 | smtp_server = EmailBackend( 33 | settings['smtp_server'], settings['smtp_port'], 34 | settings['smtp_user'], settings['smtp_password'], settings['smtp_usetls'], 35 | template_loader=jinja_environment, fail_silently=True) 36 | -------------------------------------------------------------------------------- /handlers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf8 3 | try: 4 | import psyco 5 | psyco.full() 6 | except: 7 | pass 8 | from tornado.web import RequestHandler, HTTPError 9 | from handlers.mixin import FlashMessagesMixin, ExceptionMixin 10 | from lib.session import Session 11 | import os 12 | import urllib 13 | 14 | 15 | class BaseHandler(RequestHandler, FlashMessagesMixin, ExceptionMixin): 16 | 17 | def render_string(self, template_name, **context): 18 | context.update({ 19 | 'xsrf': self.xsrf_form_html, 20 | 'request': self.request, 21 | 'user': self.current_user, 22 | 'static': self.static_url, 23 | 'handler': self, 24 | }) 25 | 26 | return self._jinja_render(path=self.get_template_path(), filename=template_name, 27 | auto_reload=self.settings['debug'], **context) 28 | 29 | def _jinja_render(self, path, filename, **context): 30 | template = self.application.jinja_env.get_template( 31 | filename, parent=path) 32 | self.write(template.render(**context)) 33 | 34 | @property 35 | def is_xhr(self): 36 | return self.request.headers.get('X-Requested-With', '').lower() == 'xmlhttprequest' 37 | 38 | @property 39 | def session(self): 40 | if hasattr(self, '_session'): 41 | return self._session 42 | else: 43 | sessionid = self.get_secure_cookie('sid') 44 | self._session = Session( 45 | self.application.session_store, sessionid, expires_days=1) 46 | if not sessionid: 47 | self.set_secure_cookie('sid', self._session.id, expires_days=1) 48 | return self._session 49 | 50 | def get_current_user(self): 51 | return self.session['user'] if 'user' in self.session else None 52 | 53 | def get_object_or_404(self, model, **kwargs): 54 | try: 55 | return model.get(**kwargs) 56 | except model.DoesNotExist: 57 | raise HTTPError(404) 58 | 59 | @property 60 | def next_url(self): 61 | return self.get_argument("next", None) 62 | 63 | 64 | class AdminBaseHandler(BaseHandler): 65 | 66 | def prepare(self): 67 | if not self.current_user: 68 | if self.request.method == "GET": 69 | url = self.get_login_url() 70 | if "?" not in url: 71 | url += "?" + \ 72 | urllib.urlencode(dict(next=self.request.full_url())) 73 | self.redirect(url) 74 | raise HTTPError(403) 75 | super(AdminBaseHandler, self).prepare() 76 | 77 | 78 | class ErrorHandler(BaseHandler): 79 | 80 | def prepare(self): 81 | super(ErrorHandler, self).prepare() 82 | self.set_status(404) 83 | raise HTTPError(404) 84 | -------------------------------------------------------------------------------- /handlers/account.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf8 3 | try: 4 | import psyco 5 | psyco.full() 6 | except: 7 | pass 8 | from handlers import BaseHandler 9 | from models import User 10 | 11 | 12 | class LoginHandler(BaseHandler): 13 | 14 | def get(self): 15 | self.render('admin/login.html') 16 | 17 | def post(self): 18 | username = self.get_argument('username', None) 19 | password = self.get_argument('password', None) 20 | if username and password: 21 | try: 22 | user = User.get((User.username == username) 23 | | (User.email == username)) 24 | if user.check_password(password): 25 | self.session['user'] = user 26 | self.session.save() 27 | self.redirect('/admin') 28 | return 29 | else: 30 | self.flash('UserName or password invidate!') 31 | except Exception, e: 32 | self.flash('%s not Found!' % username) 33 | self.render('admin/login.html') 34 | return 35 | 36 | 37 | class LogoutHandler(BaseHandler): 38 | 39 | def get(self): 40 | del self.session["user"] 41 | self.sesison.save() 42 | self.redirect(self.get_login_url()) 43 | return 44 | 45 | routes = [ 46 | (r'/account/login', LoginHandler), 47 | (r'/account/logout', LogoutHandler), 48 | ] 49 | -------------------------------------------------------------------------------- /handlers/admin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf8 3 | 4 | try: 5 | import psyco 6 | psyco.full() 7 | except: 8 | pass 9 | from handlers import BaseHandler, AdminBaseHandler 10 | from models import Post, Category, Tag, User, Link 11 | from lib.pagination import Pagination 12 | import tornado 13 | 14 | 15 | class IndexHandler(AdminBaseHandler): 16 | 17 | def get(self): 18 | self.render('admin/index.html') 19 | 20 | 21 | class CategoryHandler(AdminBaseHandler): 22 | 23 | def get(self): 24 | self.render('admin/category/index.html', 25 | categories=Category.select(), nav='category') 26 | 27 | 28 | class CateHandler(AdminBaseHandler): 29 | 30 | def post(self): 31 | name = self.get_argument('name', None) 32 | slug = self.get_argument('slug', None) 33 | q = Category.select().where(Category.name == name) 34 | if q.count() > 0: 35 | self.flash('cateegory exists!') 36 | self.render('admin/category/add.html') 37 | return 38 | else: 39 | Category.create(name=name, slug=slug) 40 | self.redirect('/admin/category') 41 | 42 | 43 | class PostsHandler(AdminBaseHandler): 44 | 45 | def get(self, page=1): 46 | posts = Pagination(Post.select(), int(page), 10) 47 | self.render('admin/post/index.html', pagination=posts, nav='post') 48 | 49 | 50 | class PostHandler(AdminBaseHandler): 51 | 52 | def get(self): 53 | self.render('admin/post/add.html', 54 | category=Category.select(), nav='post') 55 | 56 | def post(self): 57 | title = self.get_argument('title', None) 58 | slug = self.get_argument('slug', '') 59 | category_id = self.get_argument('category', 1) 60 | content = self.get_argument('content', '') 61 | tag = self.get_argument('tag', None) 62 | 63 | category = Category.get(id=int(category_id)) 64 | post = Post.create(title=title, category=category, 65 | slug=slug, content=content, tags=tag) 66 | 67 | if tag: 68 | for tag in post.taglist(): 69 | Tag.create(name=tag, post=post.id) 70 | self.render('admin/post/add.html') 71 | 72 | 73 | class PostUpdateHandler(AdminBaseHandler): 74 | 75 | def get(self, postid): 76 | try: 77 | post = Post.get(id=postid) 78 | except Post.DoesNotExist: 79 | raise tornado.web.HTTPError(404) 80 | 81 | category = Category.select() 82 | self.render('admin/post/update.html', post=post, category=category) 83 | 84 | def post(self, postid): 85 | title = self.get_argument('title', None) 86 | slug = self.get_argument('slug', '') 87 | category_id = self.get_argument('category', 1) 88 | content = self.get_argument('content', '') 89 | tags = self.get_argument('tag', '') 90 | category = Category.get(id=int(category_id)) 91 | 92 | Post.update(title=title, slug=slug, 93 | category=category, content=content, tags=tags).where(Post.id == postid).execute() 94 | 95 | tag_list = set(tags.split(",")) 96 | if tag_list: 97 | for tag in tag_list: 98 | try: 99 | Tag.get(name=tag, post=postid) 100 | except Tag.DoesNotExist: 101 | Tag.create(name=tag, post=postid) 102 | self.redirect('/admin/posts') 103 | return 104 | 105 | 106 | class PostDeleteHandler(AdminBaseHandler): 107 | 108 | def get(self, postid): 109 | Post.delete().where(Post.id == postid).execute() 110 | Tag.delete().where(Tag.post == postid).execute() 111 | self.redirect('/admin/posts') 112 | return 113 | 114 | 115 | class UsersHandler(AdminBaseHandler): 116 | 117 | def get(self): 118 | return self.render('admin/user/index.html', users=User.select(), nav='user') 119 | 120 | 121 | class CommentsHandler(AdminBaseHandler): 122 | 123 | def get(self): 124 | self.render('admin/comment/comment.html') 125 | 126 | 127 | class LinksHandler(AdminBaseHandler): 128 | 129 | def get(self, page=1): 130 | pagination = Pagination(Link.select(), int(page), 1) 131 | self.render('admin/link/index.html', pagination=pagination, nav='link') 132 | 133 | def post(self): 134 | name = self.get_argument('name', None) 135 | url = self.get_argument('url') 136 | if name and url: 137 | Link.create(name=name, url=url) 138 | self.redirect('/admin/links') 139 | 140 | 141 | routes = [ 142 | (r'/admin', IndexHandler), 143 | (r'/admin/posts', PostsHandler), 144 | (r'/admin/posts/(\d+)', PostsHandler), 145 | (r'/admin/post/add', PostHandler), 146 | (r'/admin/post/(\d+)/update', PostUpdateHandler), 147 | (r'/admin/post/(\d+)/delete', PostDeleteHandler), 148 | (r'/admin/category', CategoryHandler), 149 | (r'/admin/category/add', CateHandler), 150 | (r'/admin/users', UsersHandler), 151 | (r'/admin/links', LinksHandler), 152 | (r'/admin/links/(\d+)', LinksHandler), 153 | ] 154 | -------------------------------------------------------------------------------- /handlers/blog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf8 3 | try: 4 | import psyco 5 | psyco.full() 6 | except: 7 | pass 8 | from jinja2 import FileSystemLoader 9 | from handlers import BaseHandler 10 | from models import Post, Category, Tag, Link, Comment 11 | import os 12 | import re 13 | import urllib 14 | from datetime import datetime 15 | from lib.pagination import Pagination 16 | import peewee 17 | from peewee import fn 18 | from peewee import RawQuery 19 | from tornado.web import StaticFileHandler 20 | from core import db 21 | 22 | 23 | class BlogHandler(BaseHandler): 24 | 25 | @property 26 | def redis(self): 27 | return self.application.redis 28 | 29 | def get_recent_posts(self): 30 | return Post.select().paginate(1, 5) 31 | 32 | def get_random_posts(self): 33 | if isinstance(db.database, peewee.SqliteDatabase): 34 | return Post.select().order_by(fn.Random()).limit(5) 35 | else: 36 | return Post.select().order_by(fn.Rand()).limit(5) 37 | 38 | def get_category(self): 39 | return Category.select() 40 | 41 | def get_tagcloud(self): 42 | return Tag.select(Tag, fn.count(Tag.name).alias('count')).group_by(Tag.name) 43 | 44 | def get_links(self): 45 | return Link.select() 46 | 47 | def get_archives(self): 48 | if isinstance(db.database, peewee.SqliteDatabase): 49 | return RawQuery(Post, "select strftime('%Y',created) year,strftime('%m',created) month,count(id) count from posts group by month") 50 | elif isinstance(db.database, peewee.MySQLDatabase): 51 | return RawQuery(Post, "select date_format(created,'%Y') year,date_format(created,'%m') month,count(id) count from posts group by month") 52 | return None 53 | 54 | def get_calendar_widget(self): 55 | pass 56 | 57 | def get_recent_comments(self): 58 | return Comment.select().order_by(Comment.created.desc()).limit(5) 59 | 60 | def render(self, template_name, **context): 61 | tpl = '%s/%s' % (self.settings.get('theme_name'), template_name) 62 | return BaseHandler.render(self, tpl, **context) 63 | 64 | 65 | class IndexHandler(BlogHandler): 66 | 67 | def get(self, page=1): 68 | p = self.get_argument('p', None) 69 | if p: 70 | post = Post.get(id=int(p)) 71 | post.readnum += 1 72 | post.save() 73 | self.render('post.html', post=post) 74 | else: 75 | pagination = Pagination(Post.select(), int(page), per_page=8) 76 | self.render('index.html', pagination=pagination) 77 | 78 | 79 | class PostHandler(BlogHandler): 80 | 81 | def get(self, postid): 82 | post = self.get_object_or_404(Post, id=int(postid)) 83 | post.readnum += 1 84 | post.save() 85 | author = self.get_cookie('comment_author') 86 | email = self.get_cookie('comment_email') 87 | website = self.get_cookie('comment_website') 88 | self.render('post.html', post=post, comment_author=author, 89 | comment_email=email, comment_website=website) 90 | 91 | 92 | class ArchiveHandler(BlogHandler): 93 | 94 | def get(self, year, month, page=1): 95 | format = '%%%s-%s%%' % (year, month) 96 | posts = Post.select().where(Post.created ** format) 97 | pagination = Pagination(posts, int(page), per_page=8) 98 | self.render('archive.html', 99 | year=year, month=month, 100 | pagination=pagination, flag='archives', 101 | obj_url='/archives/%s/%s' % (year, month)) 102 | 103 | 104 | class CategoryHandler(BlogHandler): 105 | 106 | def get(self, name, page=1): 107 | posts = Post.select().join(Category).where(Category.name == name) 108 | pagination = Pagination(posts, int(page), per_page=8) 109 | self.render('archive.html', pagination=pagination, name=name, 110 | obj_url='/category/%s' % (name), flag='category') 111 | 112 | 113 | class TagHandler(BlogHandler): 114 | 115 | def get(self, tagname, page=1): 116 | tags = Tag.select().where(Tag.name == tagname) 117 | postids = [tag.post for tag in tags] 118 | pagination = Pagination(Post.select().where( 119 | Post.id << postids), int(page), per_page=8) 120 | self.render('archive.html', pagination=pagination, 121 | name=tagname, obj_url='/tag/%s' % (tagname), flag='tag') 122 | 123 | 124 | class FeedHandler(BaseHandler): 125 | 126 | def get(self): 127 | posts = Post.select().paginate(1, 10) 128 | self.set_header("Content-Type", "application/atom+xml") 129 | self.render('feed.xml', posts=posts) 130 | 131 | 132 | class CommentFeedHandler(BaseHandler): 133 | 134 | def get(self, postid): 135 | self.set_header("Content-Type", "application/atom+xml") 136 | post = Post.get(id=int(postid)) 137 | self.render('comment_feed.xml', post=post) 138 | 139 | _email_re = re.compile( 140 | r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom 141 | # quoted-string 142 | r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' 143 | r')@(?:[A-Z0-9]+(?:-*[A-Z0-9]+)*\.)+[A-Z]{2,6}$', re.IGNORECASE) 144 | 145 | _url_re = re.compile(r'(http://[^/\\]+)', re.I) 146 | 147 | 148 | class PostCommentHandler(BaseHandler): 149 | 150 | @property 151 | def mail_connection(self): 152 | return self.application.email_backend 153 | 154 | def post(self): 155 | postid = self.get_argument('comment_post_ID') 156 | author = self.get_argument('author', None) 157 | email = self.get_argument('email', None) 158 | url = self.get_argument('url', None) 159 | comment = self.get_argument('comment', None) 160 | parent_id = self.get_argument('comment_parent', None) 161 | 162 | if postid: 163 | post = Post.get(id=int(postid)) 164 | if author and email and comment: 165 | if len(author) > 18: 166 | self.flash('UserName is too long.') 167 | return self.redirect("%s#respond" % (post.url)) 168 | if not _email_re.match(email): 169 | self.flash(u'Email address is invalid.') 170 | return self.redirect("%s#respond" % (post.url)) 171 | if url and not _url_re.match(url): 172 | self.flash(u'website is invalid.') 173 | return self.redirect("%s#respond" % (post.url)) 174 | 175 | comment = Comment.create(post=post, ip=self.request.remote_ip, 176 | author=author, email=email, website=url, 177 | content=comment, parent_id=parent_id) 178 | self.set_cookie('comment_author', author) 179 | self.set_cookie('comment_email', email) 180 | self.set_cookie('comment_website', url) 181 | return self.redirect(comment.url) 182 | else: 183 | self.flash(u"请填写必要信息(姓名和电子邮件和评论内容)") 184 | return self.redirect("%s#respond" % (post.url)) 185 | 186 | 187 | class SitemapHandler(BaseHandler): 188 | 189 | def get(self): 190 | self.set_header("Content-Type", "text/xml") 191 | self.render('sitemap.xml', posts=Post.select(), today=datetime.today()) 192 | 193 | 194 | class BaiduSitemapHandler(BaseHandler): 195 | 196 | def get(self): 197 | self.set_header("Content-Type", "text/xml") 198 | self.render('baidu.xml', posts=Post.select()) 199 | 200 | routes = [ 201 | (r"/", IndexHandler), 202 | (r'/page/(\d+)', IndexHandler), 203 | (r'/post/post-(\d+).html', PostHandler), 204 | (r'/tag/([^/]+)', TagHandler), 205 | (r'/tag/([^/]+)/(\d+)', TagHandler), 206 | (r'/category/([^/]+)', CategoryHandler), 207 | (r'/category/([^/]+)/(\d+)', CategoryHandler), 208 | (r'/feed', FeedHandler), 209 | (r'/sitemap.xml', SitemapHandler), 210 | (r'/baidu.xml', BaiduSitemapHandler), 211 | (r'/archives/(\d+)/(\d+)', ArchiveHandler), 212 | (r'/archive/(\d+)/feed', CommentFeedHandler), 213 | (r'/post/new_comment', PostCommentHandler), 214 | ] 215 | -------------------------------------------------------------------------------- /handlers/mixin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf8 3 | try: 4 | import psyco 5 | psyco.full() 6 | except: 7 | pass 8 | from pygments import highlight 9 | from pygments.lexers import get_lexer_for_filename 10 | from pygments.formatters import HtmlFormatter 11 | import traceback 12 | import sys 13 | import os 14 | import httplib 15 | import tornado 16 | 17 | 18 | class FlashMessagesMixin(object): 19 | 20 | @property 21 | def messages(self): 22 | if not hasattr(self, '_messages'): 23 | messages = self.get_secure_cookie('flash_messages') 24 | self._messages = [] 25 | if messages: 26 | self._messages = tornado.escape.json_decode(messages) 27 | return self._messages 28 | 29 | def flash(self, message, type='error'): 30 | self.messages.append((type, message)) 31 | self.set_secure_cookie( 32 | 'flash_messages', tornado.escape.json_encode(self.messages)) 33 | 34 | def get_flashed_messages(self): 35 | messages = self.messages 36 | self._messages = [] 37 | self.clear_cookie('flash_messages') 38 | return messages 39 | 40 | 41 | class ExceptionMixin(object): 42 | 43 | def get_error_html(self, status_code, **kwargs): 44 | def get_snippet(fp, target_line, num_lines): 45 | if fp.endswith('.html'): 46 | fp = os.path.join(self.get_template_path(), fp) 47 | half_lines = (num_lines / 2) 48 | try: 49 | with open(fp) as f: 50 | all_lines = [line for line in f] 51 | code = ''.join( 52 | all_lines[target_line - half_lines:target_line + half_lines]) 53 | formatter = HtmlFormatter( 54 | linenos=True, linenostart=target_line - half_lines, hl_lines=[half_lines + 1]) 55 | lexer = get_lexer_for_filename(fp) 56 | return highlight(code, lexer, formatter) 57 | except Exception, ex: 58 | return '' 59 | 60 | if self.application.settings.get('debug', False) is False: 61 | full_message = kwargs.get('exception', None) 62 | if not full_message or unicode(full_message) == '': 63 | full_message = 'Sky is falling!' 64 | return "%(code)d: %(message)s

%(code)d: %(message)s

%(full_message)s" % { 65 | "code": status_code, 66 | "message": httplib.responses[status_code], 67 | "full_message": full_message, 68 | } 69 | else: 70 | exception = kwargs.get('exception', None) 71 | return self.render_string('errors/exception.html', sys=sys, traceback=traceback, os=os, 72 | get_snippet=get_snippet, exception=exception, status_code=status_code, kwargs=kwargs) 73 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/lib/__init__.py -------------------------------------------------------------------------------- /lib/database.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | try: 4 | import psyco 5 | psyco.full() 6 | except: 7 | pass 8 | import peewee 9 | import sys 10 | from playhouse.signals import Model as _model 11 | from playhouse.signals import post_save 12 | from lib.helpers import load_class 13 | 14 | 15 | class Database(object): 16 | 17 | def __init__(self, kw): 18 | self.config = kw 19 | self.load_database() 20 | self.Model = self.get_model_class() 21 | 22 | def load_database(self): 23 | try: 24 | self.db = self.config.pop('db') 25 | self.engine = self.config.pop('engine') 26 | except KeyError: 27 | raise Exception( 28 | 'Please specify a "db" and "engine" for your database') 29 | 30 | try: 31 | self.database_class = load_class(self.engine) 32 | assert issubclass(self.database_class, peewee.Database) 33 | except ImportError: 34 | raise Exception('Unable to import: "%s"' % self.engine) 35 | except AttributeError: 36 | raise Exception('Database engine not found: "%s"' % self.engine) 37 | except AssertionError: 38 | raise Exception( 39 | 'Database engine not a subclass of peewee.Database: "%s"' % self.engine) 40 | self.database = self.database_class(self.db, **self.config) 41 | 42 | def get_model_class(self): 43 | class BaseModel(_model): 44 | 45 | class Meta: 46 | database = self.database 47 | return BaseModel 48 | 49 | def connect(self): 50 | self.database.connect() 51 | 52 | def close(self): 53 | try: 54 | self.database.close() 55 | except: 56 | pass 57 | -------------------------------------------------------------------------------- /lib/filters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf8 3 | try: 4 | import psyco 5 | psyco.full() 6 | except: 7 | pass 8 | from datetime import datetime 9 | from lib.markdown import Markdown 10 | 11 | markdowner = Markdown() 12 | 13 | 14 | def datetimeformat(value, format='%Y-%m-%d %H:%M'): 15 | return value.strftime(format) 16 | 17 | 18 | def truncate_words(s, num=50, end_text='...'): 19 | s = unicode(s, 'utf8') 20 | length = int(num) 21 | if len(s) > length: 22 | s = s[:length] 23 | if not s[-1].endswith(end_text): 24 | s = s + end_text 25 | return s 26 | 27 | 28 | def mdconvert(value): 29 | return markdowner.convert(value) 30 | 31 | 32 | def null(value): 33 | return value if value else "" 34 | 35 | 36 | def register_filters(): 37 | filters = {} 38 | filters['truncate_words'] = truncate_words 39 | filters['datetimeformat'] = datetimeformat 40 | filters['markdown'] = mdconvert 41 | filters['null'] = null 42 | return filters 43 | -------------------------------------------------------------------------------- /lib/helpers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf8 3 | try: 4 | import psyco 5 | psyco.full() 6 | except: 7 | pass 8 | from markdown import Markdown 9 | from random import choice 10 | import string 11 | import sys 12 | 13 | 14 | def create_token(length=16): 15 | chars = list(string.letters + string.digits) 16 | salt = ''.join([choice(chars) for i in range(length)]) 17 | return salt 18 | 19 | 20 | def load_class(s): 21 | path, klass = s.rsplit('.', 1) 22 | __import__(path) 23 | mod = sys.modules[path] 24 | return getattr(mod, klass) 25 | 26 | 27 | def setting_from_object(obj): 28 | settings = dict() 29 | for key in dir(obj): 30 | if key.isupper(): 31 | settings[key.lower()] = getattr(obj, key) 32 | return settings 33 | 34 | 35 | class ObjectDict(dict): 36 | 37 | def __getattr__(self, key): 38 | if key in self: 39 | return self[key] 40 | return None 41 | 42 | def __setattr__(self, key, value): 43 | self[key] = value 44 | 45 | 46 | class cached_property(object): 47 | 48 | def __init__(self, func, name=None, doc=None): 49 | self.__name__ = name or func.__name__ 50 | self.__module__ = func.__module__ 51 | self.__doc__ = doc or func.__doc__ 52 | self.func = func 53 | 54 | def __get__(self, obj, type=None): 55 | if obj is None: 56 | return self 57 | value = obj.__dict__.get(self.__name__, None) 58 | if value is None: 59 | value = self.func(obj) 60 | obj.__dict__[self.__name__] = value 61 | return value 62 | 63 | 64 | def find_subclasses(klass, include_self=False): 65 | accum = [] 66 | for child in klass.__subclasses__(): 67 | accum.extend(find_subclasses(child, True)) 68 | if include_self: 69 | accum.append(klass) 70 | return accum 71 | -------------------------------------------------------------------------------- /lib/mail/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf8 3 | try: 4 | import psyco 5 | psyco.full() 6 | except:pass 7 | import smtplib 8 | import socket 9 | import threading 10 | 11 | class EmailBackend(): 12 | def __init__(self, host=None, port=None, username=None, password=None, 13 | use_tls=None, fail_silently=False, template_loader=None,**kwargs): 14 | self.host = host or '127.0.0.1' 15 | self.port = port or 25 16 | self.username = username or None 17 | self.password = password or None 18 | if use_tls is None: 19 | self.use_tls = None 20 | else: 21 | self.use_tls = use_tls 22 | self.connection = None 23 | self.fail_silently = fail_silently 24 | self.template_loader = template_loader 25 | self._lock = threading.RLock() 26 | 27 | def open(self): 28 | if self.connection: 29 | return False 30 | try: 31 | if self.use_tls: 32 | self.connection = smtplib.SMTP_SSL(self.host, self.port) 33 | else: 34 | self.connection = smtplib.SMTP(self.host, self.port) 35 | if self.username and self.password: 36 | self.connection.login(self.username, self.password) 37 | return True 38 | except: 39 | if not self.fail_silently: 40 | raise 41 | 42 | def close(self): 43 | try: 44 | try: 45 | self.connection.quit() 46 | except socket.sslerror: 47 | self.connection.close() 48 | except: 49 | if self.fail_silently: 50 | return 51 | raise 52 | finally: 53 | self.connection = None 54 | 55 | def send_message(self,email_messages,callback=None): 56 | if not email_messages: 57 | return 58 | self._lock.acquire() 59 | try: 60 | new_conn_created = self.open() 61 | if not self.connection: 62 | return 63 | num_sent = 0 64 | for message in email_messages: 65 | sent = self._send(message) 66 | if sent: 67 | num_sent += 1 68 | if new_conn_created: 69 | self.close() 70 | finally: 71 | self._lock.release() 72 | return num_sent 73 | 74 | def _sanitize(self, email): 75 | name, domain = email.split('@', 1) 76 | email = '@'.join([name, domain.encode('idna')]) 77 | return email 78 | 79 | def _send(self, email_message): 80 | if not email_message.recipients(): 81 | return False 82 | from_email = self._sanitize(email_message.from_email) 83 | recipients = map(self._sanitize, email_message.recipients()) 84 | try: 85 | self.connection.sendmail(from_email, recipients,email_message.message().as_string()) 86 | except: 87 | if not self.fail_silently: 88 | raise 89 | return False 90 | return True 91 | -------------------------------------------------------------------------------- /lib/mail/encoding.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf8 3 | try: 4 | import psyco 5 | psyco.full() 6 | except:pass 7 | import types 8 | import urllib 9 | import locale 10 | import datetime 11 | import codecs 12 | from decimal import Decimal 13 | 14 | class Promise(object): 15 | pass 16 | 17 | class TornadomainUnicodeDecodeError(UnicodeDecodeError): 18 | def __init__(self, obj, *args): 19 | self.obj = obj 20 | UnicodeDecodeError.__init__(self, *args) 21 | 22 | def __str__(self): 23 | original = UnicodeDecodeError.__str__(self) 24 | return '%s. You passed in %r (%s)' % (original, self.obj, 25 | type(self.obj)) 26 | 27 | class StrAndUnicode(object): 28 | """ 29 | A class whose __str__ returns its __unicode__ as a UTF-8 bytestring. 30 | 31 | Useful as a mix-in. 32 | """ 33 | def __str__(self): 34 | return self.__unicode__().encode('utf-8') 35 | 36 | def smart_unicode(s, encoding='utf-8', strings_only=False, errors='strict'): 37 | """ 38 | Returns a unicode object representing 's'. Treats bytestrings using the 39 | 'encoding' codec. 40 | 41 | If strings_only is True, don't convert (some) non-string-like objects. 42 | """ 43 | if isinstance(s, Promise): 44 | # The input is the result of a gettext_lazy() call. 45 | return s 46 | return force_unicode(s, encoding, strings_only, errors) 47 | 48 | def is_protected_type(obj): 49 | """Determine if the object instance is of a protected type. 50 | 51 | Objects of protected types are preserved as-is when passed to 52 | force_unicode(strings_only=True). 53 | """ 54 | return isinstance(obj, ( 55 | types.NoneType, 56 | int, long, 57 | datetime.datetime, datetime.date, datetime.time, 58 | float, Decimal) 59 | ) 60 | 61 | def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'): 62 | """ 63 | Similar to smart_unicode, except that lazy instances are resolved to 64 | strings, rather than kept as lazy objects. 65 | 66 | If strings_only is True, don't convert (some) non-string-like objects. 67 | """ 68 | # Handle the common case first, saves 30-40% in performance when s 69 | # is an instance of unicode. This function gets called often in that 70 | # setting. 71 | if isinstance(s, unicode): 72 | return s 73 | if strings_only and is_protected_type(s): 74 | return s 75 | try: 76 | if not isinstance(s, basestring,): 77 | if hasattr(s, '__unicode__'): 78 | s = unicode(s) 79 | else: 80 | try: 81 | s = unicode(str(s), encoding, errors) 82 | except UnicodeEncodeError: 83 | if not isinstance(s, Exception): 84 | raise 85 | # If we get to here, the caller has passed in an Exception 86 | # subclass populated with non-ASCII data without special 87 | # handling to display as a string. We need to handle this 88 | # without raising a further exception. We do an 89 | # approximation to what the Exception's standard str() 90 | # output should be. 91 | s = ' '.join([force_unicode(arg, encoding, strings_only, 92 | errors) for arg in s]) 93 | elif not isinstance(s, unicode): 94 | # Note: We use .decode() here, instead of unicode(s, encoding, 95 | # errors), so that if s is a SafeString, it ends up being a 96 | # SafeUnicode at the end. 97 | s = s.decode(encoding, errors) 98 | except UnicodeDecodeError, e: 99 | if not isinstance(s, Exception): 100 | raise TornadomainUnicodeDecodeError(s, *e.args) 101 | else: 102 | # If we get to here, the caller has passed in an Exception 103 | # subclass populated with non-ASCII bytestring data without a 104 | # working unicode method. Try to handle this without raising a 105 | # further exception by individually forcing the exception args 106 | # to unicode. 107 | s = ' '.join([force_unicode(arg, encoding, strings_only, 108 | errors) for arg in s]) 109 | return s 110 | 111 | def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'): 112 | """ 113 | Returns a bytestring version of 's', encoded as specified in 'encoding'. 114 | 115 | If strings_only is True, don't convert (some) non-string-like objects. 116 | """ 117 | if strings_only and isinstance(s, (types.NoneType, int)): 118 | return s 119 | if isinstance(s, Promise): 120 | return unicode(s).encode(encoding, errors) 121 | elif not isinstance(s, basestring): 122 | try: 123 | return str(s) 124 | except UnicodeEncodeError: 125 | if isinstance(s, Exception): 126 | # An Exception subclass containing non-ASCII data that doesn't 127 | # know how to print itself properly. We shouldn't raise a 128 | # further exception. 129 | return ' '.join([smart_str(arg, encoding, strings_only, 130 | errors) for arg in s]) 131 | return unicode(s).encode(encoding, errors) 132 | elif isinstance(s, unicode): 133 | return s.encode(encoding, errors) 134 | elif s and encoding != 'utf-8': 135 | return s.decode('utf-8', errors).encode(encoding, errors) 136 | else: 137 | return s 138 | 139 | def iri_to_uri(iri): 140 | """ 141 | Convert an Internationalized Resource Identifier (IRI) portion to a URI 142 | portion that is suitable for inclusion in a URL. 143 | 144 | This is the algorithm from section 3.1 of RFC 3987. However, since we are 145 | assuming input is either UTF-8 or unicode already, we can simplify things a 146 | little from the full method. 147 | 148 | Returns an ASCII string containing the encoded result. 149 | """ 150 | # The list of safe characters here is constructed from the "reserved" and 151 | # "unreserved" characters specified in sections 2.2 and 2.3 of RFC 3986: 152 | # reserved = gen-delims / sub-delims 153 | # gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" 154 | # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 155 | # / "*" / "+" / "," / ";" / "=" 156 | # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 157 | # Of the unreserved characters, urllib.quote already considers all but 158 | # the ~ safe. 159 | # The % character is also added to the list of safe characters here, as the 160 | # end of section 3.1 of RFC 3987 specifically mentions that % must not be 161 | # converted. 162 | if iri is None: 163 | return iri 164 | return urllib.quote(smart_str(iri), safe="/#%[]=:;$&()+,!?*@'~") 165 | 166 | def filepath_to_uri(path): 167 | """Convert an file system path to a URI portion that is suitable for 168 | inclusion in a URL. 169 | 170 | We are assuming input is either UTF-8 or unicode already. 171 | 172 | This method will encode certain chars that would normally be recognized as 173 | special chars for URIs. Note that this method does not encode the ' 174 | character, as it is a valid character within URIs. See 175 | encodeURIComponent() JavaScript function for more details. 176 | 177 | Returns an ASCII string containing the encoded result. 178 | """ 179 | if path is None: 180 | return path 181 | # I know about `os.sep` and `os.altsep` but I want to leave 182 | # some flexibility for hardcoding separators. 183 | return urllib.quote(smart_str(path).replace("\\", "/"), safe="/~!*()'") 184 | 185 | # The encoding of the default system locale but falls back to the 186 | # given fallback encoding if the encoding is unsupported by python or could 187 | # not be determined. See tickets #10335 and #5846 188 | try: 189 | DEFAULT_LOCALE_ENCODING = locale.getdefaultlocale()[1] or 'ascii' 190 | codecs.lookup(DEFAULT_LOCALE_ENCODING) 191 | except: 192 | DEFAULT_LOCALE_ENCODING = 'ascii' -------------------------------------------------------------------------------- /lib/mail/message.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #coding=utf8 3 | try: 4 | import psyco 5 | psyco.full() 6 | except:pass 7 | import time 8 | import os 9 | import socket 10 | import random 11 | from email.MIMEText import MIMEText 12 | from email.Header import Header 13 | from email import Charset, Encoders 14 | from email.Utils import formatdate, getaddresses, formataddr, parseaddr 15 | from .encoding import smart_str, force_unicode 16 | from tornado import gen 17 | 18 | DEFAULT_CHARSET = 'utf8' 19 | DEFAULT_FROM_EMAIL = 'noreply@localhost' 20 | 21 | class BadHeaderError(ValueError): 22 | pass 23 | 24 | class CachedDnsName(object): 25 | def __str__(self): 26 | return self.get_fqdn() 27 | 28 | def get_fqdn(self): 29 | if not hasattr(self, '_fqdn'): 30 | self._fqdn = socket.getfqdn() 31 | return self._fqdn 32 | 33 | DNS_NAME = CachedDnsName() 34 | 35 | def forbid_multi_line_headers(name, val, encoding): 36 | encoding = encoding or DEFAULT_CHARSET 37 | val = force_unicode(val) 38 | if '\n' in val or '\r' in val: 39 | raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name)) 40 | try: 41 | val = val.encode('ascii') 42 | except UnicodeEncodeError: 43 | if name.lower() in ('to', 'from', 'cc'): 44 | val = ', '.join(sanitize_address(addr, encoding) for addr in getaddresses((val,))) 45 | else: 46 | val = str(Header(val, encoding)) 47 | else: 48 | if name.lower() == 'subject': 49 | val = Header(val) 50 | return name, val 51 | 52 | 53 | def sanitize_address(addr, encoding): 54 | if isinstance(addr, basestring): 55 | addr = parseaddr(force_unicode(addr)) 56 | nm, addr = addr 57 | nm = str(Header(nm, encoding)) 58 | try: 59 | addr = addr.encode('ascii') 60 | except UnicodeEncodeError: # IDN 61 | if u'@' in addr: 62 | localpart, domain = addr.split(u'@', 1) 63 | localpart = str(Header(localpart, encoding)) 64 | domain = domain.encode('idna') 65 | addr = '@'.join([localpart, domain]) 66 | else: 67 | addr = str(Header(addr, encoding)) 68 | return formataddr((nm, addr)) 69 | 70 | 71 | class SafeMIMEText(MIMEText): 72 | def __init__(self, text, subtype, charset): 73 | self.encoding = charset 74 | MIMEText.__init__(self, text, subtype, charset) 75 | 76 | def __setitem__(self, name, val): 77 | name, val = forbid_multi_line_headers(name, val, self.encoding) 78 | MIMEText.__setitem__(self, name, val) 79 | 80 | class EmailMessage(object): 81 | content_subtype = 'plain' 82 | mixed_subtype = 'mixed' 83 | encoding = None 84 | def __init__(self, subject, body='', from_email=None, to=None, cc=None, 85 | connection=None): 86 | 87 | if to: 88 | assert not isinstance(to, basestring), '"to" argument must be a list or tuple' 89 | self.to = list(to) 90 | else: 91 | self.to = [] 92 | if cc: 93 | assert not isinstance(cc, basestring), '"cc" argument must be a list or tuple' 94 | self.cc = list(cc) 95 | else: 96 | self.cc = [] 97 | self.from_email = from_email or DEFAULT_FROM_EMAIL 98 | self.subject = subject 99 | self.body = body 100 | self.connection = connection 101 | 102 | def message(self): 103 | encoding = self.encoding or DEFAULT_CHARSET 104 | msg = SafeMIMEText(smart_str(self.body, encoding), 105 | self.content_subtype, encoding) 106 | msg['Subject'] = self.subject 107 | msg['From'] = self.from_email 108 | msg['To'] = ', '.join(self.to) 109 | if self.cc: 110 | msg['Cc'] = ', '.join(self.cc) 111 | return msg 112 | 113 | def recipients(self): 114 | return self.to + self.cc 115 | 116 | @gen.engine 117 | def send(self): 118 | yield gen.Task(self.connection.send_message, [self]) 119 | 120 | 121 | class TemplateEmailMessage(EmailMessage): 122 | content_subtype = "html" 123 | def __init__(self, subject, template, from_email=None, to=None, cc=None, 124 | connection=None, params={}): 125 | if not connection.template_loader: 126 | raise Exception("Must to set a template_loader to EmailBackend") 127 | 128 | body = connection.template_loader.get_template(template).render(**params) 129 | 130 | super(TemplateEmailMessage, self).__init__(subject, body, from_email, to, 131 | cc, connection) 132 | 133 | -------------------------------------------------------------------------------- /lib/markdown.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2007-2008 ActiveState Corp. 3 | # License: MIT (http://www.opensource.org/licenses/mit-license.php) 4 | 5 | r"""A fast and complete Python implementation of Markdown. 6 | 7 | [from http://daringfireball.net/projects/markdown/] 8 | > Markdown is a text-to-HTML filter; it translates an easy-to-read / 9 | > easy-to-write structured text format into HTML. Markdown's text 10 | > format is most similar to that of plain text email, and supports 11 | > features such as headers, *emphasis*, code blocks, blockquotes, and 12 | > links. 13 | > 14 | > Markdown's syntax is designed not as a generic markup language, but 15 | > specifically to serve as a front-end to (X)HTML. You can use span-level 16 | > HTML tags anywhere in a Markdown document, and you can use block level 17 | > HTML tags (like
and as well). 18 | 19 | Module usage: 20 | 21 | >>> import markdown2 22 | >>> markdown2.markdown("*boo!*") # or use `html = markdown_path(PATH)` 23 | u'

boo!

\n' 24 | 25 | >>> markdowner = Markdown() 26 | >>> markdowner.convert("*boo!*") 27 | u'

boo!

\n' 28 | >>> markdowner.convert("**boom!**") 29 | u'

boom!

\n' 30 | 31 | This implementation of Markdown implements the full "core" syntax plus a 32 | number of extras (e.g., code syntax coloring, footnotes) as described on 33 | . 34 | """ 35 | 36 | cmdln_desc = """A fast and complete Python implementation of Markdown, a 37 | text-to-HTML conversion tool for web writers. 38 | """ 39 | 40 | # Dev Notes: 41 | # - There is already a Python markdown processor 42 | # (http://www.freewisdom.org/projects/python-markdown/). 43 | # - Python's regex syntax doesn't have '\z', so I'm using '\Z'. I'm 44 | # not yet sure if there implications with this. Compare 'pydoc sre' 45 | # and 'perldoc perlre'. 46 | 47 | __version_info__ = (1, 0, 1, 14) # first three nums match Markdown.pl 48 | __version__ = '1.0.1.14' 49 | __author__ = "Trent Mick" 50 | 51 | import os 52 | import sys 53 | from pprint import pprint 54 | import re 55 | import logging 56 | try: 57 | from hashlib import md5 58 | except ImportError: 59 | from md5 import md5 60 | import optparse 61 | from random import random 62 | import codecs 63 | 64 | 65 | #---- Python version compat 66 | 67 | if sys.version_info[:2] < (2, 4): 68 | from sets import Set as set 69 | 70 | def reversed(sequence): 71 | for i in sequence[::-1]: 72 | yield i 73 | 74 | def _unicode_decode(s, encoding, errors='xmlcharrefreplace'): 75 | return unicode(s, encoding, errors) 76 | else: 77 | def _unicode_decode(s, encoding, errors='strict'): 78 | return s.decode(encoding, errors) 79 | 80 | 81 | #---- globals 82 | 83 | DEBUG = False 84 | log = logging.getLogger("markdown") 85 | 86 | DEFAULT_TAB_WIDTH = 4 87 | 88 | # Table of hash values for escaped characters: 89 | 90 | 91 | def _escape_hash(s): 92 | # Lame attempt to avoid possible collision with someone actually 93 | # using the MD5 hexdigest of one of these chars in there text. 94 | # Other ideas: random.random(), uuid.uuid() 95 | # return md5(s).hexdigest() # Markdown.pl effectively does this. 96 | return 'md5-' + md5(s).hexdigest() 97 | g_escape_table = dict([(ch, _escape_hash(ch)) 98 | for ch in '\\`*_{}[]()>#+-.!']) 99 | 100 | 101 | #---- exceptions 102 | 103 | class MarkdownError(Exception): 104 | pass 105 | 106 | 107 | #---- public api 108 | 109 | def markdown_path(path, encoding="utf-8", 110 | html4tags=False, tab_width=DEFAULT_TAB_WIDTH, 111 | safe_mode=None, extras=None, link_patterns=None, 112 | use_file_vars=False): 113 | text = codecs.open(path, 'r', encoding).read() 114 | return Markdown(html4tags=html4tags, tab_width=tab_width, 115 | safe_mode=safe_mode, extras=extras, 116 | link_patterns=link_patterns, 117 | use_file_vars=use_file_vars).convert(text) 118 | 119 | 120 | def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH, 121 | safe_mode=None, extras=None, link_patterns=None, 122 | use_file_vars=False): 123 | return Markdown(html4tags=html4tags, tab_width=tab_width, 124 | safe_mode=safe_mode, extras=extras, 125 | link_patterns=link_patterns, 126 | use_file_vars=use_file_vars).convert(text) 127 | 128 | 129 | class Markdown(object): 130 | # The dict of "extras" to enable in processing -- a mapping of 131 | # extra name to argument for the extra. Most extras do not have an 132 | # argument, in which case the value is None. 133 | # 134 | # This can be set via (a) subclassing and (b) the constructor 135 | # "extras" argument. 136 | extras = None 137 | 138 | urls = None 139 | titles = None 140 | html_blocks = None 141 | html_spans = None 142 | html_removed_text = "[HTML_REMOVED]" # for compat with markdown.py 143 | 144 | # Used to track when we're inside an ordered or unordered list 145 | # (see _ProcessListItems() for details): 146 | list_level = 0 147 | 148 | _ws_only_line_re = re.compile(r"^[ \t]+$", re.M) 149 | 150 | def __init__(self, html4tags=False, tab_width=4, safe_mode=None, 151 | extras=None, link_patterns=None, use_file_vars=False): 152 | if html4tags: 153 | self.empty_element_suffix = ">" 154 | else: 155 | self.empty_element_suffix = " />" 156 | self.tab_width = tab_width 157 | 158 | # For compatibility with earlier markdown2.py and with 159 | # markdown.py's safe_mode being a boolean, 160 | # safe_mode == True -> "replace" 161 | if safe_mode is True: 162 | self.safe_mode = "replace" 163 | else: 164 | self.safe_mode = safe_mode 165 | 166 | if self.extras is None: 167 | self.extras = {} 168 | elif not isinstance(self.extras, dict): 169 | self.extras = dict([(e, None) for e in self.extras]) 170 | if extras: 171 | if not isinstance(extras, dict): 172 | extras = dict([(e, None) for e in extras]) 173 | self.extras.update(extras) 174 | assert isinstance(self.extras, dict) 175 | self._instance_extras = self.extras.copy() 176 | self.link_patterns = link_patterns 177 | self.use_file_vars = use_file_vars 178 | self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M) 179 | 180 | def reset(self): 181 | self.urls = {} 182 | self.titles = {} 183 | self.html_blocks = {} 184 | self.html_spans = {} 185 | self.list_level = 0 186 | self.extras = self._instance_extras.copy() 187 | if "footnotes" in self.extras: 188 | self.footnotes = {} 189 | self.footnote_ids = [] 190 | 191 | def convert(self, text): 192 | """Convert the given text.""" 193 | # Main function. The order in which other subs are called here is 194 | # essential. Link and image substitutions need to happen before 195 | # _EscapeSpecialChars(), so that any *'s or _'s in the 196 | # and tags get encoded. 197 | 198 | # Clear the global hashes. If we don't clear these, you get conflicts 199 | # from other articles when generating a page which contains more than 200 | # one article (e.g. an index page that shows the N most recent 201 | # articles): 202 | self.reset() 203 | 204 | if not isinstance(text, unicode): 205 | # TODO: perhaps shouldn't presume UTF-8 for string input? 206 | text = unicode(text, 'utf-8') 207 | 208 | if self.use_file_vars: 209 | # Look for emacs-style file variable hints. 210 | emacs_vars = self._get_emacs_vars(text) 211 | if "markdown-extras" in emacs_vars: 212 | splitter = re.compile("[ ,]+") 213 | for e in splitter.split(emacs_vars["markdown-extras"]): 214 | if '=' in e: 215 | ename, earg = e.split('=', 1) 216 | try: 217 | earg = int(earg) 218 | except ValueError: 219 | pass 220 | else: 221 | ename, earg = e, None 222 | self.extras[ename] = earg 223 | 224 | # Standardize line endings: 225 | text = re.sub("\r\n|\r", "\n", text) 226 | 227 | # Make sure $text ends with a couple of newlines: 228 | text += "\n\n" 229 | 230 | # Convert all tabs to spaces. 231 | text = self._detab(text) 232 | 233 | # Strip any lines consisting only of spaces and tabs. 234 | # This makes subsequent regexen easier to write, because we can 235 | # match consecutive blank lines with /\n+/ instead of something 236 | # contorted like /[ \t]*\n+/ . 237 | text = self._ws_only_line_re.sub("", text) 238 | 239 | if self.safe_mode: 240 | text = self._hash_html_spans(text) 241 | 242 | # Turn block-level HTML blocks into hash entries 243 | text = self._hash_html_blocks(text, raw=True) 244 | 245 | # Strip link definitions, store in hashes. 246 | if "footnotes" in self.extras: 247 | # Must do footnotes first because an unlucky footnote defn 248 | # looks like a link defn: 249 | # [^4]: this "looks like a link defn" 250 | text = self._strip_footnote_definitions(text) 251 | text = self._strip_link_definitions(text) 252 | 253 | text = self._run_block_gamut(text) 254 | 255 | if "footnotes" in self.extras: 256 | text = self._add_footnotes(text) 257 | 258 | text = self._unescape_special_chars(text) 259 | 260 | if self.safe_mode: 261 | text = self._unhash_html_spans(text) 262 | 263 | text += "\n" 264 | return text 265 | 266 | _emacs_oneliner_vars_pat = re.compile( 267 | r"-\*-\s*([^\r\n]*?)\s*-\*-", re.UNICODE) 268 | # This regular expression is intended to match blocks like this: 269 | # PREFIX Local Variables: SUFFIX 270 | # PREFIX mode: Tcl SUFFIX 271 | # PREFIX End: SUFFIX 272 | # Some notes: 273 | # - "[ \t]" is used instead of "\s" to specifically exclude newlines 274 | # - "(\r\n|\n|\r)" is used instead of "$" because the sre engine does 275 | # not like anything other than Unix-style line terminators. 276 | _emacs_local_vars_pat = re.compile(r"""^ 277 | (?P(?:[^\r\n|\n|\r])*?) 278 | [\ \t]*Local\ Variables:[\ \t]* 279 | (?P.*?)(?:\r\n|\n|\r) 280 | (?P.*?\1End:) 281 | """, re.IGNORECASE | re.MULTILINE | re.DOTALL | re.VERBOSE) 282 | 283 | def _get_emacs_vars(self, text): 284 | """Return a dictionary of emacs-style local variables. 285 | 286 | Parsing is done loosely according to this spec (and according to 287 | some in-practice deviations from this): 288 | http://www.gnu.org/software/emacs/manual/html_node/emacs/Specifying-File-Variables.html#Specifying-File-Variables 289 | """ 290 | emacs_vars = {} 291 | SIZE = pow(2, 13) # 8kB 292 | 293 | # Search near the start for a '-*-'-style one-liner of variables. 294 | head = text[:SIZE] 295 | if "-*-" in head: 296 | match = self._emacs_oneliner_vars_pat.search(head) 297 | if match: 298 | emacs_vars_str = match.group(1) 299 | assert '\n' not in emacs_vars_str 300 | emacs_var_strs = [s.strip() for s in emacs_vars_str.split(';') 301 | if s.strip()] 302 | if len(emacs_var_strs) == 1 and ':' not in emacs_var_strs[0]: 303 | # While not in the spec, this form is allowed by emacs: 304 | # -*- Tcl -*- 305 | # where the implied "variable" is "mode". This form 306 | # is only allowed if there are no other variables. 307 | emacs_vars["mode"] = emacs_var_strs[0].strip() 308 | else: 309 | for emacs_var_str in emacs_var_strs: 310 | try: 311 | variable, value = emacs_var_str.strip().split(':', 1) 312 | except ValueError: 313 | log.debug("emacs variables error: malformed -*- " 314 | "line: %r", emacs_var_str) 315 | continue 316 | # Lowercase the variable name because Emacs allows "Mode" 317 | # or "mode" or "MoDe", etc. 318 | emacs_vars[variable.lower()] = value.strip() 319 | 320 | tail = text[-SIZE:] 321 | if "Local Variables" in tail: 322 | match = self._emacs_local_vars_pat.search(tail) 323 | if match: 324 | prefix = match.group("prefix") 325 | suffix = match.group("suffix") 326 | lines = match.group("content").splitlines(0) 327 | # print "prefix=%r, suffix=%r, content=%r, lines: %s"\ 328 | # % (prefix, suffix, match.group("content"), lines) 329 | 330 | # Validate the Local Variables block: proper prefix and suffix 331 | # usage. 332 | for i, line in enumerate(lines): 333 | if not line.startswith(prefix): 334 | log.debug("emacs variables error: line '%s' " 335 | "does not use proper prefix '%s'" 336 | % (line, prefix)) 337 | return {} 338 | # Don't validate suffix on last line. Emacs doesn't care, 339 | # neither should we. 340 | if i != len(lines) - 1 and not line.endswith(suffix): 341 | log.debug("emacs variables error: line '%s' " 342 | "does not use proper suffix '%s'" 343 | % (line, suffix)) 344 | return {} 345 | 346 | # Parse out one emacs var per line. 347 | continued_for = None 348 | # no var on the last line ("PREFIX End:") 349 | for line in lines[:-1]: 350 | if prefix: 351 | line = line[len(prefix):] # strip prefix 352 | if suffix: 353 | line = line[:-len(suffix)] # strip suffix 354 | line = line.strip() 355 | if continued_for: 356 | variable = continued_for 357 | if line.endswith('\\'): 358 | line = line[:-1].rstrip() 359 | else: 360 | continued_for = None 361 | emacs_vars[variable] += ' ' + line 362 | else: 363 | try: 364 | variable, value = line.split(':', 1) 365 | except ValueError: 366 | log.debug("local variables error: missing colon " 367 | "in local variables entry: '%s'" % line) 368 | continue 369 | # Do NOT lowercase the variable name, because Emacs only 370 | # allows "mode" (and not "Mode", "MoDe", etc.) in this 371 | # block. 372 | value = value.strip() 373 | if value.endswith('\\'): 374 | value = value[:-1].rstrip() 375 | continued_for = variable 376 | else: 377 | continued_for = None 378 | emacs_vars[variable] = value 379 | 380 | # Unquote values. 381 | for var, val in emacs_vars.items(): 382 | if len(val) > 1 and (val.startswith('"') and val.endswith('"') 383 | or val.startswith('"') and val.endswith('"')): 384 | emacs_vars[var] = val[1:-1] 385 | 386 | return emacs_vars 387 | 388 | # Cribbed from a post by Bart Lateur: 389 | # 390 | _detab_re = re.compile(r'(.*?)\t', re.M) 391 | 392 | def _detab_sub(self, match): 393 | g1 = match.group(1) 394 | return g1 + (' ' * (self.tab_width - len(g1) % self.tab_width)) 395 | 396 | def _detab(self, text): 397 | r"""Remove (leading?) tabs from a file. 398 | 399 | >>> m = Markdown() 400 | >>> m._detab("\tfoo") 401 | ' foo' 402 | >>> m._detab(" \tfoo") 403 | ' foo' 404 | >>> m._detab("\t foo") 405 | ' foo' 406 | >>> m._detab(" foo") 407 | ' foo' 408 | >>> m._detab(" foo\n\tbar\tblam") 409 | ' foo\n bar blam' 410 | """ 411 | if '\t' not in text: 412 | return text 413 | return self._detab_re.subn(self._detab_sub, text)[0] 414 | 415 | _block_tags_a = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del' 416 | _strict_tag_block_re = re.compile(r""" 417 | ( # save in \1 418 | ^ # start of line (with re.M) 419 | <(%s) # start tag = \2 420 | \b # word break 421 | (.*\n)*? # any number of lines, minimally matching 422 | # the matching end tag 423 | [ \t]* # trailing spaces/tabs 424 | (?=\n+|\Z) # followed by a newline or end of document 425 | ) 426 | """ % _block_tags_a, 427 | re.X | re.M) 428 | 429 | _block_tags_b = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math' 430 | _liberal_tag_block_re = re.compile(r""" 431 | ( # save in \1 432 | ^ # start of line (with re.M) 433 | <(%s) # start tag = \2 434 | \b # word break 435 | (.*\n)*? # any number of lines, minimally matching 436 | .* # the matching end tag 437 | [ \t]* # trailing spaces/tabs 438 | (?=\n+|\Z) # followed by a newline or end of document 439 | ) 440 | """ % _block_tags_b, 441 | re.X | re.M) 442 | 443 | def _hash_html_block_sub(self, match, raw=False): 444 | html = match.group(1) 445 | if raw and self.safe_mode: 446 | html = self._sanitize_html(html) 447 | key = _hash_text(html) 448 | self.html_blocks[key] = html 449 | return "\n\n" + key + "\n\n" 450 | 451 | def _hash_html_blocks(self, text, raw=False): 452 | """Hashify HTML blocks 453 | 454 | We only want to do this for block-level HTML tags, such as headers, 455 | lists, and tables. That's because we still want to wrap

s around 456 | "paragraphs" that are wrapped in non-block-level tags, such as anchors, 457 | phrase emphasis, and spans. The list of tags we're looking for is 458 | hard-coded. 459 | 460 | @param raw {boolean} indicates if these are raw HTML blocks in 461 | the original source. It makes a difference in "safe" mode. 462 | """ 463 | if '<' not in text: 464 | return text 465 | 466 | # Pass `raw` value into our calls to self._hash_html_block_sub. 467 | hash_html_block_sub = _curry(self._hash_html_block_sub, raw=raw) 468 | 469 | # First, look for nested blocks, e.g.: 470 | #

471 | #
472 | # tags for inner block must be indented. 473 | #
474 | #
475 | # 476 | # The outermost tags must start at the left margin for this to match, and 477 | # the inner nested divs must be indented. 478 | # We need to do this before the next, more liberal match, because the next 479 | # match will start at the first `
` and stop at the first `
`. 480 | text = self._strict_tag_block_re.sub(hash_html_block_sub, text) 481 | 482 | # Now match more liberally, simply from `\n` to `\n` 483 | text = self._liberal_tag_block_re.sub(hash_html_block_sub, text) 484 | 485 | # Special case just for
. It was easier to make a special 486 | # case than to make the other regex more complicated. 487 | if "", start_idx) + 3 502 | except ValueError, ex: 503 | break 504 | 505 | # Start position for next comment block search. 506 | start = end_idx 507 | 508 | # Validate whitespace before comment. 509 | if start_idx: 510 | # - Up to `tab_width - 1` spaces before start_idx. 511 | for i in range(self.tab_width - 1): 512 | if text[start_idx - 1] != ' ': 513 | break 514 | start_idx -= 1 515 | if start_idx == 0: 516 | break 517 | # - Must be preceded by 2 newlines or hit the start of 518 | # the document. 519 | if start_idx == 0: 520 | pass 521 | elif start_idx == 1 and text[0] == '\n': 522 | start_idx = 0 # to match minute detail of Markdown.pl regex 523 | elif text[start_idx - 2:start_idx] == '\n\n': 524 | pass 525 | else: 526 | break 527 | 528 | # Validate whitespace after comment. 529 | # - Any number of spaces and tabs. 530 | while end_idx < len(text): 531 | if text[end_idx] not in ' \t': 532 | break 533 | end_idx += 1 534 | # - Must be following by 2 newlines or hit end of text. 535 | if text[end_idx:end_idx + 2] not in ('', '\n', '\n\n'): 536 | continue 537 | 538 | # Escape and hash (must match `_hash_html_block_sub`). 539 | html = text[start_idx:end_idx] 540 | if raw and self.safe_mode: 541 | html = self._sanitize_html(html) 542 | key = _hash_text(html) 543 | self.html_blocks[key] = html 544 | text = text[:start_idx] + "\n\n" + \ 545 | key + "\n\n" + text[end_idx:] 546 | 547 | if "xml" in self.extras: 548 | # Treat XML processing instructions and namespaced one-liner 549 | # tags as if they were block HTML tags. E.g., if standalone 550 | # (i.e. are their own paragraph), the following do not get 551 | # wrapped in a

tag: 552 | # 553 | # 554 | # 555 | _xml_oneliner_re = _xml_oneliner_re_from_tab_width(self.tab_width) 556 | text = _xml_oneliner_re.sub(hash_html_block_sub, text) 557 | 558 | return text 559 | 560 | def _strip_link_definitions(self, text): 561 | # Strips link definitions from text, stores the URLs and titles in 562 | # hash references. 563 | less_than_tab = self.tab_width - 1 564 | 565 | # Link defs are in the form: 566 | # [id]: url "optional title" 567 | _link_def_re = re.compile(r""" 568 | ^[ ]{0,%d}\[(.+)\]: # id = \1 569 | [ \t]* 570 | \n? # maybe *one* newline 571 | [ \t]* 572 | ? # url = \2 573 | [ \t]* 574 | (?: 575 | \n? # maybe one newline 576 | [ \t]* 577 | (?<=\s) # lookbehind for whitespace 578 | ['"(] 579 | ([^\n]*) # title = \3 580 | ['")] 581 | [ \t]* 582 | )? # title is optional 583 | (?:\n+|\Z) 584 | """ % less_than_tab, re.X | re.M | re.U) 585 | return _link_def_re.sub(self._extract_link_def_sub, text) 586 | 587 | def _extract_link_def_sub(self, match): 588 | id, url, title = match.groups() 589 | key = id.lower() # Link IDs are case-insensitive 590 | self.urls[key] = self._encode_amps_and_angles(url) 591 | if title: 592 | self.titles[key] = title.replace('"', '"') 593 | return "" 594 | 595 | def _extract_footnote_def_sub(self, match): 596 | id, text = match.groups() 597 | text = _dedent(text, skip_first_line=not text.startswith('\n')).strip() 598 | normed_id = re.sub(r'\W', '-', id) 599 | # Ensure footnote text ends with a couple newlines (for some 600 | # block gamut matches). 601 | self.footnotes[normed_id] = text + "\n\n" 602 | return "" 603 | 604 | def _strip_footnote_definitions(self, text): 605 | """A footnote definition looks like this: 606 | 607 | [^note-id]: Text of the note. 608 | 609 | May include one or more indented paragraphs. 610 | 611 | Where, 612 | - The 'note-id' can be pretty much anything, though typically it 613 | is the number of the footnote. 614 | - The first paragraph may start on the next line, like so: 615 | 616 | [^note-id]: 617 | Text of the note. 618 | """ 619 | less_than_tab = self.tab_width - 1 620 | footnote_def_re = re.compile(r''' 621 | ^[ ]{0,%d}\[\^(.+)\]: # id = \1 622 | [ \t]* 623 | ( # footnote text = \2 624 | # First line need not start with the spaces. 625 | (?:\s*.*\n+) 626 | (?: 627 | (?:[ ]{%d} | \t) # Subsequent lines must be indented. 628 | .*\n+ 629 | )* 630 | ) 631 | # Lookahead for non-space at line-start, or end of doc. 632 | (?:(?=^[ ]{0,%d}\S)|\Z) 633 | ''' % (less_than_tab, self.tab_width, self.tab_width), 634 | re.X | re.M) 635 | return footnote_def_re.sub(self._extract_footnote_def_sub, text) 636 | 637 | _hr_res = [ 638 | re.compile(r"^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$", re.M), 639 | re.compile(r"^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$", re.M), 640 | re.compile(r"^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$", re.M), 641 | ] 642 | 643 | def _run_block_gamut(self, text): 644 | # These are all the transformations that form block-level 645 | # tags like paragraphs, headers, and list items. 646 | 647 | text = self._do_headers(text) 648 | 649 | # Do Horizontal Rules: 650 | hr = "\n tags around block-level tags. 667 | text = self._hash_html_blocks(text) 668 | 669 | text = self._form_paragraphs(text) 670 | 671 | return text 672 | 673 | def _pyshell_block_sub(self, match): 674 | lines = match.group(0).splitlines(0) 675 | _dedentlines(lines) 676 | indent = ' ' * self.tab_width 677 | s = ('\n' # separate from possible cuddled paragraph 678 | + indent + ('\n' + indent).join(lines) 679 | + '\n\n') 680 | return s 681 | 682 | def _prepare_pyshell_blocks(self, text): 683 | """Ensure that Python interactive shell sessions are put in 684 | code blocks -- even if not properly indented. 685 | """ 686 | if ">>>" not in text: 687 | return text 688 | 689 | less_than_tab = self.tab_width - 1 690 | _pyshell_block_re = re.compile(r""" 691 | ^([ ]{0,%d})>>>[ ].*\n # first line 692 | ^(\1.*\S+.*\n)* # any number of subsequent lines 693 | ^\n # ends with a blank line 694 | """ % less_than_tab, re.M | re.X) 695 | 696 | return _pyshell_block_re.sub(self._pyshell_block_sub, text) 697 | 698 | def _run_span_gamut(self, text): 699 | # These are all the transformations that occur *within* block-level 700 | # tags like paragraphs, headers, and list items. 701 | 702 | text = self._do_code_spans(text) 703 | 704 | text = self._escape_special_chars(text) 705 | 706 | # Process anchor and image tags. 707 | text = self._do_links(text) 708 | 709 | # Make links out of things like `` 710 | # Must come after _do_links(), because you can use < and > 711 | # delimiters in inline links like [this](). 712 | text = self._do_auto_links(text) 713 | 714 | if "link-patterns" in self.extras: 715 | text = self._do_link_patterns(text) 716 | 717 | text = self._encode_amps_and_angles(text) 718 | 719 | text = self._do_italics_and_bold(text) 720 | 721 | # Do hard breaks: 722 | text = re.sub(r" {2,}\n", " 734 | | 735 | # auto-link (e.g., ) 736 | <\w+[^>]*> 737 | | 738 | # comment 739 | | 740 | <\?.*?\?> # processing instruction 741 | ) 742 | """, re.X) 743 | 744 | def _escape_special_chars(self, text): 745 | # Python markdown note: the HTML tokenization here differs from 746 | # that in Markdown.pl, hence the behaviour for subtle cases can 747 | # differ (I believe the tokenizer here does a better job because 748 | # it isn't susceptible to unmatched '<' and '>' in HTML tags). 749 | # Note, however, that '>' is not allowed in an auto-link URL 750 | # here. 751 | escaped = [] 752 | is_html_markup = False 753 | for token in self._sorta_html_tokenize_re.split(text): 754 | if is_html_markup: 755 | # Within tags/HTML-comments/auto-links, encode * and _ 756 | # so they don't conflict with their use in Markdown for 757 | # italics and strong. We're replacing each such 758 | # character with its corresponding MD5 checksum value; 759 | # this is likely overkill, but it should prevent us from 760 | # colliding with the escape values by accident. 761 | escaped.append(token.replace('*', g_escape_table['*']) 762 | .replace('_', g_escape_table['_'])) 763 | else: 764 | escaped.append(self._encode_backslash_escapes(token)) 765 | is_html_markup = not is_html_markup 766 | return ''.join(escaped) 767 | 768 | def _hash_html_spans(self, text): 769 | # Used for safe_mode. 770 | 771 | def _is_auto_link(s): 772 | if ':' in s and self._auto_link_re.match(s): 773 | return True 774 | elif '@' in s and self._auto_email_link_re.match(s): 775 | return True 776 | return False 777 | 778 | tokens = [] 779 | is_html_markup = False 780 | for token in self._sorta_html_tokenize_re.split(text): 781 | if is_html_markup and not _is_auto_link(token): 782 | sanitized = self._sanitize_html(token) 783 | key = _hash_text(sanitized) 784 | self.html_spans[key] = sanitized 785 | tokens.append(key) 786 | else: 787 | tokens.append(token) 788 | is_html_markup = not is_html_markup 789 | return ''.join(tokens) 790 | 791 | def _unhash_html_spans(self, text): 792 | for key, sanitized in self.html_spans.items(): 793 | text = text.replace(key, sanitized) 794 | return text 795 | 796 | def _sanitize_html(self, s): 797 | if self.safe_mode == "replace": 798 | return self.html_removed_text 799 | elif self.safe_mode == "escape": 800 | replacements = [ 801 | ('&', '&'), 802 | ('<', '<'), 803 | ('>', '>'), 804 | ] 805 | for before, after in replacements: 806 | s = s.replace(before, after) 807 | return s 808 | else: 809 | raise MarkdownError("invalid value for 'safe_mode': %r (must be " 810 | "'escape' or 'replace')" % self.safe_mode) 811 | 812 | _tail_of_inline_link_re = re.compile(r''' 813 | # Match tail of: [text](/url/) or [text](/url/ "title") 814 | \( # literal paren 815 | [ \t]* 816 | (?P # \1 817 | <.*?> 818 | | 819 | .*? 820 | ) 821 | [ \t]* 822 | ( # \2 823 | (['"]) # quote char = \3 824 | (?P.*?) 825 | \3 # matching quote 826 | )? # title is optional 827 | \) 828 | ''', re.X | re.S) 829 | _tail_of_reference_link_re = re.compile(r''' 830 | # Match tail of: [text][id] 831 | [ ]? # one optional space 832 | (?:\n[ ]*)? # one optional newline followed by spaces 833 | \[ 834 | (?P<id>.*?) 835 | \] 836 | ''', re.X | re.S) 837 | 838 | def _do_links(self, text): 839 | """Turn Markdown link shortcuts into XHTML <a> and <img> tags. 840 | 841 | This is a combination of Markdown.pl's _DoAnchors() and 842 | _DoImages(). They are done together because that simplified the 843 | approach. It was necessary to use a different approach than 844 | Markdown.pl because of the lack of atomic matching support in 845 | Python's regex engine used in $g_nested_brackets. 846 | """ 847 | MAX_LINK_TEXT_SENTINEL = 3000 # markdown2 issue 24 848 | 849 | # `anchor_allowed_pos` is used to support img links inside 850 | # anchors, but not anchors inside anchors. An anchor's start 851 | # pos must be `>= anchor_allowed_pos`. 852 | anchor_allowed_pos = 0 853 | 854 | curr_pos = 0 855 | while True: # Handle the next link. 856 | # The next '[' is the start of: 857 | # - an inline anchor: [text](url "title") 858 | # - a reference anchor: [text][id] 859 | # - an inline img: ![text](url "title") 860 | # - a reference img: ![text][id] 861 | # - a footnote ref: [^id] 862 | # (Only if 'footnotes' extra enabled) 863 | # - a footnote defn: [^id]: ... 864 | # (Only if 'footnotes' extra enabled) These have already 865 | # been stripped in _strip_footnote_definitions() so no 866 | # need to watch for them. 867 | # - a link definition: [id]: url "title" 868 | # These have already been stripped in 869 | # _strip_link_definitions() so no need to watch for them. 870 | # - not markup: [...anything else... 871 | try: 872 | start_idx = text.index('[', curr_pos) 873 | except ValueError: 874 | break 875 | text_length = len(text) 876 | 877 | # Find the matching closing ']'. 878 | # Markdown.pl allows *matching* brackets in link text so we 879 | # will here too. Markdown.pl *doesn't* currently allow 880 | # matching brackets in img alt text -- we'll differ in that 881 | # regard. 882 | bracket_depth = 0 883 | for p in range(start_idx + 1, min(start_idx + MAX_LINK_TEXT_SENTINEL, 884 | text_length)): 885 | ch = text[p] 886 | if ch == ']': 887 | bracket_depth -= 1 888 | if bracket_depth < 0: 889 | break 890 | elif ch == '[': 891 | bracket_depth += 1 892 | else: 893 | # Closing bracket not found within sentinel length. 894 | # This isn't markup. 895 | curr_pos = start_idx + 1 896 | continue 897 | link_text = text[start_idx + 1:p] 898 | 899 | # Possibly a footnote ref? 900 | if "footnotes" in self.extras and link_text.startswith("^"): 901 | normed_id = re.sub(r'\W', '-', link_text[1:]) 902 | if normed_id in self.footnotes: 903 | self.footnote_ids.append(normed_id) 904 | result = '<sup class="footnote-ref" id="fnref-%s">' \ 905 | '<a href="#fn-%s">%s</a></sup>' \ 906 | % (normed_id, normed_id, len(self.footnote_ids)) 907 | text = text[:start_idx] + result + text[p + 1:] 908 | else: 909 | # This id isn't defined, leave the markup alone. 910 | curr_pos = p + 1 911 | continue 912 | 913 | # Now determine what this is by the remainder. 914 | p += 1 915 | if p == text_length: 916 | return text 917 | 918 | # Inline anchor or img? 919 | if text[p] == '(': # attempt at perf improvement 920 | match = self._tail_of_inline_link_re.match(text, p) 921 | if match: 922 | # Handle an inline anchor or img. 923 | is_img = start_idx > 0 and text[start_idx - 1] == "!" 924 | if is_img: 925 | start_idx -= 1 926 | 927 | url, title = match.group("url"), match.group("title") 928 | if url and url[0] == '<': 929 | url = url[1:-1] # '<url>' -> 'url' 930 | # We've got to encode these to avoid conflicting 931 | # with italics/bold. 932 | url = url.replace('*', g_escape_table['*']) \ 933 | .replace('_', g_escape_table['_']) 934 | if title: 935 | title_str = ' title="%s"' \ 936 | % title.replace('*', g_escape_table['*']) \ 937 | .replace('_', g_escape_table['_']) \ 938 | .replace('"', '"') 939 | else: 940 | title_str = '' 941 | if is_img: 942 | result = '<img src="%s" alt="%s"%s%s' \ 943 | % (url, link_text.replace('"', '"'), 944 | title_str, self.empty_element_suffix) 945 | curr_pos = start_idx + len(result) 946 | text = text[:start_idx] + result + text[match.end():] 947 | elif start_idx >= anchor_allowed_pos: 948 | result_head = '<a href="%s"%s>' % (url, title_str) 949 | result = '%s%s</a>' % (result_head, link_text) 950 | # <img> allowed from curr_pos on, <a> from 951 | # anchor_allowed_pos on. 952 | curr_pos = start_idx + len(result_head) 953 | anchor_allowed_pos = start_idx + len(result) 954 | text = text[:start_idx] + result + text[match.end():] 955 | else: 956 | # Anchor not allowed here. 957 | curr_pos = start_idx + 1 958 | continue 959 | 960 | # Reference anchor or img? 961 | else: 962 | match = self._tail_of_reference_link_re.match(text, p) 963 | if match: 964 | # Handle a reference-style anchor or img. 965 | is_img = start_idx > 0 and text[start_idx - 1] == "!" 966 | if is_img: 967 | start_idx -= 1 968 | link_id = match.group("id").lower() 969 | if not link_id: 970 | link_id = link_text.lower() # for links like [this][] 971 | if link_id in self.urls: 972 | url = self.urls[link_id] 973 | # We've got to encode these to avoid conflicting 974 | # with italics/bold. 975 | url = url.replace('*', g_escape_table['*']) \ 976 | .replace('_', g_escape_table['_']) 977 | title = self.titles.get(link_id) 978 | if title: 979 | title = title.replace('*', g_escape_table['*']) \ 980 | .replace('_', g_escape_table['_']) 981 | title_str = ' title="%s"' % title 982 | else: 983 | title_str = '' 984 | if is_img: 985 | result = '<img src="%s" alt="%s"%s%s' \ 986 | % (url, link_text.replace('"', '"'), 987 | title_str, self.empty_element_suffix) 988 | curr_pos = start_idx + len(result) 989 | text = text[:start_idx] + \ 990 | result + text[match.end():] 991 | elif start_idx >= anchor_allowed_pos: 992 | result = '<a href="%s"%s>%s</a>' \ 993 | % (url, title_str, link_text) 994 | result_head = '<a href="%s"%s>' % (url, title_str) 995 | result = '%s%s</a>' % (result_head, link_text) 996 | # <img> allowed from curr_pos on, <a> from 997 | # anchor_allowed_pos on. 998 | curr_pos = start_idx + len(result_head) 999 | anchor_allowed_pos = start_idx + len(result) 1000 | text = text[:start_idx] + \ 1001 | result + text[match.end():] 1002 | else: 1003 | # Anchor not allowed here. 1004 | curr_pos = start_idx + 1 1005 | else: 1006 | # This id isn't defined, leave the markup alone. 1007 | curr_pos = match.end() 1008 | continue 1009 | 1010 | # Otherwise, it isn't markup. 1011 | curr_pos = start_idx + 1 1012 | 1013 | return text 1014 | 1015 | _setext_h_re = re.compile(r'^(.+)[ \t]*\n(=+|-+)[ \t]*\n+', re.M) 1016 | 1017 | def _setext_h_sub(self, match): 1018 | n = {"=": 1, "-": 2}[match.group(2)[0]] 1019 | demote_headers = self.extras.get("demote-headers") 1020 | if demote_headers: 1021 | n = min(n + demote_headers, 6) 1022 | return "<h%d>%s</h%d>\n\n" \ 1023 | % (n, self._run_span_gamut(match.group(1)), n) 1024 | 1025 | _atx_h_re = re.compile(r''' 1026 | ^(\#{1,6}) # \1 = string of #'s 1027 | [ \t]* 1028 | (.+?) # \2 = Header text 1029 | [ \t]* 1030 | (?<!\\) # ensure not an escaped trailing '#' 1031 | \#* # optional closing #'s (not counted) 1032 | \n+ 1033 | ''', re.X | re.M) 1034 | 1035 | def _atx_h_sub(self, match): 1036 | n = len(match.group(1)) 1037 | demote_headers = self.extras.get("demote-headers") 1038 | if demote_headers: 1039 | n = min(n + demote_headers, 6) 1040 | return "<h%d>%s</h%d>\n\n" \ 1041 | % (n, self._run_span_gamut(match.group(2)), n) 1042 | 1043 | def _do_headers(self, text): 1044 | # Setext-style headers: 1045 | # Header 1 1046 | # ======== 1047 | # 1048 | # Header 2 1049 | # -------- 1050 | text = self._setext_h_re.sub(self._setext_h_sub, text) 1051 | 1052 | # atx-style headers: 1053 | # # Header 1 1054 | # ## Header 2 1055 | # ## Header 2 with closing hashes ## 1056 | # ... 1057 | # ###### Header 6 1058 | text = self._atx_h_re.sub(self._atx_h_sub, text) 1059 | 1060 | return text 1061 | 1062 | _marker_ul_chars = '*+-' 1063 | _marker_any = r'(?:[%s]|\d+\.)' % _marker_ul_chars 1064 | _marker_ul = '(?:[%s])' % _marker_ul_chars 1065 | _marker_ol = r'(?:\d+\.)' 1066 | 1067 | def _list_sub(self, match): 1068 | lst = match.group(1) 1069 | lst_type = match.group(3) in self._marker_ul_chars and "ul" or "ol" 1070 | result = self._process_list_items(lst) 1071 | if self.list_level: 1072 | return "<%s>\n%s</%s>\n" % (lst_type, result, lst_type) 1073 | else: 1074 | return "<%s>\n%s</%s>\n\n" % (lst_type, result, lst_type) 1075 | 1076 | def _do_lists(self, text): 1077 | # Form HTML ordered (numbered) and unordered (bulleted) lists. 1078 | 1079 | for marker_pat in (self._marker_ul, self._marker_ol): 1080 | # Re-usable pattern to match any entire ul or ol list: 1081 | less_than_tab = self.tab_width - 1 1082 | whole_list = r''' 1083 | ( # \1 = whole list 1084 | ( # \2 1085 | [ ]{0,%d} 1086 | (%s) # \3 = first list item marker 1087 | [ \t]+ 1088 | ) 1089 | (?:.+?) 1090 | ( # \4 1091 | \Z 1092 | | 1093 | \n{2,} 1094 | (?=\S) 1095 | (?! # Negative lookahead for another list item marker 1096 | [ \t]* 1097 | %s[ \t]+ 1098 | ) 1099 | ) 1100 | ) 1101 | ''' % (less_than_tab, marker_pat, marker_pat) 1102 | 1103 | # We use a different prefix before nested lists than top-level lists. 1104 | # See extended comment in _process_list_items(). 1105 | # 1106 | # Note: There's a bit of duplication here. My original implementation 1107 | # created a scalar regex pattern as the conditional result of the test on 1108 | # $g_list_level, and then only ran the $text =~ s{...}{...}egmx 1109 | # substitution once, using the scalar as the pattern. This worked, 1110 | # everywhere except when running under MT on my hosting account at Pair 1111 | # Networks. There, this caused all rebuilds to be killed by the reaper (or 1112 | # perhaps they crashed, but that seems incredibly unlikely given that the 1113 | # same script on the same server ran fine *except* under MT. I've spent 1114 | # more time trying to figure out why this is happening than I'd like to 1115 | # admit. My only guess, backed up by the fact that this workaround works, 1116 | # is that Perl optimizes the substition when it can figure out that the 1117 | # pattern will never change, and when this optimization isn't on, we run 1118 | # afoul of the reaper. Thus, the slightly redundant code to that uses two 1119 | # static s/// patterns rather than one conditional pattern. 1120 | 1121 | if self.list_level: 1122 | sub_list_re = re.compile("^" + whole_list, re.X | re.M | re.S) 1123 | text = sub_list_re.sub(self._list_sub, text) 1124 | else: 1125 | list_re = re.compile(r"(?:(?<=\n\n)|\A\n?)" + whole_list, 1126 | re.X | re.M | re.S) 1127 | text = list_re.sub(self._list_sub, text) 1128 | 1129 | return text 1130 | 1131 | _list_item_re = re.compile(r''' 1132 | (\n)? # leading line = \1 1133 | (^[ \t]*) # leading whitespace = \2 1134 | (%s) [ \t]+ # list marker = \3 1135 | ((?:.+?) # list item text = \4 1136 | (\n{1,2})) # eols = \5 1137 | (?= \n* (\Z | \2 (%s) [ \t]+)) 1138 | ''' % (_marker_any, _marker_any), 1139 | re.M | re.X | re.S) 1140 | 1141 | _last_li_endswith_two_eols = False 1142 | 1143 | def _list_item_sub(self, match): 1144 | item = match.group(4) 1145 | leading_line = match.group(1) 1146 | leading_space = match.group(2) 1147 | if leading_line or "\n\n" in item or self._last_li_endswith_two_eols: 1148 | item = self._run_block_gamut(self._outdent(item)) 1149 | else: 1150 | # Recursion for sub-lists: 1151 | item = self._do_lists(self._outdent(item)) 1152 | if item.endswith('\n'): 1153 | item = item[:-1] 1154 | item = self._run_span_gamut(item) 1155 | self._last_li_endswith_two_eols = (len(match.group(5)) == 2) 1156 | return "<li>%s</li>\n" % item 1157 | 1158 | def _process_list_items(self, list_str): 1159 | # Process the contents of a single ordered or unordered list, 1160 | # splitting it into individual list items. 1161 | 1162 | # The $g_list_level global keeps track of when we're inside a list. 1163 | # Each time we enter a list, we increment it; when we leave a list, 1164 | # we decrement. If it's zero, we're not in a list anymore. 1165 | # 1166 | # We do this because when we're not inside a list, we want to treat 1167 | # something like this: 1168 | # 1169 | # I recommend upgrading to version 1170 | # 8. Oops, now this line is treated 1171 | # as a sub-list. 1172 | # 1173 | # As a single paragraph, despite the fact that the second line starts 1174 | # with a digit-period-space sequence. 1175 | # 1176 | # Whereas when we're inside a list (or sub-list), that line will be 1177 | # treated as the start of a sub-list. What a kludge, huh? This is 1178 | # an aspect of Markdown's syntax that's hard to parse perfectly 1179 | # without resorting to mind-reading. Perhaps the solution is to 1180 | # change the syntax rules such that sub-lists must start with a 1181 | # starting cardinal number; e.g. "1." or "a.". 1182 | self.list_level += 1 1183 | self._last_li_endswith_two_eols = False 1184 | list_str = list_str.rstrip('\n') + '\n' 1185 | list_str = self._list_item_re.sub(self._list_item_sub, list_str) 1186 | self.list_level -= 1 1187 | return list_str 1188 | 1189 | def _get_pygments_lexer(self, lexer_name): 1190 | try: 1191 | from pygments import lexers, util 1192 | except ImportError: 1193 | return None 1194 | try: 1195 | return lexers.get_lexer_by_name(lexer_name) 1196 | except util.ClassNotFound: 1197 | return None 1198 | 1199 | def _color_with_pygments(self, codeblock, lexer, **formatter_opts): 1200 | import pygments 1201 | import pygments.formatters 1202 | 1203 | class HtmlCodeFormatter(pygments.formatters.HtmlFormatter): 1204 | 1205 | def _wrap_code(self, inner): 1206 | """A function for use in a Pygments Formatter which 1207 | wraps in <code> tags. 1208 | """ 1209 | yield 0, "<code>" 1210 | for tup in inner: 1211 | yield tup 1212 | yield 0, "</code>" 1213 | 1214 | def wrap(self, source, outfile): 1215 | """Return the source with a code, pre, and div.""" 1216 | return self._wrap_div(self._wrap_pre(self._wrap_code(source))) 1217 | 1218 | formatter = HtmlCodeFormatter(cssclass="codehilite", **formatter_opts) 1219 | return pygments.highlight(codeblock, lexer, formatter) 1220 | 1221 | def _code_block_sub(self, match): 1222 | codeblock = match.group(1) 1223 | codeblock = self._outdent(codeblock) 1224 | codeblock = self._detab(codeblock) 1225 | codeblock = codeblock.lstrip('\n') # trim leading newlines 1226 | codeblock = codeblock.rstrip() # trim trailing whitespace 1227 | 1228 | if "code-color" in self.extras and codeblock.startswith(":::"): 1229 | lexer_name, rest = codeblock.split('\n', 1) 1230 | lexer_name = lexer_name[3:].strip() 1231 | lexer = self._get_pygments_lexer(lexer_name) 1232 | codeblock = rest.lstrip("\n") # Remove lexer declaration line. 1233 | if lexer: 1234 | formatter_opts = self.extras['code-color'] or {} 1235 | colored = self._color_with_pygments(codeblock, lexer, 1236 | **formatter_opts) 1237 | return "\n\n%s\n\n" % colored 1238 | 1239 | codeblock = self._encode_code(codeblock) 1240 | return "\n\n<pre><code>%s\n</code></pre>\n\n" % codeblock 1241 | 1242 | def _do_code_blocks(self, text): 1243 | """Process Markdown `<pre><code>` blocks.""" 1244 | code_block_re = re.compile(r''' 1245 | (?:\n\n|\A) 1246 | ( # $1 = the code block -- one or more lines, starting with a space/tab 1247 | (?: 1248 | (?:[ ]{%d} | \t) # Lines must start with a tab or a tab-width of spaces 1249 | .*\n+ 1250 | )+ 1251 | ) 1252 | ((?=^[ ]{0,%d}\S)|\Z) # Lookahead for non-space at line-start, or end of doc 1253 | ''' % (self.tab_width, self.tab_width), 1254 | re.M | re.X) 1255 | 1256 | return code_block_re.sub(self._code_block_sub, text) 1257 | 1258 | # Rules for a code span: 1259 | # - backslash escapes are not interpreted in a code span 1260 | # - to include one or or a run of more backticks the delimiters must 1261 | # be a longer run of backticks 1262 | # - cannot start or end a code span with a backtick; pad with a 1263 | # space and that space will be removed in the emitted HTML 1264 | # See `test/tm-cases/escapes.text` for a number of edge-case 1265 | # examples. 1266 | _code_span_re = re.compile(r''' 1267 | (?<!\\) 1268 | (`+) # \1 = Opening run of ` 1269 | (?!`) # See Note A test/tm-cases/escapes.text 1270 | (.+?) # \2 = The code block 1271 | (?<!`) 1272 | \1 # Matching closer 1273 | (?!`) 1274 | ''', re.X | re.S) 1275 | 1276 | def _code_span_sub(self, match): 1277 | c = match.group(2).strip(" \t") 1278 | c = self._encode_code(c) 1279 | return "<code>%s</code>" % c 1280 | 1281 | def _do_code_spans(self, text): 1282 | # * Backtick quotes are used for <code></code> spans. 1283 | # 1284 | # * You can use multiple backticks as the delimiters if you want to 1285 | # include literal backticks in the code span. So, this input: 1286 | # 1287 | # Just type ``foo `bar` baz`` at the prompt. 1288 | # 1289 | # Will translate to: 1290 | # 1291 | # <p>Just type <code>foo `bar` baz</code> at the prompt.</p> 1292 | # 1293 | # There's no arbitrary limit to the number of backticks you 1294 | # can use as delimters. If you need three consecutive backticks 1295 | # in your code, use four for delimiters, etc. 1296 | # 1297 | # * You can use spaces to get literal backticks at the edges: 1298 | # 1299 | # ... type `` `bar` `` ... 1300 | # 1301 | # Turns to: 1302 | # 1303 | # ... type <code>`bar`</code> ... 1304 | return self._code_span_re.sub(self._code_span_sub, text) 1305 | 1306 | def _encode_code(self, text): 1307 | """Encode/escape certain characters inside Markdown code runs. 1308 | The point is that in code, these characters are literals, 1309 | and lose their special Markdown meanings. 1310 | """ 1311 | replacements = [ 1312 | # Encode all ampersands; HTML entities are not 1313 | # entities within a Markdown code span. 1314 | ('&', '&'), 1315 | # Do the angle bracket song and dance: 1316 | ('<', '<'), 1317 | ('>', '>'), 1318 | # Now, escape characters that are magic in Markdown: 1319 | ('*', g_escape_table['*']), 1320 | ('_', g_escape_table['_']), 1321 | ('{', g_escape_table['{']), 1322 | ('}', g_escape_table['}']), 1323 | ('[', g_escape_table['[']), 1324 | (']', g_escape_table[']']), 1325 | ('\\', g_escape_table['\\']), 1326 | ] 1327 | for before, after in replacements: 1328 | text = text.replace(before, after) 1329 | return text 1330 | 1331 | _strong_re = re.compile(r"(\*\*|__)(?=\S)(.+?[*_]*)(?<=\S)\1", re.S) 1332 | _em_re = re.compile(r"(\*|_)(?=\S)(.+?)(?<=\S)\1", re.S) 1333 | _code_friendly_strong_re = re.compile( 1334 | r"\*\*(?=\S)(.+?[*_]*)(?<=\S)\*\*", re.S) 1335 | _code_friendly_em_re = re.compile(r"\*(?=\S)(.+?)(?<=\S)\*", re.S) 1336 | 1337 | def _do_italics_and_bold(self, text): 1338 | # <strong> must go first: 1339 | if "code-friendly" in self.extras: 1340 | text = self._code_friendly_strong_re.sub( 1341 | r"<strong>\1</strong>", text) 1342 | text = self._code_friendly_em_re.sub(r"<em>\1</em>", text) 1343 | else: 1344 | text = self._strong_re.sub(r"<strong>\2</strong>", text) 1345 | text = self._em_re.sub(r"<em>\2</em>", text) 1346 | return text 1347 | 1348 | _block_quote_re = re.compile(r''' 1349 | ( # Wrap whole match in \1 1350 | ( 1351 | ^[ \t]*>[ \t]? # '>' at the start of a line 1352 | .+\n # rest of the first line 1353 | (.+\n)* # subsequent consecutive lines 1354 | \n* # blanks 1355 | )+ 1356 | ) 1357 | ''', re.M | re.X) 1358 | _bq_one_level_re = re.compile('^[ \t]*>[ \t]?', re.M) 1359 | 1360 | _html_pre_block_re = re.compile(r'(\s*<pre>.+?</pre>)', re.S) 1361 | 1362 | def _dedent_two_spaces_sub(self, match): 1363 | return re.sub(r'(?m)^ ', '', match.group(1)) 1364 | 1365 | def _block_quote_sub(self, match): 1366 | bq = match.group(1) 1367 | bq = self._bq_one_level_re.sub('', bq) # trim one level of quoting 1368 | bq = self._ws_only_line_re.sub('', bq) # trim whitespace-only lines 1369 | bq = self._run_block_gamut(bq) # recurse 1370 | 1371 | bq = re.sub('(?m)^', ' ', bq) 1372 | # These leading spaces screw with <pre> content, so we need to fix 1373 | # that: 1374 | bq = self._html_pre_block_re.sub(self._dedent_two_spaces_sub, bq) 1375 | 1376 | return "<blockquote>\n%s\n</blockquote>\n\n" % bq 1377 | 1378 | def _do_block_quotes(self, text): 1379 | if '>' not in text: 1380 | return text 1381 | return self._block_quote_re.sub(self._block_quote_sub, text) 1382 | 1383 | def _form_paragraphs(self, text): 1384 | # Strip leading and trailing lines: 1385 | text = text.strip('\n') 1386 | 1387 | # Wrap <p> tags. 1388 | grafs = re.split(r"\n{2,}", text) 1389 | for i, graf in enumerate(grafs): 1390 | if graf in self.html_blocks: 1391 | # Unhashify HTML blocks 1392 | grafs[i] = self.html_blocks[graf] 1393 | else: 1394 | # Wrap <p> tags. 1395 | graf = self._run_span_gamut(graf) 1396 | grafs[i] = "<p>" + graf.lstrip(" \t") + "</p>" 1397 | 1398 | return "\n\n".join(grafs) 1399 | 1400 | def _add_footnotes(self, text): 1401 | if self.footnotes: 1402 | footer = [ 1403 | '<div class="footnotes">', 1404 | '<hr' + self.empty_element_suffix, 1405 | '<ol>', 1406 | ] 1407 | for i, id in enumerate(self.footnote_ids): 1408 | if i != 0: 1409 | footer.append('') 1410 | footer.append('<li id="fn-%s">' % id) 1411 | footer.append(self._run_block_gamut(self.footnotes[id])) 1412 | backlink = ('<a href="#fnref-%s" ' 1413 | 'class="footnoteBackLink" ' 1414 | 'title="Jump back to footnote %d in the text.">' 1415 | '↩</a>' % (id, i + 1)) 1416 | if footer[-1].endswith("</p>"): 1417 | footer[-1] = footer[-1][:-len("</p>")] \ 1418 | + ' ' + backlink + "</p>" 1419 | else: 1420 | footer.append("\n<p>%s</p>" % backlink) 1421 | footer.append('</li>') 1422 | footer.append('</ol>') 1423 | footer.append('</div>') 1424 | return text + '\n\n' + '\n'.join(footer) 1425 | else: 1426 | return text 1427 | 1428 | # Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: 1429 | # http://bumppo.net/projects/amputator/ 1430 | _ampersand_re = re.compile(r'&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)') 1431 | _naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I) 1432 | _naked_gt_re = re.compile(r'''(?<![a-z?!/'"-])>''', re.I) 1433 | 1434 | def _encode_amps_and_angles(self, text): 1435 | # Smart processing for ampersands and angle brackets that need 1436 | # to be encoded. 1437 | text = self._ampersand_re.sub('&', text) 1438 | 1439 | # Encode naked <'s 1440 | text = self._naked_lt_re.sub('<', text) 1441 | 1442 | # Encode naked >'s 1443 | # Note: Other markdown implementations (e.g. Markdown.pl, PHP 1444 | # Markdown) don't do this. 1445 | text = self._naked_gt_re.sub('>', text) 1446 | return text 1447 | 1448 | def _encode_backslash_escapes(self, text): 1449 | for ch, escape in g_escape_table.items(): 1450 | text = text.replace("\\" + ch, escape) 1451 | return text 1452 | 1453 | _auto_link_re = re.compile(r'<((https?|ftp):[^\'">\s]+)>', re.I) 1454 | 1455 | def _auto_link_sub(self, match): 1456 | g1 = match.group(1) 1457 | return '<a href="%s">%s</a>' % (g1, g1) 1458 | 1459 | _auto_email_link_re = re.compile(r""" 1460 | < 1461 | (?:mailto:)? 1462 | ( 1463 | [-.\w]+ 1464 | \@ 1465 | [-\w]+(\.[-\w]+)*\.[a-z]+ 1466 | ) 1467 | > 1468 | """, re.I | re.X | re.U) 1469 | 1470 | def _auto_email_link_sub(self, match): 1471 | return self._encode_email_address( 1472 | self._unescape_special_chars(match.group(1))) 1473 | 1474 | def _do_auto_links(self, text): 1475 | text = self._auto_link_re.sub(self._auto_link_sub, text) 1476 | text = self._auto_email_link_re.sub(self._auto_email_link_sub, text) 1477 | return text 1478 | 1479 | def _encode_email_address(self, addr): 1480 | # Input: an email address, e.g. "foo@example.com" 1481 | # 1482 | # Output: the email address as a mailto link, with each character 1483 | # of the address encoded as either a decimal or hex entity, in 1484 | # the hopes of foiling most address harvesting spam bots. E.g.: 1485 | # 1486 | # <a href="mailto:foo@e 1487 | # xample.com">foo 1488 | # @example.com</a> 1489 | # 1490 | # Based on a filter by Matthew Wickline, posted to the BBEdit-Talk 1491 | # mailing list: <http://tinyurl.com/yu7ue> 1492 | chars = [_xml_encode_email_char_at_random(ch) 1493 | for ch in "mailto:" + addr] 1494 | # Strip the mailto: from the visible part. 1495 | addr = '<a href="%s">%s</a>' \ 1496 | % (''.join(chars), ''.join(chars[7:])) 1497 | return addr 1498 | 1499 | def _do_link_patterns(self, text): 1500 | """Caveat emptor: there isn't much guarding against link 1501 | patterns being formed inside other standard Markdown links, e.g. 1502 | inside a [link def][like this]. 1503 | 1504 | Dev Notes: *Could* consider prefixing regexes with a negative 1505 | lookbehind assertion to attempt to guard against this. 1506 | """ 1507 | link_from_hash = {} 1508 | for regex, repl in self.link_patterns: 1509 | replacements = [] 1510 | for match in regex.finditer(text): 1511 | if hasattr(repl, "__call__"): 1512 | href = repl(match) 1513 | else: 1514 | href = match.expand(repl) 1515 | replacements.append((match.span(), href)) 1516 | for (start, end), href in reversed(replacements): 1517 | escaped_href = ( 1518 | href.replace('"', '"') # b/c of attr quote 1519 | # To avoid markdown <em> and <strong>: 1520 | .replace('*', g_escape_table['*']) 1521 | .replace('_', g_escape_table['_'])) 1522 | link = '<a href="%s">%s</a>' % (escaped_href, text[start:end]) 1523 | hash = md5(link).hexdigest() 1524 | link_from_hash[hash] = link 1525 | text = text[:start] + hash + text[end:] 1526 | for hash, link in link_from_hash.items(): 1527 | text = text.replace(hash, link) 1528 | return text 1529 | 1530 | def _unescape_special_chars(self, text): 1531 | # Swap back in all the special characters we've hidden. 1532 | for ch, hash in g_escape_table.items(): 1533 | text = text.replace(hash, ch) 1534 | return text 1535 | 1536 | def _outdent(self, text): 1537 | # Remove one level of line-leading tabs or spaces 1538 | return self._outdent_re.sub('', text) 1539 | 1540 | 1541 | class MarkdownWithExtras(Markdown): 1542 | """A markdowner class that enables most extras: 1543 | 1544 | - footnotes 1545 | - code-color (only has effect if 'pygments' Python module on path) 1546 | 1547 | These are not included: 1548 | - pyshell (specific to Python-related documenting) 1549 | - code-friendly (because it *disables* part of the syntax) 1550 | - link-patterns (because you need to specify some actual 1551 | link-patterns anyway) 1552 | """ 1553 | extras = ["footnotes", "code-color"] 1554 | 1555 | 1556 | #---- internal support functions 1557 | 1558 | # From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549 1559 | def _curry(*args, **kwargs): 1560 | function, args = args[0], args[1:] 1561 | 1562 | def result(*rest, **kwrest): 1563 | combined = kwargs.copy() 1564 | combined.update(kwrest) 1565 | return function(*args + rest, **combined) 1566 | return result 1567 | 1568 | # Recipe: regex_from_encoded_pattern (1.0) 1569 | 1570 | 1571 | def _regex_from_encoded_pattern(s): 1572 | """'foo' -> re.compile(re.escape('foo')) 1573 | '/foo/' -> re.compile('foo') 1574 | '/foo/i' -> re.compile('foo', re.I) 1575 | """ 1576 | if s.startswith('/') and s.rfind('/') != 0: 1577 | # Parse it: /PATTERN/FLAGS 1578 | idx = s.rfind('/') 1579 | pattern, flags_str = s[1:idx], s[idx + 1:] 1580 | flag_from_char = { 1581 | "i": re.IGNORECASE, 1582 | "l": re.LOCALE, 1583 | "s": re.DOTALL, 1584 | "m": re.MULTILINE, 1585 | "u": re.UNICODE, 1586 | } 1587 | flags = 0 1588 | for char in flags_str: 1589 | try: 1590 | flags |= flag_from_char[char] 1591 | except KeyError: 1592 | raise ValueError("unsupported regex flag: '%s' in '%s' " 1593 | "(must be one of '%s')" 1594 | % (char, s, ''.join(flag_from_char.keys()))) 1595 | return re.compile(s[1:idx], flags) 1596 | else: # not an encoded regex 1597 | return re.compile(re.escape(s)) 1598 | 1599 | # Recipe: dedent (0.1.2) 1600 | 1601 | 1602 | def _dedentlines(lines, tabsize=8, skip_first_line=False): 1603 | """_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines 1604 | 1605 | "lines" is a list of lines to dedent. 1606 | "tabsize" is the tab width to use for indent width calculations. 1607 | "skip_first_line" is a boolean indicating if the first line should 1608 | be skipped for calculating the indent width and for dedenting. 1609 | This is sometimes useful for docstrings and similar. 1610 | 1611 | Same as dedent() except operates on a sequence of lines. Note: the 1612 | lines list is modified **in-place**. 1613 | """ 1614 | DEBUG = False 1615 | if DEBUG: 1616 | print "dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\ 1617 | % (tabsize, skip_first_line) 1618 | indents = [] 1619 | margin = None 1620 | for i, line in enumerate(lines): 1621 | if i == 0 and skip_first_line: 1622 | continue 1623 | indent = 0 1624 | for ch in line: 1625 | if ch == ' ': 1626 | indent += 1 1627 | elif ch == '\t': 1628 | indent += tabsize - (indent % tabsize) 1629 | elif ch in '\r\n': 1630 | continue # skip all-whitespace lines 1631 | else: 1632 | break 1633 | else: 1634 | continue # skip all-whitespace lines 1635 | if DEBUG: 1636 | print "dedent: indent=%d: %r" % (indent, line) 1637 | if margin is None: 1638 | margin = indent 1639 | else: 1640 | margin = min(margin, indent) 1641 | if DEBUG: 1642 | print "dedent: margin=%r" % margin 1643 | 1644 | if margin is not None and margin > 0: 1645 | for i, line in enumerate(lines): 1646 | if i == 0 and skip_first_line: 1647 | continue 1648 | removed = 0 1649 | for j, ch in enumerate(line): 1650 | if ch == ' ': 1651 | removed += 1 1652 | elif ch == '\t': 1653 | removed += tabsize - (removed % tabsize) 1654 | elif ch in '\r\n': 1655 | if DEBUG: 1656 | print "dedent: %r: EOL -> strip up to EOL" % line 1657 | lines[i] = lines[i][j:] 1658 | break 1659 | else: 1660 | raise ValueError("unexpected non-whitespace char %r in " 1661 | "line %r while removing %d-space margin" 1662 | % (ch, line, margin)) 1663 | if DEBUG: 1664 | print "dedent: %r: %r -> removed %d/%d"\ 1665 | % (line, ch, removed, margin) 1666 | if removed == margin: 1667 | lines[i] = lines[i][j + 1:] 1668 | break 1669 | elif removed > margin: 1670 | lines[i] = ' ' * (removed - margin) + lines[i][j + 1:] 1671 | break 1672 | else: 1673 | if removed: 1674 | lines[i] = lines[i][removed:] 1675 | return lines 1676 | 1677 | 1678 | def _dedent(text, tabsize=8, skip_first_line=False): 1679 | """_dedent(text, tabsize=8, skip_first_line=False) -> dedented text 1680 | 1681 | "text" is the text to dedent. 1682 | "tabsize" is the tab width to use for indent width calculations. 1683 | "skip_first_line" is a boolean indicating if the first line should 1684 | be skipped for calculating the indent width and for dedenting. 1685 | This is sometimes useful for docstrings and similar. 1686 | 1687 | textwrap.dedent(s), but don't expand tabs to spaces 1688 | """ 1689 | lines = text.splitlines(1) 1690 | _dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line) 1691 | return ''.join(lines) 1692 | 1693 | 1694 | class _memoized(object): 1695 | """Decorator that caches a function's return value each time it is called. 1696 | If called later with the same arguments, the cached value is returned, and 1697 | not re-evaluated. 1698 | 1699 | http://wiki.python.org/moin/PythonDecoratorLibrary 1700 | """ 1701 | 1702 | def __init__(self, func): 1703 | self.func = func 1704 | self.cache = {} 1705 | 1706 | def __call__(self, *args): 1707 | try: 1708 | return self.cache[args] 1709 | except KeyError: 1710 | self.cache[args] = value = self.func(*args) 1711 | return value 1712 | except TypeError: 1713 | # uncachable -- for instance, passing a list as an argument. 1714 | # Better to not cache than to blow up entirely. 1715 | return self.func(*args) 1716 | 1717 | def __repr__(self): 1718 | """Return the function's docstring.""" 1719 | return self.func.__doc__ 1720 | 1721 | 1722 | def _xml_oneliner_re_from_tab_width(tab_width): 1723 | """Standalone XML processing instruction regex.""" 1724 | return re.compile(r""" 1725 | (?: 1726 | (?<=\n\n) # Starting after a blank line 1727 | | # or 1728 | \A\n? # the beginning of the doc 1729 | ) 1730 | ( # save in $1 1731 | [ ]{0,%d} 1732 | (?: 1733 | <\?\w+\b\s+.*?\?> # XML processing instruction 1734 | | 1735 | <\w+:\w+\b\s+.*?/> # namespaced single tag 1736 | ) 1737 | [ \t]* 1738 | (?=\n{2,}|\Z) # followed by a blank line or end of document 1739 | ) 1740 | """ % (tab_width - 1), re.X) 1741 | _xml_oneliner_re_from_tab_width = _memoized(_xml_oneliner_re_from_tab_width) 1742 | 1743 | 1744 | def _hr_tag_re_from_tab_width(tab_width): 1745 | return re.compile(r""" 1746 | (?: 1747 | (?<=\n\n) # Starting after a blank line 1748 | | # or 1749 | \A\n? # the beginning of the doc 1750 | ) 1751 | ( # save in \1 1752 | [ ]{0,%d} 1753 | <(hr) # start tag = \2 1754 | \b # word break 1755 | ([^<>])*? # 1756 | /?> # the matching end tag 1757 | [ \t]* 1758 | (?=\n{2,}|\Z) # followed by a blank line or end of document 1759 | ) 1760 | """ % (tab_width - 1), re.X) 1761 | _hr_tag_re_from_tab_width = _memoized(_hr_tag_re_from_tab_width) 1762 | 1763 | 1764 | def _xml_encode_email_char_at_random(ch): 1765 | r = random() 1766 | # Roughly 10% raw, 45% hex, 45% dec. 1767 | # '@' *must* be encoded. I [John Gruber] insist. 1768 | # Issue 26: '_' must be encoded. 1769 | if r > 0.9 and ch not in "@_": 1770 | return ch 1771 | elif r < 0.45: 1772 | # The [1:] is to drop leading '0': 0x63 -> x63 1773 | return '&#%s;' % hex(ord(ch))[1:] 1774 | else: 1775 | return '&#%s;' % ord(ch) 1776 | 1777 | 1778 | def _hash_text(text): 1779 | return 'md5:' + md5(text.encode("utf-8")).hexdigest() 1780 | 1781 | 1782 | #---- mainline 1783 | 1784 | class _NoReflowFormatter(optparse.IndentedHelpFormatter): 1785 | """An optparse formatter that does NOT reflow the description.""" 1786 | 1787 | def format_description(self, description): 1788 | return description or "" 1789 | 1790 | 1791 | def _test(): 1792 | import doctest 1793 | doctest.testmod() 1794 | 1795 | 1796 | def main(argv=None): 1797 | if argv is None: 1798 | argv = sys.argv 1799 | if not logging.root.handlers: 1800 | logging.basicConfig() 1801 | 1802 | usage = "usage: %prog [PATHS...]" 1803 | version = "%prog " + __version__ 1804 | parser = optparse.OptionParser(prog="markdown2", usage=usage, 1805 | version=version, description=cmdln_desc, 1806 | formatter=_NoReflowFormatter()) 1807 | parser.add_option("-v", "--verbose", dest="log_level", 1808 | action="store_const", const=logging.DEBUG, 1809 | help="more verbose output") 1810 | parser.add_option("--encoding", 1811 | help="specify encoding of text content") 1812 | parser.add_option("--html4tags", action="store_true", default=False, 1813 | help="use HTML 4 style for empty element tags") 1814 | parser.add_option("-s", "--safe", metavar="MODE", dest="safe_mode", 1815 | help="sanitize literal HTML: 'escape' escapes " 1816 | "HTML meta chars, 'replace' replaces with an " 1817 | "[HTML_REMOVED] note") 1818 | parser.add_option("-x", "--extras", action="append", 1819 | help="Turn on specific extra features (not part of " 1820 | "the core Markdown spec). Supported values: " 1821 | "'code-friendly' disables _/__ for emphasis; " 1822 | "'code-color' adds code-block syntax coloring; " 1823 | "'link-patterns' adds auto-linking based on patterns; " 1824 | "'footnotes' adds the footnotes syntax;" 1825 | "'xml' passes one-liner processing instructions and namespaced XML tags;" 1826 | "'pyshell' to put unindented Python interactive shell sessions in a <code> block.") 1827 | parser.add_option("--use-file-vars", 1828 | help="Look for and use Emacs-style 'markdown-extras' " 1829 | "file var to turn on extras. See " 1830 | "<http://code.google.com/p/python-markdown2/wiki/Extras>.") 1831 | parser.add_option("--link-patterns-file", 1832 | help="path to a link pattern file") 1833 | parser.add_option("--self-test", action="store_true", 1834 | help="run internal self-tests (some doctests)") 1835 | parser.add_option("--compare", action="store_true", 1836 | help="run against Markdown.pl as well (for testing)") 1837 | parser.set_defaults(log_level=logging.INFO, compare=False, 1838 | encoding="utf-8", safe_mode=None, use_file_vars=False) 1839 | opts, paths = parser.parse_args() 1840 | log.setLevel(opts.log_level) 1841 | 1842 | if opts.self_test: 1843 | return _test() 1844 | 1845 | if opts.extras: 1846 | extras = {} 1847 | for s in opts.extras: 1848 | splitter = re.compile("[,;: ]+") 1849 | for e in splitter.split(s): 1850 | if '=' in e: 1851 | ename, earg = e.split('=', 1) 1852 | try: 1853 | earg = int(earg) 1854 | except ValueError: 1855 | pass 1856 | else: 1857 | ename, earg = e, None 1858 | extras[ename] = earg 1859 | else: 1860 | extras = None 1861 | 1862 | if opts.link_patterns_file: 1863 | link_patterns = [] 1864 | f = open(opts.link_patterns_file) 1865 | try: 1866 | for i, line in enumerate(f.readlines()): 1867 | if not line.strip(): 1868 | continue 1869 | if line.lstrip().startswith("#"): 1870 | continue 1871 | try: 1872 | pat, href = line.rstrip().rsplit(None, 1) 1873 | except ValueError: 1874 | raise MarkdownError("%s:%d: invalid link pattern line: %r" 1875 | % (opts.link_patterns_file, i + 1, line)) 1876 | link_patterns.append( 1877 | (_regex_from_encoded_pattern(pat), href)) 1878 | finally: 1879 | f.close() 1880 | else: 1881 | link_patterns = None 1882 | 1883 | from os.path import join, dirname, abspath, exists 1884 | markdown_pl = join(dirname(dirname(abspath(__file__))), "test", 1885 | "Markdown.pl") 1886 | for path in paths: 1887 | if opts.compare: 1888 | print "==== Markdown.pl ====" 1889 | perl_cmd = 'perl %s "%s"' % (markdown_pl, path) 1890 | o = os.popen(perl_cmd) 1891 | perl_html = o.read() 1892 | o.close() 1893 | sys.stdout.write(perl_html) 1894 | print "==== markdown2.py ====" 1895 | html = markdown_path(path, encoding=opts.encoding, 1896 | html4tags=opts.html4tags, 1897 | safe_mode=opts.safe_mode, 1898 | extras=extras, link_patterns=link_patterns, 1899 | use_file_vars=opts.use_file_vars) 1900 | sys.stdout.write( 1901 | html.encode(sys.stdout.encoding or "utf-8", 'xmlcharrefreplace')) 1902 | if opts.compare: 1903 | test_dir = join(dirname(dirname(abspath(__file__))), "test") 1904 | if exists(join(test_dir, "test_markdown2.py")): 1905 | sys.path.insert(0, test_dir) 1906 | from test_markdown2 import norm_html_from_html 1907 | norm_html = norm_html_from_html(html) 1908 | norm_perl_html = norm_html_from_html(perl_html) 1909 | else: 1910 | norm_html = html 1911 | norm_perl_html = perl_html 1912 | print "==== match? %r ====" % (norm_perl_html == norm_html) 1913 | 1914 | 1915 | if __name__ == "__main__": 1916 | sys.exit(main(sys.argv)) 1917 | -------------------------------------------------------------------------------- /lib/pagination.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding=utf-8 3 | try: 4 | import psyco 5 | psyco.full() 6 | except: 7 | pass 8 | from math import ceil 9 | 10 | 11 | class Pagination(object): 12 | 13 | def __init__(self, query, page, per_page=20): 14 | #: pagination object. 15 | self.query = query 16 | #: the current page number (1 indexed) 17 | self.page = page 18 | #: the number of items to be displayed on a page. 19 | self.per_page = per_page 20 | #: the total number of items matching the query 21 | self.total = self.query.count() 22 | self.items = self.query.paginate(page, per_page) 23 | 24 | @property 25 | def pages(self): 26 | """The total number of pages""" 27 | return int(ceil(self.total / float(self.per_page))) 28 | 29 | @property 30 | def has_prev(self): 31 | """True if a previous page exists""" 32 | return self.page > 1 33 | 34 | @property 35 | def has_next(self): 36 | """True if a next page exists.""" 37 | return self.page < self.pages 38 | 39 | def prev(self): 40 | assert self.query is not None 41 | return self.query.paginate(self.page - 1, self.per_page) 42 | 43 | def next(self): 44 | assert self.query is not None 45 | return self.query.paginate(self.page + 1, self.per_page) 46 | 47 | def iter_pages(self, left_edge=2, left_current=2, 48 | right_current=5, right_edge=2): 49 | """Iterates over the page numbers in the pagination. The four 50 | parameters control the thresholds how many numbers should be produced 51 | from the sides. Skipped page numbers are represented as `None`. 52 | This is how you could render such a pagination in the templates: 53 | 54 | .. sourcecode:: html+jinja 55 | 56 | {% macro render_pagination(pagination, endpoint) %} 57 | <div class=pagination> 58 | {%- for page in pagination.iter_pages() %} 59 | {% if page %} 60 | {% if page != pagination.page %} 61 | <a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a> 62 | {% else %} 63 | <strong>{{ page }}</strong> 64 | {% endif %} 65 | {% else %} 66 | <span class=ellipsis>…</span> 67 | {% endif %} 68 | {%- endfor %} 69 | </div> 70 | {% endmacro %} 71 | """ 72 | last = 0 73 | for num in xrange(1, self.pages + 1): 74 | if num <= left_edge or \ 75 | (num > self.page - left_current - 1 and 76 | num < self.page + right_current) or \ 77 | num > self.pages - right_edge: 78 | if last + 1 != num: 79 | yield None 80 | yield num 81 | last = num 82 | -------------------------------------------------------------------------------- /lib/session.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding=utf-8 3 | 4 | try: 5 | import psyco 6 | psyco.full() 7 | except: 8 | pass 9 | import cPickle as pickle 10 | from uuid import uuid4 11 | import time 12 | import logging 13 | 14 | 15 | class RedisSessionStore(object): 16 | 17 | def __init__(self, redis_connection, **options): 18 | self.options = { 19 | 'key_prefix': 'session', 20 | 'expire': 7200, 21 | } 22 | self.options.update(options) 23 | self.redis = redis_connection 24 | 25 | def prefixed(self, sid): 26 | return '%s:%s' % (self.options['key_prefix'], sid) 27 | 28 | def generate_sid(self): 29 | return uuid4().get_hex() 30 | 31 | def get_session(self, sid, name): 32 | data = self.redis.hget(self.prefixed(sid), name) 33 | session = pickle.loads(data) if data else dict() 34 | return session 35 | 36 | def set_session(self, sid, session_data, name, expiry=None): 37 | self.redis.hset(self.prefixed(sid), name, pickle.dumps(session_data)) 38 | expiry = expiry or self.options['expire'] 39 | if expiry: 40 | self.redis.expire(self.prefixed(sid), expiry) 41 | 42 | def delete_session(self, sid): 43 | self.redis.delete(self.prefixed(sid)) 44 | 45 | 46 | class Session(object): 47 | 48 | def __init__(self, session_store, session_id=None, expires_days=None): 49 | self._store = session_store 50 | self._sid = session_id if session_id else self._store.generate_sid() 51 | self._dirty = False 52 | self.set_expires(expires_days) 53 | try: 54 | self._data = self._store.get_session(self._sid, 'data') 55 | except: 56 | logging.error('Can not connect Redis server.') 57 | self._data = {} 58 | 59 | def clear(self): 60 | self._store.delete_session(self._sid) 61 | 62 | @property 63 | def id(self): 64 | return self._sid 65 | 66 | def access(self, remote_ip): 67 | access_info = {'remote_ip': remote_ip, 'time': '%.6f' % time.time()} 68 | self._store.set_session( 69 | self._sid, 70 | 'last_access', 71 | pickle.dumps(access_info)) 72 | 73 | def last_access(self): 74 | access_info = self._store.get_session(self._sid, 'last_access') 75 | return pickle.loads(access_info) 76 | 77 | def set_expires(self, days): 78 | self._expiry = days * 86400 if days else None 79 | 80 | def __getitem__(self, key): 81 | return self._data[key] 82 | 83 | def __setitem__(self, key, value): 84 | self._data[key] = value 85 | self._dirty = True 86 | 87 | def __delitem__(self, key): 88 | del self._data[key] 89 | self._dirty = True 90 | 91 | def __len__(self): 92 | return len(self._data) 93 | 94 | def __contains__(self, key): 95 | return key in self._data 96 | 97 | def __iter__(self): 98 | for key in self._data: 99 | yield key 100 | 101 | def __repr__(self): 102 | return self._data.__repr__() 103 | 104 | def __del__(self): 105 | self.save() 106 | 107 | def save(self): 108 | if self._dirty: 109 | self._store.set_session( 110 | self._sid, self._data, 'data', self._expiry) 111 | self._dirty = False 112 | -------------------------------------------------------------------------------- /manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf8 3 | try: 4 | import psyco 5 | psyco.full() 6 | except: 7 | pass 8 | import tornado 9 | import tornado.web 10 | from tornado.httpserver import HTTPServer 11 | from tornado.options import define, options 12 | from tornado.web import url 13 | import sys 14 | 15 | from lib import filters, session 16 | 17 | from core import jinja_environment, smtp_server 18 | from core import settings 19 | from core import redis_server 20 | 21 | define("cmd", default='runserver', metavar="runserver|createuser") 22 | define("port", default=9000, type=int) 23 | define("autoreload", default=False, type=bool) 24 | 25 | 26 | class Application(tornado.web.Application): 27 | 28 | def __init__(self): 29 | from urls import routes as handlers 30 | 31 | # init jiaja2 environment 32 | self.jinja_env = jinja_environment 33 | 34 | # register filters for jinja2 35 | self.jinja_env.filters.update(filters.register_filters()) 36 | self.jinja_env.tests.update({}) 37 | 38 | self.jinja_env.globals['settings'] = settings 39 | tornado.web.Application.__init__(self, handlers, **settings) 40 | self.session_store = session.RedisSessionStore(redis_server) 41 | self.email_backend = smtp_server 42 | 43 | 44 | def runserver(): 45 | http_server = HTTPServer(Application(), xheaders=True) 46 | http_server.listen(options.port) 47 | loop = tornado.ioloop.IOLoop.instance() 48 | print 'Server running on http://0.0.0.0:%d' % (options.port) 49 | loop.start() 50 | 51 | 52 | def createuser(): 53 | username = raw_input('input username: ') 54 | if username: 55 | from models import User 56 | q = User.select().where(User.username == username.strip()) 57 | if q.count() > 0: 58 | print 'username [ %s ] exists! please choice another one and try it again!' % (username) 59 | sys.exit(0) 60 | email = raw_input('input your Email: ') 61 | password = raw_input('input password: ') 62 | User.create(username=username, email=email.strip(), 63 | password=User.create_password(password)) 64 | print '%s created!' % (username) 65 | else: 66 | print 'username is null,exit!' 67 | sys.exit(0) 68 | 69 | 70 | def syncdb(): 71 | from lib.helpers import find_subclasses 72 | from models import db 73 | models = find_subclasses(db.Model) 74 | for model in models: 75 | if model.table_exists(): 76 | model.drop_table() 77 | model.create_table() 78 | print 'created table:', model._meta.db_table 79 | 80 | if __name__ == '__main__': 81 | tornado.options.parse_command_line() 82 | if options.cmd == 'runserver': 83 | runserver() 84 | elif options.cmd == 'createuser': 85 | createuser() 86 | elif options.cmd == 'syncdb': 87 | syncdb() 88 | -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | try: 4 | import psyco 5 | psyco.full() 6 | except: 7 | pass 8 | import peewee 9 | import datetime 10 | import hashlib 11 | import urllib 12 | from core import db 13 | from lib.helpers import create_token, cached_property 14 | from core import smtp_server, settings 15 | from config import DOMAIN as domain 16 | 17 | 18 | class User(db.Model): 19 | username = peewee.CharField() 20 | password = peewee.CharField() 21 | email = peewee.CharField() 22 | 23 | @staticmethod 24 | def create_password(raw): 25 | salt = create_token(8) 26 | passwd = '%s%s%s' % (salt, raw, 'blog_engine') 27 | hsh = hashlib.sha1(passwd).hexdigest() 28 | return "%s$%s" % (salt, hsh) 29 | 30 | def check_password(self, raw): 31 | if '$' not in self.password: 32 | return False 33 | salt, hsh = self.password.split('$') 34 | passwd = '%s%s%s' % (salt, raw, 'blog_engine') 35 | verify = hashlib.sha1(passwd).hexdigest() 36 | return verify == hsh 37 | 38 | class Meta: 39 | db_table = 'users' 40 | 41 | 42 | class Category(db.Model): 43 | name = peewee.CharField() 44 | slug = peewee.CharField() 45 | 46 | @property 47 | def url(self): 48 | return '/category/%s' % (urllib.quote(self.name.encode('utf8'))) 49 | 50 | class Meta: 51 | db_table = 'category' 52 | 53 | 54 | class Post(db.Model): 55 | title = peewee.CharField() 56 | slug = peewee.CharField(index=True, max_length=100) 57 | category = peewee.ForeignKeyField(Category, related_name='posts') 58 | content = peewee.TextField() 59 | readnum = peewee.IntegerField(default=0) 60 | tags = peewee.CharField(null=True) 61 | slug = peewee.CharField(null=True) 62 | created = peewee.DateTimeField(default=datetime.datetime.now) 63 | 64 | @property 65 | def url(self): 66 | return '/post/post-%d.html' % (self.id) 67 | 68 | @property 69 | def absolute_url(self): 70 | return '%s%s' % (domain, self.url) 71 | 72 | @property 73 | def comment_feed(self): 74 | return '%s/archive/%s/feed'(domain, self.id) 75 | 76 | @cached_property 77 | def prev(self): 78 | posts = Post.select().where(Post.created < self.created)\ 79 | .order_by(Post.created) 80 | return posts.get() if posts.exists() else None 81 | 82 | @cached_property 83 | def next(self): 84 | posts = Post.select().where(Post.created > self.created)\ 85 | .order_by(Post.created) 86 | return posts.get() if posts.exists() else None 87 | 88 | @property 89 | def summary(self): 90 | return self.content.split('<!--more-->')[0] if self.content else self.content 91 | 92 | def taglist(self): 93 | if self.tags: 94 | tags = [tag.strip() for tag in self.tags.split(",")] 95 | return set(tags) 96 | else: 97 | return None 98 | 99 | class Meta: 100 | db_table = "posts" 101 | order_by = ('-created',) 102 | 103 | 104 | class Tag(db.Model): 105 | name = peewee.CharField(max_length=50) 106 | post = peewee.IntegerField() 107 | 108 | @property 109 | def url(self): 110 | return '/tag/%s' % (urllib.quote(self.name.encode('utf8'))) 111 | 112 | 113 | class Comment(db.Model): 114 | post = peewee.ForeignKeyField(Post, related_name='comments') 115 | author = peewee.CharField() 116 | website = peewee.CharField(null=True) 117 | email = peewee.CharField() 118 | content = peewee.TextField() 119 | ip = peewee.TextField() 120 | parent_id = peewee.IntegerField(null=True) 121 | created = peewee.DateTimeField(default=datetime.datetime.now) 122 | 123 | @property 124 | def parent(self): 125 | p = Comment.select().where(Comment.parent_id == self.parent_id, Comment.id == self.id) 126 | return p.get() if p.exists() else None 127 | 128 | @property 129 | def url(self): 130 | return '%s/post/post-%s.html#comment-%s' % (domain, self.post.id, self.id) 131 | 132 | def gravatar_url(self, size=80): 133 | return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ 134 | (hashlib.md5(self.email.strip().lower().encode('utf-8')).hexdigest(), 135 | size) 136 | 137 | class Meta: 138 | db_table = 'comments' 139 | 140 | 141 | class Link(db.Model): 142 | name = peewee.CharField() 143 | url = peewee.CharField() 144 | 145 | class Meta: 146 | db_table = 'links' 147 | 148 | 149 | from playhouse.signals import post_save 150 | from lib.mail.message import TemplateEmailMessage 151 | 152 | 153 | @post_save(sender=Comment) 154 | def send_email(model_class, instance, created): 155 | if instance.parent_id == '0': 156 | message = TemplateEmailMessage(u"收到新的评论", 'mail/new_comment.html', 157 | settings['smtp_user'], to=[settings['admin_email']], connection=smtp_server, params={'comment': instance}) 158 | else: 159 | message = TemplateEmailMessage(u"评论有新的回复", 'mail/reply_comment.html', 160 | settings['smtp_user'], to=[instance.email], connection=smtp_server, params={'comment': instance}) 161 | message.send() 162 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tornado 2 | jinja2 3 | redis 4 | peewee 5 | markdown 6 | pygments -------------------------------------------------------------------------------- /static/default/images/app-systempref.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/static/default/images/app-systempref.png -------------------------------------------------------------------------------- /static/default/images/arrow2-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/static/default/images/arrow2-left.png -------------------------------------------------------------------------------- /static/default/images/arrow2-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/static/default/images/arrow2-right.png -------------------------------------------------------------------------------- /static/default/images/calendar2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/static/default/images/calendar2.png -------------------------------------------------------------------------------- /static/default/images/chat_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/static/default/images/chat_512.png -------------------------------------------------------------------------------- /static/default/images/comment_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/static/default/images/comment_white.png -------------------------------------------------------------------------------- /static/default/images/folder-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/static/default/images/folder-black.png -------------------------------------------------------------------------------- /static/default/images/folder-open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/static/default/images/folder-open.png -------------------------------------------------------------------------------- /static/default/images/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/static/default/images/folder.png -------------------------------------------------------------------------------- /static/default/images/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/static/default/images/link.png -------------------------------------------------------------------------------- /static/default/images/tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/static/default/images/tag.png -------------------------------------------------------------------------------- /static/default/images/text-x-generic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/static/default/images/text-x-generic.png -------------------------------------------------------------------------------- /static/default/images/text_list_bullets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/static/default/images/text_list_bullets.png -------------------------------------------------------------------------------- /static/default/style.css: -------------------------------------------------------------------------------- 1 | html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{border:0;font-family:inherit;font-size:100%;font-style:inherit;font-weight:inherit;margin:0;outline:0;padding:0;vertical-align:baseline;}body{font-size:12px;font-family:Arial,Helvetica,Tahoma,sans-serif;color:#333;line-height:160%;}h1,h2,h3,h4,h5,h6,b,strong{font-weight:bold;}img{border:none;}em{font-style:italic;}.cb{clear:both;}li{list-style-type:none;}a{text-decoration:none;color:#360;}a:hover{color:#3C0;}#header-wrap{height:115px;width:auto;background-color:#004600;color:white;}#header{width:960px;margin:0 auto;}#header a{color:#CCC;}#header h1,#header h2{font-size:45px;padding:25px 0 0;}#header h1 a,#header h2 a{color:white;}.h_top p{color:#ccc;margin-top:15px;}ul.main-menu{margin-top:10px;float:right;font-size:14px;}ul.main-menu li{float:left;margin-right:18px;position:relative;}#header ul.main-menu a:hover{color:white;}#header .current-menu-item a{color:white;}.main-submenu{position:absolute;width:105px;background-color:#004600;padding-top:8px;display:none;z-index:9;left:0;}.main-submenu li{width:100px;padding-left:5px;line-height:200%;}#header .main-submenu li a{font-size:12px;}.main-submenu ul.main-submenu{display:none;}#wrap{width:960px;margin:20px auto 0;}#main{margin-bottom:20px;}#content{width:670px;float:left;}.post-list .post-meta{width:160px;float:left;margin-right:20px;overflow:hidden;}.post-list .post-wrap{float:left;width:480px;overflow:hidden;}.post-list .post-content img{max-width:480px;width:auto;height:auto;-width:expression( width>480 ? '480px':true );display:block;margin:0 auto;}.post-list .post-meta p{padding-left:20px;border-bottom:1px dotted #ccc;margin-bottom:5px;line-height:180%;color:#777;}.post-list .post-meta a{color:#777;}.post-list p.time{background:url(images/calendar2.png) no-repeat left 3px;}.post-list .comment{background:url(images/chat_512.png) no-repeat left 3px;}.post-list .category{background:url(images/folder.png) no-repeat left 3px;}.post-list .tags{background:url(images/tag.png) no-repeat left 3px;}.post-list p.edit-link{border-bottom:none;}li.post-list{border-bottom:2px solid #BBCBB6;margin-bottom:20px;padding-bottom:20px;}.post-wrap h2{font-size:18px;margin-bottom:10px;}.post-content img{margin-bottom:5px;}.post-content{line-height:180%;}.paging{text-align:center;color:gray;font-family:"Times New Roman",Georgia,Times,serif;}.paging span,.paging a{margin-right:10px;}ul#sidebar{width:250px;float:right;overflow:hidden;line-height:180%;}li.widget{margin-bottom:20px;}h3.widgettitle{font-size:14px;padding-5px;border-bottom:2px solid #BBCBB6;margin-bottom:5px;padding-bottom:8px;}.widget ul li{background:url(images/text_list_bullets.png) no-repeat left 5px;padding-left:18px;}.widget_recent_entries ul li{background:url(images/text-x-generic.png) no-repeat left 5px;}.widget_archive ul li{background:url(images/folder-black.png) no-repeat left 5px;}.widget_categories ul li{background:url(images/folder-open.png) no-repeat left 5px;}.widget_meta ul li{background:url(images/app-systempref.png) no-repeat left 5px;}.widget_recent_comments ul li{background:url(images/comment_white.png) no-repeat left 5px;}.widget_links ul li{background:url(images/link.png) no-repeat left 5px;}table#wp-calendar{width:100%;text-align:center;}table#wp-calendar a,table#wp-calendar caption{font-weight:bold;}.widget_tag_cloud a{margin-right:5px;}.widget_tag_cloud a span{vertical-align:super;font-size:.8em;}#footer{border-top:1px dotted #BBCBB6;padding-top:20px;padding-bottom:20px;}#copyright{float:left;width:670px;}.daxiawp{float:right;width:300px;color:gray;font-style:italic;}.daxiawp a{color:gray;}.archive #content h1,.search #content h1{font-size:18px;margin-bottom:20px;padding-bottom:10px;border-bottom:2px solid #BBCBB6;}.singular h1{font-size:22px;border-bottom:2px solid #BBCBB6;margin-bottom:15px;padding-bottom:10px;}.singular img{max-width:650px;height:auto;width:auto;-width:expression( width>650 ? '650px':true );}.singular .post-content p{margin-bottom:10px;text-indent:2em;}img.aligncenter{margin:10px auto;display:block;}img.alignleft{display:block;float:left;margin-right:10px;}img.alignright{display:block;float:right;margin-left:10px;}.link-pages{text-align:center;margin:10px 0;}.singular div.post-meta{margin:10px 0;border-top:1px dotted #BBCBB6;padding-top:10px;line-height:220%;}.singular .post-meta div{padding-left:18px;}.singular div.time{background:url(images/calendar2.png) no-repeat left 5px;}.singular div.category{background:url(images/folder.png) no-repeat left 5px;}.singular div.tags{background:url(images/tag.png) no-repeat left 5px;}.singular div.previous{background:url(images/arrow2-left.png) no-repeat left 5px;}.singular div.next{background:url(images/arrow2-right.png) no-repeat left 5px;}#comments{border-top:2px solid #BBCBB6;}.comments-list h4,h3#reply-title{margin:10px 0;}.comments-list a{color:gray;}.comments-list ul.children{margin-left:40px;}.comments-list li.depth-1{margin-bottom:10px;}.form-allowed-tags{color:gray;}#respond .required,.comment-awaiting-moderation{color:red;}#commentform p{margin-bottom:10px;}#commentform label{display:inline-block;width:70px;line-height:18px;}#commentform .comment-form-author label,#commentform .comment-form-email label{width:62px;}input#author,input#email,input#url{height:18px;line-height:18px;width:250px;}textarea#comment{width:90%;}.error404 #content h1{font-size:16px;margin:20px auto;width:350px;}#ie7 #commentform .comment-form-author label,#ie7 #commentform .comment-form-email label,#ie6 #commentform .comment-form-author label,#ie6 #commentform .comment-form-email label{width:59px;}#ie6 .main-submenu,#ie7 .main-submenu{top:18px;}.label-success{padding:3px 3px 2px;font-size:11.25px;font-weight:bold;color:white;background-color:#999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;background-color:#468847;} -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dengmin/logpress-tornado/7bc22ba144bfd00956f0eb8c2adfb1b3992b549d/static/favicon.ico -------------------------------------------------------------------------------- /static/fluid/print.css: -------------------------------------------------------------------------------- 1 | body{font-family:'Lucida Grande',Verdana,Arial,Sans-Serif;}#header{border-bottom:1px solid #aaa;}a{background:transparent;color:black;text-decoration:none;}.search{display:none;}#hmenu,#nav{display:none;}#sidebar{display:none;}#footer{display:none;}.post blockquote{padding:0 0 0 1em;border-left:0.4em solid #ccc;font-size:small;}.postmetadata,.post-meta{clear:both;font-size:small;}.navigation{display:none;}#respond,#commentform,#comments .reply{display:none;}.commentlist img.avatar{float:right;margin:0 0 0 10px;padding:3px;border:1px solid #eee;}.aligncenter,div.aligncenter{display:block;margin-left:auto;margin-right:auto;}.alignleft{float:left;margin:5px 5px 5px 0;}.alignright{float:right;margin:5px 0 5px 5px;} -------------------------------------------------------------------------------- /static/fluid/style.css: -------------------------------------------------------------------------------- 1 | body{margin:0;background:#fff;color:#444;font-size:62.5%;font-family:'Lucida Grande',Verdana,Arial,Sans-Serif;text-align:center;}a{color:#258;text-decoration:none;}a:hover{text-decoration:underline;}h1,h1 a,h2,h2 a,h3,h4,h5,h6{margin-bottom:0;color:#27a;text-align:left;}h1{font-size:2.6em;}h2{font-size:2em;}h3{font-size:1.6em;}h4{font-size:1.2em;}h5{font-size:1em;}h6{font-size:0.8em;}img{border:0;}input{font-size:1.2em;}input[type=text],textarea{background:#fff;border:1px inset #aaa;}input[type=submit]{background:#eee;border:1px outset #aaa;}textarea{font-size:12px;}pre{font-size:12px;overflow:auto;}code{font-size:12px;background-color:#f8f8f8;color:#111;}#page{min-width:760px;margin:0 auto;text-align:left;}#wrapper{margin:0 5%;padding-right:230px;}#content{float:left;width:96%;border-right:230px solid #eee;margin-right:-230px;padding:20px 4% 20px 0;}#sidebar{float:left;width:190px;margin:0 -230px 0 0;padding:20px;background:#eee;}#footer{clear:both;padding:10px;border-top:0.2em solid #555;}#header{margin:0;padding:2em 0;height:4.8em;background:#237ab2;}#headertitle{float:left;position:absolute;top:2.4em;left:5%;}#headertitle h1{margin:0;}#headertitle h1 a{background:transparent;color:#fff;}#headertitle h1 a:hover{text-decoration:none;}#headertitle p{margin:0;background:transparent;color:#fff;font-size:1.1em;}.search{float:right;padding:1.5em 5% 0 0;}.search form{margin:0;padding:0;}.search input{display:inline;width:218px;border:1px solid #69b;margin:0;padding:0.2em 5px;background:#38b;color:#ddd;font-size:1.1em;}#navbar{border-top:0.1em solid #555;border-bottom:0.1em solid #555;background:#165279;height:2.3em;margin:0px;padding:0px;}#nav{margin:0 5%;padding:0;list-style:none;}#nav ul{padding:0.1em 0 0 0;margin:0;list-style:none;background:transparent;}#nav a{color:#c6c8c9;display:block;font-weight:bold;padding:0.5em;}#nav a:hover{background:#237ab2;color:#fff;display:block;text-decoration:none;padding:0.5em;}#nav li{float:left;margin:0;text-transform:uppercase;padding:0 2em 0 0;}#nav li li{float:left;margin:0;padding:0;width:14em;}#nav li li a,#nav li li a:link,#nav li li a:visited{background:#165279;color:#c6c8c9;width:14em;float:none;margin:0;padding:0.5em;border-bottom:1px solid #aaa;}#nav li li a:hover,#nav li li a:active{background:#237ab2;color:#fff;}#nav li ul{position:absolute;width:10em;left:-999em;}#nav li:hover ul{left:auto;display:block;}#nav li:hover ul,#nav li.sfhover ul{left:auto;}#sidebar ul{padding:0;margin:0;list-style:none;font-size:1.1em;}#sidebar ul ul{font-size:1em;}#sidebar ul li{margin:0 0 2em 0;}#sidebar ul ul{margin:0;padding:0;}#sidebar li li{margin:0.1em 0;}#sidebar li li li{padding-left:10px;}#sidebar ul h2{margin:0;padding:0;color:#4588c4;font-size:1.2em;text-transform:uppercase;}#footer{text-align:center;font-size:1em;background:#165279;color:#eee;}#footer a{color:#aac;}.post{margin:0 0 4em 0;clear:both;}.post p,.post ol li,.post ul li{margin-top:0;font-size:1.2em;line-height:1.5em;text-align:justify;}.post li li{font-size:1em;}.post blockquote{padding:0 0 0 2em;border-left:0.4em solid #ccc;font-size:0.9em;}.post blockquote blockquote{margin-left:0;font-size:1em;}.postentry a{border-bottom:1px solid #ddd;}.postentry a:hover{border-bottom:1px solid #258;text-decoration:none;}.postmetadata{clear:both;margin:1em 0;font-size:1.1em;color:#888;text-align:justify;}div.navigation{font-size:1.1em;}.postentry table{border-width:0 1px 1px 0;border-style:solid;border-color:#ccc;font-size:0.9em;}.postentry table tr td{padding:5px 10px;border-width:1px 0 0 1px;border-style:solid;border-color:#ccc;}.postentry table tr th{border-width:1px 0 0 1px;border-style:solid;border-color:#ccc;padding:5px 10px;background:#f4f4f4;color:#666;font-weight:bold;text-transform:uppercase;text-align:center;}#comments{font-size:1.2em;}.commentlist{margin:20px 0;padding:0;border-width:0 0.1em 0.1em 0;border-color:#eee;border-style:solid;}.commentlist li{list-style:none;margin:0;padding:0;border-width:0.1em 0 0 0.1em;border-color:#eee;border-style:solid;}li.comment div,li.pingback div{padding:20px;overflow:auto;}li.comment div div,li.pingback div div{padding:0;overflow:visible;}.commentlist li.even{background-color:#fafafa;}.commentlist li.odd{background-color:#f6f6f6;}ul.children li{list-style:none;}img.avatar{float:right;border:1px solid #eee;padding:2px;margin:0;background:#fff;}.comment-meta,.reply{margin:0;padding:0;font-size:0.8em;}.comment-author cite{font-style:normal;font-weight:bold;font-size:1.2em;}textarea#comment{width:100%;}#comments div.navigation{font-size:0.9em;}#wp-calendar caption{text-transform:uppercase;font-weight:bold;color:#aaa;text-align:left;}#wp-calendar thead th{font-weight:normal;color:#27a;text-align:center;}#wp-calendar tbody td{text-align:center;}#wp-calendar tbody td a{font-weight:bold;}#wp-calendar tbody td.pad{border:none;}abbr{cursor:help;border-bottom:0.1em dotted;}.aligncenter,div.aligncenter{display:block;margin-left:auto;margin-right:auto;}.alignleft{float:left;margin:5px 5px 5px 0;}.alignright{float:right;margin:5px 0 5px 5px;}.wp-caption{border:1px solid #ddd;text-align:center;background-color:#f3f3f3;padding-top:4px;margin:10px;}.wp-caption img{margin:0;padding:0;border:0 none;}.wp-caption p.wp-caption-text{font-size:11px;line-height:17px;padding:0 4px 5px;margin:0;}.tagcloud a span{vertical-align:super;} -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Crawl-delay: 10 3 | 4 | Disallow: /static/ 5 | Disallow: /admin/ 6 | Disallow: /templates/ 7 | Sitemap: {{settings.domain}}/sitemap.xml 8 | Sitemap: {{settings.domain}}/baidu.xml -------------------------------------------------------------------------------- /static/sitemap.xsl: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <xsl:stylesheet version="2.0" 3 | xmlns:html="http://www.w3.org/TR/REC-html40" 4 | xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9" 5 | xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> 6 | <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/> 7 | <xsl:template match="/"> 8 | <html xmlns="http://www.w3.org/1999/xhtml"> 9 | <head> 10 | <title>XML Sitemap 11 | 12 | 58 | 59 | 60 |

XML Sitemap

61 |
62 |

63 | This is a XML Sitemap which is supposed to be processed by search engines like Google, MSN Search and YAHOO.
64 | It was generated using the Blogging-Software WordPress and the Google Sitemap Generator Plugin by Arne Brachhold.
65 | You can find more information about XML sitemaps on sitemaps.org and Google's list of sitemap programs. 66 |

67 |
68 |
69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | high 82 | 83 | 91 | 94 | 97 | 100 | 101 | 102 |
URLPriorityChange FrequencyLastChange (GMT)
84 | 85 | 86 | 87 | 88 | 89 | 90 | 92 | 93 | 95 | 96 | 98 | 99 |
103 |
104 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /templates/admin/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Blog Administrator 7 | 8 | 9 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 43 | 44 |
45 |
46 |
47 | 56 |
57 |
58 | {%block main%}{%endblock%} 59 |
60 |
61 |
62 | 65 | 66 |
67 | 68 | 69 | -------------------------------------------------------------------------------- /templates/admin/category/index.html: -------------------------------------------------------------------------------- 1 | {%extends "admin/base.html" %} 2 | {% block main %} 3 |
4 |
5 | {{xsrf()}} 6 | 7 | 8 | 9 |
10 |
11 | 12 |

Category list

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for c in categories%} 21 | 22 | 23 | 24 | 25 | {%endfor%} 26 | 27 |
NameSlug
{{c.name}}{{c.slug}}
28 | {% endblock %} -------------------------------------------------------------------------------- /templates/admin/index.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base.html" %} 2 | {% block main %} 3 | {% endblock %} -------------------------------------------------------------------------------- /templates/admin/link/index.html: -------------------------------------------------------------------------------- 1 | {%extends "admin/base.html" %} 2 | {% block main %} 3 |
4 |
5 | {{xsrf()}} 6 | 7 | 8 | 9 |
10 |
11 | 12 |

Link list

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for o in pagination.items%} 21 | 22 | 23 | 24 | 25 | {%endfor%} 26 | 27 |
NameURL
{{o.name}}{{o.url}}
28 | {% from "macros/pagination.html" import admin_pagination%} 29 | {{admin_pagination(pagination,'/admin/links')}} 30 | {% endblock %} -------------------------------------------------------------------------------- /templates/admin/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Admin Login 6 | 7 | 8 | 9 | 43 | 44 | 45 | 46 |
47 | 48 |
49 | 50 | {{xsrf()}} 51 | {%set messages=handler.get_flashed_messages() %} 52 | {%if messages%} 53 |
54 | {% for type, msg in messages%} 55 | {{msg}} 56 | {% endfor %} 57 |
58 | {%endif%} 59 | 60 | 61 | 62 |
63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /templates/admin/post/add.html: -------------------------------------------------------------------------------- 1 | {%extends "admin/base.html"%} 2 | {%block main%} 3 |
4 | {{xsrf()}} 5 |
6 | Title 7 | 8 |
9 |
10 | Slug 11 | 12 |
13 | 21 | 22 |
23 | Tag 24 | 25 |
26 |
27 | 28 |
29 |
30 | {%endblock%} -------------------------------------------------------------------------------- /templates/admin/post/index.html: -------------------------------------------------------------------------------- 1 | {%extends "admin/base.html" %} 2 | {% block main %} 3 |
4 | New Post 5 |
6 | 7 |

Post list

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for post in pagination.items %} 18 | 19 | 20 | 21 | 22 | 24 | 25 | {%endfor%} 26 | 27 |
TitleCategoryTimeAction
{{post.title}}{{post.category.name}}{{post.created|datetimeformat}}Update 23 | Delete
28 | {% from "macros/pagination.html" import admin_pagination%} 29 | {{admin_pagination(pagination,'/admin/posts')}} 30 | {% endblock %} -------------------------------------------------------------------------------- /templates/admin/post/update.html: -------------------------------------------------------------------------------- 1 | {%extends "admin/base.html"%} 2 | {%block main%} 3 |
4 | {{xsrf()}} 5 |
6 | Title 7 | 8 |
9 |
10 | Slug 11 | 12 |
13 | 20 | 21 | 22 |
23 | Tag 24 | 25 |
26 |
27 | 28 |
29 |
30 | {%endblock%} -------------------------------------------------------------------------------- /templates/admin/user/index.html: -------------------------------------------------------------------------------- 1 | {%extends "admin/base.html" %} 2 | {% block main %} 3 | 4 | 5 |

User list

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for user in users%} 15 | 16 | 17 | 18 | 19 | 20 | 21 | {%endfor%} 22 | 23 |
IDNameEmail
{{user.id}}{{user.username}}{{user.email}}
24 | {% endblock %} -------------------------------------------------------------------------------- /templates/baidu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{settings.domain}} 4 | {{settings.site_name}} 5 | 1800 6 | {% for post in posts%} 7 | 8 | {{post.title}} 9 | {{settings.domain}}{{post.url}} 10 | {{post.summary|markdown}} 11 | 12 | {{post.tags}} 13 | {{settings.site_name}} 14 | {{settings.site_name}} 15 | {{post.created|datetimeformat}} 16 | 17 | {% endfor %} 18 | 19 | -------------------------------------------------------------------------------- /templates/comment_feed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <![CDATA[Comments on:{{post.title}}]]> 5 | 6 | 7 | {{settings.domain}} 8 | zh-cn 9 | Wed, 19 Dec 2012 01:26:06 +0000 10 | hourly 11 | 1 12 | Rss Powered By {{settings.site_name}} 13 | 14 | {% for comment in post.comments %} 15 | 16 | By: {{comment.author}} 17 | {{comment.url}} 18 | {{comment.author}} 19 | {{comment.created}} 20 | {{settings.domain}}/?p={{post.id}}#comment-{{comment.id}} 21 | 22 | {{comment.content}}

]]>
23 |
24 | {% endfor %} 25 |
26 |
-------------------------------------------------------------------------------- /templates/default/archive.html: -------------------------------------------------------------------------------- 1 | {% extends "default/base.html" %} 2 | {%block main%} 3 |
4 |
5 |

6 | 34 | {% from "macros/pagination.html" import render_pagination%} 35 | {{render_pagination(pagination,obj_url)}} 36 |
37 | 38 | {% include "default/sidebar.html" %} 39 | 40 |
41 | 42 |
43 | {%endblock%} -------------------------------------------------------------------------------- /templates/default/base.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 11 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {{settings.site_name}} 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 46 |
47 | 48 |
49 | {% block main %} 50 | {% endblock %} 51 | 64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /templates/default/comment.html: -------------------------------------------------------------------------------- 1 |
2 |

评论列表

3 |
4 |
5 |

6 | 发表评论 7 | 8 |

9 |
10 | {{xsrf()}} 11 |

电子邮件地址不会被公开。 必填项已用 * 标注

12 |

13 | 14 | 15 |

16 | 20 |

21 | 22 | 23 |

24 |

25 | 26 | 27 |

28 |

29 | 30 | 31 | 32 |

33 |
34 |
-------------------------------------------------------------------------------- /templates/default/index.html: -------------------------------------------------------------------------------- 1 | {% extends "default/base.html" %} 2 | {%block main%} 3 |
4 |
5 | 33 | {% from "macros/pagination.html" import render_pagination%} 34 | {{render_pagination(pagination,'/page')}} 35 |
36 | {% include "default/sidebar.html" %} 37 |
38 |
39 | {%endblock%} -------------------------------------------------------------------------------- /templates/default/post.html: -------------------------------------------------------------------------------- 1 | {% extends "default/base.html" %} 2 | {% block main %} 3 |
4 |
5 |

{{post.title}}

6 |
7 | {{post.content|markdown}} 8 |
9 |
10 | 31 |
32 | {%include "default/comment.html"%} 33 |
34 |
35 | {% include "default/sidebar.html" %} 36 |
37 |
38 | {% endblock %} -------------------------------------------------------------------------------- /templates/default/sidebar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/errors/404.html: -------------------------------------------------------------------------------- 1 | 404 not found -------------------------------------------------------------------------------- /templates/errors/exception.html: -------------------------------------------------------------------------------- 1 | {% set type, value, tback = sys.exc_info() %} 2 | 3 | 4 | 5 | 6 | HTTP Status {{ status_code }} 7 | 76 | 77 | 78 | 79 | 82 | 83 | {% if exception %} 84 | {% set traceback_list = traceback.extract_tb(tback) %} 85 | {% set filepath, line, method, code = traceback_list[-1] %} 86 | 87 |
88 |

Application raised {{ exception.__class__.__name__ }}: {{ exception }}

89 | 90 | 91 | 94 | 95 | 96 | 97 | 105 | 106 |
92 |

on line {{ line }} of {{ method }} in {{ os.path.basename(filepath) }}

93 |
File: {{ filepath }}
98 | {% set extension = os.path.splitext(filepath)[1][1:] %} 99 | {% if extension in ['py', 'html', 'htm'] %} 100 | {{ get_snippet(filepath, line, 10) }} 101 | {% else %} 102 |

Cannot load file, type not supported.

103 | {% endif %} 104 |
107 | 108 |

Full Traceback

109 |
110 | {% for filepath, line, method, code in traceback_list %} 111 | 112 | 113 | 116 | 117 | 118 | 119 | 127 | 128 |
114 |

on line {{ line }} of {{ method }} in {{ os.path.basename(filepath) }}

115 |
File: {{ filepath }}
120 | {% set extension = os.path.splitext(filepath)[1][1:] %} 121 | {% if extension in ['py', 'html', 'htm'] %} 122 | {{ get_snippet(filepath, line, 10) }} 123 | {% else %} 124 |

Cannot load file, type not supported.

125 | {% endif %} 126 |
129 |
130 | {% endfor %} 131 |
132 | 133 |

Request Headers

134 |
135 | 136 | {% for hk, hv in handler.request.headers.iteritems() %} 137 | 138 | 139 | 140 | 141 | {% endfor %} 142 |
{{ hk }}{{ hv }}
143 |
144 | 145 |

Response Headers

146 |
147 | 148 | {% for hk, hv in handler._headers.iteritems() %} 149 | 150 | 151 | 152 | 153 | {% endfor %} 154 |
{{ hk }}{{ hv }}
155 |
156 |
157 | {% endif %} 158 | 159 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /templates/feed.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | <![CDATA[{{settings.site_name}}]]> 5 | 6 | {{settings.domain}} 7 | zh-cn 8 | {{settings.site_desc}} 9 | hourly 10 | 1 11 | Rss Powered By {{settings.site_name}} 12 | {% for post in posts%} 13 | 14 | <![CDATA[{{post.title}}]]> 15 | {{post.absolute_url}} 16 | {{post.absolute_url}}#comments 17 | {{post.created|datetimeformat}} 18 | 19 | 20 | {{post.comment_feed}} 21 | {{post.comments.count()}} 22 | 23 | {% endfor %} 24 | 25 | 26 | -------------------------------------------------------------------------------- /templates/fluid-blue/archive.html: -------------------------------------------------------------------------------- 1 | {%extends "fluid-blue/base.html"%} 2 | {%block title%} 3 | {% if flag == 'category'%} 4 | Archive for the ‘{{name}}’ Category. 5 | {%elif flag=='tag'%} 6 | Posts tagged ‘{{name}}’ 7 | {%elif flag=='archives'%} 8 |

Archive for the ‘{{year}}-{{month}}’ Archive.

9 | {% endif %} 10 | {%endblock%} 11 | {%block main%} 12 | 13 |
14 | 15 | {% if pagination.total %} 16 | 17 | {% if flag == 'category'%} 18 |

Archive for the ‘{{name}}’ Category.

19 | {%elif flag=='tag'%} 20 |

Posts tagged ‘{{name}}’

21 | {%elif flag=='archives'%} 22 |

Archive for the ‘{{year}}-{{month}}’ Archive.

23 | {% endif %} 24 | 25 | 29 | 30 | {%for post in pagination.items %} 31 | 32 |
33 |

{{post.title}}

34 | 35 |
36 | {{post.summary|markdown}} 37 |
38 | 39 | 53 |
54 | 55 | {%endfor %} 56 | 57 | 61 | 62 | {%else%} 63 |
64 |

Not Found

65 |

Sorry, no posts matched your criteria.

66 |
67 | 68 | {%endif%} 69 | 70 |
71 | {%include "fluid-blue/sidebar.html"%} 72 | {%endblock%} -------------------------------------------------------------------------------- /templates/fluid-blue/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {%block title%}{%endblock%} {{settings.site_name}} 7 | 8 | 9 | 10 | 11 | 12 | 13 | {%block header%}{%endblock%} 14 | 15 | 16 | 17 |
18 | 30 | 31 | 39 |
40 | {%block main%} 41 | {%endblock%} 42 |
43 | 48 |
49 | 50 | -------------------------------------------------------------------------------- /templates/fluid-blue/comment.html: -------------------------------------------------------------------------------- 1 |
2 |

{{post.comments.count()}} Comments

3 |
    4 | {%for comment in post.comments%} 5 |
  1. 6 |
    7 |
    8 | 9 | 10 | {{comment.author}} 11 | says: 12 |
    13 | 16 |

    {{comment.content}} 17 |

    18 |
    19 |
    20 |
  2. 21 | {%endfor%} 22 |
23 |
24 |
25 | 26 |

Leave a Reply

27 | 28 | 31 |
32 | {{xsrf()}} 33 |

34 |

35 | 36 |

37 |

38 | 39 |

40 |

41 | 42 | 43 |

44 | {%set messages=handler.get_flashed_messages() %} 45 | {%if messages%} 46 |

47 | {% for type, msg in messages%} 48 | {{msg}} 49 | {% endfor %} 50 |

51 | {%endif%} 52 |

53 | 54 | 55 | 56 |

57 | 58 |
59 | 60 |
61 |
-------------------------------------------------------------------------------- /templates/fluid-blue/index.html: -------------------------------------------------------------------------------- 1 | {%extends "fluid-blue/base.html"%} 2 | {%block main%} 3 |
4 | 5 | {% if pagination.total%} 6 | {% for post in pagination.items %} 7 |
8 |

9 | {{post.title}} 10 | 11 |

12 | 13 |
14 | {{post.summary|markdown}} 15 |
16 | 17 | 31 |
32 | 33 | {%endfor%} 34 | 35 | 39 | 40 | {%else%} 41 |
42 |

Not Found

43 |

Sorry, no posts matched your criteria.

44 |
45 | {%endif%} 46 | 47 |
48 | {%include "fluid-blue/sidebar.html"%} 49 | {%endblock%} -------------------------------------------------------------------------------- /templates/fluid-blue/post.html: -------------------------------------------------------------------------------- 1 | {%extends "fluid-blue/base.html"%} 2 | {%block title%}{{post.title}}{%endblock%} 3 | {%block header%} 4 | {%if post%} 5 | 6 | 7 | {%endif%} 8 | {%endblock%} 9 | {%block main%} 10 |
11 | {%if post%} 12 | 20 | 21 |
22 |

23 | {{post.title}}

24 | 25 |
26 | {{post.content|markdown}} 27 |
28 | 29 | 44 |
45 | 46 | {%include "fluid-blue/comment.html"%} 47 | 48 | {%else%} 49 |
50 |

'Not Found

51 |

Sorry, no posts matched your criteria.

52 |
53 | {%endif%} 54 | 55 |
56 | {%include "fluid-blue/sidebar.html"%} 57 | {%endblock%} -------------------------------------------------------------------------------- /templates/fluid-blue/sidebar.html: -------------------------------------------------------------------------------- 1 | 79 | -------------------------------------------------------------------------------- /templates/macros/pagination.html: -------------------------------------------------------------------------------- 1 | {% macro render_pagination(pagination, endpoint) %} 2 |
3 | {% if pagination.has_prev %} 4 | 上一页 5 | {% endif %} 6 | {%- for page in pagination.iter_pages() %} 7 | {% if page %} 8 | {% if page != pagination.page %} 9 | {{ page }} 10 | {% else %} 11 | {{ page }} 12 | {% endif %} 13 | {% else %} 14 | 15 | {% endif %} 16 | {%- endfor %} 17 | {% if pagination.has_next %} 18 | 下一页 19 | {% endif %} 20 | 共 {{pagination.total}} 篇文章; {{pagination.page}}/{{pagination.pages}} 页 21 |
22 | {% endmacro %} 23 | 24 | 25 | {% macro admin_pagination(pagination, endpoint) %} 26 | 47 | {% endmacro %} -------------------------------------------------------------------------------- /templates/mail/new_comment.html: -------------------------------------------------------------------------------- 1 | 文章{{comment.post.title}} 有了新的评论
2 |

3 | 作者:{{comment.author}} (IP: {{comment.ip}})
4 | 电子邮件: {{comment.email}}
5 | URL: {{comment.weburl}}
6 | 评论:
7 | {{comment.content}} 8 | 9 |

10 | 点击回复 {{comment.url}} 11 | -------------------------------------------------------------------------------- /templates/mail/reply_comment.html: -------------------------------------------------------------------------------- 1 | {{comment.parent.author}} 您好,您之前在文章 "{{comment.post.title}}" 上的评论现在有了新的回复
2 |

3 | 您之前的评论是:
4 | {{comment.parent.content}}
5 |

6 | {{comment.author}}给您的新的回复如下:
7 | {{comment.content}}
8 |

9 | 您可以点击以下链接查看具体内容:
10 | {{comment.url}}
11 |

12 | 感谢您对 Logpress 的关注
13 |

14 | 该信件由系统自动发出, 请勿回复, 谢谢. -------------------------------------------------------------------------------- /templates/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{settings.domain}} 10 | {{today|datetimeformat("%Y-%m-%dT%H:%M:%S+00:00")}} 11 | daily 12 | 1.0 13 | 14 | 15 | {%for post in posts%} 16 | 17 | {{settings.domain}}{{post.url}} 18 | {{post.created|datetimeformat("%Y-%m-%dT%H:%M:%S+00:00") }} 19 | monthly 20 | 0.5 21 | 22 | {%endfor%} 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /urls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf8 3 | try: 4 | import psyco 5 | psyco.full() 6 | except: 7 | pass 8 | from tornado.web import url 9 | 10 | from handlers import account, admin, blog 11 | from handlers import ErrorHandler 12 | 13 | routes = [] 14 | routes.extend(blog.routes) 15 | routes.extend(account.routes) 16 | routes.extend(admin.routes) 17 | routes.append((r"/(.*)", ErrorHandler)) 18 | --------------------------------------------------------------------------------