Browse Source

some update and fix

delete some unecessary file
use newest flask_maple
use flask_babel instead of flask_babelex
use marked.js and org.js instead of topic preview endpoint
fix flask_login remember me
honmaple 6 years ago
parent
commit
523de6358b

+ 8 - 4
config.example

@@ -6,20 +6,21 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2016-05-20 12:31:46 (CST)
-# Last Update: 星期日 2018-02-11 15:43:26 (CST)
+# Last Update: Thursday 2018-07-26 11:41:00 (CST)
 #          By: jianglin
 # Description:
 # **************************************************************************
 from datetime import timedelta
-from os import path, pardir
+from os import path
 
+PATH = path.abspath(path.dirname(__file__))
 DEBUG = True
 SECRET_KEY = 'secret key'
 SECURITY_PASSWORD_SALT = 'you will never guess'
 SECRET_KEY_SALT = 'you will never guess'
 
 # avatar upload directory
-AVATAR_FOLDER = path.join(path.abspath(path.dirname(__file__)), 'avatar')
+AVATAR_FOLDER = path.join(PATH, 'avatars')
 # avatar generate range
 AVATAR_RANGE = [122, 512]
 
@@ -88,11 +89,14 @@ LOGGING = {
 SQLALCHEMY_DATABASE_URI = 'postgresql://postgres:password@localhost/your_db'
 # SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db'
 
-MSEARCH_INDEX_NAME = 'whoosh_index'
+MSEARCH_INDEX_NAME = 'msearch'
 MSEARCH_BACKEND = 'whoosh'
 # SQLALCHEMY_ECHO = True
 # SQLALCHEMY_DATABASE_URI = 'sqlite:///test.db'
 # SQLALCHEMY_DATABASE_URI = 'mysql://username:password@server/db'
+BABEL_DEFAULT_LOCALE = 'en'
+BABEL_DEFAULT_TIMEZONE = 'UTC'
+BABEL_TRANSLATION_DIRECTORIES = path.join(PATH, 'translations')
 
 # Locale
 LANGUAGES = {'en': 'English', 'zh': 'Chinese'}

+ 4 - 5
forums/__init__.py

@@ -6,7 +6,7 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2017-01-25 20:10:50 (CST)
-# Last Update: 星期日 2018-02-11 15:05:26 (CST)
+# Last Update: Thursday 2018-07-26 09:57:28 (CST)
 #          By:
 # Description:
 # **************************************************************************
@@ -21,10 +21,9 @@ from flask_maple import auth
 
 
 def create_app(config):
-    templates = os.path.abspath(
-        os.path.join(os.path.dirname(__file__), os.pardir, 'templates'))
-    static = os.path.abspath(
-        os.path.join(os.path.dirname(__file__), os.pardir, 'static'))
+    path = os.path.dirname(__file__)
+    templates = os.path.abspath(os.path.join(path, os.pardir, 'templates'))
+    static = os.path.abspath(os.path.join(path, os.pardir, 'static'))
 
     app = Flask(__name__, template_folder=templates, static_folder=static)
     app.config.from_object(config)

+ 5 - 5
forums/api/collect/views.py

@@ -6,12 +6,12 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2017-03-28 16:15:08 (CST)
-# Last Update:星期三 2017-5-10 16:35:10 (CST)
+# Last Update: Thursday 2018-07-26 10:45:40 (CST)
 #          By:
 # Description:
 # **************************************************************************
 from flask import redirect, render_template, request, url_for
-from flask_babelex import gettext as _
+from flask_babel import gettext as _
 from flask_login import current_user
 
 from flask_maple.form import form_validate
@@ -22,7 +22,7 @@ from forums.api.forms import (CollectForm, ReplyForm, TopicForm,
 from forums.api.forums.models import Board
 from forums.api.tag.models import Tags
 from forums.api.topic.models import Topic
-from forums.common.serializer import Serializer
+from flask_maple.serializer import Serializer
 from forums.common.utils import gen_filter_dict, gen_order_by
 from forums.common.views import IsAuthMethodView as MethodView
 from forums.api.message.models import MessageClient
@@ -35,7 +35,7 @@ class CollectListView(MethodView):
         query_dict = request.data
         user = request.user
         form = CollectForm()
-        page, number = self.page_info
+        page, number = self.pageinfo
         keys = ['name']
         order_by = gen_order_by(query_dict, keys)
         filter_dict = gen_filter_dict(query_dict, keys)
@@ -63,7 +63,7 @@ class CollectListView(MethodView):
 class CollectView(MethodView):
     def get(self, pk):
         user = request.user
-        page, number = self.page_info
+        page, number = self.pageinfo
         collect = Collect.query.filter_by(
             id=pk, author_id=user.id).first_or_404()
         form = CollectForm()

+ 5 - 5
forums/api/follow/views.py

@@ -6,7 +6,7 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2016-12-22 21:49:05 (CST)
-# Last Update:星期六 2017-4-1 19:52:14 (CST)
+# Last Update: Thursday 2018-07-26 10:45:39 (CST)
 #          By:
 # Description:
 # **************************************************************************
@@ -24,7 +24,7 @@ from forums.api.message.models import MessageClient
 class FollowingTagsView(MethodView):
     def get(self):
         user = request.user
-        page, number = self.page_info
+        page, number = self.pageinfo
         filter_dict = {'followers__username': user.username}
         tags = Tags.query.filter_by(**filter_dict).paginate(page, number, True)
         data = {'tags': tags}
@@ -56,7 +56,7 @@ class FollowingTagsView(MethodView):
 class FollowingTopicsView(MethodView):
     def get(self):
         user = request.user
-        page, number = self.page_info
+        page, number = self.pageinfo
         filter_dict = {'followers__username': user.username}
         topics = Topic.query.filter_by(**filter_dict).paginate(page, number,
                                                                True)
@@ -91,7 +91,7 @@ class FollowingTopicsView(MethodView):
 class FollowingUsersView(MethodView):
     def get(self):
         user = request.user
-        page, number = self.page_info
+        page, number = self.pageinfo
         users = user.following_users.paginate(page, number, True)
         data = {'users': users}
         return render_template('follow/following_users.html', **data)
@@ -123,7 +123,7 @@ class FollowingUsersView(MethodView):
 class FollowingCollectsView(MethodView):
     def get(self):
         user = request.user
-        page, number = self.page_info
+        page, number = self.pageinfo
         filter_dict = {'followers__username': user.username}
         collects = Collect.query.filter_by(**filter_dict).paginate(
             page, number, True)

+ 18 - 20
forums/api/forms.py

@@ -6,12 +6,12 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2017-03-28 12:53:02 (CST)
-# Last Update:星期日 2017-4-9 12:41:11 (CST)
+# Last Update: Thursday 2018-07-26 10:51:40 (CST)
 #          By:
 # Description:
 # **************************************************************************
 from flask import redirect, session, url_for
-from flask_babelex import lazy_gettext as _
+from flask_babel import lazy_gettext as _
 from flask_wtf import FlaskForm as Form
 from flask_wtf.file import FileAllowed, FileField, FileRequired
 from wtforms import (BooleanField, PasswordField, RadioField, SelectField,
@@ -45,14 +45,11 @@ def form_board():
 
 class BaseForm(Form):
     username = StringField(
-        _('Username:'), [DataRequired(), Length(
-            min=4, max=20)])
+        _('Username:'), [DataRequired(), Length(min=4, max=20)])
     password = PasswordField(
-        _('Password:'), [DataRequired(), Length(
-            min=4, max=20)])
+        _('Password:'), [DataRequired(), Length(min=4, max=20)])
     captcha = StringField(
-        _('Captcha:'), [DataRequired(), Length(
-            min=4, max=4)])
+        _('Captcha:'), [DataRequired(), Length(min=4, max=4)])
 
     def validate(self):
         rv = Form.validate(self)
@@ -90,10 +87,6 @@ class SortForm(Form):
     desc = SelectField('Up and Down', coerce=int, choices=DESC)
 
 
-class SearchForm(Form):
-    search = StringField(_('search'), validators=[DataRequired()])
-
-
 class MessageForm(Form):
     message = TextAreaField(_('message'), validators=[DataRequired()])
 
@@ -115,7 +108,9 @@ class CollectForm(Form):
     name = StringField(_('Name:'), [DataRequired()])
     description = TextAreaField(_('Description:'))
     is_hidden = RadioField(
-        'Is_hidden:', choices=[(0, 'is_hidden'), (1, 'is_public')], coerce=int)
+        'Is_hidden:',
+        choices=[(0, _('is_hidden')), (1, _('is_public'))],
+        coerce=int)
 
 
 choices = UserSetting.STATUS
@@ -126,8 +121,10 @@ locale = UserSetting.LOCALE
 class AvatarForm(Form):
     avatar = FileField(
         _('Upload Avatar:'),
-        validators=[FileRequired(), FileAllowed(['jpg', 'png'],
-                                                '上传文件只能为图片且图片格式为jpg,png')])
+        validators=[
+            FileRequired(),
+            FileAllowed(['jpg', 'png'], '上传文件只能为图片且图片格式为jpg,png')
+        ])
 
 
 class PrivacyForm(Form):
@@ -148,13 +145,14 @@ class ProfileForm(Form):
 
 class PasswordForm(Form):
     old_password = PasswordField(
-        _('Old Password:'), [DataRequired(), Length(
-            min=4, max=20)])
+        _('Old Password:'),
+        [DataRequired(), Length(min=4, max=20)])
     new_password = PasswordField(
-        _('New Password:'), [DataRequired(), Length(
-            min=4, max=20)])
+        _('New Password:'),
+        [DataRequired(), Length(min=4, max=20)])
     rnew_password = PasswordField(
-        _('New Password again:'), [DataRequired(), EqualTo('new_password')])
+        _('New Password again:'),
+        [DataRequired(), EqualTo('new_password')])
 
 
 class BabelForm(Form):

+ 4 - 4
forums/api/forums/views.py

@@ -6,12 +6,12 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2016-12-17 20:45:08 (CST)
-# Last Update:星期二 2017-5-2 11:52:28 (CST)
+# Last Update: Thursday 2018-07-26 10:45:40 (CST)
 #          By:
 # Description:
 # **************************************************************************
 from flask import render_template, request
-from flask_babelex import gettext as _
+from flask_babel import gettext as _
 
 from forums.api.topic.models import Topic
 from forums.common.views import BaseMethodView as MethodView
@@ -53,7 +53,7 @@ class ContactView(MethodView):
 class BoardListView(MethodView):
     def get(self):
         query_dict = request.data
-        page, number = self.page_info
+        page, number = self.pageinfo
         keys = ['name']
         order_by = gen_order_by(query_dict, keys)
         filter_dict = gen_filter_dict(query_dict, keys)
@@ -74,7 +74,7 @@ class BoardView(MethodView):
 
     def topics(self, boardId, has_children):
         query_dict = request.data
-        page, number = self.page_info
+        page, number = self.pageinfo
         keys = ['title']
         # order_by = gen_order_by(query_dict, keys)
         # filter_dict = gen_filter_dict(query_dict, keys)

+ 3 - 3
forums/api/message/models.py

@@ -6,14 +6,14 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2017-04-01 18:33:37 (CST)
-# Last Update: 星期日 2018-02-11 15:06:58 (CST)
+# Last Update: Thursday 2018-07-26 10:05:23 (CST)
 #          By:
 # Description:
 # **************************************************************************
 from flask import url_for
 from flask_login import current_user
 from forums.extension import db
-from forums.jinja import safe_markdown
+from forums.jinja import markdown
 from forums.common.models import CommonTimeMixin
 
 
@@ -112,7 +112,7 @@ class Message(CommonTimeMixin, db.Model):
 
     @property
     def title(self):
-        return safe_markdown(self.message_text.title)
+        return markdown(self.message_text.title)
         # return self.message_text.title
 
     @property

+ 2 - 2
forums/api/message/views.py

@@ -6,7 +6,7 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2017-04-01 18:34:07 (CST)
-# Last Update:星期六 2017-4-1 20:48:15 (CST)
+# Last Update: Thursday 2018-07-26 10:45:40 (CST)
 #          By:
 # Description:
 # **************************************************************************
@@ -23,7 +23,7 @@ class MessageListView(MethodView):
         query_dict = request.data
         user = request.user
         status = query_dict.pop('status', '0')
-        page, number = self.page_info
+        page, number = self.pageinfo
         messages = Message.query.filter_by(
             receiver_id=user.id,
             status=status).order_by('-created_at').paginate(page, number, True)

+ 2 - 2
forums/api/search/views.py

@@ -6,7 +6,7 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2017-03-31 17:26:28 (CST)
-# Last Update:星期三 2017-12-13 13:50:15 (CST)
+# Last Update: Thursday 2018-07-26 10:45:40 (CST)
 #          By:
 # Description:
 # **************************************************************************
@@ -18,7 +18,7 @@ from forums.api.topic.models import Topic
 class SearchView(MethodView):
     def get(self):
         query_dict = request.data
-        page, number = self.page_info
+        page, number = self.pageinfo
         keyword = query_dict.pop('keyword', None)
         include = query_dict.pop('include', '0')
         if keyword and len(keyword) >= 2:

+ 4 - 4
forums/api/tag/views.py

@@ -6,7 +6,7 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2016-12-15 22:07:04 (CST)
-# Last Update:星期日 2017-4-9 12:46:47 (CST)
+# Last Update: Thursday 2018-07-26 10:45:40 (CST)
 #          By:
 # Description:
 # **************************************************************************
@@ -28,7 +28,7 @@ class TagsListView(MethodView):
 
     def get(self):
         query_dict = request.data
-        page, number = self.page_info
+        page, number = self.pageinfo
         keys = ['name']
         order_by = gen_order_by(query_dict, keys)
         filter_dict = gen_filter_dict(query_dict, keys)
@@ -40,7 +40,7 @@ class TagsListView(MethodView):
 
 class TagsView(MethodView):
     def get(self, name):
-        page, number = self.page_info
+        page, number = self.pageinfo
         tag = Tags.query.filter_by(name=name).first_or_404()
         topics = self.topics(tag)
         data = {'title': tag.name, 'tag': tag, 'topics': topics}
@@ -48,7 +48,7 @@ class TagsView(MethodView):
 
     def topics(self, tag):
         query_dict = request.data
-        page, number = self.page_info
+        page, number = self.pageinfo
         keys = ['name']
         # order_by = gen_order_by(query_dict, keys)
         # filter_dict = gen_filter_dict(query_dict, keys)

+ 3 - 3
forums/api/topic/models.py

@@ -6,7 +6,7 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2016-12-15 20:52:07 (CST)
-# Last Update: 星期日 2018-02-11 15:06:01 (CST)
+# Last Update: Thursday 2018-07-26 11:35:50 (CST)
 #          By:
 # Description:
 # **************************************************************************
@@ -21,7 +21,7 @@ from forums.api.user.models import User
 from forums.common.models import CommonUserMixin
 from forums.extension import db
 from forums.count import Count
-from forums.jinja import safe_markdown, safe_clean, markdown
+from forums.jinja import safe_clean, markdown
 
 topic_follower = db.Table(
     'topic_follower',
@@ -93,7 +93,7 @@ class Topic(db.Model, ModelMixin):
         if self.content_type == Topic.CONTENT_TYPE_TEXT:
             return safe_clean(self.content)
         elif self.content_type == Topic.CONTENT_TYPE_MARKDOWN:
-            return markdown(self.content)
+            return markdown(self.content, False)
         return self.content
 
     @property

+ 2 - 4
forums/api/topic/urls.py

@@ -6,14 +6,14 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2016-12-15 22:15:34 (CST)
-# Last Update:星期五 2017-11-10 10:57:47 (CST)
+# Last Update: Thursday 2018-07-26 10:07:07 (CST)
 #          By:
 # Description:
 # **************************************************************************
 from flask import Blueprint
 
 from .views import (LikeView, ReplyListView, ReplyView, TopicAskView,
-                    TopicEditView, TopicListView, TopicPreviewView, TopicView)
+                    TopicEditView, TopicListView, TopicView)
 
 site = Blueprint('topic', __name__)
 
@@ -23,14 +23,12 @@ topic_top_list = TopicListView.as_view('top')
 topic = TopicView.as_view('topic')
 ask_view = TopicAskView.as_view('ask')
 edit_view = TopicEditView.as_view('edit')
-preview_view = TopicPreviewView.as_view('preview')
 
 reply_list = ReplyListView.as_view('reply_list')
 reply = ReplyView.as_view('reply')
 like_view = LikeView.as_view('reply_like')
 
 site.add_url_rule('/topic/ask', view_func=ask_view)
-site.add_url_rule('/topic/preview', view_func=preview_view)
 site.add_url_rule('/topic', view_func=topic_list)
 site.add_url_rule('/topic/top', view_func=topic_top_list)
 site.add_url_rule('/topic/good', view_func=topic_good_list)

+ 5 - 17
forums/api/topic/views.py

@@ -6,12 +6,12 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2016-12-15 22:07:39 (CST)
-# Last Update: 星期日 2018-02-11 15:07:05 (CST)
+# Last Update: Thursday 2018-07-26 11:36:27 (CST)
 #          By:
 # Description:
 # **************************************************************************
 from flask import Markup, redirect, render_template, request, url_for
-from flask_babelex import gettext as _
+from flask_babel import gettext as _
 from flask_login import current_user, login_required
 
 from flask_maple.form import form_validate
@@ -22,11 +22,10 @@ from forums.api.forms import (CollectForm, ReplyForm, TopicForm,
 from forums.api.forums.models import Board
 from forums.api.tag.models import Tags
 from forums.api.utils import gen_topic_filter, gen_topic_orderby
-from forums.common.serializer import Serializer
+from flask_maple.serializer import Serializer
 from forums.common.utils import gen_filter_dict, gen_order_by
 from forums.common.views import BaseMethodView as MethodView
 from forums.common.views import IsAuthMethodView, IsConfirmedMethodView
-from forums.jinja import safe_markdown
 
 from .models import Reply, Topic
 from .permissions import (like_permission, reply_list_permission,
@@ -59,23 +58,12 @@ class TopicEditView(IsConfirmedMethodView):
         return render_template('topic/edit.html', **data)
 
 
-class TopicPreviewView(IsConfirmedMethodView):
-    @login_required
-    def post(self):
-        post_data = request.data
-        content_type = post_data.pop('content_type', None)
-        content = post_data.pop('content', None)
-        if content_type == Topic.CONTENT_TYPE_MARKDOWN:
-            return safe_markdown(content)
-        return content
-
-
 class TopicListView(MethodView):
     decorators = (topic_list_permission, )
 
     def get(self):
         query_dict = request.data
-        page, number = self.page_info
+        page, number = self.pageinfo
         keys = ['title']
         # order_by = gen_order_by(query_dict, keys)
         # filter_dict = gen_filter_dict(query_dict, keys)
@@ -135,7 +123,7 @@ class TopicView(MethodView):
         form = ReplyForm()
         query_dict = request.data
         topic = Topic.query.filter_by(id=topicId).first_or_404()
-        page, number = self.page_info
+        page, number = self.pageinfo
         keys = ['title']
         order_by = gen_order_by(query_dict, keys)
         filter_dict = gen_filter_dict(query_dict, keys)

+ 2 - 2
forums/api/user/models.py

@@ -6,14 +6,14 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2016-12-15 21:09:08 (CST)
-# Last Update:星期五 2018-01-05 00:25:26 (CST)
+# Last Update: Wednesday 2018-07-25 18:54:54 (CST)
 #          By:
 # Description:
 # **************************************************************************
 from datetime import datetime, timedelta
 
 from flask import current_app
-from flask_babelex import lazy_gettext as _
+from flask_babel import lazy_gettext as _
 from flask_login import current_user, login_user, logout_user
 from flask_principal import Identity, identity_changed, AnonymousIdentity
 from pytz import all_timezones

+ 5 - 5
forums/api/user/views.py

@@ -6,7 +6,7 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2016-12-15 22:08:06 (CST)
-# Last Update: Thursday 2018-03-01 17:58:50 (CST)
+# Last Update: Thursday 2018-07-26 10:45:40 (CST)
 #          By:
 # Description:
 # **************************************************************************
@@ -26,7 +26,7 @@ class UserListView(MethodView):
     @login_required
     def get(self):
         query_dict = request.data
-        page, number = self.page_info
+        page, number = self.pageinfo
         keys = ['username']
         order_by = gen_order_by(query_dict, keys)
         filter_dict = gen_filter_dict(query_dict, keys)
@@ -39,7 +39,7 @@ class UserView(MethodView):
     def get(self, username):
         query_dict = request.data
         user = User.query.filter_by(username=username).first_or_404()
-        page, number = self.page_info
+        page, number = self.pageinfo
         keys = ['title']
         order_by = gen_order_by(query_dict, keys)
         filter_dict = gen_filter_dict(query_dict, keys)
@@ -65,7 +65,7 @@ class UserReplyListView(MethodView):
     def get(self, username):
         query_dict = request.data
         user = User.query.filter_by(username=username).first_or_404()
-        page, number = self.page_info
+        page, number = self.pageinfo
         keys = ['title']
         order_by = gen_order_by(query_dict, keys)
         filter_dict = gen_filter_dict(query_dict, keys)
@@ -91,7 +91,7 @@ class UserFollowerListView(MethodView):
     @login_required
     def get(self, username):
         user = User.query.filter_by(username=username).first_or_404()
-        page, number = self.page_info
+        page, number = self.pageinfo
         followers = user.followers.paginate(page, number, True)
         data = {'followers': followers, 'user': user}
         return render_template('user/followers.html', **data)

+ 2 - 3
forums/common/middleware.py

@@ -6,13 +6,13 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2016-11-12 13:29:17 (CST)
-# Last Update:星期五 2018-01-05 00:44:56 (CST)
+# Last Update: Sunday 2018-03-04 22:38:33 (CST)
 #          By:
 # Description:
 # **************************************************************************
 from flask import g, request, abort
 from flask_login import current_user
-from forums.api.forms import SortForm, SearchForm
+from forums.api.forms import SortForm
 from .records import mark_online, load_online_users
 
 
@@ -31,7 +31,6 @@ class GlobalMiddleware(object):
         g.user = current_user
         g.sort_form = SortForm()
         g.sort_form = set_form(g.sort_form)
-        g.search_form = SearchForm()
         request.user = current_user._get_current_object()
         if request.method == 'GET':
             request.data = request.args.to_dict()

+ 2 - 2
forums/common/response.py

@@ -6,12 +6,12 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2016-10-25 21:07:00 (CST)
-# Last Update:星期六 2017-4-1 22:6:39 (CST)
+# Last Update: Wednesday 2018-07-25 18:54:54 (CST)
 #          By:
 # Description:
 # **************************************************************************
 from flask import jsonify
-from flask_babelex import gettext as _
+from flask_babel import gettext as _
 
 
 class HTTPResponse(object):

+ 0 - 196
forums/common/serializer.py

@@ -1,196 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-# **************************************************************************
-# Copyright © 2016 jianglin
-# File Name: serializer.py
-# Author: jianglin
-# Email: xiyang0807@gmail.com
-# Created: 2016-12-13 22:08:23 (CST)
-# Last Update:星期一 2017-3-13 13:28:30 (CST)
-#          By:
-# Description:
-# **************************************************************************
-from sqlalchemy import inspect
-from sqlalchemy.orm.interfaces import (ONETOMANY, MANYTOMANY)
-
-
-class PageInfo(object):
-    def __init__(self, paginate):
-        self.paginate = paginate
-
-    def as_dict(self):
-        pageinfo = {
-            'items': True,
-            'pages': self.paginate.pages,
-            'has_prev': self.paginate.has_prev,
-            'page': self.paginate.page,
-            'has_next': self.paginate.has_next,
-            'iter_pages': list(
-                self.paginate.iter_pages(
-                    left_edge=1, left_current=2, right_current=3,
-                    right_edge=1))
-        }
-        return pageinfo
-
-
-class Field(object):
-    def __init__(self, source, args={}, default=None):
-        self.source = source
-        self.args = args
-        self.default = default
-
-    def data(self, instance):
-        if hasattr(instance, self.source):
-            source = getattr(instance, self.source)
-            if not callable(source):
-                return source
-            return source(**self.args)
-        return self.default
-
-
-class Serializer(object):
-    def __init__(self,
-                 instance,
-                 many=False,
-                 include=[],
-                 exclude=[],
-                 extra=[],
-                 depth=2):
-        self.instance = instance
-        self.many = many
-        self.depth = depth
-        self.include = include
-        self.exclude = exclude
-        self.extra = extra
-
-    @property
-    def data(self):
-        meta = self.Meta
-        if not self.include and hasattr(meta, 'include'):
-            self.include = meta.include
-        if not self.exclude and hasattr(meta, 'exclude'):
-            self.exclude = meta.exclude
-        if not self.extra and hasattr(meta, 'extra'):
-            self.extra = meta.extra
-        # if not self.depth:
-        #     self.depth = meta.depth if hasattr(meta, 'depth') else 2
-        # if self.include and self.exclude:
-        #     raise ValueError('include and exclude can\'t work together')
-        if self.many:
-            return self._serializerlist(self.instance, self.depth)
-        return self._serializer(self.instance, self.depth)
-
-    def _serializerlist(self, instances, depth):
-        results = []
-        for instance in instances:
-            result = self._serializer(instance, depth)
-            if result:
-                results.append(result)
-        return results
-
-    def _serializer(self, instance, depth):
-        result = {}
-        if depth == 0:
-            return result
-        depth -= 1
-        model_class = self.get_model_class(instance)
-        inp = self.get_inspect(model_class)
-        model_data = self._serializer_model(inp, instance, depth)
-        relation_data = self._serializer_relation(inp, instance, depth)
-        extra_data = self._serializer_extra(instance)
-        result.update(model_data)
-        result.update(relation_data)
-        result.update(extra_data)
-        return result
-
-    def _serializer_extra(self, instance):
-        extra = self.extra
-        result = {}
-        for e in extra:
-            # extra_column = getattr(self, e)
-            # if isinstance(extra_column, Field):
-            #     result[e] = extra_column.data(instance)
-            # else:
-            extra_column = getattr(instance, e)
-            result[e] = extra_column if not callable(
-                extra_column) else extra_column()
-        return result
-
-    def _serializer_model(self, inp, instance, depth):
-        result = {}
-        model_columns = self.get_model_columns(inp)
-        for column in model_columns:
-            result[column] = getattr(instance, column)
-        return result
-
-    def _serializer_relation(self, inp, instance, depth):
-        result = {}
-        relation_columns = self.get_relation_columns(inp)
-        for relation in relation_columns:
-            column = relation.key
-            serializer = Serializer
-            if hasattr(self, column):
-                serializer = getattr(self, column)
-            if relation.direction in [ONETOMANY, MANYTOMANY
-                                      ] and relation.uselist:
-                children = getattr(instance, column)
-                if relation.lazy == 'dynamic':
-                    children = children.all()
-                result[column] = serializer(
-                    children,
-                    many=True,
-                    exclude=[relation.back_populates],
-                    depth=depth).data if children else []
-            else:
-                child = getattr(instance, column)
-                if relation.lazy == 'dynamic':
-                    child = child.first()
-                result[column] = serializer(
-                    child,
-                    many=False,
-                    exclude=[relation.back_populates],
-                    depth=depth).data if child else {}
-        return result
-
-    def get_model_class(self, instance):
-        return getattr(instance, '__class__')
-
-    def get_inspect(self, model_class):
-        return inspect(model_class)
-
-    def get_model_columns(self, inp):
-        if self.include:
-            model_columns = [
-                column.name for column in inp.columns
-                if column.name in self.include
-            ]
-        elif self.exclude:
-            model_columns = [
-                column.name for column in inp.columns
-                if column.name not in self.exclude
-            ]
-        else:
-            model_columns = [column.name for column in inp.columns]
-
-        return model_columns
-
-    def get_relation_columns(self, inp):
-        if self.include:
-            relation_columns = [
-                relation for relation in inp.relationships
-                if relation.key in self.include
-            ]
-        elif self.exclude:
-            relation_columns = [
-                relation for relation in inp.relationships
-                if relation.key not in self.exclude
-            ]
-        else:
-            relation_columns = [relation for relation in inp.relationships]
-        return relation_columns
-
-    class Meta:
-        depth = 2
-        include = []
-        exclude = []
-        extra = []

+ 3 - 17
forums/common/views.py

@@ -6,14 +6,13 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2017-03-13 13:29:37 (CST)
-# Last Update:星期五 2017-7-28 11:35:19 (CST)
+# Last Update: Sunday 2018-03-04 22:37:00 (CST)
 #          By:
 # Description:
 # **************************************************************************
-from flask import (request, current_app, flash, redirect, url_for,
-                   render_template)
-from flask.views import MethodView
+from flask import (request, flash, redirect, url_for, render_template)
 from flask_login import login_required, current_user
+from flask_maple.views import MethodView
 from forums.permission import confirm_permission
 from forums.extension import cache
 
@@ -36,19 +35,6 @@ def is_confirmed(func):
 
 
 class BaseMethodView(MethodView):
-    @property
-    def page_info(self):
-        page = request.args.get('page', 1, type=int)
-        if hasattr(self, 'per_page'):
-            per_page = getattr(self, 'per_page')
-        else:
-            per_page = current_app.config.setdefault('PER_PAGE', 20)
-
-        number = request.args.get('number', per_page, type=int)
-        if number > 100:
-            number = per_page
-        return page, number
-
     @cache.cached(timeout=180, key_prefix=cache_key)
     def dispatch_request(self, *args, **kwargs):
         return super(BaseMethodView, self).dispatch_request(*args, **kwargs)

+ 7 - 6
forums/urls.py → forums/default.py

@@ -1,12 +1,13 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
-# **************************************************************************
-# Copyright © 2016 jianglin
-# File Name: urls.py
+# ********************************************************************************
+# Copyright © 2018 jianglin
+# File Name: default.py
 # Author: jianglin
 # Email: xiyang0807@gmail.com
-# Created: 2016-11-07 21:00:37 (CST)
-# Last Update:星期六 2017-3-25 18:23:34 (CST)
+# Created: 2018-07-26 10:00:54 (CST)
+# Last Update: Thursday 2018-07-26 10:01:16 (CST)
 #          By:
 # Description:
-# **************************************************************************
+# ********************************************************************************
+SUBDOMAIN = {"forums": True, "docs": True}

+ 3 - 1
forums/docs/__init__.py

@@ -6,14 +6,16 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2016-11-09 21:06:11 (CST)
-# Last Update:星期二 2017-9-19 12:54:23 (CST)
+# Last Update: Thursday 2018-07-26 10:03:18 (CST)
 #          By:
 # Description:
 # **************************************************************************
+from forums import default
 from .views import site as docs_site
 
 
 def init_app(app):
+    app.config.setdefault("SUBDOMAIN", default.SUBDOMAIN)
     if app.config['SUBDOMAIN']['docs']:
         app.register_blueprint(docs_site, subdomain='docs')
     else:

+ 3 - 8
forums/extension/babel.py

@@ -6,19 +6,14 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2018-02-11 14:52:25 (CST)
-# Last Update: 星期日 2018-02-11 15:31:25 (CST)
+# Last Update: Wednesday 2018-07-25 18:54:25 (CST)
 #          By:
 # Description:
 # ********************************************************************************
 from flask import request, g, current_app
-from flask_babelex import Babel, Domain
-import os
+from flask_babel import Babel
 
-translations = os.path.abspath(
-    os.path.join(
-        os.path.dirname(__file__), os.pardir, os.pardir, 'translations'))
-domain = Domain(translations)
-babel = Babel(default_domain=domain)
+babel = Babel()
 
 
 @babel.localeselector

+ 4 - 3
forums/extension/login.py

@@ -6,12 +6,12 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2018-02-11 14:54:38 (CST)
-# Last Update: 星期日 2018-02-11 15:26:53 (CST)
+# Last Update: Wednesday 2018-07-25 18:54:54 (CST)
 #          By:
 # Description:
 # ********************************************************************************
 from flask_login import LoginManager
-from flask_babelex import lazy_gettext as _
+from flask_babel import lazy_gettext as _
 
 login_manager = LoginManager()
 
@@ -25,7 +25,8 @@ def user_loader(id):
 
 def init_app(app):
     login_manager.login_view = "auth.login"
-    login_manager.session_protection = "strong"
+    # remember me only work with `basic` rathar than `strong`
+    login_manager.session_protection = "basic"
     login_manager.login_message = _("Please login to access this page.")
     # login_manager.anonymous_user = Anonymous
     login_manager.init_app(app)

+ 6 - 12
forums/jinja.py

@@ -6,7 +6,7 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2016-11-07 21:00:32 (CST)
-# Last Update: 星期日 2018-02-11 15:06:58 (CST)
+# Last Update: Thursday 2018-07-26 09:54:53 (CST)
 #          By:
 # Description:
 # **************************************************************************
@@ -15,7 +15,7 @@ from config import SITE
 
 from bleach import clean
 from flask import Markup, g
-from flask_babelex import format_datetime
+from flask_babel import format_datetime
 from misaka import HtmlRenderer, Markdown
 
 
@@ -26,18 +26,14 @@ def safe_clean(text):
     return Markup(clean(text, tags=tags, attributes=attrs, styles=styles))
 
 
-def markdown(text):
+def markdown(text, clean=True):
     renderer = HtmlRenderer()
     md = Markdown(renderer, extensions=('fenced-code', ))
+    if clean:
+        return Markup(safe_clean(md(text)))
     return Markup(md(text))
 
 
-def safe_markdown(text):
-    renderer = HtmlRenderer()
-    md = Markdown(renderer, extensions=('fenced-code', ))
-    return Markup(safe_clean(md(text)))
-
-
 def timesince(dt, default="just now"):
     now = datetime.utcnow()
     diff = now - dt
@@ -61,11 +57,9 @@ def timesince(dt, default="just now"):
 
 
 def show_time():
-    from flask_babelex import format_datetime
     if g.user.is_authenticated:
         return 'LOCALE:' + format_datetime(datetime.utcnow())
-    else:
-        return 'UTC:' + format_datetime(datetime.utcnow())
+    return 'UTC:' + format_datetime(datetime.utcnow())
 
 
 def hot_tags():

+ 3 - 1
forums/subdomain.py

@@ -6,13 +6,15 @@
 # Author: jianglin
 # Email: xiyang0807@gmail.com
 # Created: 2017-11-10 10:52:47 (CST)
-# Last Update:星期五 2018-01-05 01:15:15 (CST)
+# Last Update: Thursday 2018-07-26 10:02:02 (CST)
 #          By:
 # Description:
 # **************************************************************************
+from forums import default
 
 
 def init_app(app):
+    app.config.setdefault("SUBDOMAIN", default.SUBDOMAIN)
     if app.config['SUBDOMAIN']['forums']:
         app.url_map._rules.clear()
         app.url_map._rules_by_endpoint.clear()

+ 1 - 1
static/assets/home.js

@@ -21,4 +21,4 @@ $(document).ready(function(){$('button.topic-following').click(function(){var _$
 $(document).ready(function(){$('.like-reply').click(function(){var _$this=$(this);var replyId=_$this.attr('data-id');var like_url="/replies/"+replyId+'/like';var data=JSON.stringify({});if(_$this.hasClass('like-active')){$.ajax({type:"DELETE",url:like_url,data:data,contentType:'application/json;charset=UTF-8',success:function(response){if(response.status==='200')
 {_$this.attr("title","赞");_$this.removeClass("like-active");_$this.addClass("like-no-active");}else{window.location.href=response.url;}}});}else{$.ajax({type:"POST",url:like_url,data:data,contentType:'application/json;charset=UTF-8',success:function(response){if(response.status==='200')
 {_$this.attr("title","取消赞");_$this.removeClass("like-no-active");_$this.addClass("like-active");}else
-{window.location.href=response.url;}}});}});$('.reply-author').click(function(){var _$this=$(this);var author=_$this.attr('data-id');$('#content').focus();$('#content').val('@'+author+' ');});$('#topic-preview').click(function(){var content=$('#content').val();$.post('/topic/preview',{content:$("#content").val(),content_type:$("#content_type").val()},function(data){$("#show-preview").html(data);});});$('#tokenfield').tokenfield({limit:4});$('#topic-put-btn').click(function(){var _$this=$(this);var url='/topic/'+_$this.attr("data-id");var data={csrf_token:$('input[name="csrf_token"]').val(),title:$('input[name="title"]').val(),tags:$('input[name="tags"]').val(),category:$('select[name="category"]').val(),content:$('textarea[name="content"]').val(),content_type:$('select[name="content_type"]').val()};$.ajax({type:"PUT",url:url,data:JSON.stringify(data),contentType:'application/json;charset=UTF-8',success:function(response){if(response.status==='200'){window.location.href=url;}else{if(response.description!=""){alert(response.description);}else{alert(response.message);}}}});});});
+{window.location.href=response.url;}}});}});$('.reply-author').click(function(){var _$this=$(this);var author=_$this.attr('data-id');$('#content').focus();$('#content').val('@'+author+' ');});$('#topic-preview').click(function(){var contentType=$('#content_type').val();if(contentType=="1"){$("#show-preview").html(marked($('#content').val()));}else if(contentType=="2"){var parser=new Org.Parser();var orgDocument=parser.parse($('#content').val());var orgHTMLDocument=orgDocument.convert(Org.ConverterHTML,{headerOffset:1,exportFromLineNumber:false,suppressSubScriptHandling:false,suppressAutoLink:false});$("#show-preview").html(orgHTMLDocument.toString());}else{$("#show-preview").html($('#content').val());}});$('#tokenfield').tokenfield({limit:4});$('#topic-put-btn').click(function(){var _$this=$(this);var url='/topic/'+_$this.attr("data-id");var data={csrf_token:$('input[name="csrf_token"]').val(),title:$('input[name="title"]').val(),tags:$('input[name="tags"]').val(),category:$('select[name="category"]').val(),content:$('textarea[name="content"]').val(),content_type:$('select[name="content_type"]').val()};$.ajax({type:"PUT",url:url,data:JSON.stringify(data),contentType:'application/json;charset=UTF-8',success:function(response){if(response.status==='200'){window.location.href=url;}else{if(response.description!=""){alert(response.description);}else{alert(response.message);}}}});});});

+ 1650 - 0
static/libs/js/org.js

@@ -0,0 +1,1650 @@
+// Generated by export.rb at Wed Oct 8 13:35:23 UTC 2014
+/*
+  Copyright (c) 2014 Masafumi Oyamada
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+  THE SOFTWARE.
+*/
+
+var Org = (function () {
+  var exports = {};
+
+  // ------------------------------------------------------------
+  // Syntax
+  // ------------------------------------------------------------
+
+  var Syntax = {
+    rules: {},
+
+    define: function (name, syntax) {
+      this.rules[name] = syntax;
+      var methodName = "is" + name.substring(0, 1).toUpperCase() + name.substring(1);
+      this[methodName] = function (line) {
+        return this.rules[name].exec(line);
+      };
+    }
+  };
+
+  Syntax.define("header", /^(\*+)\s+(.*)$/); // m[1] => level, m[2] => content
+  Syntax.define("preformatted", /^(\s*):(?: (.*)$|$)/); // m[1] => indentation, m[2] => content
+  Syntax.define("unorderedListElement", /^(\s*)(?:-|\+|\s+\*)\s+(.*)$/); // m[1] => indentation, m[2] => content
+  Syntax.define("orderedListElement", /^(\s*)(\d+)(?:\.|\))\s+(.*)$/); // m[1] => indentation, m[2] => number, m[3] => content
+  Syntax.define("tableSeparator", /^(\s*)\|((?:\+|-)*?)\|?$/); // m[1] => indentation, m[2] => content
+  Syntax.define("tableRow", /^(\s*)\|(.*?)\|?$/); // m[1] => indentation, m[2] => content
+  Syntax.define("blank", /^$/);
+  Syntax.define("horizontalRule", /^(\s*)-{5,}$/); //
+  Syntax.define("directive", /^(\s*)#\+(?:(begin|end)_)?(.*)$/i); // m[1] => indentation, m[2] => type, m[3] => content
+  Syntax.define("comment", /^(\s*)#(.*)$/);
+  Syntax.define("line", /^(\s*)(.*)$/);
+
+  // ------------------------------------------------------------
+  // Token
+  // ------------------------------------------------------------
+
+  function Token() {
+  }
+
+  Token.prototype = {
+    isListElement: function () {
+      return this.type === Lexer.tokens.orderedListElement ||
+        this.type === Lexer.tokens.unorderedListElement;
+    },
+
+    isTableElement: function () {
+      return this.type === Lexer.tokens.tableSeparator ||
+        this.type === Lexer.tokens.tableRow;
+    }
+  };
+
+  // ------------------------------------------------------------
+  // Lexer
+  // ------------------------------------------------------------
+
+  function Lexer(stream) {
+    this.stream = stream;
+    this.tokenStack = [];
+  }
+
+  Lexer.prototype = {
+    tokenize: function (line) {
+      var token = new Token();
+      token.fromLineNumber = this.stream.lineNumber;
+
+      if (Syntax.isHeader(line)) {
+        token.type        = Lexer.tokens.header;
+        token.indentation = 0;
+        token.content     = RegExp.$2;
+        // specific
+        token.level       = RegExp.$1.length;
+      } else if (Syntax.isPreformatted(line)) {
+        token.type        = Lexer.tokens.preformatted;
+        token.indentation = RegExp.$1.length;
+        token.content     = RegExp.$2;
+      } else if (Syntax.isUnorderedListElement(line)) {
+        token.type        = Lexer.tokens.unorderedListElement;
+        token.indentation = RegExp.$1.length;
+        token.content     = RegExp.$2;
+      } else if (Syntax.isOrderedListElement(line)) {
+        token.type        = Lexer.tokens.orderedListElement;
+        token.indentation = RegExp.$1.length;
+        token.content     = RegExp.$3;
+        // specific
+        token.number      = RegExp.$2;
+      } else if (Syntax.isTableSeparator(line)) {
+        token.type        = Lexer.tokens.tableSeparator;
+        token.indentation = RegExp.$1.length;
+        token.content     = RegExp.$2;
+      } else if (Syntax.isTableRow(line)) {
+        token.type        = Lexer.tokens.tableRow;
+        token.indentation = RegExp.$1.length;
+        token.content     = RegExp.$2;
+      } else if (Syntax.isBlank(line)) {
+        token.type        = Lexer.tokens.blank;
+        token.indentation = 0;
+        token.content     = null;
+      } else if (Syntax.isHorizontalRule(line)) {
+        token.type        = Lexer.tokens.horizontalRule;
+        token.indentation = RegExp.$1.length;
+        token.content     = null;
+      } else if (Syntax.isDirective(line)) {
+        token.type        = Lexer.tokens.directive;
+        token.indentation = RegExp.$1.length;
+        token.content     = RegExp.$3;
+        // decide directive type (begin, end or oneshot)
+        var directiveTypeString = RegExp.$2;
+        if (/^begin/i.test(directiveTypeString))
+          token.beginDirective = true;
+        else if (/^end/i.test(directiveTypeString))
+          token.endDirective = true;
+        else
+          token.oneshotDirective = true;
+      } else if (Syntax.isComment(line)) {
+        token.type        = Lexer.tokens.comment;
+        token.indentation = RegExp.$1.length;
+        token.content     = RegExp.$2;
+      } else if (Syntax.isLine(line)) {
+        token.type        = Lexer.tokens.line;
+        token.indentation = RegExp.$1.length;
+        token.content     = RegExp.$2;
+      } else {
+        throw new Error("SyntaxError: Unknown line: " + line);
+      }
+
+      return token;
+    },
+
+    pushToken: function (token) {
+      this.tokenStack.push(token);
+    },
+
+    pushDummyTokenByType: function (type) {
+      var token = new Token();
+      token.type = type;
+      this.tokenStack.push(token);
+    },
+
+    peekStackedToken: function () {
+      return this.tokenStack.length > 0 ?
+        this.tokenStack[this.tokenStack.length - 1] : null;
+    },
+
+    getStackedToken: function () {
+      return this.tokenStack.length > 0 ?
+        this.tokenStack.pop() : null;
+    },
+
+    peekNextToken: function () {
+      return this.peekStackedToken() ||
+        this.tokenize(this.stream.peekNextLine());
+    },
+
+    getNextToken: function () {
+      return this.getStackedToken() ||
+        this.tokenize(this.stream.getNextLine());
+    },
+
+    hasNext: function () {
+      return this.stream.hasNext();
+    },
+
+    getLineNumber: function () {
+      return this.stream.lineNumber;
+    }
+  };
+
+  Lexer.tokens = {};
+  [
+    "header",
+    "orderedListElement",
+    "unorderedListElement",
+    "tableRow",
+    "tableSeparator",
+    "preformatted",
+    "line",
+    "horizontalRule",
+    "blank",
+    "directive",
+    "comment"
+  ].forEach(function (tokenName, i) {
+    Lexer.tokens[tokenName] = i;
+  });
+
+  // ------------------------------------------------------------
+  // Exports
+  // ------------------------------------------------------------
+
+  if (typeof exports !== "undefined")
+    exports.Lexer = Lexer;
+
+  function PrototypeNode(type, children) {
+    this.type = type;
+    this.children = [];
+
+    if (children) {
+      for (var i = 0, len = children.length; i < len; ++i) {
+        this.appendChild(children[i]);
+      }
+    }
+  }
+  PrototypeNode.prototype = {
+    previousSibling: null,
+    parent: null,
+    get firstChild() {
+      return this.children.length < 1 ?
+        null : this.children[0];
+    },
+    get lastChild() {
+      return this.children.length < 1 ?
+        null : this.children[this.children.length - 1];
+    },
+    appendChild: function (newChild) {
+      var previousSibling = this.children.length < 1 ?
+            null : this.lastChild;
+      this.children.push(newChild);
+      newChild.previousSibling = previousSibling;
+      newChild.parent = this;
+    },
+    toString: function () {
+      var string = "<" + this.type + ">";
+
+      if (typeof this.value !== "undefined") {
+        string += " " + this.value;
+      } else if (this.children) {
+        string += "\n" + this.children.map(function (child, idx) {
+          return "#" + idx + " " + child.toString();
+        }).join("\n").split("\n").map(function (line) {
+          return "  " + line;
+        }).join("\n");
+      }
+
+      return string;
+    }
+  };
+
+  var Node = {
+    types: {},
+
+    define: function (name, postProcess) {
+      this.types[name] = name;
+
+      var methodName = "create" + name.substring(0, 1).toUpperCase() + name.substring(1);
+      var postProcessGiven = typeof postProcess === "function";
+
+      this[methodName] = function (children, options) {
+        var node = new PrototypeNode(name, children);
+
+        if (postProcessGiven)
+          postProcess(node, options || {});
+
+        return node;
+      };
+    }
+  };
+
+  Node.define("text", function (node, options) {
+    node.value = options.value;
+  });
+  Node.define("header", function (node, options) {
+    node.level = options.level;
+  });
+  Node.define("orderedList");
+  Node.define("unorderedList");
+  Node.define("definitionList");
+  Node.define("listElement");
+  Node.define("paragraph");
+  Node.define("preformatted");
+  Node.define("table");
+  Node.define("tableRow");
+  Node.define("tableCell");
+  Node.define("horizontalRule");
+  Node.define("directive");
+
+  // Inline
+  Node.define("inlineContainer");
+
+  Node.define("bold");
+  Node.define("italic");
+  Node.define("underline");
+  Node.define("code");
+  Node.define("verbatim");
+  Node.define("dashed");
+  Node.define("link", function (node, options) {
+    node.src = options.src;
+  });
+
+  if (typeof exports !== "undefined")
+    exports.Node = Node;
+
+  function Stream(sequence) {
+    this.sequences = sequence.split(/\r?\n/);
+    this.totalLines = this.sequences.length;
+    this.lineNumber = 0;
+  }
+
+  Stream.prototype.peekNextLine = function () {
+    return this.hasNext() ? this.sequences[this.lineNumber] : null;
+  };
+
+  Stream.prototype.getNextLine = function () {
+    return this.hasNext() ? this.sequences[this.lineNumber++] : null;
+  };
+
+  Stream.prototype.hasNext = function () {
+    return this.lineNumber < this.totalLines;
+  };
+
+  if (typeof exports !== "undefined") {
+    exports.Stream = Stream;
+  }
+
+  // var Stream = require("./stream.js").Stream;
+  // var Lexer  = require("./lexer.js").Lexer;
+  // var Node   = require("./node.js").Node;
+
+  function Parser() {
+    this.inlineParser = new InlineParser();
+  }
+
+  Parser.parseStream = function (stream, options) {
+    var parser = new Parser();
+    parser.initStatus(stream, options);
+    parser.parseNodes();
+    return parser.nodes;
+  };
+
+  Parser.prototype = {
+    initStatus: function (stream, options) {
+      if (typeof stream === "string")
+        stream = new Stream(stream);
+      this.lexer = new Lexer(stream);
+      this.nodes = [];
+      this.options = {
+        toc: true,
+        num: true,
+        "^": "{}",
+        multilineCell: false
+      };
+      // Override option values
+      if (options && typeof options === "object") {
+        for (var key in options) {
+          this.options[key] = options[key];
+        }
+      }
+      this.document = {
+        options: this.options,
+        convert: function (ConverterClass, exportOptions) {
+          var converter = new ConverterClass(this, exportOptions);
+          return converter.result;
+        }
+      };
+    },
+
+    parse: function (stream, options) {
+      this.initStatus(stream, options);
+      this.parseDocument();
+      this.document.nodes = this.nodes;
+      return this.document;
+    },
+
+    createErrorReport: function (message) {
+      return new Error(message + " at line " + this.lexer.getLineNumber());
+    },
+
+    skipBlank: function () {
+      var blankToken = null;
+      while (this.lexer.peekNextToken().type === Lexer.tokens.blank)
+        blankToken = this.lexer.getNextToken();
+      return blankToken;
+    },
+
+    setNodeOriginFromToken: function (node, token) {
+      node.fromLineNumber = token.fromLineNumber;
+      return node;
+    },
+
+    appendNode: function (newNode) {
+      var previousSibling = this.nodes.length > 0 ? this.nodes[this.nodes.length - 1] : null;
+      this.nodes.push(newNode);
+      newNode.previousSibling = previousSibling;
+    },
+
+    // ------------------------------------------------------------
+    // <Document> ::= <Element>*
+    // ------------------------------------------------------------
+
+    parseDocument: function () {
+      this.parseTitle();
+      this.parseNodes();
+    },
+
+    parseNodes: function () {
+      while (this.lexer.hasNext()) {
+        var element = this.parseElement();
+        if (element) this.appendNode(element);
+      }
+    },
+
+    parseTitle: function () {
+      this.skipBlank();
+
+      if (this.lexer.hasNext() &&
+          this.lexer.peekNextToken().type === Lexer.tokens.line)
+        this.document.title = this.createTextNode(this.lexer.getNextToken().content);
+      else
+        this.document.title = null;
+
+      this.lexer.pushDummyTokenByType(Lexer.tokens.blank);
+    },
+
+    // ------------------------------------------------------------
+    // <Element> ::= (<Header> | <List>
+    //              | <Preformatted> | <Paragraph>
+    //              | <Table>)*
+    // ------------------------------------------------------------
+
+    parseElement: function () {
+      var element = null;
+
+      switch (this.lexer.peekNextToken().type) {
+      case Lexer.tokens.header:
+        element = this.parseHeader();
+        break;
+      case Lexer.tokens.preformatted:
+        element = this.parsePreformatted();
+        break;
+      case Lexer.tokens.orderedListElement:
+      case Lexer.tokens.unorderedListElement:
+        element = this.parseList();
+        break;
+      case Lexer.tokens.line:
+        element = this.parseText();
+        break;
+      case Lexer.tokens.tableRow:
+      case Lexer.tokens.tableSeparator:
+        element = this.parseTable();
+        break;
+      case Lexer.tokens.blank:
+        this.skipBlank();
+        if (this.lexer.hasNext()) {
+          if (this.lexer.peekNextToken().type === Lexer.tokens.line)
+            element = this.parseParagraph();
+          else
+            element = this.parseElement();
+        }
+        break;
+      case Lexer.tokens.horizontalRule:
+        this.lexer.getNextToken();
+        element = Node.createHorizontalRule();
+        break;
+      case Lexer.tokens.directive:
+        element = this.parseDirective();
+        break;
+      case Lexer.tokens.comment:
+        // Skip
+        this.lexer.getNextToken();
+        break;
+      default:
+        throw this.createErrorReport("Unhandled token: " + this.lexer.peekNextToken().type);
+      }
+
+      return element;
+    },
+
+    parseElementBesidesDirectiveEnd: function () {
+      try {
+        // Temporary, override the definition of `parseElement`
+        this.parseElement = this.parseElementBesidesDirectiveEndBody;
+        return this.parseElement();
+      } finally {
+        this.parseElement = this.originalParseElement;
+      }
+    },
+
+    parseElementBesidesDirectiveEndBody: function () {
+      if (this.lexer.peekNextToken().type === Lexer.tokens.directive &&
+          this.lexer.peekNextToken().endDirective) {
+        return null;
+      }
+
+      return this.originalParseElement();
+    },
+
+    // ------------------------------------------------------------
+    // <Header>
+    //
+    // : preformatted
+    // : block
+    // ------------------------------------------------------------
+
+    parseHeader: function () {
+      var headerToken = this.lexer.getNextToken();
+      var header = Node.createHeader([
+        this.createTextNode(headerToken.content) // TODO: Parse inline markups
+      ], { level: headerToken.level });
+      this.setNodeOriginFromToken(header, headerToken);
+
+      return header;
+    },
+
+    // ------------------------------------------------------------
+    // <Preformatted>
+    //
+    // : preformatted
+    // : block
+    // ------------------------------------------------------------
+
+    parsePreformatted: function () {
+      var preformattedFirstToken = this.lexer.peekNextToken();
+      var preformatted = Node.createPreformatted([]);
+      this.setNodeOriginFromToken(preformatted, preformattedFirstToken);
+
+      var textContents = [];
+
+      while (this.lexer.hasNext()) {
+        var token = this.lexer.peekNextToken();
+        if (token.type !== Lexer.tokens.preformatted ||
+            token.indentation < preformattedFirstToken.indentation)
+          break;
+        this.lexer.getNextToken();
+        textContents.push(token.content);
+      }
+
+      preformatted.appendChild(this.createTextNode(textContents.join("\n"), true /* no emphasis */));
+
+      return preformatted;
+    },
+
+    // ------------------------------------------------------------
+    // <List>
+    //
+    //  - foo
+    //    1. bar
+    //    2. baz
+    // ------------------------------------------------------------
+
+    // XXX: not consider codes (e.g., =Foo::Bar=)
+    definitionPattern: /^(.*?) :: *(.*)$/,
+
+    parseList: function () {
+      var rootToken = this.lexer.peekNextToken();
+      var list;
+      var isDefinitionList = false;
+
+      if (this.definitionPattern.test(rootToken.content)) {
+        list = Node.createDefinitionList([]);
+        isDefinitionList = true;
+      } else {
+        list = rootToken.type === Lexer.tokens.unorderedListElement ?
+          Node.createUnorderedList([]) : Node.createOrderedList([]);
+      }
+      this.setNodeOriginFromToken(list, rootToken);
+
+      while (this.lexer.hasNext()) {
+        var nextToken = this.lexer.peekNextToken();
+        if (!nextToken.isListElement() || nextToken.indentation !== rootToken.indentation)
+          break;
+        list.appendChild(this.parseListElement(rootToken.indentation, isDefinitionList));
+      }
+
+      return list;
+    },
+
+    unknownDefinitionTerm: "???",
+
+    parseListElement: function (rootIndentation, isDefinitionList) {
+      var listElementToken = this.lexer.getNextToken();
+      var listElement = Node.createListElement([]);
+      this.setNodeOriginFromToken(listElement, listElementToken);
+
+      listElement.isDefinitionList = isDefinitionList;
+
+      if (isDefinitionList) {
+        var match = this.definitionPattern.exec(listElementToken.content);
+        listElement.term = [
+          this.createTextNode(match && match[1] ? match[1] : this.unknownDefinitionTerm)
+        ];
+        listElement.appendChild(this.createTextNode(match ? match[2] : listElementToken.content));
+      } else {
+        listElement.appendChild(this.createTextNode(listElementToken.content));
+      }
+
+      while (this.lexer.hasNext()) {
+        var blankToken = this.skipBlank();
+        if (!this.lexer.hasNext())
+          break;
+
+        var notBlankNextToken = this.lexer.peekNextToken();
+        if (blankToken && !notBlankNextToken.isListElement())
+          this.lexer.pushToken(blankToken); // Recover blank token only when next line is not listElement.
+        if (notBlankNextToken.indentation <= rootIndentation)
+          break;                  // end of the list
+
+        var element = this.parseElement(); // recursive
+        if (element)
+          listElement.appendChild(element);
+      }
+
+      return listElement;
+    },
+
+    // ------------------------------------------------------------
+    // <Table> ::= <TableRow>+
+    // ------------------------------------------------------------
+
+    parseTable: function () {
+      var nextToken = this.lexer.peekNextToken();
+      var table = Node.createTable([]);
+      this.setNodeOriginFromToken(table, nextToken);
+      var sawSeparator = false;
+
+      var allowMultilineCell = nextToken.type === Lexer.tokens.tableSeparator && this.options.multilineCell;
+
+      while (this.lexer.hasNext() &&
+             (nextToken = this.lexer.peekNextToken()).isTableElement()) {
+        if (nextToken.type === Lexer.tokens.tableRow) {
+          var tableRow = this.parseTableRow(allowMultilineCell);
+          table.appendChild(tableRow);
+        } else {
+          // Lexer.tokens.tableSeparator
+          sawSeparator = true;
+          this.lexer.getNextToken();
+        }
+      }
+
+      if (sawSeparator && table.children.length) {
+        table.children[0].children.forEach(function (cell) {
+          cell.isHeader = true;
+        });
+      }
+
+      return table;
+    },
+
+    // ------------------------------------------------------------
+    // <TableRow> ::= <TableCell>+
+    // ------------------------------------------------------------
+
+    parseTableRow: function (allowMultilineCell) {
+      var tableRowTokens = [];
+
+      while (this.lexer.peekNextToken().type === Lexer.tokens.tableRow) {
+        tableRowTokens.push(this.lexer.getNextToken());
+        if (!allowMultilineCell) {
+          break;
+        }
+      }
+
+      if (!tableRowTokens.length) {
+        throw this.createErrorReport("Expected table row");
+      }
+
+      var firstTableRowToken = tableRowTokens.shift();
+      var tableCellTexts = firstTableRowToken.content.split("|");
+
+      tableRowTokens.forEach(function (rowToken) {
+        rowToken.content.split("|").forEach(function (cellText, cellIdx) {
+          tableCellTexts[cellIdx] = (tableCellTexts[cellIdx] || "") + "\n" + cellText;
+        });
+      });
+
+      // TODO: Prepare two pathes: (1)
+      var tableCells = tableCellTexts.map(
+        // TODO: consider '|' escape?
+        function (text) {
+          return Node.createTableCell(Parser.parseStream(text));
+        }, this);
+
+      return this.setNodeOriginFromToken(Node.createTableRow(tableCells), firstTableRowToken);
+    },
+
+    // ------------------------------------------------------------
+    // <Directive> ::= "#+.*"
+    // ------------------------------------------------------------
+
+    parseDirective: function () {
+      var directiveToken = this.lexer.getNextToken();
+      var directiveNode = this.createDirectiveNodeFromToken(directiveToken);
+
+      if (directiveToken.endDirective)
+        throw this.createErrorReport("Unmatched 'end' directive for " + directiveNode.directiveName);
+
+      if (directiveToken.oneshotDirective) {
+        this.interpretDirective(directiveNode);
+        return directiveNode;
+      }
+
+      if (!directiveToken.beginDirective)
+        throw this.createErrorReport("Invalid directive " + directiveNode.directiveName);
+
+      // Parse begin ~ end
+      directiveNode.children = [];
+      if (this.isVerbatimDirective(directiveNode))
+        return this.parseDirectiveBlockVerbatim(directiveNode);
+      else
+        return this.parseDirectiveBlock(directiveNode);
+    },
+
+    createDirectiveNodeFromToken: function (directiveToken) {
+      var matched = /^[ ]*([^ ]*)[ ]*(.*)[ ]*$/.exec(directiveToken.content);
+
+      var directiveNode = Node.createDirective(null);
+      this.setNodeOriginFromToken(directiveNode, directiveToken);
+      directiveNode.directiveName = matched[1].toLowerCase();
+      directiveNode.directiveArguments = this.parseDirectiveArguments(matched[2]);
+      directiveNode.directiveOptions = this.parseDirectiveOptions(matched[2]);
+      directiveNode.directiveRawValue = matched[2];
+
+      return directiveNode;
+    },
+
+    isVerbatimDirective: function (directiveNode) {
+      var directiveName = directiveNode.directiveName;
+      return directiveName === "src" || directiveName === "example";
+    },
+
+    parseDirectiveBlock: function (directiveNode, verbatim) {
+      this.lexer.pushDummyTokenByType(Lexer.tokens.blank);
+
+      while (this.lexer.hasNext()) {
+        var nextToken = this.lexer.peekNextToken();
+        if (nextToken.type === Lexer.tokens.directive &&
+            nextToken.endDirective &&
+            this.createDirectiveNodeFromToken(nextToken).directiveName === directiveNode.directiveName) {
+          // Close directive
+          this.lexer.getNextToken();
+          return directiveNode;
+        }
+        var element = this.parseElementBesidesDirectiveEnd();
+        if (element)
+          directiveNode.appendChild(element);
+      }
+
+      throw this.createErrorReport("Unclosed directive " + directiveNode.directiveName);
+    },
+
+    parseDirectiveBlockVerbatim: function (directiveNode) {
+      var textContent = [];
+
+      while (this.lexer.hasNext()) {
+        var nextToken = this.lexer.peekNextToken();
+        if (nextToken.type === Lexer.tokens.directive &&
+            nextToken.endDirective &&
+            this.createDirectiveNodeFromToken(nextToken).directiveName === directiveNode.directiveName) {
+          this.lexer.getNextToken();
+          directiveNode.appendChild(this.createTextNode(textContent.join("\n"), true));
+          return directiveNode;
+        }
+        textContent.push(this.lexer.stream.getNextLine());
+      }
+
+      throw this.createErrorReport("Unclosed directive " + directiveNode.directiveName);
+    },
+
+    parseDirectiveArguments: function (parameters) {
+      return parameters.split(/[ ]+/).filter(function (param) {
+        return param.length && param[0] !== "-";
+      });
+    },
+
+    parseDirectiveOptions: function (parameters) {
+      return parameters.split(/[ ]+/).filter(function (param) {
+        return param.length && param[0] === "-";
+      });
+    },
+
+    interpretDirective: function (directiveNode) {
+      // http://orgmode.org/manual/Export-options.html
+      switch (directiveNode.directiveName) {
+      case "options:":
+        this.interpretOptionDirective(directiveNode);
+        break;
+      case "title:":
+        this.document.title = directiveNode.directiveRawValue;
+        break;
+      case "author:":
+        this.document.author = directiveNode.directiveRawValue;
+        break;
+      case "email:":
+        this.document.email = directiveNode.directiveRawValue;
+        break;
+      }
+    },
+
+    interpretOptionDirective: function (optionDirectiveNode) {
+      optionDirectiveNode.directiveArguments.forEach(function (pairString) {
+        var pair = pairString.split(":");
+        this.options[pair[0]] = this.convertLispyValue(pair[1]);
+      }, this);
+    },
+
+    convertLispyValue: function (lispyValue) {
+      switch (lispyValue) {
+      case "t":
+        return true;
+      case "nil":
+        return false;
+      default:
+        if (/^[0-9]+$/.test(lispyValue))
+          return parseInt(lispyValue);
+        return lispyValue;
+      }
+    },
+
+    // ------------------------------------------------------------
+    // <Paragraph> ::= <Blank> <Line>*
+    // ------------------------------------------------------------
+
+    parseParagraph: function () {
+      var paragraphFisrtToken = this.lexer.peekNextToken();
+      var paragraph = Node.createParagraph([]);
+      this.setNodeOriginFromToken(paragraph, paragraphFisrtToken);
+
+      var textContents = [];
+
+      while (this.lexer.hasNext()) {
+        var nextToken = this.lexer.peekNextToken();
+        if (nextToken.type !== Lexer.tokens.line
+            || nextToken.indentation < paragraphFisrtToken.indentation)
+          break;
+        this.lexer.getNextToken();
+        textContents.push(nextToken.content);
+      }
+
+      paragraph.appendChild(this.createTextNode(textContents.join("\n")));
+
+      return paragraph;
+    },
+
+    parseText: function (noEmphasis) {
+      var lineToken = this.lexer.getNextToken();
+      return this.createTextNode(lineToken.content, noEmphasis);
+    },
+
+    // ------------------------------------------------------------
+    // <Text> (DOM Like)
+    // ------------------------------------------------------------
+
+    createTextNode: function (text, noEmphasis) {
+      return noEmphasis ? Node.createText(null, { value: text })
+        : this.inlineParser.parseEmphasis(text);
+    }
+  };
+  Parser.prototype.originalParseElement = Parser.prototype.parseElement;
+
+  // ------------------------------------------------------------
+  // Parser for Inline Elements
+  //
+  // @refs org-emphasis-regexp-components
+  // ------------------------------------------------------------
+
+  function InlineParser() {
+    this.preEmphasis     = " \t\\('\"";
+    this.postEmphasis    = "- \t.,:!?;'\"\\)";
+    this.borderForbidden = " \t\r\n,\"'";
+    this.bodyRegexp      = "[\\s\\S]*?";
+    this.markers         = "*/_=~+";
+
+    this.emphasisPattern = this.buildEmphasisPattern();
+    this.linkPattern = /\[\[([^\]]*)\](?:\[([^\]]*)\])?\]/g; // \1 => link, \2 => text
+  }
+
+  InlineParser.prototype = {
+    parseEmphasis: function (text) {
+      var emphasisPattern = this.emphasisPattern;
+      emphasisPattern.lastIndex = 0;
+
+      var result = [],
+          match,
+          previousLast = 0,
+          savedLastIndex;
+
+      while ((match = emphasisPattern.exec(text))) {
+        var whole  = match[0];
+        var pre    = match[1];
+        var marker = match[2];
+        var body   = match[3];
+        var post   = match[4];
+
+        {
+          // parse links
+          var matchBegin = emphasisPattern.lastIndex - whole.length;
+          var beforeContent = text.substring(previousLast, matchBegin + pre.length);
+          savedLastIndex = emphasisPattern.lastIndex;
+          result.push(this.parseLink(beforeContent));
+          emphasisPattern.lastIndex = savedLastIndex;
+        }
+
+        var bodyNode = [Node.createText(null, { value: body })];
+        var bodyContainer = this.emphasizeElementByMarker(bodyNode, marker);
+        result.push(bodyContainer);
+
+        previousLast = emphasisPattern.lastIndex - post.length;
+      }
+
+      if (emphasisPattern.lastIndex === 0 ||
+          emphasisPattern.lastIndex !== text.length - 1)
+        result.push(this.parseLink(text.substring(previousLast)));
+
+      if (result.length === 1) {
+        // Avoid duplicated inline container wrapping
+        return result[0];
+      } else {
+        return Node.createInlineContainer(result);
+      }
+    },
+
+    depth: 0,
+    parseLink: function (text) {
+      var linkPattern = this.linkPattern;
+      linkPattern.lastIndex = 0;
+
+      var match,
+          result = [],
+          previousLast = 0,
+          savedLastIndex;
+
+      while ((match = linkPattern.exec(text))) {
+        var whole = match[0];
+        var src   = match[1];
+        var title = match[2];
+
+        // parse before content
+        var matchBegin = linkPattern.lastIndex - whole.length;
+        var beforeContent = text.substring(previousLast, matchBegin);
+        result.push(Node.createText(null, { value: beforeContent }));
+
+        // parse link
+        var link = Node.createLink([]);
+        link.src = src;
+        if (title) {
+          savedLastIndex = linkPattern.lastIndex;
+          link.appendChild(this.parseEmphasis(title));
+          linkPattern.lastIndex = savedLastIndex;
+        } else {
+          link.appendChild(Node.createText(null, { value: src }));
+        }
+        result.push(link);
+
+        previousLast = linkPattern.lastIndex;
+      }
+
+      if (linkPattern.lastIndex === 0 ||
+          linkPattern.lastIndex !== text.length - 1)
+        result.push(Node.createText(null, { value: text.substring(previousLast) }));
+
+      return Node.createInlineContainer(result);
+    },
+
+    emphasizeElementByMarker: function (element, marker) {
+      switch (marker) {
+      case "*":
+        return Node.createBold(element);
+      case "/":
+        return Node.createItalic(element);
+      case "_":
+        return Node.createUnderline(element);
+      case "=":
+      case "~":
+        return Node.createCode(element);
+      case "+":
+        return Node.createDashed(element);
+      }
+    },
+
+    buildEmphasisPattern: function () {
+      return new RegExp(
+        "([" + this.preEmphasis + "]|^|\r?\n)" +               // \1 => pre
+          "([" + this.markers + "])" +                         // \2 => marker
+          "([^" + this.borderForbidden + "]|" +                // \3 => body
+          "[^" + this.borderForbidden + "]" +
+          this.bodyRegexp +
+          "[^" + this.borderForbidden + "])" +
+          "\\2" +
+          "([" + this.postEmphasis +"]|$|\r?\n)",              // \4 => post
+          // flags
+          "g"
+      );
+    }
+  };
+
+  if (typeof exports !== "undefined") {
+    exports.Parser = Parser;
+    exports.InlineParser = InlineParser;
+  }
+
+  // var Node = require("../node.js").Node;
+
+  function Converter() {
+  }
+
+  Converter.prototype = {
+    exportOptions: {
+      headerOffset: 1,
+      exportFromLineNumber: false,
+      suppressSubScriptHandling: false,
+      suppressAutoLink: false
+    },
+
+    untitled: "Untitled",
+    result: null,
+
+    // TODO: Manage TODO lists
+
+    initialize: function (orgDocument, exportOptions) {
+      this.orgDocument = orgDocument;
+      this.documentOptions = orgDocument.options || {};
+      this.exportOptions = exportOptions || {};
+
+      this.headers = [];
+      this.headerOffset =
+        typeof this.exportOptions.headerOffset === "number" ? this.exportOptions.headerOffset : 1;
+      this.sectionNumbers = [0];
+    },
+
+    createTocItem: function (headerNode, parentTocs) {
+        var childTocs = [];
+        childTocs.parent = parentTocs;
+        var tocItem = { headerNode: headerNode, childTocs: childTocs };
+        return tocItem;
+    },
+
+    computeToc: function (exportTocLevel) {
+      if (typeof exportTocLevel !== "number")
+        exportTocLevel = Infinity;
+
+      var toc = [];
+      toc.parent = null;
+
+      var previousLevel = 1;
+      var currentTocs = toc;  // first
+
+      for (var i = 0; i < this.headers.length; ++i) {
+        var headerNode = this.headers[i];
+
+        if (headerNode.level > exportTocLevel)
+          continue;
+
+        var levelDiff = headerNode.level - previousLevel;
+        if (levelDiff > 0) {
+          for (var j = 0; j < levelDiff; ++j) {
+            if (currentTocs.length === 0) {
+              // Create a dummy tocItem
+              var dummyHeader = Node.createHeader([], {
+                level: previousLevel + j
+              });
+              dummyHeader.sectionNumberText = "";
+              currentTocs.push(this.createTocItem(dummyHeader, currentTocs));
+            }
+            currentTocs = currentTocs[currentTocs.length - 1].childTocs;
+          }
+        } else if (levelDiff < 0) {
+          levelDiff = -levelDiff;
+          for (var k = 0; k < levelDiff; ++k) {
+            currentTocs = currentTocs.parent;
+          }
+        }
+
+        currentTocs.push(this.createTocItem(headerNode, currentTocs));
+
+        previousLevel = headerNode.level;
+      }
+
+      return toc;
+    },
+
+    convertNode: function (node, recordHeader, insideCodeElement) {
+      if (!insideCodeElement) {
+        if (node.type === Node.types.directive) {
+          if (node.directiveName === "example" ||
+              node.directiveName === "src") {
+            insideCodeElement = true;
+          }
+        } else if (node.type === Node.types.preformatted) {
+          insideCodeElement = true;
+        }
+      }
+
+      if (typeof node === "string") {
+        node = Node.createText(null, { value: node });
+      }
+
+      var childText = node.children ? this.convertNodes(node.children, recordHeader, insideCodeElement) : "";
+      var text;
+
+      var auxData = this.computeAuxDataForNode(node);
+
+      switch (node.type) {
+      case Node.types.header:
+        // Parse task status
+        var taskStatus = null;
+        if (childText.indexOf("TODO ") === 0)
+          taskStatus = "todo";
+        else if (childText.indexOf("DONE ") === 0)
+          taskStatus = "done";
+
+        // Compute section number
+        var sectionNumberText = null;
+        if (recordHeader) {
+          var thisHeaderLevel = node.level;
+          var previousHeaderLevel = this.sectionNumbers.length;
+          if (thisHeaderLevel > previousHeaderLevel) {
+            // Fill missing section number
+            var levelDiff = thisHeaderLevel - previousHeaderLevel;
+            for (var j = 0; j < levelDiff; ++j) {
+              this.sectionNumbers[thisHeaderLevel - 1 - j] = 0; // Extend
+            }
+          } else if (thisHeaderLevel < previousHeaderLevel) {
+            this.sectionNumbers.length = thisHeaderLevel; // Collapse
+          }
+          this.sectionNumbers[thisHeaderLevel - 1]++;
+          sectionNumberText = this.sectionNumbers.join(".");
+          node.sectionNumberText = sectionNumberText; // Can be used in ToC
+        }
+
+        text = this.convertHeader(node, childText, auxData,
+                                  taskStatus, sectionNumberText);
+
+        if (recordHeader)
+          this.headers.push(node);
+        break;
+      case Node.types.orderedList:
+        text = this.convertOrderedList(node, childText, auxData);
+        break;
+      case Node.types.unorderedList:
+        text = this.convertUnorderedList(node, childText, auxData);
+        break;
+      case Node.types.definitionList:
+        text = this.convertDefinitionList(node, childText, auxData);
+        break;
+      case Node.types.listElement:
+        if (node.isDefinitionList) {
+          var termText = this.convertNodes(node.term, recordHeader, insideCodeElement);
+          text = this.convertDefinitionItem(node, childText, auxData,
+                                            termText, childText);
+        } else {
+          text = this.convertListItem(node, childText, auxData);
+        }
+        break;
+      case Node.types.paragraph:
+        text = this.convertParagraph(node, childText, auxData);
+        break;
+      case Node.types.preformatted:
+        text = this.convertPreformatted(node, childText, auxData);
+        break;
+      case Node.types.table:
+        text = this.convertTable(node, childText, auxData);
+        break;
+      case Node.types.tableRow:
+        text = this.convertTableRow(node, childText, auxData);
+        break;
+      case Node.types.tableCell:
+        if (node.isHeader)
+          text = this.convertTableHeader(node, childText, auxData);
+        else
+          text = this.convertTableCell(node, childText, auxData);
+        break;
+      case Node.types.horizontalRule:
+        text = this.convertHorizontalRule(node, childText, auxData);
+        break;
+        // ============================================================ //
+        // Inline
+        // ============================================================ //
+      case Node.types.inlineContainer:
+        text = this.convertInlineContainer(node, childText, auxData);
+        break;
+      case Node.types.bold:
+        text = this.convertBold(node, childText, auxData);
+        break;
+      case Node.types.italic:
+        text = this.convertItalic(node, childText, auxData);
+        break;
+      case Node.types.underline:
+        text = this.convertUnderline(node, childText, auxData);
+        break;
+      case Node.types.code:
+        text = this.convertCode(node, childText, auxData);
+        break;
+      case Node.types.dashed:
+        text = this.convertDashed(node, childText, auxData);
+        break;
+      case Node.types.link:
+        text = this.convertLink(node, childText, auxData);
+        break;
+      case Node.types.directive:
+        switch (node.directiveName) {
+        case "quote":
+          text = this.convertQuote(node, childText, auxData);
+          break;
+        case "example":
+          text = this.convertExample(node, childText, auxData);
+          break;
+        case "src":
+          text = this.convertSrc(node, childText, auxData);
+          break;
+        default:
+          text = childText;
+        }
+        break;
+      case Node.types.text:
+        text = this.convertText(node.value, insideCodeElement);
+        break;
+      default:
+        throw "Unknown node type: " + node.type;
+      }
+
+      if (typeof this.postProcess === "function") {
+        text = this.postProcess(node, text, insideCodeElement);
+      }
+
+      return text;
+    },
+
+    convertText: function (text, insideCodeElement) {
+      var escapedText = this.escapeSpecialChars(text, insideCodeElement);
+
+      if (!this.exportOptions.suppressSubScriptHandling && !insideCodeElement) {
+        escapedText = this.makeSubscripts(escapedText, insideCodeElement);
+      }
+      if (!this.exportOptions.suppressAutoLink) {
+        escapedText = this.linkURL(escapedText);
+      }
+
+      return escapedText;
+    },
+
+    convertNodes: function (nodes, recordHeader, insideCodeElement) {
+      return nodes.map(function (node) {
+        return this.convertNode(node, recordHeader, insideCodeElement);
+      }, this).join("");
+    },
+
+    getNodeTextContent: function (node) {
+      if (node.type === Node.types.text)
+        return this.escapeSpecialChars(node.value);
+      else
+        return node.children ? node.children.map(this.getNodeTextContent, this).join("") : "";
+    },
+
+    // @Override
+    escapeSpecialChars: function (text) {
+      throw "Implement escapeSpecialChars";
+    },
+
+    // http://daringfireball.net/2010/07/improved_regex_for_matching_urls
+    urlPattern: /\b(?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’])/i,
+
+    // @Override
+    linkURL: function (text) {
+      var self = this;
+      return text.replace(this.urlPattern, function (matched) {
+        if (matched.indexOf("://") < 0)
+          matched = "http://" + matched;
+        return self.makeLink(matched);
+      });
+    },
+
+    makeLink: function (url) {
+      throw "Implement makeLink";
+    },
+
+    makeSubscripts: function (text) {
+      if (this.documentOptions["^"] === "{}")
+        return text.replace(/\b([^_ \t]*)_{([^}]*)}/g,
+                            this.makeSubscript);
+      else if (this.documentOptions["^"])
+        return text.replace(/\b([^_ \t]*)_([^_]*)\b/g,
+                            this.makeSubscript);
+      else
+        return text;
+    },
+
+    makeSubscript: function (match, body, subscript) {
+      throw "Implement makeSubscript";
+    },
+
+    imageExtensionPattern: new RegExp("(" + [
+      "bmp", "png", "jpeg", "jpg", "gif", "tiff",
+      "tif", "xbm", "xpm", "pbm", "pgm", "ppm"
+    ].join("|") + ")$", "i")
+  };
+
+  if (typeof exports !== "undefined")
+    exports.Converter = Converter;
+
+  // var Converter = require("./converter.js").Converter;
+  // var Node = require("../node.js").Node;
+
+  function ConverterHTML(orgDocument, exportOptions) {
+    this.initialize(orgDocument, exportOptions);
+    this.result = this.convert();
+  }
+
+  ConverterHTML.prototype = {
+    __proto__: Converter.prototype,
+
+    convert: function () {
+      var title = this.orgDocument.title ? this.convertNode(this.orgDocument.title) : this.untitled;
+      var titleHTML = this.tag("h1", title);
+      var contentHTML = this.convertNodes(this.orgDocument.nodes, true /* record headers */);
+      var toc = this.computeToc(this.documentOptions["toc"]);
+      var tocHTML = this.tocToHTML(toc);
+
+      return {
+        title: title,
+        titleHTML: titleHTML,
+        contentHTML: contentHTML,
+        tocHTML: tocHTML,
+        toc: toc,
+        toString: function () {
+          return titleHTML + tocHTML + "\n" + contentHTML;
+        }
+      };
+    },
+
+    tocToHTML: function (toc) {
+      function tocToHTMLFunction(tocList) {
+        var html = "";
+        for (var i = 0; i < tocList.length; ++i) {
+          var tocItem = tocList[i];
+          var sectionNumberText = tocItem.headerNode.sectionNumberText;
+          var sectionNumber = this.documentOptions.num ?
+                this.inlineTag("span", sectionNumberText, {
+                  "class": "section-number"
+                }) : "";
+          var header = this.getNodeTextContent(tocItem.headerNode);
+          var headerLink = this.inlineTag("a", sectionNumber + header, {
+            href: "#header-" + sectionNumberText.replace(/\./g, "-")
+          });
+          var subList = tocItem.childTocs.length ? tocToHTMLFunction.call(this, tocItem.childTocs) : "";
+          html += this.tag("li", headerLink + subList);
+        }
+        return this.tag("ul", html);
+      }
+
+      return tocToHTMLFunction.call(this, toc);
+    },
+
+    computeAuxDataForNode: function (node) {
+      while (node.parent &&
+             node.parent.type === Node.types.inlineContainer) {
+        node = node.parent;
+      }
+      var attributesNode = node.previousSibling;
+      var attributesText = "";
+      while (attributesNode &&
+             attributesNode.type === Node.types.directive &&
+             attributesNode.directiveName === "attr_html:") {
+        attributesText += attributesNode.directiveRawValue + " ";
+        attributesNode = attributesNode.previousSibling;
+      }
+      return attributesText;
+    },
+
+    // ----------------------------------------------------
+    // Node conversion
+    // ----------------------------------------------------
+
+    convertHeader: function (node, childText, auxData,
+                             taskStatus, sectionNumberText) {
+      var headerAttributes = {};
+
+      if (taskStatus) {
+        childText = this.inlineTag("span", childText.substring(0, 4), {
+          "class": "task-status " + taskStatus
+        }) + childText.substring(5);
+      }
+
+      if (sectionNumberText) {
+        childText = this.inlineTag("span", sectionNumberText, {
+          "class": "section-number"
+        }) + childText;
+        headerAttributes["id"] = "header-" + sectionNumberText.replace(/\./g, "-");
+      }
+
+      if (taskStatus)
+        headerAttributes["class"] = "task-status " + taskStatus;
+
+      return this.tag("h" + (this.headerOffset + node.level),
+                      childText, headerAttributes, auxData);
+    },
+
+    convertOrderedList: function (node, childText, auxData) {
+      return this.tag("ol", childText, null, auxData);
+    },
+
+    convertUnorderedList: function (node, childText, auxData) {
+      return this.tag("ul", childText, null, auxData);
+    },
+
+    convertDefinitionList: function (node, childText, auxData) {
+      return this.tag("dl", childText, null, auxData);
+    },
+
+    convertDefinitionItem: function (node, childText, auxData,
+                                     term, definition) {
+      return this.tag("dt", term) + this.tag("dd", definition);
+    },
+
+    convertListItem: function (node, childText, auxData) {
+      if (this.exportOptions.suppressCheckboxHandling) {
+        return this.tag("li", childText, null, auxData);
+      } else {
+        var listItemAttributes = {};
+        var listItemText = childText;
+        // Embed checkbox
+        if (/^\s*\[(X| |-)\]([\s\S]*)/.exec(listItemText)) {
+          listItemText = RegExp.$2 ;
+          var checkboxIndicator = RegExp.$1;
+
+          var checkboxAttributes = { type: "checkbox" };
+          switch (checkboxIndicator) {
+          case "X":
+            checkboxAttributes["checked"] = "true";
+            listItemAttributes["data-checkbox-status"] = "done";
+            break;
+          case "-":
+            listItemAttributes["data-checkbox-status"] = "intermediate";
+            break;
+          default:
+            listItemAttributes["data-checkbox-status"] = "undone";
+            break;
+          }
+
+          listItemText = this.inlineTag("input", null, checkboxAttributes) + listItemText;
+        }
+
+        return this.tag("li", listItemText, listItemAttributes, auxData);
+      }
+    },
+
+    convertParagraph: function (node, childText, auxData) {
+      return this.tag("p", childText, null, auxData);
+    },
+
+    convertPreformatted: function (node, childText, auxData) {
+      return this.tag("pre", childText, null, auxData);
+    },
+
+    convertTable: function (node, childText, auxData) {
+      return this.tag("table", this.tag("tbody", childText), null, auxData);
+    },
+
+    convertTableRow: function (node, childText, auxData) {
+      return this.tag("tr", childText);
+    },
+
+    convertTableHeader: function (node, childText, auxData) {
+      return this.tag("th", childText);
+    },
+
+    convertTableCell: function (node, childText, auxData) {
+      return this.tag("td", childText);
+    },
+
+    convertHorizontalRule: function (node, childText, auxData) {
+      return this.tag("hr", null, null, auxData);
+    },
+
+    convertInlineContainer: function (node, childText, auxData) {
+      return childText;
+    },
+
+    convertBold: function (node, childText, auxData) {
+      return this.inlineTag("b", childText);
+    },
+
+    convertItalic: function (node, childText, auxData) {
+      return this.inlineTag("i", childText);
+    },
+
+    convertUnderline: function (node, childText, auxData) {
+      return this.inlineTag("span", childText, {
+        style: "text-decoration:underline;"
+      });
+    },
+
+    convertCode: function (node, childText, auxData) {
+      return this.inlineTag("code", childText);
+    },
+
+    convertDashed: function (node, childText, auxData) {
+      return this.inlineTag("del", childText);
+    },
+
+    convertLink: function (node, childText, auxData) {
+      if (this.imageExtensionPattern.exec(node.src)) {
+        var imgText = this.getNodeTextContent(node);
+        return this.inlineTag("img", null, {
+          src: node.src,
+          alt: imgText,
+          title: imgText
+        }, auxData);
+      } else {
+        return this.inlineTag("a", childText, { href: node.src });
+      }
+    },
+
+    convertQuote: function (node, childText, auxData) {
+      return this.tag("blockquote", childText, null, auxData);
+    },
+
+    convertExample: function (node, childText, auxData) {
+      return this.tag("pre", childText, null, auxData);
+    },
+
+    convertSrc: function (node, childText, auxData) {
+      var codeLanguage = node.directiveArguments.length
+            ? node.directiveArguments[0]
+            : "unknown";
+      childText = this.tag("code", childText, {
+        "class": "language-" + codeLanguage
+      }, auxData);
+      return this.tag("pre", childText, {
+        "class": "prettyprint"
+      });
+    },
+
+    // ----------------------------------------------------
+    // Supplemental methods
+    // ----------------------------------------------------
+
+    replaceMap: {
+      // [replacing pattern, predicate]
+      "&": ["&#38;", null],
+      "<": ["&#60;", null],
+      ">": ["&#62;", null],
+      '"': ["&#34;", null],
+      "'": ["&#39;", null],
+      "->": ["&#10132;", function (text, insideCodeElement) {
+        return this.exportOptions.translateSymbolArrow && !insideCodeElement;
+      }]
+    },
+
+    replaceRegexp: null,
+
+    // @implement @override
+    escapeSpecialChars: function (text, insideCodeElement) {
+      if (!this.replaceRegexp) {
+        this.replaceRegexp = new RegExp(Object.keys(this.replaceMap).join("|"), "g");
+      }
+
+      var replaceMap = this.replaceMap;
+      var self = this;
+      return text.replace(this.replaceRegexp, function (matched) {
+        if (!replaceMap[matched]) {
+          throw "escapeSpecialChars: Invalid match";
+        }
+
+        var predicate = replaceMap[matched][1];
+        if (typeof predicate === "function" &&
+            !predicate.call(self, text, insideCodeElement)) {
+          // Not fullfill the predicate
+          return matched;
+        }
+
+        return replaceMap[matched][0];
+      });
+    },
+
+    // @implement
+    postProcess: function (node, currentText, insideCodeElement) {
+      if (this.exportOptions.exportFromLineNumber &&
+          typeof node.fromLineNumber === "number") {
+        // Wrap with line number information
+        currentText = this.inlineTag("div", currentText, {
+          "data-line-number": node.fromLineNumber
+        });
+      }
+      return currentText;
+    },
+
+    // @implement
+    makeLink: function (url) {
+      return "<a href=\"" + url + "\">" + decodeURIComponent(url) + "</a>";
+    },
+
+    // @implement
+    makeSubscript: function (match, body, subscript) {
+      return "<span class=\"org-subscript-parent\">" +
+        body +
+        "</span><span class=\"org-subscript-child\">" +
+        subscript +
+        "</span>";
+    },
+
+    // ----------------------------------------------------
+    // Specific methods
+    // ----------------------------------------------------
+
+    attributesObjectToString: function (attributesObject) {
+      var attributesString = "";
+      for (var attributeName in attributesObject) {
+        if (attributesObject.hasOwnProperty(attributeName)) {
+          attributesString += " " + attributeName + "=\"" + attributesObject[attributeName] + "\"";
+        }
+      }
+      return attributesString;
+    },
+
+    inlineTag: function (name, innerText, attributesObject, auxAttributesText) {
+      attributesObject = attributesObject || {};
+
+      var htmlString = "<" + name;
+      // TODO: check duplicated attributes
+      if (auxAttributesText)
+        htmlString += " " + auxAttributesText;
+      htmlString += this.attributesObjectToString(attributesObject);
+
+      if (innerText === null)
+        return htmlString + "/>";
+
+      htmlString += ">" + innerText + "</" + name + ">";
+
+      return htmlString;
+    },
+
+    tag: function (name, innerText, attributesObject, auxAttributesText) {
+      return this.inlineTag(name, innerText, attributesObject, auxAttributesText) + "\n";
+    }
+  };
+
+  if (typeof exports !== "undefined")
+    exports.ConverterHTML = ConverterHTML;
+
+  return exports;
+})();

+ 93 - 84
static/styles/topic.js

@@ -1,90 +1,99 @@
 $(document).ready(function(){
-  $('.like-reply').click(function() {
-    var _$this = $(this);
-    var replyId = _$this.attr('data-id');
-    var like_url = "/replies/" + replyId + '/like';
-    var data = JSON.stringify({
+    $('.like-reply').click(function() {
+        var _$this = $(this);
+        var replyId = _$this.attr('data-id');
+        var like_url = "/replies/" + replyId + '/like';
+        var data = JSON.stringify({
+        });
+        if(_$this.hasClass('like-active')){
+            $.ajax ({
+                type : "DELETE",
+                url : like_url,
+                data:data,
+                contentType: 'application/json;charset=UTF-8',
+                success: function(response) {
+                    if (response.status === '200')
+                    {
+                        _$this.attr("title","赞");
+                        _$this.removeClass("like-active");
+                        _$this.addClass("like-no-active");
+                    } else {
+                        window.location.href = response.url;
+                    }
+                }});
+        }else {
+            $.ajax ({
+                type : "POST",
+                url : like_url,
+                data:data,
+                contentType: 'application/json;charset=UTF-8',
+                success: function(response) {
+                    if (response.status === '200')
+                    {
+                        _$this.attr("title","取消赞");
+                        _$this.removeClass("like-no-active");
+                        _$this.addClass("like-active");
+                    } else
+                    {
+                        window.location.href = response.url;
+                    }
+                }});
+        }
     });
-    if(_$this.hasClass('like-active')){
-      $.ajax ({
-        type : "DELETE",
-        url : like_url,
-        data:data,
-        contentType: 'application/json;charset=UTF-8',
-        success: function(response) {
-          if (response.status === '200')
-          {
-            _$this.attr("title","赞");
-            _$this.removeClass("like-active");
-            _$this.addClass("like-no-active");
-          } else {
-            window.location.href = response.url;
-          }
-        }});
-    }else {
-      $.ajax ({
-        type : "POST",
-        url : like_url,
-        data:data,
-        contentType: 'application/json;charset=UTF-8',
-        success: function(response) {
-          if (response.status === '200')
-          {
-            _$this.attr("title","取消赞");
-            _$this.removeClass("like-no-active");
-            _$this.addClass("like-active");
-          } else
-          {
-            window.location.href = response.url;
-          }
-        }});
-    }
-  });
-  $('.reply-author').click(function() {
-    var _$this = $(this);
-    var author = _$this.attr('data-id');
-    $('#content').focus();
-    $('#content').val('@' + author + ' ');
-  });
-  $('#topic-preview').click(function() {
-    var content = $('#content').val();
-    $.post('/topic/preview', {
-      content: $("#content").val(),
-      content_type: $("#content_type").val()
-    }, function(data) {
-      $("#show-preview").html(data);
+    $('.reply-author').click(function() {
+        var _$this = $(this);
+        var author = _$this.attr('data-id');
+        $('#content').focus();
+        $('#content').val('@' + author + ' ');
     });
-  });
-  $('#tokenfield').tokenfield({
-    limit:4
-  });
-  $('#topic-put-btn').click(function() {
-    var _$this = $(this);
-    var url = '/topic/' + _$this.attr("data-id");
-    var data = {
-      csrf_token:$('input[name="csrf_token"]').val(),
-      title:$('input[name="title"]').val(),
-      tags:$('input[name="tags"]').val(),
-      category:$('select[name="category"]').val(),
-      content:$('textarea[name="content"]').val(),
-      content_type:$('select[name="content_type"]').val()
-    };
-    $.ajax ({
-      type : "PUT",
-      url : url,
-      data:JSON.stringify(data),
-      contentType: 'application/json;charset=UTF-8',
-      success: function(response) {
-        if (response.status === '200') {
-          window.location.href= url;
-        }else {
-          if (response.description != ""){
-            alert(response.description);
-          }else {
-            alert(response.message);
-          }
+    $('#topic-preview').click(function() {
+        var contentType = $('#content_type').val();
+        if (contentType == "1") {
+            $("#show-preview").html(marked($('#content').val()));
+        } else if (contentType == "2") {
+            var parser = new Org.Parser();
+            var orgDocument = parser.parse($('#content').val());
+            var orgHTMLDocument = orgDocument.convert(Org.ConverterHTML, {
+                headerOffset: 1,
+                exportFromLineNumber: false,
+                suppressSubScriptHandling: false,
+                suppressAutoLink: false
+            });
+            $("#show-preview").html(orgHTMLDocument.toString());
+        } else {
+            $("#show-preview").html($('#content').val());
         }
-      }
     });
-  });
+    $('#tokenfield').tokenfield({
+        limit:4
+    });
+    $('#topic-put-btn').click(function() {
+        var _$this = $(this);
+        var url = '/topic/' + _$this.attr("data-id");
+        var data = {
+            csrf_token:$('input[name="csrf_token"]').val(),
+            title:$('input[name="title"]').val(),
+            tags:$('input[name="tags"]').val(),
+            category:$('select[name="category"]').val(),
+            content:$('textarea[name="content"]').val(),
+            content_type:$('select[name="content_type"]').val()
+        };
+        $.ajax ({
+            type : "PUT",
+            url : url,
+            data:JSON.stringify(data),
+            contentType: 'application/json;charset=UTF-8',
+            success: function(response) {
+                if (response.status === '200') {
+                    window.location.href= url;
+                }else {
+                    if (response.description != ""){
+                        alert(response.description);
+                    }else {
+                        alert(response.message);
+                    }
+                }
+            }
+        });
+    });
 });

+ 1 - 2
templates/base/header.html

@@ -66,7 +66,7 @@
       <ul class="nav navbar-nav">
         {{ navlist(url_for('forums.forums'),_('Forums')) }}
         {{ navlist(url_for('docs.list'),_('Wiki')) }}
-        {{ navlist('http://honmaple.org',_('Blog')) }}
+        {{ navlist('https://honmaple.me',_('Blog')) }}
         {{ navlist(url_for('tag.list'),_('TagList')) }}
         {{ navlist(url_for('topic.good'),_('Good')) }}
       </ul>
@@ -82,4 +82,3 @@
     </div>
   </div>
 </nav>
-

+ 2 - 2
templates/collect/create.html

@@ -18,8 +18,8 @@
           </div>
           {% for subfield in form.is_hidden %}
           <div class="form-group" style="display:inline;">
-            {{subfield.label}}
-            {{subfield}}
+            {{ subfield.label }}
+            {{ subfield }}
           </div>
           {% endfor %}
       </div>

+ 12 - 1
templates/topic/ask.html

@@ -1,5 +1,13 @@
 {% extends 'base/base.html' %}
-{% block content %}
+
+{%- block script %}
+{{ super() }}
+<script src="https://cdn.bootcss.com/marked/0.4.0/marked.min.js"></script>
+<script src="{{ url_for("static",filename="libs/js/org.js") }}"></script>
+{%- endblock -%}
+
+{%- block style %}
+{{ super() }}
 <style>
  .tokenfield .token {
      border: 1px solid #5cb85c;
@@ -7,6 +15,9 @@
      color:#eee;
  }
 </style>
+{%- endblock -%}
+
+{% block content %}
 {{ breadcrumb(active=_('Ask'))}}
 <div class="panel panel-primary">
   <div class="panel-heading">{{ _('Ask') }}</div>

+ 1 - 1
templates/topic/ask/form.html

@@ -33,6 +33,6 @@
     {{ place.content() }}
   </div>
   <div class="col-sm-10" style="margin-bottom:8px;">
-    {{ form.content(class="form-control",rows="8",onchange="preview()",placeholder="请输入问题描述") }}
+    {{ form.content(class="form-control",rows="8",placeholder="请输入问题描述") }}
   </div>
 </div>

+ 3 - 3
translations/babel.cfg

@@ -1,4 +1,4 @@
-[ignore: ../**/static/**.html]
-[python: ../**.py]
-[jinja2: ../templates/**.html]
+[ignore: **/static/**.html]
+[python: **.py]
+[jinja2: **.html]
 extensions=jinja2.ext.autoescape,jinja2.ext.with_

BIN
translations/zh/LC_MESSAGES/messages.mo


+ 97 - 93
translations/zh/LC_MESSAGES/messages.po

@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PROJECT VERSION\n"
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2017-04-21 19:08+0800\n"
+"POT-Creation-Date: 2018-07-26 11:31+0800\n"
 "PO-Revision-Date: 2016-06-16 14:36+0800\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language: zh\n"
@@ -16,180 +16,172 @@ msgstr ""
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"Generated-By: Babel 2.4.0\n"
+"Generated-By: Babel 2.6.0\n"
 
-#: forums/extension.py:67
-msgid "Please login to access this page."
-msgstr "这个页面要求登陆,请登陆"
-
-#: forums/api/auth/forms.py:22 forums/api/forms.py:48
+#: forums/api/forms.py:48
 msgid "Username:"
 msgstr "用户名:"
 
-#: forums/api/auth/forms.py:25 forums/api/forms.py:51
+#: forums/api/forms.py:50
 msgid "Password:"
 msgstr "密码:"
 
-#: forums/api/auth/forms.py:28 forums/api/forms.py:54
+#: forums/api/forms.py:52
 msgid "Captcha:"
 msgstr "验证码:"
 
-#: forums/api/auth/forms.py:39 forums/api/forms.py:65
+#: forums/api/forms.py:62
 msgid "The captcha is error"
 msgstr "验证码错误"
 
-#: forums/api/auth/forms.py:46 forums/api/forms.py:72
+#: forums/api/forms.py:69
 msgid "Email:"
 msgstr "邮箱"
 
-#: forums/api/auth/forms.py:50 forums/api/forms.py:76
+#: forums/api/forms.py:73
 msgid "Remember me"
 msgstr "记住我"
 
-#: forums/api/forms.py:79 forums/api/topic/views.py:84
+#: forums/api/forms.py:76 forums/api/topic/views.py:73
 #: templates/topic/topic_good.html:13 templates/topic/topic_list.html:4
 #: templates/topic/topic_list.html:6 templates/topic/topic_list.html:8
 #: templates/topic/topic_top.html:13
 msgid "All Topics"
 msgstr "所有主题"
 
-#: forums/api/forms.py:79
+#: forums/api/forms.py:76
 msgid "One Day"
 msgstr "一天之内"
 
-#: forums/api/forms.py:79
+#: forums/api/forms.py:76
 msgid "One Week"
 msgstr "一周之内"
 
-#: forums/api/forms.py:80
+#: forums/api/forms.py:77
 msgid "One Month"
 msgstr "一月之内"
 
-#: forums/api/forms.py:80
+#: forums/api/forms.py:77
 msgid "One Year"
 msgstr "一年之内"
 
-#: forums/api/forms.py:82
+#: forums/api/forms.py:79
 msgid "Publish"
 msgstr "发表时间"
 
-#: forums/api/forms.py:82 templates/forums/_macro.html:48
+#: forums/api/forms.py:79 templates/forums/_macro.html:48
 #: templates/topic/_list_macro.html:13
 msgid "Author"
 msgstr "发表作者"
 
-#: forums/api/forms.py:84
+#: forums/api/forms.py:81
 msgid "Desc"
 msgstr "降序"
 
-#: forums/api/forms.py:84
+#: forums/api/forms.py:81
 msgid "Asc"
 msgstr "升序"
 
-#: forums/api/forms.py:88
+#: forums/api/forms.py:85
 msgid "Choice"
 msgstr "选择"
 
-#: forums/api/forms.py:94
-msgid "search"
-msgstr "搜索"
-
-#: forums/api/forms.py:98
+#: forums/api/forms.py:91
 msgid "message"
 msgstr "私信"
 
-#: forums/api/forms.py:102
+#: forums/api/forms.py:95
 msgid "Title:"
 msgstr "标题:"
 
-#: forums/api/forms.py:103 forums/api/forms.py:111
+#: forums/api/forms.py:96 forums/api/forms.py:104
 msgid "Content:"
 msgstr "内容:"
 
-#: forums/api/forms.py:104
+#: forums/api/forms.py:97
 msgid "Category:"
 msgstr "分类:"
 
-#: forums/api/forms.py:105
+#: forums/api/forms.py:98
 msgid "Tags:"
 msgstr "节点:"
 
-#: forums/api/forms.py:107
+#: forums/api/forms.py:100
 msgid "ContentType"
 msgstr "标记语言"
 
-#: forums/api/forms.py:115
+#: forums/api/forms.py:108
 msgid "Name:"
 msgstr "收藏夹名:"
 
-#: forums/api/forms.py:116
+#: forums/api/forms.py:109
 msgid "Description:"
 msgstr "描述:"
 
-#: forums/api/forms.py:128
+#: forums/api/forms.py:112
+msgid "is_hidden"
+msgstr "私密"
+
+#: forums/api/forms.py:112
+msgid "is_public"
+msgstr "公开"
+
+#: forums/api/forms.py:123
 msgid "Upload Avatar:"
 msgstr "上传头像"
 
-#: forums/api/forms.py:135
+#: forums/api/forms.py:132
 msgid "Online status:"
 msgstr "在线状态:"
 
-#: forums/api/forms.py:136
+#: forums/api/forms.py:133
 msgid "Topic List:"
 msgstr "主题列表:"
 
-#: forums/api/forms.py:138
+#: forums/api/forms.py:135
 msgid "Reply List:"
 msgstr "回复列表:"
 
-#: forums/api/forms.py:139
+#: forums/api/forms.py:136
 msgid "Notebook List:"
 msgstr "笔记列表:"
 
-#: forums/api/forms.py:140
+#: forums/api/forms.py:137
 msgid "Collect List:"
 msgstr "收藏列表:"
 
-#: forums/api/forms.py:144
+#: forums/api/forms.py:141
 msgid "Introduce:"
 msgstr "个人介绍:"
 
-#: forums/api/forms.py:145
+#: forums/api/forms.py:142
 msgid "School:"
 msgstr "所在学校:"
 
-#: forums/api/forms.py:146
+#: forums/api/forms.py:143
 msgid "Signature:"
 msgstr "个性签名:"
 
-#: forums/api/forms.py:151
+#: forums/api/forms.py:148
 msgid "Old Password:"
 msgstr "原密码:"
 
-#: forums/api/forms.py:154
+#: forums/api/forms.py:151
 msgid "New Password:"
 msgstr "新密码:"
 
-#: forums/api/forms.py:157
+#: forums/api/forms.py:154
 msgid "New Password again:"
 msgstr "重复新密码:"
 
-#: forums/api/forms.py:161
+#: forums/api/forms.py:159
 msgid "Timezone:"
 msgstr "时区设置:"
 
-#: forums/api/forms.py:162
+#: forums/api/forms.py:160
 msgid "Locale:"
 msgstr "语言设置:"
 
-#: forums/api/auth/views.py:101
-msgid "Please confirm  your email!"
-msgstr "请验证你的邮箱!"
-
-#: forums/api/auth/views.py:146
-msgid "Please confirm  your email"
-msgstr "请验证你的邮箱!"
-
 #: forums/api/forums/views.py:37
 msgid "About - "
 msgstr "关于 - "
@@ -210,33 +202,33 @@ msgstr "提问 - "
 msgid "Edit -"
 msgstr "编辑 - "
 
-#: forums/api/topic/views.py:87 templates/topic/topic_good.html:13
+#: forums/api/topic/views.py:76 templates/topic/topic_good.html:13
 #: templates/topic/topic_list.html:6
 msgid "Good Topics"
 msgstr "社区精华帖子"
 
-#: forums/api/topic/views.py:90 templates/topic/topic_list.html:4
+#: forums/api/topic/views.py:79 templates/topic/topic_list.html:4
 #: templates/topic/topic_top.html:13
 msgid "Top Topics"
 msgstr "精华文章"
 
-#: forums/api/user/models.py:206
+#: forums/api/user/models.py:178
 msgid "ALLOW ALL USER"
 msgstr "允许所有人"
 
-#: forums/api/user/models.py:206
+#: forums/api/user/models.py:178
 msgid "ALLOW AUTHENTICATED USER"
 msgstr "只允许登陆用户"
 
-#: forums/api/user/models.py:207
+#: forums/api/user/models.py:179
 msgid "ALLOW OWN"
 msgstr "仅自己"
 
-#: forums/api/user/models.py:211
+#: forums/api/user/models.py:183
 msgid "Chinese"
 msgstr "中文"
 
-#: forums/api/user/models.py:211
+#: forums/api/user/models.py:183
 msgid "English"
 msgstr "英文"
 
@@ -280,6 +272,10 @@ msgstr ""
 msgid "Other error"
 msgstr ""
 
+#: forums/extension/login.py:30
+msgid "Please login to access this page."
+msgstr "这个页面要求登陆,请登陆"
+
 #: templates/auth/forget.html:1
 msgid "Forget Password - "
 msgstr "忘记密码 - "
@@ -293,7 +289,7 @@ msgid "Login - "
 msgstr "登陆 - "
 
 #: templates/auth/login.html:5 templates/auth/login.html:8
-#: templates/base/base.html:70 templates/topic/reply/form.html:15
+#: templates/base/base.html:73 templates/topic/reply/form.html:15
 msgid "Login"
 msgstr "登陆"
 
@@ -302,34 +298,26 @@ msgid "Register - "
 msgstr "注册 - "
 
 #: templates/auth/register.html:5 templates/auth/register.html:8
-#: templates/base/base.html:69
+#: templates/base/base.html:72
 msgid "Register"
 msgstr "注册"
 
-#: templates/base/base.html:53
+#: templates/base/base.html:56 templates/base/header.html:35
 msgid "Home Page"
 msgstr "主页"
 
-#: templates/base/base.html:54
+#: templates/base/base.html:57 templates/base/header.html:36
 msgid "Setting"
 msgstr "帐号设置"
 
-#: templates/base/base.html:56
+#: templates/base/base.html:59 templates/base/header.html:38
 msgid "Logout"
 msgstr "注销"
 
-#: templates/base/base.html:60
+#: templates/base/base.html:63
 msgid "NoticeList"
 msgstr "通知"
 
-#: templates/base/base.html:72 templates/base/link.html:10
-msgid "TagList"
-msgstr "节点"
-
-#: templates/base/base.html:73 templates/base/link.html:2
-msgid "UserList"
-msgstr "用户列表"
-
 #: templates/base/form.html:14
 msgid "save"
 msgstr "保存"
@@ -338,22 +326,30 @@ msgstr "保存"
 msgid "Index"
 msgstr "社区主页"
 
-#: templates/base/header.html:19 templates/forums/index.html:10
+#: templates/base/header.html:67 templates/forums/index.html:10
 msgid "Forums"
 msgstr "社区"
 
-#: templates/base/header.html:20 templates/forums/index.html:11
+#: templates/base/header.html:68 templates/forums/index.html:11
 msgid "Wiki"
 msgstr "文档"
 
-#: templates/base/header.html:21 templates/forums/index.html:12
+#: templates/base/header.html:69 templates/forums/index.html:12
 msgid "Blog"
 msgstr "博客"
 
-#: templates/base/header.html:22 templates/forums/index.html:13
+#: templates/base/header.html:70 templates/base/link.html:10
+msgid "TagList"
+msgstr "节点"
+
+#: templates/base/header.html:71 templates/forums/index.html:13
 msgid "Good"
 msgstr "精华文章"
 
+#: templates/base/link.html:2
+msgid "UserList"
+msgstr "用户列表"
+
 #: templates/base/link.html:18
 msgid "TopicList"
 msgstr "所有主题"
@@ -543,35 +539,35 @@ msgstr "没有通知"
 msgid "Userlist"
 msgstr "用户列表"
 
-#: templates/maple/footer.html:23
+#: templates/maple/footer.html:39
 msgid "Help"
 msgstr "帮助"
 
-#: templates/maple/footer.html:25
+#: templates/maple/footer.html:41
 msgid "About"
 msgstr "关于"
 
-#: templates/maple/footer.html:27
+#: templates/maple/footer.html:43
 msgid "Contact me"
 msgstr "联系我"
 
-#: templates/maple/footer.html:33
+#: templates/maple/footer.html:49
 msgid "Now users online:"
 msgstr "当前在线用户:"
 
-#: templates/maple/footer.html:34
+#: templates/maple/footer.html:50
 msgid "Registered users online:"
 msgstr "注册用户:"
 
-#: templates/maple/footer.html:35
+#: templates/maple/footer.html:51
 msgid "Guests online:"
 msgstr "当前访客:"
 
-#: templates/maple/footer.html:39
+#: templates/maple/footer.html:55
 msgid "Highest online:"
 msgstr "最高在线:"
 
-#: templates/maple/footer.html:40
+#: templates/maple/footer.html:56
 msgid "Time of highest online:"
 msgstr "最高在线时间:"
 
@@ -615,7 +611,7 @@ msgstr "所有标签"
 msgid "No Tags"
 msgstr "暂无标签"
 
-#: templates/topic/ask.html:10 templates/topic/ask.html:12
+#: templates/topic/ask.html:21 templates/topic/ask.html:23
 msgid "Ask"
 msgstr "提问"
 
@@ -653,17 +649,17 @@ msgstr "你需要"
 msgid "before you can reply."
 msgstr "后才能发表回复"
 
-#: templates/topic/reply/itemlist.html:4
+#: templates/topic/reply/itemlist.html:6
 #, python-format
 msgid "Received %(total)s replies"
 msgstr "共收到%(total)s条回复"
 
-#: templates/topic/reply/itemlist.html:6 templates/user/followers.html:12
+#: templates/topic/reply/itemlist.html:8 templates/user/followers.html:12
 #: templates/user/replies.html:12 templates/user/user.html:12
 msgid "time"
 msgstr "时间"
 
-#: templates/topic/reply/itemlist.html:7 templates/user/followers.html:13
+#: templates/topic/reply/itemlist.html:9 templates/user/followers.html:13
 #: templates/user/replies.html:13
 msgid "likers"
 msgstr "点赞"
@@ -821,3 +817,11 @@ msgstr "最后回复来自%(author)s"
 #~ msgid "search content"
 #~ msgstr "搜索内容"
 
+#~ msgid "search"
+#~ msgstr "搜索"
+
+#~ msgid "Please confirm  your email!"
+#~ msgstr "请验证你的邮箱!"
+
+#~ msgid "Please confirm  your email"
+#~ msgstr "请验证你的邮箱!"