├── .gitignore
├── INSTALL
├── README.textile
├── app.py
├── filter.py
├── forms.py
├── handlers.py
├── markdown.py
├── migrations
└── 00001_create_asks_answers_users.sql
├── models.py
├── run_app
├── session.py
├── static
├── css
│ ├── app.css
│ └── wmd.css
├── favicon.ico
├── img
│ ├── add_pic.gif
│ ├── comment_bg.gif
│ ├── edit.gif
│ ├── link_out.gif
│ ├── lquote.gif
│ ├── more_bg.gif
│ ├── rquote.gif
│ ├── vote.gif
│ ├── wmd-buttons-bg.gif
│ ├── wmd-buttons.png
│ └── x.gif
└── js
│ ├── app.js
│ ├── jquery.js
│ ├── showdown.js
│ └── wmd.js
├── templates
├── _comments.html
├── ask.html
├── ask_show.html
├── base.html
├── home.html
├── login.html
├── profile.html
├── register.html
├── settings.html
└── sidebar.html
└── utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.pyc
2 | tmp/session/*
3 |
--------------------------------------------------------------------------------
/INSTALL:
--------------------------------------------------------------------------------
1 | sudo easy_install Jinja2
2 | sudo easy_install mongoengine
3 | sudo easy_install formencode
4 |
5 |
--------------------------------------------------------------------------------
/README.textile:
--------------------------------------------------------------------------------
1 | This project is code by Python + Torando, a QA community, clone with "Quora":http://quora.com
2 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | # coding: UTF-8
2 | import os
3 | import re
4 | import tornado.auth
5 | import tornado.httpserver
6 | import tornado.ioloop
7 | import tornado.options
8 | import tornado.web
9 | import tornado.autoreload
10 | import unicodedata
11 | from tornado.options import define, options
12 | from jinja2 import Template, Environment, FileSystemLoader
13 | from handlers import *
14 | import filter
15 | import session
16 | from mongoengine import *
17 |
18 | def markdown_tag(str):
19 | return markdown.markdown(str)
20 |
21 | define("port", default=8888, help="run on the given port", type=int)
22 | define("mongo_host", default="127.0.0.1:3306", help="database host")
23 | define("mongo_database", default="quora", help="database name")
24 |
25 | class Application(tornado.web.Application):
26 | def __init__(self):
27 | handlers = [
28 | (r"/", HomeHandler),
29 | (r"/login", LoginHandler),
30 | (r"/register", RegisterHandler),
31 | (r"/logout", LogoutHandler),
32 | (r"/ask/([^/]+)", AskShowHandler),
33 | (r"/feed", FeedHandler),
34 | (r"/ask/([^/]+)/answer", AnswerHandler),
35 | (r"/ask/([^/]+)/flag", FlagAskHandler),
36 | (r"/ask", AskHandler),
37 | (r"/answer/([^/]+)/vote", AnswerVoteHandler),
38 | (r"/comment/([^/]+)/([^/]+)", CommentHandler),
39 | (r"/ask", AskHandler),
40 | (r"/settings", SettingsHandler),
41 | (r"/([^/]+)", ProfileHandler),
42 | ]
43 | settings = dict(
44 | app_name=u"我知",
45 | template_path=os.path.join(os.path.dirname(__file__), "templates"),
46 | static_path=os.path.join(os.path.dirname(__file__), "static"),
47 | xsrf_cookies=True,
48 | cookie_secret="81o0TzKaPpGtYdkL5gEmGepeuuYi7EPnp2XdTP1o&Vo=",
49 | login_url="/login",
50 | session_secret='08091287&^(01',
51 | session_dir=os.path.join(os.path.dirname(__file__), "tmp/session"),
52 | )
53 | self.session_manager = session.TornadoSessionManager(settings["session_secret"], settings["session_dir"])
54 | tornado.web.Application.__init__(self, handlers, **settings)
55 |
56 | # Connection MongoDB
57 | connect(options.mongo_database)
58 |
59 | def main():
60 | tornado.options.parse_command_line()
61 | http_server = tornado.httpserver.HTTPServer(Application())
62 | http_server.listen(options.port)
63 | instance = tornado.ioloop.IOLoop.instance()
64 | tornado.autoreload.start(instance)
65 | instance.start()
66 |
67 | if __name__ == "__main__":
68 | main()
69 |
--------------------------------------------------------------------------------
/filter.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | import re
3 | import markdown as Markdown
4 | from jinja2.utils import urlize, escape
5 | import urllib, hashlib
6 |
7 | def markdown(value):
8 | return Markdown.markdown(value)
9 |
10 | def md_body(value):
11 | value = urlize(value,32,True)
12 | return markdown(value)
13 |
14 | def tags_name_tag(tags,limit = 0):
15 | html = []
16 | if not tags: return ""
17 | if limit > 0:
18 | tags = tags[0:limit]
19 | for tag in tags:
20 | html.append('%s' % (tag,tag))
21 | return ",".join(html)
22 |
23 | def user_name_tag(user):
24 | return '%s' % (user.login,user.name)
25 |
26 | def strftime(value, type='normal'):
27 | if type == 'normal':
28 | format="%Y-%m-%d %H:%M"
29 | elif type == 'long':
30 | format="%Y-%m-%d %H:%M:%S"
31 | else:
32 | format="%m-%d %H:%M"
33 | return value.strftime(format)
34 |
35 | def strfdate(value,type='normal'):
36 | if type == 'normal':
37 | format="%Y-%m-%d"
38 | elif type == "long":
39 | format="%Y-%m-%d"
40 | else:
41 | format="%m-%d"
42 | return value.strftime(format)
43 |
44 | # check value is in list
45 | def inlist(value,list):
46 | if list.count(value) > 0:
47 | return True
48 | return false
49 |
50 | def avatar(user, size = 40):
51 | gravatar_url = "http://www.gravatar.com/avatar/" + hashlib.md5(user.email).hexdigest() + "?"
52 | gravatar_url += urllib.urlencode({'s':str(size)})
53 | return "" % (user.login,gravatar_url,size,user.name)
54 |
55 |
56 |
--------------------------------------------------------------------------------
/forms.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | import re
3 | import logging
4 | import urlparse
5 | import formencode
6 | from formencode import htmlfill, validators
7 |
8 |
9 | class BaseForm(formencode.Schema):
10 | """
11 | by Felinx Lee
12 | https://bitbucket.org/felinx/poweredsites/src/8448db5ba387/poweredsites/forms/base.py
13 | """
14 | allow_extra_fields = True
15 | filter_extra_fields = True
16 |
17 | _xsrf = validators.PlainText(not_empty=True, max=32)
18 |
19 | def __init__(self, handler, form_id = None):
20 |
21 | self._parmas = {}
22 | self._values = {}
23 | self._form_errors = {}
24 | self.form_id = form_id
25 | arguments = {}
26 |
27 | # re-parse qs, keep_blankvalues for formencode to validate
28 | # so formencode not_empty setting work.
29 | request = handler.request
30 | content_type = request.headers.get("Content-Type", "")
31 |
32 | if request.method == "POST":
33 | if content_type.startswith("application/x-www-form-urlencoded"):
34 | arguments = urlparse.parse_qs(request.body, keep_blank_values=1)
35 |
36 | for k, v in arguments.iteritems():
37 | if len(v) == 1:
38 | self._parmas[k] = v[0]
39 | else:
40 | # keep a list of values as list (or set)
41 | self._parmas[k] = v
42 |
43 | self._handler = handler
44 | self._result = True
45 |
46 | def validate(self):
47 | try:
48 | self._values = self.to_python(self._parmas)
49 | self._result = True
50 | self.__after__()
51 | except formencode.Invalid, error:
52 | self._values = error.value
53 | self._form_errors = error.error_dict or {}
54 | self._result = False
55 |
56 | # map values to define form propertys and decode utf8
57 | for k in self._values.keys():
58 | exec("self.%s = self._values[\"%s\"].decode('utf8')" % (k,k))
59 |
60 | return self._result
61 |
62 | # add custom error msg
63 | def add_error(self, attr, msg):
64 | self._result = False
65 | self._form_errors[attr] = msg
66 |
67 | def render(self, template_name, **kwargs):
68 | html = self._handler.render_string(template_name, **kwargs)
69 |
70 | if not self._result:
71 | html = htmlfill.render(html,
72 | defaults=self._values,
73 | errors=self._form_errors,
74 | encoding="utf8")
75 |
76 | self._handler.finish(html)
77 |
78 | # post process hook
79 | def __after__(self):
80 | pass
81 |
82 |
83 |
84 |
85 |
86 | class LoginForm(BaseForm):
87 | login = validators.String(not_empty=True,strip=True)
88 | password = validators.String(not_empty=True)
89 |
90 | class RegisterForm(BaseForm):
91 | login = validators.String(not_empty=True,strip=True,min=4,max=20)
92 | email = validators.Email(not_empty=True,strip=True)
93 | name = validators.String(not_empty=True,strip=True)
94 | password = validators.String(not_empty=True)
95 | password_confirm = validators.String(not_empty=True)
96 | chained_validators = [validators.FieldsMatch('password', 'password_confirm')]
97 |
98 | class SettingsForm(BaseForm):
99 | email = validators.Email(not_empty=True,strip=True)
100 | name = validators.String(not_empty=True,strip=True)
101 | blog = validators.URL(not_empty=True,strip=True)
102 | bio = validators.String(not_empty=True,max=300)
103 |
104 | class AskForm(BaseForm):
105 | title = validators.String(not_empty=True,min=5,max=255,strip=True)
106 | body = validators.String()
107 | tags = validators.String(strip=True)
108 |
109 | class AnswerForm(BaseForm):
110 | answer_body = validators.String(not_empty=True,min=2,strip=True)
111 |
112 |
113 |
--------------------------------------------------------------------------------
/handlers.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | import sys
3 | import tornado.web
4 | import tornado.auth
5 | from jinja2 import Template, Environment, FileSystemLoader
6 | from pymongo.objectid import ObjectId
7 |
8 | import filter, utils, session
9 | from forms import *
10 | from models import *
11 |
12 | class BaseHandler(tornado.web.RequestHandler):
13 |
14 | def __init__(self, application, request, **kwargs):
15 | tornado.web.RequestHandler.__init__(self, application, request, **kwargs)
16 | self.session = session.TornadoSession(application.session_manager, self)
17 | self._title = self.settings['app_name']
18 |
19 | def render_string(self,template,**args):
20 | env = Environment(loader=FileSystemLoader(self.settings['template_path']))
21 | env.filters['markdown'] = filter.markdown
22 | env.filters['md_body'] = filter.md_body
23 | env.filters['tags_name_tag'] = filter.tags_name_tag
24 | env.filters['user_name_tag'] = filter.user_name_tag
25 | env.filters['strftime'] = filter.strftime
26 | env.filters['strfdate'] = filter.strfdate
27 | env.filters['avatar'] = filter.avatar
28 | env.filters['truncate_lines'] = utils.truncate_lines
29 | template = env.get_template(template)
30 | return template.render(settings=self.settings,
31 | title=self._title,
32 | notice_message=self.notice_message,
33 | current_user=self.current_user,
34 | static_url=self.static_url,
35 | modules=self.ui['modules'],
36 | xsrf_form_html=self.xsrf_form_html,
37 | **args)
38 |
39 | def render(self, template, **args):
40 | self.finish(self.render_string(template, **args))
41 |
42 | def get_current_user(self):
43 | user_id = self.get_secure_cookie("user_id")
44 | if not user_id: return None
45 | try:
46 | return User.objects(id = user_id).first()
47 | except:
48 | return None
49 |
50 | def notice(self,msg,type = "success"):
51 | type = type.lower()
52 | if ['error','success','warring'].count(type) == 0:
53 | type = "success"
54 | self.session["notice_%s" % type] = msg
55 | self.session.save()
56 |
57 | @property
58 | def notice_message(self):
59 | try:
60 | msg = self.session['notice_error']
61 | self.session['notice_error'] = None
62 | self.session['notice_success'] = None
63 | self.session['notice_warring'] = None
64 | self.session.save()
65 | if not msg:
66 | return ""
67 | else:
68 | return msg
69 | except:
70 | return ""
71 |
72 | def render_404(self):
73 | raise tornado.web.HTTPError(404)
74 |
75 | def set_title(self, str):
76 | self._title = u"%s - %s" % (str,self.settings['app_name'])
77 |
78 | class HomeHandler(BaseHandler):
79 | @tornado.web.authenticated
80 | def get(self):
81 | last_id = self.get_argument("last", None)
82 | if not last_id:
83 | asks = Ask.objects.order_by("-replied_at").limit(10)
84 | else:
85 | asks = Ask.order_by("-replied_at").objects(id_lt = last_id).limit(10)
86 | if not asks:
87 | self.redirect("/ask")
88 | else:
89 | self.render("home.html", asks=asks)
90 |
91 |
92 | class AskHandler(BaseHandler):
93 | @tornado.web.authenticated
94 | def get(self):
95 | ask = Ask()
96 | self.set_title(u"提问题")
97 | self.render("ask.html",ask=ask)
98 |
99 | @tornado.web.authenticated
100 | def post(self):
101 | self.set_title(u"提问题")
102 | frm = AskForm(self)
103 | if not frm.validate():
104 | frm.render("ask.html")
105 | return
106 |
107 | ask = Ask(title=frm.title,
108 | body = frm.body,
109 | summary = utils.truncate_lines(frm.body,3,500),
110 | user = self.current_user,
111 | tags = utils.format_tags(frm.tags))
112 | try:
113 | ask.save()
114 | self.redirect("/ask/%s" % ask.id)
115 | except Exception,exc:
116 | self.notice(exc,"error")
117 | frm.render("ask.html")
118 |
119 |
120 |
121 | class AskShowHandler(BaseHandler):
122 | @tornado.web.authenticated
123 | def get(self,id):
124 | ask = Ask.objects(id=id).first()
125 | if not ask:
126 | render_404
127 | answers = Answer.objects(ask=ask).order_by("-vote","created_at")
128 | self.set_title(ask.title)
129 | self.render("ask_show.html",ask=ask, answers=answers)
130 |
131 |
132 | class AnswerHandler(BaseHandler):
133 | @tornado.web.authenticated
134 | def get(self,ask_id):
135 | self.redirect("/ask/%s" % ask_id)
136 |
137 | @tornado.web.authenticated
138 | def post(self,ask_id):
139 | ask = Ask.objects(id=ask_id).first()
140 | self.set_title(u"回答")
141 | frm = AnswerForm(self)
142 | if not frm.validate():
143 | frm.render("ask_show.html",ask=ask)
144 | return
145 |
146 | answer = Answer(ask=ask,
147 | body=frm.answer_body,
148 | user=self.current_user)
149 | try:
150 | answer.save()
151 | Ask.objects(id=ask_id).update_one(inc__answers_count=1,set__replied_at=answer.created_at)
152 | self.redirect("/ask/%s" % ask_id)
153 | except Exception,exc:
154 | self.notice(exc,"error")
155 | frm.render("ask_show.html", ask=ask)
156 |
157 | class AnswerVoteHandler(BaseHandler):
158 | @tornado.web.authenticated
159 | def get(self, id):
160 | up = True
161 | if self.get_argument("up","0") == "0":
162 | up = False
163 | result = Answer.do_vote(id, up, self.current_user)
164 | self.write(str(result))
165 |
166 | class LogoutHandler(BaseHandler):
167 | @tornado.web.authenticated
168 | def get(self):
169 | self.set_secure_cookie("user_id","")
170 | self.redirect("/login")
171 |
172 | class LoginHandler(BaseHandler):
173 | def get(self):
174 | self.set_title(u"登陆")
175 | self.render("login.html")
176 |
177 | def post(self):
178 | self.set_title(u"登陆")
179 | frm = LoginForm(self)
180 | if not frm.validate():
181 | frm.render("login.html")
182 | return
183 |
184 | password = utils.md5(frm.password)
185 | user = User.objects(login=frm.login,
186 | password=password).first()
187 | if not user:
188 | frm.add_error("password", "不正确")
189 | frm.render("login.html")
190 |
191 | self.set_secure_cookie("user_id", str(user.id))
192 | self.redirect(self.get_argument("next","/"))
193 |
194 | class RegisterHandler(BaseHandler):
195 | def get(self):
196 | self.set_title(u"注册")
197 | user = User()
198 | self.render("register.html", user=user)
199 |
200 | def post(self):
201 | self.set_title(u"注册")
202 | frm = RegisterForm(self)
203 | if not frm.validate():
204 | frm.render("register.html")
205 | return
206 |
207 | user = User(name=frm.name,
208 | login=frm.login,
209 | email=frm.email,
210 | password=utils.md5(frm.password))
211 | try:
212 | user.save()
213 | self.set_secure_cookie("user_id",str(user.id))
214 | self.redirect("/")
215 | except Exception,exc:
216 | self.notice(exc,"error")
217 | frm.render("register.html")
218 |
219 | class FeedHandler(BaseHandler):
220 | def get(self):
221 | self.render("feed.html")
222 |
223 |
224 | class CommentHandler(BaseHandler):
225 | @tornado.web.authenticated
226 | def post(self, commentable_type, commentable_id):
227 | commentable_type = commentable_type.lower()
228 | if ["ask","answer"].count(commentable_type) == 0: return ""
229 | comment = Comment(id=utils.sid(),
230 | body=self.get_argument("body",None),
231 | user=self.current_user)
232 | if commentable_type == "ask":
233 | Ask.objects(id=commentable_id).update_one(push__comments=comment)
234 | elif commentable_type == "answer":
235 | Answer.objects(id=commentable_id).update_one(push__comments=comment)
236 | comment_hash = { "success":1,
237 | "user_id":str(self.current_user.id),
238 | "name":self.current_user.name }
239 | self.write(tornado.escape.json_encode(comment_hash))
240 |
241 | class FlagAskHandler(BaseHandler):
242 | @tornado.web.authenticated
243 | def get(self, id):
244 | ask = Ask.objects(id=id)
245 | flag = self.get_argument("flag","1")
246 | if not ask:
247 | self.write("0")
248 | return
249 |
250 | if flag == "1":
251 | if ask.first().flagged_users.count(self.current_user):
252 | self.write("-1")
253 | return
254 | ask.update_one(push__flagged_users=self.current_user)
255 | else:
256 | ask.update_one(pull__flagged_users=self.current_user)
257 |
258 | class ProfileHandler(BaseHandler):
259 | def get(self, login):
260 | user = User.objects(login=login).first()
261 | if not user:
262 | self.render_404
263 | return
264 | self.set_title(user.name)
265 | self.render("profile.html",user=user)
266 |
267 |
268 | class SettingsHandler(BaseHandler):
269 | @tornado.web.authenticated
270 | def get(self):
271 | self.set_title(u"设置")
272 | self.render("settings.html")
273 |
274 | @tornado.web.authenticated
275 | def post(self):
276 | self.set_title(u"设置")
277 | frm = SettingsForm(self)
278 | if not frm.validate():
279 | frm.render("settings.html")
280 | return
281 |
282 | User.objects(id=self.current_user.id).update_one(set__name=frm.name,
283 | set__email=frm.email,
284 | set__blog=frm.blog,
285 | set__bio=frm.bio)
286 | self.notice("保存成功", 'success')
287 | self.redirect("/settings")
288 |
289 |
--------------------------------------------------------------------------------
/migrations/00001_create_asks_answers_users.sql:
--------------------------------------------------------------------------------
1 | SET SESSION storage_engine = "InnoDB";
2 | SET SESSION time_zone = "+0:00";
3 | ALTER DATABASE CHARACTER SET "utf8";
4 |
5 | CREATE DATABASE wenda;
6 | use wenda;
7 |
8 | DROP TABLE IF EXISTS `asks`;
9 | CREATE TABLE asks (
10 | id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
11 | user_id INT,
12 | title VARCHAR(512) NOT NULL,
13 | body MEDIUMTEXT NOT NULL,
14 | summary VARCHAR(512) NOT NULL,
15 | created_at DATETIME NOT NULL,
16 | updated_at TIMESTAMP NOT NULL,
17 | answers_count INT NOT NULL default 0,
18 | KEY `idx_user_id` (`user_id`)
19 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
20 |
21 |
22 | DROP TABLE IF EXISTS `users`;
23 | CREATE TABLE users (
24 | id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
25 | name VARCHAR(20) NOT NULL,
26 | email VARCHAR(255) NOT NULL,
27 | password VARCHAR(255) NOT NULL,
28 | bio VARCHAR(255),
29 | created_at DATETIME NOT NULL,
30 | updated_at TIMESTAMP NOT NULL
31 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
32 |
33 | DROP TABLE IF EXISTS `answers`;
34 | CREATE TABLE `answers` (
35 | id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
36 | ask_id INT NOT NULL,
37 | user_id INT,
38 | body MEDIUMTEXT NOT NULL,
39 | created_at DATETIME NOT NULL,
40 | updated_at TIMESTAMP NOT NULL,
41 | KEY `idx_ask_id` (`ask_id`)
42 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
43 |
44 |
--------------------------------------------------------------------------------
/models.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | from mongoengine import *
3 | import datetime
4 |
5 | class User(Document):
6 | login = StringField(required=True,min_length=4,max_length=20)
7 | email = EmailField(required=True,unique=True)
8 | name = StringField(required=True,min_length=2)
9 | password = StringField(required=True)
10 | blog = URLField()
11 | bio = StringField(max_length=1000)
12 | created_at = DateTimeField(default=datetime.datetime.now)
13 |
14 | class Comment(EmbeddedDocument):
15 | id = StringField(required=True)
16 | body = StringField(required=True,min_length=4, max_length=2000)
17 | user = ReferenceField(User)
18 | created_at = DateTimeField(default=datetime.datetime.now)
19 |
20 | class Vote(EmbeddedDocument):
21 | user = ReferenceField(User,required=True)
22 | up = BooleanField(required=True,default=True)
23 |
24 | class Ask(Document):
25 | title = StringField(required=True,min_length=5,max_length=255)
26 | body = StringField()
27 | summary = StringField()
28 | user = ReferenceField(User)
29 | tags = ListField(StringField(max_length=30))
30 | comments = ListField(EmbeddedDocumentField(Comment))
31 | answers_count = IntField(required=True,default=0)
32 | flagged_users = ListField(ReferenceField(User))
33 | created_at = DateTimeField(default=datetime.datetime.now)
34 | replied_at = DateTimeField(default=datetime.datetime.now)
35 |
36 | class Answer(Document):
37 | ask = ReferenceField(Ask)
38 | body = StringField()
39 | user = ReferenceField(User)
40 | comments = ListField(EmbeddedDocumentField(Comment))
41 | vote = IntField(required=True,default=0)
42 | votes = ListField(EmbeddedDocumentField(Vote))
43 | created_at = DateTimeField(default=datetime.datetime.now)
44 |
45 | @staticmethod
46 | def do_vote(id, up, user):
47 | answer = Answer.objects(id=id).first()
48 | if not answer: return 0
49 |
50 | new_vote = Vote(user=user,up=up)
51 | for old_vote in answer.votes:
52 | # if there exist this user's vote
53 | if old_vote.user.id == user.id:
54 | # check is vote type equal this time type
55 | if old_vote.up == up:
56 | return -1
57 | else:
58 | # remove old voted_user
59 | Answer.objects(id=id).update_one(pull__votes=old_vote)
60 | break
61 |
62 | if up == True:
63 | vote_num = 1
64 | else:
65 | vote_num = -1
66 |
67 | Answer.objects(id=id).update_one(inc__vote=vote_num,
68 | push__votes=new_vote)
69 | return 1
70 |
71 |
72 |
--------------------------------------------------------------------------------
/run_app:
--------------------------------------------------------------------------------
1 | python app.py --port=5001 --mongo_host=127.0.0.1 --logging=error --log_file_prefix=/home/jason/wwwroot/quora/app.log &
2 |
--------------------------------------------------------------------------------
/session.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 |
4 | from: http://caines.ca/blog/programming/sessions-in-tornado/
5 | Usage:
6 | In your application script,
7 | settings["session_secret"] = 'some secret password!!'
8 | settings["session_dir"] = 'sessions' # the directory to store sessions in
9 | application.session_manager = session.TornadoSessionManager(settings["session_secret"], settings["session_dir"])
10 |
11 | In your RequestHandler (probably in __init__),
12 | self.session = session.TornadoSession(self.application.session_manager, self)
13 |
14 | After that, you can use it like this (in get(), post(), etc):
15 | self.session['blah'] = 1234
16 | self.save()
17 | blah = self.session['blah']
18 |
19 | etc.
20 |
21 |
22 | the basic session mechanism is this:
23 | * take some data, pickle it, store it somewhere.
24 | * assign an id to it. run that id through a HMAC (NOT just a hash function) to prevent tampering.
25 | * put the id and HMAC output in a cookie.
26 | * when you get a request, load the id, verify the HMAC. if it matches, load the data from wherever you put it and depickle it.
27 |
28 |
29 | """
30 |
31 | import pickle
32 | import os.path
33 | import hmac
34 | import hashlib
35 | import uuid
36 |
37 | class Session(dict):
38 | """ A Session is basically a dict with a session_id and an hmac_digest string to verify access rights """
39 | def __init__(self, session_id, hmac_digest):
40 | self.session_id = session_id
41 | self.hmac_digest = hmac_digest
42 |
43 |
44 | class SessionManager(object):
45 | """ SessionManager handles the cookie and file read/writes for a Session """
46 | def __init__(self, secret, session_dir = ''):
47 | self.secret = secret
48 |
49 | # figure out where to store the session file
50 | if session_dir == '':
51 | session_dir = os.path.join(os.path.dirname(__file__), 'sessions')
52 | self.session_dir = session_dir
53 |
54 |
55 | def _read(self, session_id):
56 | session_path = self._get_session_path(session_id)
57 | try :
58 | data = pickle.load(open(session_path))
59 | if type(data) == type({}):
60 | return data
61 | else:
62 | return {}
63 | except IOError:
64 | return {}
65 |
66 | def get(self, session_id = None, hmac_digest = None):
67 | # set up the session state (create it from scratch, or from parameters
68 | if session_id == None:
69 | session_should_exist = False
70 | session_id = self._generate_uid()
71 | hmac_digest = self._get_hmac_digest(session_id)
72 | else:
73 | session_should_exist = True
74 | session_id = session_id
75 | hmac_digest = hmac_digest # keyed-Hash Message Authentication Code
76 |
77 | # make sure the HMAC digest we generate matches the given one, to validate
78 | expected_hmac_digest = self._get_hmac_digest(session_id)
79 | if hmac_digest != expected_hmac_digest:
80 | raise InvalidSessionException()
81 |
82 | # create the session object
83 | session = Session(session_id, hmac_digest)
84 |
85 | # read the session file, if this is a pre-existing session
86 | if session_should_exist:
87 | data = self._read(session_id)
88 | for i, j in data.iteritems():
89 | session[i] = j
90 |
91 | return session
92 |
93 | def _get_session_path(self, session_id):
94 | return os.path.join(self.session_dir, 'SESSION' + str(session_id))
95 |
96 | def set(self, session):
97 | session_path = self._get_session_path(session.session_id)
98 | session_file = open(session_path, 'wb')
99 | pickle.dump(dict(session.items()), session_file)
100 | session_file.close()
101 |
102 | def _get_hmac_digest(self, session_id):
103 | return hmac.new(session_id, self.secret, hashlib.sha1).hexdigest()
104 |
105 | def _generate_uid(self):
106 | base = hashlib.md5( self.secret + str(uuid.uuid4()) )
107 | return base.hexdigest()
108 |
109 | class TornadoSessionManager(SessionManager):
110 | """ A TornadoSessionManager is a SessionManager that is specifically for use in Tornado, using Tornado's cookies """
111 |
112 | def get(self, requestHandler = None):
113 | if requestHandler == None:
114 | return super(TornadoSessionManager, self).get()
115 | else:
116 | session_id = requestHandler.get_secure_cookie("session_id")
117 | hmac_digest = requestHandler.get_secure_cookie("hmac_digest")
118 | return super(TornadoSessionManager, self).get(session_id, hmac_digest)
119 |
120 |
121 | def set(self, requestHandler, session):
122 | requestHandler.set_secure_cookie("session_id", session.session_id)
123 | requestHandler.set_secure_cookie("hmac_digest", session.hmac_digest)
124 | return super(TornadoSessionManager, self).set(session)
125 |
126 | class TornadoSession(Session):
127 | """ A TornadoSession is a Session object for use in Tornado """
128 | def __init__(self, tornado_session_manager, request_handler):
129 | self.session_manager = tornado_session_manager
130 | self.request_handler = request_handler
131 | # get the session object's data and transfer it to this session item
132 | try:
133 | plain_session = tornado_session_manager.get(request_handler)
134 | except InvalidSessionException:
135 | plain_session = tornado_session_manager.get()
136 |
137 | for i, j in plain_session.iteritems():
138 | self[i] = j
139 | self.session_id = plain_session.session_id
140 | self.hmac_digest = plain_session.hmac_digest
141 |
142 |
143 |
144 | def save(self):
145 | self.session_manager.set(self.request_handler, self)
146 |
147 | class InvalidSessionException(Exception):
148 | pass
149 |
--------------------------------------------------------------------------------
/static/css/app.css:
--------------------------------------------------------------------------------
1 | body {font-family:"Helvetica Neue",Helvetica,Arial,default;font-size:80%; }
2 | * {margin:0;padding:0;}
3 | a {text-decoration:none;}
4 | a:link,a:visited { color: #19558D; }
5 | a:hover {text-decoration:underline;}
6 | img {border:0;}
7 | input {font-family:Helvetica,Arial,default;outline:none;}
8 | hr {border:0;border-top:1px solid #bcc4d3;}
9 | h1 {font-size:1.4em;margin:0 0 10px;}
10 | ul,ol { list-style-position :inside; margin:2px 10px; }
11 |
12 | input,textarea { border: 1px solid #999; width: 250px; padding:3px; }
13 | input.long,textarea.long { width: 700px; }
14 | textarea.long { height:300px; }
15 | button { border-color: #062C50; background: #19558D; text-shadow: none; color: white; min-width: 40px; cursor: pointer;
16 | -moz-border-radius: 3px;
17 | -webkit-border-radius: 3px;
18 | font-weight: bold; font-size: .9em; display: inline-block; padding: 3px 10px; text-align: center; }
19 | body { text-align:center; }
20 | .container { width:1260px; margin:0 auto; text-align:left; }
21 | #header {
22 | background: #505759;
23 | min-height: 35px;
24 | padding-top: 18px;
25 | color:#FFF;
26 | }
27 | #header #site_name { background:#a82400; color:#FFF; height:35px; width:68px; text-align:center; line-height:35px; float:left;
28 | font-size:20px; font-weight:bold; }
29 | #header #site_name a { color:#FFF; text-decoration:none; }
30 | #header #add_ask { display:inline-block; background:#000; height:35px; line-height:35px; padding:0 6px; overflow:hidden; }
31 | #header #add_ask input { width:360px; font-size: 16px;height: 18px; line-height:18px; border:0; margin-top: 2px; }
32 | #header #add_ask a { color: #F0F0F0; font-weight: bold; font-size: 13px; padding: 10px 10px; text-align: center;}
33 |
34 | #header #user_bar { padding-top:10px; }
35 | #header #user_bar a { color: #B9BBBC; font-weight:bold; font-size:13px; display:inline-block; margin-right:8px; }
36 | #footer { clear:both; padding-top:25px; height:80px; }
37 |
38 | #main { margin:20px 0; }
39 | .left_wrapper { float:left; width:870px; }
40 | #footer .left_wrapper ,
41 | #main .left_wrapper { margin-left:70px; margin-right:20px; width:780px; }
42 | .sidebar { float:right: width:450px; }
43 |
44 | .form { }
45 | .form .row { margin:5px 0; }
46 | .form .row label { font-size: 12px; color:#666; }
47 | .form span.error-message { color:red; }
48 |
49 | .ask { margin-bottom:15px; border-bottom:1px solid #ddd; padding-bottom:10px; }
50 | .ask h1 { font-size: 1.8em; line-height: 1.3em; letter-spacing: -1px; margin-bottom: 0; }
51 | .ask .title { font-size: 1.3em; line-height: 1.2em; font-weight: bold;}
52 | .ask .info { color: #999; margin:3px 0; }
53 | .md_body { font-size:1.2em; color: #333; line-height:130%; }
54 | .md_body blockquote { padding-left:28px; padding-top:8px; background:#FAEBBC url(/static/img/lquote.gif) left top no-repeat; }
55 | .md_body blockquote p { padding-right:28px; padding-bottom:8px; font-size:1em; background:url(/static/img/rquote.gif) bottom right no-repeat; }
56 | .md_body code { background: #F4F4F4; display:block; margin:5px; padding:2px; font-size:.6em; font-family:monaco,monospace; line-height:120%; }
57 | .md_body a { background:url(/static/img/link_out.gif) 4px right no-repeat; padding-right:10px; }
58 | a.page_more { margin-top:10px; padding:4px 0px; -moz-border-radius: 3px; -webkit-border-radius: 3px;
59 | display:block; text-align:center; font-weight: bold; cursor: pointer; color:#333;
60 | background:#ddd url(/static/img/more_bg.gif); border: 1px solid #bbb; font-size: .9em; }
61 | a.page_more:hover { color: #19558D; border-bottom: 1px solid #999; text-decoration: none; }
62 | a.page_more:active { background: #E0E0E0; border-color: #999; text-shadow: none; }
63 |
64 | a.tag { margin:0px 2px; }
65 |
66 | .answers { margin:20px 0; }
67 | .answers .vote_buttons { float:left; width:20px; position:relative; margin-top:5px; margin-right:8px; margin-left:-27px; }
68 | .answers .vote_buttons a:link,
69 | .answers .vote_buttons a:visited { display:block; width:20px; height:17px; background: #DFEAF4 url(/static/img/vote.gif); margin-bottom: 1px;
70 | -moz-border-radius: 3px; -webkit-border-radius: 3px; }
71 | .answers .vote_buttons a:hover,
72 | .answers .vote_buttons a.voted { background-color:#19558D; background-position: -17px left; }
73 | .answers .vote_buttons a.vote_down:link,
74 | .answers .vote_buttons a.vote_down:visited { background-position: top -20px; }
75 | .answers .vote_buttons a.vote_down:hover,
76 | .answers .vote_buttons a.vote_down.voted { background-color:#19558D; background-position: -20px -17px; }
77 | .answers .answer_border { padding-right:30px; }
78 | .answers .avatar_border { margin-top:2px; float:right; width:24px; margin-left:6px; }
79 |
80 | .answers .answer { border-bottom:1px solid #DDD; padding:8px 0; margin-bottom:3px; }
81 | .answers .answer .info { color: #999; margin:3px 0; }
82 |
83 | .ask .info a.user,
84 | .answer .info a.user { font-weight:bold; }
85 | .answer .votes { color:#999; margin-bottom:4px; }
86 | .answer .votes a { color:#999; }
87 |
88 | .answer .action,
89 | .ask .action { color:#999; margin:3px 0; }
90 | .ask .action a,
91 | .answer .action a { color:#538DC2; }
92 | .answer .action a.voted { color:#666; }
93 | .ask .action a.flagged { color:#999; }
94 |
95 | .comments { margin-top:10px; }
96 | .comments h2 { font-size:1em; color:#333; }
97 | .comments .comment { padding-left:15px; margin-top:8px; background:url(/static/img/comment_bg.gif) left top no-repeat; }
98 | .comments .comment .md_body { font-size:80%; }
99 | .comments .comment .info {font-size: .9em;}
100 | .comments .form { height: 50px; }
101 | .comments .form textarea { float:left; height: 30px; width:300px; margin-right:6px; }
102 |
103 | #login { margin: 20px 0; }
104 |
105 |
106 | .user_profile {}
107 | .user_profile .bio { margin-top:10px; }
108 |
--------------------------------------------------------------------------------
/static/css/wmd.css:
--------------------------------------------------------------------------------
1 |
2 | .wmd-panel
3 | {
4 | margin-left: 25%;
5 | margin-right: 25%;
6 | width: 50%;
7 | min-width: 500px;
8 | }
9 |
10 | #wmd-editor
11 | {
12 | background-color: Aquamarine;
13 | }
14 |
15 | .wmd-button-bar
16 | {
17 | width: 100%;
18 | /*
19 | background-color: Silver;
20 | */
21 | }
22 |
23 | .wmd-input
24 | {
25 | height: 500px;
26 | width: 100%;
27 | background-color: Gainsboro;
28 | border: 1px solid DarkGray;
29 | }
30 |
31 | .wmd-preview
32 | {
33 | background-color: LightGray;
34 | }
35 |
36 | .wmd-output
37 | {
38 | background-color: Pink;
39 | }
40 |
41 | .wmd-button-row
42 | {
43 | position: relative;
44 | margin-left: 0px;
45 | margin-right: 0px;
46 | margin-bottom: 5px;
47 | padding: 0px;
48 | height: 20px;
49 | }
50 |
51 | .wmd-spacer
52 | {
53 | width: 1px;
54 | height: 16px;
55 | margin-left: 7px;
56 | margin-right:7px;
57 | margin-bottom:3px;
58 | position: relative;
59 | background-color: Silver;
60 | display: inline-block;
61 | list-style: none;
62 | }
63 |
64 | .wmd-button
65 | {
66 | border:1px solid #ddd;
67 | padding: 4px 2px 3px;
68 | width: 18px;
69 | height: 13px;
70 | margin-right:2px;
71 | cursor: pointer;
72 | position: relative;
73 | background-image: url(/static/img/wmd-buttons.png);
74 | background-repeat: no-repeat;
75 | background-position: 0px 0px;
76 | display: inline-block;
77 | list-style: none;
78 | }
79 | .wmd-button:hover { border:1px solid #658bc3; }
80 |
81 | .wmd-button > a
82 | {
83 | border:0;
84 | position: absolute;
85 | display: inline-block;
86 | }
87 |
88 |
89 | /* sprite button slicing style information */
90 | .wmd-bold-button {background-position: 0px 0;}
91 | .wmd-italic-button {background-position: -20px 0;}
92 | .wmd-spacer1 {}
93 | .wmd-link-button {background-position: -40px 0;}
94 | .wmd-quote-button {background-position: -60px 0;}
95 | .wmd-code-button {background-position: -80px 0;}
96 | .wmd-image-button {background-position: -100px 0;}
97 | .wmd-spacer2 {}
98 | .wmd-olist-button {background-position: -120px 0;}
99 | .wmd-ulist-button {background-position: -140px 0;}
100 | .wmd-heading-button {background-position: -160px 0;}
101 | .wmd-hr-button {background-position: -180px 0;}
102 | .wmd-spacer3 {}
103 | .wmd-undo-button {background-position: -200px 0;}
104 | .wmd-redo-button {background-position: -220px 0;}
105 | .wmd-help-button {background-position: -240px 0;}
106 |
107 |
108 | .wmd-prompt-background
109 | {
110 | background-color: Black;
111 | }
112 |
113 | .wmd-prompt-dialog
114 | {
115 | border: 1px solid #999999;
116 | background-color: #F5F5F5;
117 | }
118 |
119 | .wmd-prompt-dialog > div {
120 | font-size: 0.8em;
121 | font-family: arial, helvetica, sans-serif;
122 | }
123 |
124 |
125 | .wmd-prompt-dialog > form > input[type="text"] {
126 | border: 1px solid #999999;
127 | color: black;
128 | }
129 |
130 | .wmd-prompt-dialog > form > input[type="button"]{
131 | border: 1px solid #888888;
132 | font-family: trebuchet MS, helvetica, sans-serif;
133 | font-size: 0.8em;
134 | font-weight: bold;
135 | }
136 |
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renxing/quora-python/e1fc1fef717150e0e99fbbc3bb8ed487b44ef902/static/favicon.ico
--------------------------------------------------------------------------------
/static/img/add_pic.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renxing/quora-python/e1fc1fef717150e0e99fbbc3bb8ed487b44ef902/static/img/add_pic.gif
--------------------------------------------------------------------------------
/static/img/comment_bg.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renxing/quora-python/e1fc1fef717150e0e99fbbc3bb8ed487b44ef902/static/img/comment_bg.gif
--------------------------------------------------------------------------------
/static/img/edit.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renxing/quora-python/e1fc1fef717150e0e99fbbc3bb8ed487b44ef902/static/img/edit.gif
--------------------------------------------------------------------------------
/static/img/link_out.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renxing/quora-python/e1fc1fef717150e0e99fbbc3bb8ed487b44ef902/static/img/link_out.gif
--------------------------------------------------------------------------------
/static/img/lquote.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renxing/quora-python/e1fc1fef717150e0e99fbbc3bb8ed487b44ef902/static/img/lquote.gif
--------------------------------------------------------------------------------
/static/img/more_bg.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renxing/quora-python/e1fc1fef717150e0e99fbbc3bb8ed487b44ef902/static/img/more_bg.gif
--------------------------------------------------------------------------------
/static/img/rquote.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renxing/quora-python/e1fc1fef717150e0e99fbbc3bb8ed487b44ef902/static/img/rquote.gif
--------------------------------------------------------------------------------
/static/img/vote.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renxing/quora-python/e1fc1fef717150e0e99fbbc3bb8ed487b44ef902/static/img/vote.gif
--------------------------------------------------------------------------------
/static/img/wmd-buttons-bg.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renxing/quora-python/e1fc1fef717150e0e99fbbc3bb8ed487b44ef902/static/img/wmd-buttons-bg.gif
--------------------------------------------------------------------------------
/static/img/wmd-buttons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renxing/quora-python/e1fc1fef717150e0e99fbbc3bb8ed487b44ef902/static/img/wmd-buttons.png
--------------------------------------------------------------------------------
/static/img/x.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renxing/quora-python/e1fc1fef717150e0e99fbbc3bb8ed487b44ef902/static/img/x.gif
--------------------------------------------------------------------------------
/static/js/app.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/renxing/quora-python/e1fc1fef717150e0e99fbbc3bb8ed487b44ef902/static/js/app.js
--------------------------------------------------------------------------------
/static/js/showdown.js:
--------------------------------------------------------------------------------
1 | //
2 | // showdown.js -- A javascript port of Markdown.
3 | //
4 | // Copyright (c) 2007 John Fraser.
5 | //
6 | // Original Markdown Copyright (c) 2004-2005 John Gruber
7 | //
tags get encoded.
95 | //
96 | // Clear the global hashes. If we don't clear these, you get conflicts
97 | // from other articles when generating a page which contains more than
98 | // one article (e.g. an index page that shows the N most recent
99 | // articles):
100 | g_urls = [];
101 | g_titles = [];
102 | g_html_blocks = [];
103 |
104 | // attacklab: Replace ~ with ~T
105 | // This lets us use tilde as an escape char to avoid md5 hashes
106 | // The choice of character is arbitray; anything that isn't
107 | // magic in Markdown will work.
108 | text = text.replace(/~/g, "~T");
109 |
110 | // attacklab: Replace $ with ~D
111 | // RegExp interprets $ as a special character
112 | // when it's in a replacement string
113 | text = text.replace(/\$/g, "~D");
114 |
115 | // Standardize line endings
116 | text = text.replace(/\r\n/g, "\n"); // DOS to Unix
117 | text = text.replace(/\r/g, "\n"); // Mac to Unix
118 | // Make sure text begins and ends with a couple of newlines:
119 | text = "\n\n" + text + "\n\n";
120 |
121 | // Convert all tabs to spaces.
122 | text = _Detab(text);
123 |
124 | // Strip any lines consisting only of spaces and tabs.
125 | // This makes subsequent regexen easier to write, because we can
126 | // match consecutive blank lines with /\n+/ instead of something
127 | // contorted like /[ \t]*\n+/ .
128 | text = text.replace(/^[ \t]+$/mg, "");
129 |
130 | // Turn block-level HTML blocks into hash entries
131 | text = _HashHTMLBlocks(text);
132 |
133 | // Strip link definitions, store in hashes.
134 | text = _StripLinkDefinitions(text);
135 |
136 | text = _RunBlockGamut(text);
137 |
138 | text = _UnescapeSpecialChars(text);
139 |
140 | // attacklab: Restore dollar signs
141 | text = text.replace(/~D/g, "$$");
142 |
143 | // attacklab: Restore tildes
144 | text = text.replace(/~T/g, "~");
145 |
146 | // ** GFM ** Auto-link URLs and emails
147 | text = text.replace(/https?\:\/\/[^"\s\<\>]*[^.,;'">\:\s\<\>\)\]\!]/g, function (wholeMatch, matchIndex){
148 | var left = text.slice(0, matchIndex);
149 | var right = text.slice(matchIndex);
150 | if (left.match(/<(a|img)[^>]+>?$/) && right.match(/^[^>]*>/)) {return wholeMatch;}
151 | return "" + wholeMatch + "";
152 | });
153 | text = text.replace(/[a-z0-9_\-+=.]+@[a-z0-9\-]+(\.[a-z0-9-]+)+/ig, function (wholeMatch) {
154 | return "" + wholeMatch + "";
155 | });
156 |
157 |
158 | return text;
159 | }
160 |
161 | var _StripLinkDefinitions = function (text) {
162 | //
163 | // Strips link definitions from text, stores the URLs and titles in
164 | // hash references.
165 | //
166 | // Link defs are in the form: ^[id]: url "optional title"
167 | /*
168 | var text = text.replace(/
169 | ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1
170 | [ \t]*
171 | \n? // maybe *one* newline
172 | [ \t]*
173 | (\S+?)>? // url = $2
174 | [ \t]*
175 | \n? // maybe one newline
176 | [ \t]*
177 | (?:
178 | (\n*) // any lines skipped = $3 attacklab: lookbehind removed
179 | ["(]
180 | (.+?) // title = $4
181 | [")]
182 | [ \t]*
183 | )? // title is optional
184 | (?:\n+|$)
185 | /gm,
186 | function(){...});
187 | */
188 | var text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm, function (wholeMatch, m1, m2, m3, m4) {
189 | m1 = m1.toLowerCase();
190 | g_urls[m1] = _EncodeAmpsAndAngles(m2); // Link IDs are case-insensitive
191 | if (m3) {
192 | // Oops, found blank lines, so it's not a title.
193 | // Put back the parenthetical statement we stole.
194 | return m3 + m4;
195 | } else if (m4) {
196 | g_titles[m1] = m4.replace(/"/g, """);
197 | }
198 |
199 | // Completely remove the definition from the text
200 | return "";
201 | });
202 |
203 | return text;
204 | }
205 |
206 | var _HashHTMLBlocks = function (text) {
207 | // attacklab: Double up blank lines to reduce lookaround
208 | text = text.replace(/\n/g, "\n\n");
209 |
210 | // Hashify HTML blocks:
211 | // We only want to do this for block-level HTML tags, such as headers,
212 | // lists, and tables. That's because we still want to wrap
s around 213 | // "paragraphs" that are wrapped in non-block-level tags, such as anchors, 214 | // phrase emphasis, and spans. The list of tags we're looking for is 215 | // hard-coded: 216 | var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del"; 217 | var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math"; 218 | 219 | // First, look for nested blocks, e.g.: 220 | //
tags around block-level tags.
362 | text = _HashHTMLBlocks(text);
363 | text = _FormParagraphs(text);
364 |
365 | return text;
366 | }
367 |
368 |
369 | var _RunSpanGamut = function (text) {
370 | //
371 | // These are all the transformations that occur *within* block-level
372 | // tags like paragraphs, headers, and list items.
373 | //
374 | text = _DoCodeSpans(text);
375 | text = _EscapeSpecialCharsWithinTagAttributes(text);
376 | text = _EncodeBackslashEscapes(text);
377 |
378 | // Process anchor and image tags. Images must come first,
379 | // because ![foo][f] looks like an anchor.
380 | text = _DoImages(text);
381 | text = _DoAnchors(text);
382 |
383 | // Make links out of things like ` Just type tags
1031 | //
1032 | // Strip leading and trailing lines:
1033 | text = text.replace(/^\n+/g, "");
1034 | text = text.replace(/\n+$/g, "");
1035 |
1036 | var grafs = text.split(/\n{2,}/g);
1037 | var grafsOut = new Array();
1038 |
1039 | //
1040 | // Wrap tags.
1041 | //
1042 | var end = grafs.length;
1043 | for (var i = 0; i < end; i++) {
1044 | var str = grafs[i];
1045 |
1046 | // if this is an HTML marker, copy it
1047 | if (str.search(/~K(\d+)K/g) >= 0) {
1048 | grafsOut.push(str);
1049 | }
1050 | else if (str.search(/\S/) >= 0) {
1051 | str = _RunSpanGamut(str);
1052 | str = str.replace(/\n/g, " ");
1054 | str += " Enter the image URL. You can also add a title, which will be displayed as a tool tip. Example: Enter the web address. You can also add a title, which will be displayed as a tool tip. Example:
\n");
392 |
393 | return text;
394 | }
395 |
396 | var _EscapeSpecialCharsWithinTagAttributes = function (text) {
397 | //
398 | // Within tags -- meaning between < and > -- encode [\ ` * _] so they
399 | // don't conflict with their use in Markdown for code, italics and strong.
400 | //
401 | // Build a regex to find HTML tags and comments. See Friedl's
402 | // "Mastering Regular Expressions", 2nd Ed., pp. 200-201.
403 | var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|)/gi;
404 |
405 | text = text.replace(regex, function (wholeMatch) {
406 | var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, "$1`");
407 | tag = escapeCharacters(tag, "\\`*_");
408 | return tag;
409 | });
410 |
411 | return text;
412 | }
413 |
414 | var _DoAnchors = function (text) {
415 | //
416 | // Turn Markdown link shortcuts into XHTML tags.
417 | //
418 | //
419 | // First, handle reference-style links: [link text] [id]
420 | //
421 | /*
422 | text = text.replace(/
423 | ( // wrap whole match in $1
424 | \[
425 | (
426 | (?:
427 | \[[^\]]*\] // allow brackets nested one level
428 | |
429 | [^\[] // or anything else
430 | )*
431 | )
432 | \]
433 |
434 | [ ]? // one optional space
435 | (?:\n[ ]*)? // one optional newline followed by spaces
436 |
437 | \[
438 | (.*?) // id = $3
439 | \]
440 | )()()()() // pad remaining backreferences
441 | /g,_DoAnchors_callback);
442 | */
443 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag);
444 |
445 | //
446 | // Next, inline-style links: [link text](url "optional title")
447 | //
448 | /*
449 | text = text.replace(/
450 | ( // wrap whole match in $1
451 | \[
452 | (
453 | (?:
454 | \[[^\]]*\] // allow brackets nested one level
455 | |
456 | [^\[\]] // or anything else
457 | )
458 | )
459 | \]
460 | \( // literal paren
461 | [ \t]*
462 | () // no id, so leave $3 empty
463 | (.*?)>? // href = $4
464 | [ \t]*
465 | ( // $5
466 | (['"]) // quote char = $6
467 | (.*?) // Title = $7
468 | \6 // matching quote
469 | [ \t]* // ignore any spaces/tabs between closing quote and )
470 | )? // title is optional
471 | \)
472 | )
473 | /g,writeAnchorTag);
474 | */
475 | text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()(.*?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeAnchorTag);
476 |
477 | //
478 | // Last, handle reference-style shortcuts: [link text]
479 | // These must come last in case you've also got [link test][1]
480 | // or [link test](/foo)
481 | //
482 | /*
483 | text = text.replace(/
484 | ( // wrap whole match in $1
485 | \[
486 | ([^\[\]]+) // link text = $2; can't contain '[' or ']'
487 | \]
488 | )()()()()() // pad rest of backreferences
489 | /g, writeAnchorTag);
490 | */
491 | text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag);
492 |
493 | return text;
494 | }
495 |
496 | var writeAnchorTag = function (wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
497 | if (m7 === undefined) m7 = "";
498 | var whole_match = m1;
499 | var link_text = m2;
500 | var link_id = m3.toLowerCase();
501 | var url = m4;
502 | var title = m7;
503 | var blank_target = false;
504 |
505 | if (url == "") {
506 | if (link_id == "") {
507 | // lower-case and turn embedded newlines into spaces
508 | link_id = link_text.toLowerCase().replace(/ ?\n/g, " ");
509 | } else {
510 | if (link_id[0]=="!") {
511 | blank_target = true;
512 | link_id = link_id.substr(1);
513 | }
514 | }
515 | url = "#" + link_id;
516 |
517 | if (g_urls[link_id] !== undefined) {
518 | url = g_urls[link_id];
519 | if (g_titles[link_id] !== undefined) {
520 | title = g_titles[link_id];
521 | }
522 | }
523 | else {
524 | if (whole_match.search(/\(\s*\)$/m) > -1) {
525 | // Special case for explicit empty url
526 | url = "";
527 | } else {
528 | return whole_match;
529 | }
530 | }
531 | } else {
532 | if (url[0]=="!") {
533 | blank_target = true;
534 | url = url.substr(1);
535 | }
536 | }
537 |
538 | url = escapeCharacters(url, "*_");
539 | var result = "" + link_text + "";
552 |
553 | return result;
554 | }
555 |
556 |
557 | var _DoImages = function (text) {
558 | //
559 | // Turn Markdown image shortcuts into tags.
560 | //
561 | //
562 | // First, handle reference-style labeled images: ![alt text][id]
563 | //
564 | /*
565 | text = text.replace(/
566 | ( // wrap whole match in $1
567 | !\[
568 | (.*?) // alt text = $2
569 | \]
570 |
571 | [ ]? // one optional space
572 | (?:\n[ ]*)? // one optional newline followed by spaces
573 |
574 | \[
575 | (.*?) // id = $3
576 | \]
577 | )()()()() // pad rest of backreferences
578 | /g,writeImageTag);
579 | */
580 | text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeImageTag);
581 |
582 | //
583 | // Next, handle inline images: 
584 | // Don't forget: encode * and _
585 | /*
586 | text = text.replace(/
587 | ( // wrap whole match in $1
588 | !\[
589 | (.*?) // alt text = $2
590 | \]
591 | \s? // One optional whitespace character
592 | \( // literal paren
593 | [ \t]*
594 | () // no id, so leave $3 empty
595 | (\S+?)>? // src url = $4
596 | [ \t]*
597 | ( // $5
598 | (['"]) // quote char = $6
599 | (.*?) // title = $7
600 | \6 // matching quote
601 | [ \t]*
602 | )? // title is optional
603 | \)
604 | )
605 | /g,writeImageTag);
606 | */
607 | text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()(\S+?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeImageTag);
608 |
609 | return text;
610 | }
611 |
612 | var writeImageTag = function (wholeMatch, m1, m2, m3, m4, m5, m6, m7) {
613 | var whole_match = m1;
614 | var alt_text = m2;
615 | var link_id = m3.toLowerCase();
616 | var url = m4;
617 | var title = m7;
618 |
619 | if (!title) title = "";
620 |
621 | if (url == "") {
622 | if (link_id == "") {
623 | // lower-case and turn embedded newlines into spaces
624 | link_id = alt_text.toLowerCase().replace(/ ?\n/g, " ");
625 | }
626 | url = "#" + link_id;
627 |
628 | if (g_urls[link_id] !== undefined) {
629 | url = g_urls[link_id];
630 | if (g_titles[link_id] !== undefined) {
631 | title = g_titles[link_id];
632 | }
633 | }
634 | else {
635 | return whole_match;
636 | }
637 | }
638 |
639 | alt_text = alt_text.replace(/"/g, """);
640 | url = escapeCharacters(url, "*_");
641 | var result = "
";
651 |
652 | return result;
653 | }
654 |
655 |
656 | var _DoHeaders = function (text) {
657 |
658 | // Setext-style headers:
659 | // Header 1
660 | // ========
661 | //
662 | // Header 2
663 | // --------
664 | //
665 | text = text.replace(/^(.+)[ \t]*\n=+[ \t]*\n+/gm, function (wholeMatch, m1) {
666 | return hashBlock("
" + _RunSpanGamut(m1) + "
");
667 | });
668 |
669 | text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, function (matchFound, m1) {
670 | return hashBlock("" + _RunSpanGamut(m1) + "
");
671 | });
672 |
673 | // atx-style headers:
674 | // # Header 1
675 | // ## Header 2
676 | // ## Header 2 with closing hashes ##
677 | // ...
678 | // ###### Header 6
679 | //
680 | /*
681 | text = text.replace(/
682 | ^(\#{1,6}) // $1 = string of #'s
683 | [ \t]*
684 | (.+?) // $2 = Header text
685 | [ \t]*
686 | \#* // optional closing #'s (not counted)
687 | \n+
688 | /gm, function() {...});
689 | */
690 |
691 | text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm, function (wholeMatch, m1, m2) {
692 | var h_level = m1.length;
693 | return hashBlock("` blocks.
846 | //
847 | /*
848 | text = text.replace(text,
849 | /(?:\n\n|^)
850 | ( // $1 = the code block -- one or more lines, starting with a space/tab
851 | (?:
852 | (?:[ ]{4}|\t) // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
853 | .*\n+
854 | )+
855 | )
856 | (\n*[ ]{0,3}[^ \t\n]|(?=~0)) // attacklab: g_tab_width
857 | /g,function(){...});
858 | */
859 |
860 | // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
861 | text += "~0";
862 |
863 | text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g, function (wholeMatch, m1, m2) {
864 | var codeblock = m1;
865 | var nextChar = m2;
866 |
867 | codeblock = _EncodeCode(_Outdent(codeblock));
868 | codeblock = _Detab(codeblock);
869 | codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines
870 | codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace
871 | codeblock = "
";
872 |
873 | return hashBlock(codeblock) + nextChar;
874 | });
875 |
876 | // attacklab: strip sentinel
877 | text = text.replace(/~0/, "");
878 |
879 | return text;
880 | }
881 |
882 | var hashBlock = function (text) {
883 | text = text.replace(/(^\n+|\n+$)/g, "");
884 | return "\n\n~K" + (g_html_blocks.push(text) - 1) + "K\n\n";
885 | }
886 |
887 |
888 | var _DoCodeSpans = function (text) {
889 | //
890 | // * Backtick quotes are used for " + codeblock + "\n
spans.
891 | //
892 | // * You can use multiple backticks as the delimiters if you want to
893 | // include literal backticks in the code span. So, this input:
894 | //
895 | // Just type ``foo `bar` baz`` at the prompt.
896 | //
897 | // Will translate to:
898 | //
899 | //
foo `bar` baz
at the prompt.`bar`
...
912 | //
913 | /*
914 | text = text.replace(/
915 | (^|[^\\]) // Character before opening ` can't be a backslash
916 | (`+) // $2 = Opening run of `
917 | ( // $3 = The code block
918 | [^\r]*?
919 | [^`] // attacklab: work around lack of lookbehind
920 | )
921 | \2 // Matching closer
922 | (?!`)
923 | /gm, function(){...});
924 | */
925 |
926 | text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, function (wholeMatch, m1, m2, m3, m4) {
927 | var c = m3;
928 | c = c.replace(/^([ \t]*)/g, ""); // leading whitespace
929 | c = c.replace(/[ \t]*$/g, ""); // trailing whitespace
930 | c = _EncodeCode(c);
931 | return m1 + "" + c + "
";
932 | });
933 |
934 | return text;
935 | }
936 |
937 |
938 | var _EncodeCode = function (text) {
939 | //
940 | // Encode/escape certain characters inside Markdown code runs.
941 | // The point is that in code, these characters are literals,
942 | // and lose their special Markdown meanings.
943 | //
944 | // Encode all ampersands; HTML entities are not
945 | // entities within a Markdown code span.
946 | text = text.replace(/&/g, "&");
947 |
948 | // Do the angle bracket song and dance:
949 | text = text.replace(//g, ">");
951 |
952 | // Encode "smart" quotes
953 | text = text.replace(/‘/g, "‘");
954 | text = text.replace(/’/g, "’");
955 | text = text.replace(/“/g, "“");
956 | text = text.replace(/”/g, "”");
957 | text = text.replace(/–/g, "—");
958 |
959 |
960 | // Now, escape characters that are magic in Markdown:
961 | text = escapeCharacters(text, "\*_{}[]\\", false);
962 |
963 | // jj the line above breaks this:
964 | //---
965 | //* Item
966 | // 1. Subitem
967 | // special char: *
968 | //---
969 | return text;
970 | }
971 |
972 |
973 | var _DoItalicsAndBold = function (text) {
974 |
975 | // must go first:
976 | text = text.replace(/(\*\*|__)(?=\S)([^\r]*?\S[*_]*)\1/g, "$2");
977 |
978 | text = text.replace(/(\w)_(\w)/g, "$1~E95E$2"); // ** GFM ** "~E95E" == escaped "_"
979 | text = text.replace(/(\*|_)(?=\S)([^\r]*?\S)\1/g, "$2");
980 |
981 | return text;
982 | }
983 |
984 |
985 | var _DoBlockQuotes = function (text) {
986 |
987 | /*
988 | text = text.replace(/
989 | ( // Wrap whole match in $1
990 | (
991 | ^[ \t]*>[ \t]? // '>' at the start of a line
992 | .+\n // rest of the first line
993 | (.+\n)* // subsequent consecutive lines
994 | \n* // blanks
995 | )+
996 | )
997 | /gm, function(){...});
998 | */
999 |
1000 | text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, function (wholeMatch, m1) {
1001 | var bq = m1;
1002 |
1003 | // attacklab: hack around Konqueror 3.5.4 bug:
1004 | // "----------bug".replace(/^-/g,"") == "bug"
1005 | bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting
1006 | // attacklab: clean up hack
1007 | bq = bq.replace(/~0/g, "");
1008 |
1009 | bq = bq.replace(/^[ \t]+$/gm, ""); // trim whitespace-only lines
1010 | bq = _RunBlockGamut(bq); // recurse
1011 | bq = bq.replace(/(^|\n)/g, "$1 ");
1012 | // These leading spaces screw with content, so we need to fix that:
1013 | bq = bq.replace(/(\s*
[^\r]+?<\/pre>)/gm, function (wholeMatch, m1) {
1014 | var pre = m1;
1015 | // attacklab: hack around Konqueror 3.5.4 bug:
1016 | pre = pre.replace(/^ /mg, "~0");
1017 | pre = pre.replace(/~0/g, "");
1018 | return pre;
1019 | });
1020 |
1021 | return hashBlock("
\n" + bq + "\n
");
1022 | });
1023 | return text;
1024 | }
1025 |
1026 |
1027 | var _FormParagraphs = function (text) {
1028 | //
1029 | // Params:
1030 | // $text - string to process with html
"); // ** GFM **
1053 | str = str.replace(/^([ \t]*)/g, "
http://i.imgur.com/1cZl4.jpg \"Optional title\"
http://www.google.com/ \"Optional title\"]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i
49 | }
50 | }; // }}}
51 | WMDEditor.prototype = {
52 | getPanels: function () {
53 | return {
54 | buttonBar: (typeof this.options.button_bar == 'string') ? document.getElementById(this.options.button_bar) : this.options.button_bar,
55 | preview: (typeof this.options.preview == 'string') ? document.getElementById(this.options.preview) : this.options.preview,
56 | output: (typeof this.options.output == 'string') ? document.getElementById(this.options.output) : this.options.output,
57 | input: (typeof this.options.input == 'string') ? document.getElementById(this.options.input) : this.options.input
58 | };
59 | },
60 |
61 | startEditor: function () {
62 | this.panels = this.getPanels();
63 | this.previewMgr = new PreviewManager(this);
64 | edit = new this.editor(this.previewMgr.refresh);
65 | this.previewMgr.refresh(true);
66 | }
67 | };
68 |
69 |
70 | var util = { // {{{
71 | // Returns true if the DOM element is visible, false if it's hidden.
72 | // Checks if display is anything other than none.
73 | isVisible: function (elem) {
74 | // shamelessly copied from jQuery
75 | return elem.offsetWidth > 0 || elem.offsetHeight > 0;
76 | },
77 |
78 | // Adds a listener callback to a DOM element which is fired on a specified
79 | // event.
80 | addEvent: function (elem, event, listener) {
81 | if (elem.attachEvent) {
82 | // IE only. The "on" is mandatory.
83 | elem.attachEvent("on" + event, listener);
84 | }
85 | else {
86 | // Other browsers.
87 | elem.addEventListener(event, listener, false);
88 | }
89 | },
90 |
91 | // Removes a listener callback from a DOM element which is fired on a specified
92 | // event.
93 | removeEvent: function (elem, event, listener) {
94 | if (elem.detachEvent) {
95 | // IE only. The "on" is mandatory.
96 | elem.detachEvent("on" + event, listener);
97 | }
98 | else {
99 | // Other browsers.
100 | elem.removeEventListener(event, listener, false);
101 | }
102 | },
103 |
104 | // Converts \r\n and \r to \n.
105 | fixEolChars: function (text) {
106 | text = text.replace(/\r\n/g, "\n");
107 | text = text.replace(/\r/g, "\n");
108 | return text;
109 | },
110 |
111 | // Extends a regular expression. Returns a new RegExp
112 | // using pre + regex + post as the expression.
113 | // Used in a few functions where we have a base
114 | // expression and we want to pre- or append some
115 | // conditions to it (e.g. adding "$" to the end).
116 | // The flags are unchanged.
117 | //
118 | // regex is a RegExp, pre and post are strings.
119 | extendRegExp: function (regex, pre, post) {
120 |
121 | if (pre === null || pre === undefined) {
122 | pre = "";
123 | }
124 | if (post === null || post === undefined) {
125 | post = "";
126 | }
127 |
128 | var pattern = regex.toString();
129 | var flags = "";
130 |
131 | // Replace the flags with empty space and store them.
132 | // Technically, this can match incorrect flags like "gmm".
133 | var result = pattern.match(/\/([gim]*)$/);
134 | if (result === null) {
135 | flags = result[0];
136 | }
137 | else {
138 | flags = "";
139 | }
140 |
141 | // Remove the flags and slash delimiters from the regular expression.
142 | pattern = pattern.replace(/(^\/|\/[gim]*$)/g, "");
143 | pattern = pre + pattern + post;
144 |
145 | return new RegExp(pattern, flags);
146 | },
147 |
148 | // Sets the image for a button passed to the WMD editor.
149 | // Returns a new element with the image attached.
150 | // Adds several style properties to the image.
151 | //
152 | // XXX-ANAND: Is this used anywhere?
153 | createImage: function (img) {
154 |
155 | var imgPath = imageDirectory + img;
156 |
157 | var elem = document.createElement("img");
158 | elem.className = "wmd-button";
159 | elem.src = imgPath;
160 |
161 | return elem;
162 | },
163 |
164 | // This simulates a modal dialog box and asks for the URL when you
165 | // click the hyperlink or image buttons.
166 | //
167 | // text: The html for the input box.
168 | // defaultInputText: The default value that appears in the input box.
169 | // makeLinkMarkdown: The function which is executed when the prompt is dismissed, either via OK or Cancel
170 | prompt: function (text, defaultInputText, makeLinkMarkdown) {
171 |
172 | // These variables need to be declared at this level since they are used
173 | // in multiple functions.
174 | var dialog; // The dialog box.
175 | var background; // The background beind the dialog box.
176 | var input; // The text box where you enter the hyperlink.
177 | if (defaultInputText === undefined) {
178 | defaultInputText = "";
179 | }
180 |
181 | // Used as a keydown event handler. Esc dismisses the prompt.
182 | // Key code 27 is ESC.
183 | var checkEscape = function (key) {
184 | var code = (key.charCode || key.keyCode);
185 | if (code === 27) {
186 | close(true);
187 | }
188 | };
189 |
190 | // Dismisses the hyperlink input box.
191 | // isCancel is true if we don't care about the input text.
192 | // isCancel is false if we are going to keep the text.
193 | var close = function (isCancel) {
194 | util.removeEvent(document.body, "keydown", checkEscape);
195 | var text = input.value;
196 |
197 | if (isCancel) {
198 | text = null;
199 | }
200 | else {
201 | // Fixes common pasting errors.
202 | text = text.replace('http://http://', 'http://');
203 | text = text.replace('http://https://', 'https://');
204 | text = text.replace('http://ftp://', 'ftp://');
205 | }
206 |
207 | dialog.parentNode.removeChild(dialog);
208 | background.parentNode.removeChild(background);
209 | makeLinkMarkdown(text);
210 | return false;
211 | };
212 |
213 | // Creates the background behind the hyperlink text entry box.
214 | // Most of this has been moved to CSS but the div creation and
215 | // browser-specific hacks remain here.
216 | var createBackground = function () {
217 | background = document.createElement("div");
218 | background.className = "wmd-prompt-background";
219 | style = background.style;
220 | style.position = "absolute";
221 | style.top = "0";
222 |
223 | style.zIndex = "10000";
224 |
225 | // Some versions of Konqueror don't support transparent colors
226 | // so we make the whole window transparent.
227 | //
228 | // Is this necessary on modern konqueror browsers?
229 | if (browser.isKonqueror) {
230 | style.backgroundColor = "transparent";
231 | }
232 | else if (browser.isIE) {
233 | style.filter = "alpha(opacity=50)";
234 | }
235 | else {
236 | style.opacity = "0.5";
237 | }
238 |
239 | var pageSize = position.getPageSize();
240 | style.height = pageSize[1] + "px";
241 |
242 | if (browser.isIE) {
243 | style.left = document.documentElement.scrollLeft;
244 | style.width = document.documentElement.clientWidth;
245 | }
246 | else {
247 | style.left = "0";
248 | style.width = "100%";
249 | }
250 |
251 | document.body.appendChild(background);
252 | };
253 |
254 | // Create the text input box form/window.
255 | var createDialog = function () {
256 |
257 | // The main dialog box.
258 | dialog = document.createElement("div");
259 | dialog.className = "wmd-prompt-dialog";
260 | dialog.style.padding = "10px;";
261 | dialog.style.position = "fixed";
262 | dialog.style.width = "400px";
263 | dialog.style.zIndex = "10001";
264 |
265 | // The dialog text.
266 | var question = document.createElement("div");
267 | question.innerHTML = text;
268 | question.style.padding = "5px";
269 | dialog.appendChild(question);
270 |
271 | // The web form container for the text box and buttons.
272 | var form = document.createElement("form");
273 | form.onsubmit = function () {
274 | return close(false);
275 | };
276 | style = form.style;
277 | style.padding = "0";
278 | style.margin = "0";
279 | style.cssFloat = "left";
280 | style.width = "100%";
281 | style.textAlign = "center";
282 | style.position = "relative";
283 | dialog.appendChild(form);
284 |
285 | // The input text box
286 | input = document.createElement("input");
287 | input.type = "text";
288 | input.value = defaultInputText;
289 | style = input.style;
290 | style.display = "block";
291 | style.width = "80%";
292 | style.marginLeft = style.marginRight = "auto";
293 | form.appendChild(input);
294 |
295 | // The ok button
296 | var okButton = document.createElement("input");
297 | okButton.type = "button";
298 | okButton.onclick = function () {
299 | return close(false);
300 | };
301 | okButton.value = "OK";
302 | style = okButton.style;
303 | style.margin = "10px";
304 | style.display = "inline";
305 | style.width = "7em";
306 |
307 |
308 | // The cancel button
309 | var cancelButton = document.createElement("input");
310 | cancelButton.type = "button";
311 | cancelButton.onclick = function () {
312 | return close(true);
313 | };
314 | cancelButton.value = "Cancel";
315 | style = cancelButton.style;
316 | style.margin = "10px";
317 | style.display = "inline";
318 | style.width = "7em";
319 |
320 | // The order of these buttons is different on macs.
321 | if (/mac/.test(nav.platform.toLowerCase())) {
322 | form.appendChild(cancelButton);
323 | form.appendChild(okButton);
324 | }
325 | else {
326 | form.appendChild(okButton);
327 | form.appendChild(cancelButton);
328 | }
329 |
330 | util.addEvent(document.body, "keydown", checkEscape);
331 | dialog.style.top = "50%";
332 | dialog.style.left = "50%";
333 | dialog.style.display = "block";
334 | if (browser.isIE_5or6) {
335 | dialog.style.position = "absolute";
336 | dialog.style.top = document.documentElement.scrollTop + 200 + "px";
337 | dialog.style.left = "50%";
338 | }
339 | document.body.appendChild(dialog);
340 |
341 | // This has to be done AFTER adding the dialog to the form if you
342 | // want it to be centered.
343 | dialog.style.marginTop = -(position.getHeight(dialog) / 2) + "px";
344 | dialog.style.marginLeft = -(position.getWidth(dialog) / 2) + "px";
345 | };
346 |
347 | createBackground();
348 |
349 | // Why is this in a zero-length timeout?
350 | // Is it working around a browser bug?
351 | window.setTimeout(function () {
352 | createDialog();
353 |
354 | var defTextLen = defaultInputText.length;
355 | if (input.selectionStart !== undefined) {
356 | input.selectionStart = 0;
357 | input.selectionEnd = defTextLen;
358 | }
359 | else if (input.createTextRange) {
360 | var range = input.createTextRange();
361 | range.collapse(false);
362 | range.moveStart("character", -defTextLen);
363 | range.moveEnd("character", defTextLen);
364 | range.select();
365 | }
366 | input.focus();
367 | }, 0);
368 | },
369 |
370 | extend: function () {
371 | function _update(a, b) {
372 | for (var k in b) {
373 | a[k] = b[k];
374 | }
375 | return a;
376 | }
377 |
378 | var d = {};
379 | for (var i = 0; i < arguments.length; i++) {
380 | _update(d, arguments[i]);
381 | }
382 | return d;
383 | }
384 | }; // }}}
385 | var position = { // {{{
386 | // UNFINISHED
387 | // The assignment in the while loop makes jslint cranky.
388 | // I'll change it to a better loop later.
389 | getTop: function (elem, isInner) {
390 | var result = elem.offsetTop;
391 | if (!isInner) {
392 | while (elem = elem.offsetParent) {
393 | result += elem.offsetTop;
394 | }
395 | }
396 | return result;
397 | },
398 |
399 | getHeight: function (elem) {
400 | return elem.offsetHeight || elem.scrollHeight;
401 | },
402 |
403 | getWidth: function (elem) {
404 | return elem.offsetWidth || elem.scrollWidth;
405 | },
406 |
407 | getPageSize: function () {
408 | var scrollWidth, scrollHeight;
409 | var innerWidth, innerHeight;
410 |
411 | // It's not very clear which blocks work with which browsers.
412 | if (self.innerHeight && self.scrollMaxY) {
413 | scrollWidth = document.body.scrollWidth;
414 | scrollHeight = self.innerHeight + self.scrollMaxY;
415 | }
416 | else if (document.body.scrollHeight > document.body.offsetHeight) {
417 | scrollWidth = document.body.scrollWidth;
418 | scrollHeight = document.body.scrollHeight;
419 | }
420 | else {
421 | scrollWidth = document.body.offsetWidth;
422 | scrollHeight = document.body.offsetHeight;
423 | }
424 |
425 | if (self.innerHeight) {
426 | // Non-IE browser
427 | innerWidth = self.innerWidth;
428 | innerHeight = self.innerHeight;
429 | }
430 | else if (document.documentElement && document.documentElement.clientHeight) {
431 | // Some versions of IE (IE 6 w/ a DOCTYPE declaration)
432 | innerWidth = document.documentElement.clientWidth;
433 | innerHeight = document.documentElement.clientHeight;
434 | }
435 | else if (document.body) {
436 | // Other versions of IE
437 | innerWidth = document.body.clientWidth;
438 | innerHeight = document.body.clientHeight;
439 | }
440 |
441 | var maxWidth = Math.max(scrollWidth, innerWidth);
442 | var maxHeight = Math.max(scrollHeight, innerHeight);
443 | return [maxWidth, maxHeight, innerWidth, innerHeight];
444 | }
445 | }; // }}}
446 | // The input textarea state/contents.
447 | // This is used to implement undo/redo by the undo manager.
448 | var TextareaState = function (textarea, wmd) { // {{{
449 | // Aliases
450 | var stateObj = this;
451 | var inputArea = textarea;
452 |
453 | this.init = function () {
454 |
455 | if (!util.isVisible(inputArea)) {
456 | return;
457 | }
458 |
459 | this.setInputAreaSelectionStartEnd();
460 | this.scrollTop = inputArea.scrollTop;
461 | if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) {
462 | this.text = inputArea.value;
463 | }
464 |
465 | };
466 |
467 | // Sets the selected text in the input box after we've performed an
468 | // operation.
469 | this.setInputAreaSelection = function () {
470 |
471 | if (!util.isVisible(inputArea)) {
472 | return;
473 | }
474 |
475 | if (inputArea.selectionStart !== undefined && !browser.isOpera) {
476 |
477 | inputArea.focus();
478 | inputArea.selectionStart = stateObj.start;
479 | inputArea.selectionEnd = stateObj.end;
480 | inputArea.scrollTop = stateObj.scrollTop;
481 | }
482 | else if (document.selection) {
483 |
484 | if (typeof(document.activeElement)!="unknown" && document.activeElement && document.activeElement !== inputArea) {
485 | return;
486 | }
487 |
488 | inputArea.focus();
489 | var range = inputArea.createTextRange();
490 | range.moveStart("character", -inputArea.value.length);
491 | range.moveEnd("character", -inputArea.value.length);
492 | range.moveEnd("character", stateObj.end);
493 | range.moveStart("character", stateObj.start);
494 | range.select();
495 | }
496 | };
497 |
498 | this.setInputAreaSelectionStartEnd = function () {
499 |
500 | if (inputArea.selectionStart || inputArea.selectionStart === 0) {
501 |
502 | stateObj.start = inputArea.selectionStart;
503 | stateObj.end = inputArea.selectionEnd;
504 | }
505 | else if (document.selection) {
506 |
507 | stateObj.text = util.fixEolChars(inputArea.value);
508 |
509 | // IE loses the selection in the textarea when buttons are
510 | // clicked. On IE we cache the selection and set a flag
511 | // which we check for here.
512 | var range;
513 | if (wmd.ieRetardedClick && wmd.ieCachedRange) {
514 | range = wmd.ieCachedRange;
515 | wmd.ieRetardedClick = false;
516 | }
517 | else {
518 | range = document.selection.createRange();
519 | }
520 |
521 | var fixedRange = util.fixEolChars(range.text);
522 | var marker = "\x07";
523 | var markedRange = marker + fixedRange + marker;
524 | range.text = markedRange;
525 | var inputText = util.fixEolChars(inputArea.value);
526 |
527 | range.moveStart("character", -markedRange.length);
528 | range.text = fixedRange;
529 |
530 | stateObj.start = inputText.indexOf(marker);
531 | stateObj.end = inputText.lastIndexOf(marker) - marker.length;
532 |
533 | var len = stateObj.text.length - util.fixEolChars(inputArea.value).length;
534 |
535 | if (len) {
536 | range.moveStart("character", -fixedRange.length);
537 | while (len--) {
538 | fixedRange += "\n";
539 | stateObj.end += 1;
540 | }
541 | range.text = fixedRange;
542 | }
543 |
544 | this.setInputAreaSelection();
545 | }
546 | };
547 |
548 | // Restore this state into the input area.
549 | this.restore = function () {
550 |
551 | if (stateObj.text != undefined && stateObj.text != inputArea.value) {
552 | inputArea.value = stateObj.text;
553 | }
554 | this.setInputAreaSelection();
555 | inputArea.scrollTop = stateObj.scrollTop;
556 | };
557 |
558 | // Gets a collection of HTML chunks from the inptut textarea.
559 | this.getChunks = function () {
560 |
561 | var chunk = new Chunks();
562 |
563 | chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start));
564 | chunk.startTag = "";
565 | chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end));
566 | chunk.endTag = "";
567 | chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end));
568 | chunk.scrollTop = stateObj.scrollTop;
569 |
570 | return chunk;
571 | };
572 |
573 | // Sets the TextareaState properties given a chunk of markdown.
574 | this.setChunks = function (chunk) {
575 |
576 | chunk.before = chunk.before + chunk.startTag;
577 | chunk.after = chunk.endTag + chunk.after;
578 |
579 | if (browser.isOpera) {
580 | chunk.before = chunk.before.replace(/\n/g, "\r\n");
581 | chunk.selection = chunk.selection.replace(/\n/g, "\r\n");
582 | chunk.after = chunk.after.replace(/\n/g, "\r\n");
583 | }
584 |
585 | this.start = chunk.before.length;
586 | this.end = chunk.before.length + chunk.selection.length;
587 | this.text = chunk.before + chunk.selection + chunk.after;
588 | this.scrollTop = chunk.scrollTop;
589 | };
590 |
591 | this.init();
592 | }; // }}}
593 | // Chunks {{{
594 | // before: contains all the text in the input box BEFORE the selection.
595 | // after: contains all the text in the input box AFTER the selection.
596 | var Chunks = function () {};
597 |
598 | // startRegex: a regular expression to find the start tag
599 | // endRegex: a regular expresssion to find the end tag
600 | Chunks.prototype.findTags = function (startRegex, endRegex) {
601 |
602 | var chunkObj = this;
603 | var regex;
604 |
605 | if (startRegex) {
606 |
607 | regex = util.extendRegExp(startRegex, "", "$");
608 |
609 | this.before = this.before.replace(regex, function (match) {
610 | chunkObj.startTag = chunkObj.startTag + match;
611 | return "";
612 | });
613 |
614 | regex = util.extendRegExp(startRegex, "^", "");
615 |
616 | this.selection = this.selection.replace(regex, function (match) {
617 | chunkObj.startTag = chunkObj.startTag + match;
618 | return "";
619 | });
620 | }
621 |
622 | if (endRegex) {
623 |
624 | regex = util.extendRegExp(endRegex, "", "$");
625 |
626 | this.selection = this.selection.replace(regex, function (match) {
627 | chunkObj.endTag = match + chunkObj.endTag;
628 | return "";
629 | });
630 |
631 | regex = util.extendRegExp(endRegex, "^", "");
632 |
633 | this.after = this.after.replace(regex, function (match) {
634 | chunkObj.endTag = match + chunkObj.endTag;
635 | return "";
636 | });
637 | }
638 | };
639 |
640 | // If remove is false, the whitespace is transferred
641 | // to the before/after regions.
642 | //
643 | // If remove is true, the whitespace disappears.
644 | Chunks.prototype.trimWhitespace = function (remove) {
645 |
646 | this.selection = this.selection.replace(/^(\s*)/, "");
647 |
648 | if (!remove) {
649 | this.before += re.$1;
650 | }
651 |
652 | this.selection = this.selection.replace(/(\s*)$/, "");
653 |
654 | if (!remove) {
655 | this.after = re.$1 + this.after;
656 | }
657 | };
658 |
659 |
660 | Chunks.prototype.addBlankLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) {
661 |
662 | if (nLinesBefore === undefined) {
663 | nLinesBefore = 1;
664 | }
665 |
666 | if (nLinesAfter === undefined) {
667 | nLinesAfter = 1;
668 | }
669 |
670 | nLinesBefore++;
671 | nLinesAfter++;
672 |
673 | var regexText;
674 | var replacementText;
675 |
676 | // New bug discovered in Chrome, which appears to be related to use of RegExp.$1
677 | // Hack it to hold the match results. Sucks because we're double matching...
678 | var match = /(^\n*)/.exec(this.selection);
679 |
680 | this.selection = this.selection.replace(/(^\n*)/, "");
681 | this.startTag = this.startTag + (match ? match[1] : "");
682 | match = /(\n*$)/.exec(this.selection);
683 | this.selection = this.selection.replace(/(\n*$)/, "");
684 | this.endTag = this.endTag + (match ? match[1] : "");
685 | match = /(^\n*)/.exec(this.startTag);
686 | this.startTag = this.startTag.replace(/(^\n*)/, "");
687 | this.before = this.before + (match ? match[1] : "");
688 | match = /(\n*$)/.exec(this.endTag);
689 | this.endTag = this.endTag.replace(/(\n*$)/, "");
690 | this.after = this.after + (match ? match[1] : "");
691 |
692 | if (this.before) {
693 |
694 | regexText = replacementText = "";
695 |
696 | while (nLinesBefore--) {
697 | regexText += "\\n?";
698 | replacementText += "\n";
699 | }
700 |
701 | if (findExtraNewlines) {
702 | regexText = "\\n*";
703 | }
704 | this.before = this.before.replace(new re(regexText + "$", ""), replacementText);
705 | }
706 |
707 | if (this.after) {
708 |
709 | regexText = replacementText = "";
710 |
711 | while (nLinesAfter--) {
712 | regexText += "\\n?";
713 | replacementText += "\n";
714 | }
715 | if (findExtraNewlines) {
716 | regexText = "\\n*";
717 | }
718 |
719 | this.after = this.after.replace(new re(regexText, ""), replacementText);
720 | }
721 | };
722 | // }}} - END CHUNKS
723 | // Watches the input textarea, polling at an interval and runs
724 | // a callback function if anything has changed.
725 | var InputPoller = function (textarea, callback, interval) { // {{{
726 | var pollerObj = this;
727 | var inputArea = textarea;
728 |
729 | // Stored start, end and text. Used to see if there are changes to the input.
730 | var lastStart;
731 | var lastEnd;
732 | var markdown;
733 |
734 | var killHandle; // Used to cancel monitoring on destruction.
735 | // Checks to see if anything has changed in the textarea.
736 | // If so, it runs the callback.
737 | this.tick = function () {
738 |
739 | if (!util.isVisible(inputArea)) {
740 | return;
741 | }
742 |
743 | // Update the selection start and end, text.
744 | if (inputArea.selectionStart || inputArea.selectionStart === 0) {
745 | var start = inputArea.selectionStart;
746 | var end = inputArea.selectionEnd;
747 | if (start != lastStart || end != lastEnd) {
748 | lastStart = start;
749 | lastEnd = end;
750 |
751 | if (markdown != inputArea.value) {
752 | markdown = inputArea.value;
753 | return true;
754 | }
755 | }
756 | }
757 | return false;
758 | };
759 |
760 |
761 | var doTickCallback = function () {
762 |
763 | if (!util.isVisible(inputArea)) {
764 | return;
765 | }
766 |
767 | // If anything has changed, call the function.
768 | if (pollerObj.tick()) {
769 | callback();
770 | }
771 | };
772 |
773 | // Set how often we poll the textarea for changes.
774 | var assignInterval = function () {
775 | killHandle = window.setInterval(doTickCallback, interval);
776 | };
777 |
778 | this.destroy = function () {
779 | window.clearInterval(killHandle);
780 | };
781 |
782 | assignInterval();
783 | }; // }}}
784 | var PreviewManager = function (wmd) { // {{{
785 | var managerObj = this;
786 | var converter;
787 | var poller;
788 | var timeout;
789 | var elapsedTime;
790 | var oldInputText;
791 | var htmlOut;
792 | var maxDelay = 3000;
793 | var startType = "delayed"; // The other legal value is "manual"
794 | // Adds event listeners to elements and creates the input poller.
795 | var setupEvents = function (inputElem, listener) {
796 |
797 | util.addEvent(inputElem, "input", listener);
798 | inputElem.onpaste = listener;
799 | inputElem.ondrop = listener;
800 |
801 | util.addEvent(inputElem, "keypress", listener);
802 | util.addEvent(inputElem, "keydown", listener);
803 | // previewPollInterval is set at the top of this file.
804 | poller = new InputPoller(wmd.panels.input, listener, wmd.options.previewPollInterval);
805 | };
806 |
807 | var getDocScrollTop = function () {
808 |
809 | var result = 0;
810 |
811 | if (window.innerHeight) {
812 | result = window.pageYOffset;
813 | }
814 | else if (document.documentElement && document.documentElement.scrollTop) {
815 | result = document.documentElement.scrollTop;
816 | }
817 | else if (document.body) {
818 | result = document.body.scrollTop;
819 | }
820 |
821 | return result;
822 | };
823 |
824 | var makePreviewHtml = function () {
825 |
826 | // If there are no registered preview and output panels
827 | // there is nothing to do.
828 | if (!wmd.panels.preview && !wmd.panels.output) {
829 | return;
830 | }
831 |
832 | var text = wmd.panels.input.value;
833 | if (text && text == oldInputText) {
834 | return; // Input text hasn't changed.
835 | }
836 | else {
837 | oldInputText = text;
838 | }
839 |
840 | var prevTime = new Date().getTime();
841 |
842 | if (!converter && wmd.showdown) {
843 | converter = new wmd.showdown.converter();
844 | }
845 |
846 | if (converter) {
847 | text = converter.makeHtml(text);
848 | }
849 |
850 | // Calculate the processing time of the HTML creation.
851 | // It's used as the delay time in the event listener.
852 | var currTime = new Date().getTime();
853 | elapsedTime = currTime - prevTime;
854 |
855 | pushPreviewHtml(text);
856 | htmlOut = text;
857 | };
858 |
859 | // setTimeout is already used. Used as an event listener.
860 | var applyTimeout = function () {
861 |
862 | if (timeout) {
863 | window.clearTimeout(timeout);
864 | timeout = undefined;
865 | }
866 |
867 | if (startType !== "manual") {
868 |
869 | var delay = 0;
870 |
871 | if (startType === "delayed") {
872 | delay = elapsedTime;
873 | }
874 |
875 | if (delay > maxDelay) {
876 | delay = maxDelay;
877 | }
878 | timeout = window.setTimeout(makePreviewHtml, delay);
879 | }
880 | };
881 |
882 | var getScaleFactor = function (panel) {
883 | if (panel.scrollHeight <= panel.clientHeight) {
884 | return 1;
885 | }
886 | return panel.scrollTop / (panel.scrollHeight - panel.clientHeight);
887 | };
888 |
889 | var setPanelScrollTops = function () {
890 |
891 | if (wmd.panels.preview) {
892 | wmd.panels.preview.scrollTop = (wmd.panels.preview.scrollHeight - wmd.panels.preview.clientHeight) * getScaleFactor(wmd.panels.preview);;
893 | }
894 |
895 | if (wmd.panels.output) {
896 | wmd.panels.output.scrollTop = (wmd.panels.output.scrollHeight - wmd.panels.output.clientHeight) * getScaleFactor(wmd.panels.output);;
897 | }
898 | };
899 |
900 | this.refresh = function (requiresRefresh) {
901 |
902 | if (requiresRefresh) {
903 | oldInputText = "";
904 | makePreviewHtml();
905 | }
906 | else {
907 | applyTimeout();
908 | }
909 | };
910 |
911 | this.processingTime = function () {
912 | return elapsedTime;
913 | };
914 |
915 | // The output HTML
916 | this.output = function () {
917 | return htmlOut;
918 | };
919 |
920 | // The mode can be "manual" or "delayed"
921 | this.setUpdateMode = function (mode) {
922 | startType = mode;
923 | managerObj.refresh();
924 | };
925 |
926 | var isFirstTimeFilled = true;
927 |
928 | var pushPreviewHtml = function (text) {
929 |
930 | var emptyTop = position.getTop(wmd.panels.input) - getDocScrollTop();
931 |
932 | // Send the encoded HTML to the output textarea/div.
933 | if (wmd.panels.output) {
934 | // The value property is only defined if the output is a textarea.
935 | if (wmd.panels.output.value !== undefined) {
936 | wmd.panels.output.value = text;
937 | }
938 | // Otherwise we are just replacing the text in a div.
939 | // Send the HTML wrapped in
";
944 | }
945 | }
946 |
947 | if (wmd.panels.preview) {
948 | // original WMD code allowed javascript injection, like this:
949 | //
940 | else {
941 | var newText = text.replace(/&/g, "&");
942 | newText = newText.replace(/
" + newText + "
950 | // now, we first ensure elements (and attributes of IMG and A elements) are in a whitelist
951 | // and if not in whitelist, replace with blanks in preview to prevent XSS attacks
952 | // when editing malicious markdown
953 | // code courtesy of https://github.com/polestarsoft/wmd/commit/e7a09c9170ea23e7e806425f46d7423af2a74641
954 | if (wmd.options.tagFilter.enabled) {
955 | text = text.replace(/<[^<>]*>?/gi, function (tag) {
956 | return (tag.match(wmd.options.tagFilter.allowedTags) || tag.match(wmd.options.tagFilter.patternLink) || tag.match(wmd.options.tagFilter.patternImage)) ? tag : "";
957 | });
958 | }
959 | wmd.panels.preview.innerHTML = text;
960 | }
961 |
962 | setPanelScrollTops();
963 |
964 | if (isFirstTimeFilled) {
965 | isFirstTimeFilled = false;
966 | return;
967 | }
968 |
969 | var fullTop = position.getTop(wmd.panels.input) - getDocScrollTop();
970 |
971 | if (browser.isIE) {
972 | window.setTimeout(function () {
973 | window.scrollBy(0, fullTop - emptyTop);
974 | }, 0);
975 | }
976 | else {
977 | window.scrollBy(0, fullTop - emptyTop);
978 | }
979 | };
980 |
981 | var init = function () {
982 |
983 | setupEvents(wmd.panels.input, applyTimeout);
984 | makePreviewHtml();
985 |
986 | if (wmd.panels.preview) {
987 | wmd.panels.preview.scrollTop = 0;
988 | }
989 | if (wmd.panels.output) {
990 | wmd.panels.output.scrollTop = 0;
991 | }
992 | };
993 |
994 | this.destroy = function () {
995 | if (poller) {
996 | poller.destroy();
997 | }
998 | };
999 |
1000 | init();
1001 | }; // }}}
1002 | // Handles pushing and popping TextareaStates for undo/redo commands.
1003 | // I should rename the stack variables to list.
1004 | var UndoManager = function (wmd, textarea, pastePollInterval, callback) { // {{{
1005 | var undoObj = this;
1006 | var undoStack = []; // A stack of undo states
1007 | var stackPtr = 0; // The index of the current state
1008 | var mode = "none";
1009 | var lastState; // The last state
1010 | var poller;
1011 | var timer; // The setTimeout handle for cancelling the timer
1012 | var inputStateObj;
1013 |
1014 | // Set the mode for later logic steps.
1015 | var setMode = function (newMode, noSave) {
1016 |
1017 | if (mode != newMode) {
1018 | mode = newMode;
1019 | if (!noSave) {
1020 | saveState();
1021 | }
1022 | }
1023 |
1024 | if (!browser.isIE || mode != "moving") {
1025 | timer = window.setTimeout(refreshState, 1);
1026 | }
1027 | else {
1028 | inputStateObj = null;
1029 | }
1030 | };
1031 |
1032 | var refreshState = function () {
1033 | inputStateObj = new TextareaState(textarea, wmd);
1034 | poller.tick();
1035 | timer = undefined;
1036 | };
1037 |
1038 | this.setCommandMode = function () {
1039 | mode = "command";
1040 | saveState();
1041 | timer = window.setTimeout(refreshState, 0);
1042 | };
1043 |
1044 | this.canUndo = function () {
1045 | return stackPtr > 1;
1046 | };
1047 |
1048 | this.canRedo = function () {
1049 | if (undoStack[stackPtr + 1]) {
1050 | return true;
1051 | }
1052 | return false;
1053 | };
1054 |
1055 | // Removes the last state and restores it.
1056 | this.undo = function () {
1057 |
1058 | if (undoObj.canUndo()) {
1059 | if (lastState) {
1060 | // What about setting state -1 to null or checking for undefined?
1061 | lastState.restore();
1062 | lastState = null;
1063 | }
1064 | else {
1065 | undoStack[stackPtr] = new TextareaState(textarea, wmd);
1066 | undoStack[--stackPtr].restore();
1067 |
1068 | if (callback) {
1069 | callback();
1070 | }
1071 | }
1072 | }
1073 |
1074 | mode = "none";
1075 | textarea.focus();
1076 | refreshState();
1077 | };
1078 |
1079 | // Redo an action.
1080 | this.redo = function () {
1081 |
1082 | if (undoObj.canRedo()) {
1083 |
1084 | undoStack[++stackPtr].restore();
1085 |
1086 | if (callback) {
1087 | callback();
1088 | }
1089 | }
1090 |
1091 | mode = "none";
1092 | textarea.focus();
1093 | refreshState();
1094 | };
1095 |
1096 | // Push the input area state to the stack.
1097 | var saveState = function () {
1098 |
1099 | var currState = inputStateObj || new TextareaState(textarea, wmd);
1100 |
1101 | if (!currState) {
1102 | return false;
1103 | }
1104 | if (mode == "moving") {
1105 | if (!lastState) {
1106 | lastState = currState;
1107 | }
1108 | return;
1109 | }
1110 | if (lastState) {
1111 | if (undoStack[stackPtr - 1].text != lastState.text) {
1112 | undoStack[stackPtr++] = lastState;
1113 | }
1114 | lastState = null;
1115 | }
1116 | undoStack[stackPtr++] = currState;
1117 | undoStack[stackPtr + 1] = null;
1118 | if (callback) {
1119 | callback();
1120 | }
1121 | };
1122 |
1123 | var handleCtrlYZ = function (event) {
1124 |
1125 | var handled = false;
1126 |
1127 | if (event.ctrlKey || event.metaKey) {
1128 |
1129 | // IE and Opera do not support charCode.
1130 | var keyCode = event.charCode || event.keyCode;
1131 | var keyCodeChar = String.fromCharCode(keyCode);
1132 |
1133 | switch (keyCodeChar) {
1134 |
1135 | case "y":
1136 | undoObj.redo();
1137 | handled = true;
1138 | break;
1139 |
1140 | case "z":
1141 | if (!event.shiftKey) {
1142 | undoObj.undo();
1143 | }
1144 | else {
1145 | undoObj.redo();
1146 | }
1147 | handled = true;
1148 | break;
1149 | }
1150 | }
1151 |
1152 | if (handled) {
1153 | if (event.preventDefault) {
1154 | event.preventDefault();
1155 | }
1156 | if (window.event) {
1157 | window.event.returnValue = false;
1158 | }
1159 | return;
1160 | }
1161 | };
1162 |
1163 | // Set the mode depending on what is going on in the input area.
1164 | var handleModeChange = function (event) {
1165 |
1166 | if (!event.ctrlKey && !event.metaKey) {
1167 |
1168 | var keyCode = event.keyCode;
1169 |
1170 | if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) {
1171 | // 33 - 40: page up/dn and arrow keys
1172 | // 63232 - 63235: page up/dn and arrow keys on safari
1173 | setMode("moving");
1174 | }
1175 | else if (keyCode == 8 || keyCode == 46 || keyCode == 127) {
1176 | // 8: backspace
1177 | // 46: delete
1178 | // 127: delete
1179 | setMode("deleting");
1180 | }
1181 | else if (keyCode == 13) {
1182 | // 13: Enter
1183 | setMode("newlines");
1184 | }
1185 | else if (keyCode == 27) {
1186 | // 27: escape
1187 | setMode("escape");
1188 | }
1189 | else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) {
1190 | // 16-20 are shift, etc.
1191 | // 91: left window key
1192 | // I think this might be a little messed up since there are
1193 | // a lot of nonprinting keys above 20.
1194 | setMode("typing");
1195 | }
1196 | }
1197 | };
1198 |
1199 | var setEventHandlers = function () {
1200 |
1201 | util.addEvent(textarea, "keypress", function (event) {
1202 | // keyCode 89: y
1203 | // keyCode 90: z
1204 | if ((event.ctrlKey || event.metaKey) && (event.keyCode == 89 || event.keyCode == 90)) {
1205 | event.preventDefault();
1206 | }
1207 | });
1208 |
1209 | var handlePaste = function () {
1210 | if (browser.isIE || (inputStateObj && inputStateObj.text != textarea.value)) {
1211 | if (timer == undefined) {
1212 | mode = "paste";
1213 | saveState();
1214 | refreshState();
1215 | }
1216 | }
1217 | };
1218 |
1219 | poller = new InputPoller(textarea, handlePaste, pastePollInterval);
1220 |
1221 | util.addEvent(textarea, "keydown", handleCtrlYZ);
1222 | util.addEvent(textarea, "keydown", handleModeChange);
1223 |
1224 | util.addEvent(textarea, "mousedown", function () {
1225 | setMode("moving");
1226 | });
1227 | textarea.onpaste = handlePaste;
1228 | textarea.ondrop = handlePaste;
1229 | };
1230 |
1231 | var init = function () {
1232 | setEventHandlers();
1233 | refreshState();
1234 | saveState();
1235 | };
1236 |
1237 | this.destroy = function () {
1238 | if (poller) {
1239 | poller.destroy();
1240 | }
1241 | };
1242 |
1243 | init();
1244 | }; //}}}
1245 | WMDEditor.util = util;
1246 | WMDEditor.position = position;
1247 | WMDEditor.TextareaState = TextareaState;
1248 | WMDEditor.InputPoller = InputPoller;
1249 | WMDEditor.PreviewManager = PreviewManager;
1250 | WMDEditor.UndoManager = UndoManager;
1251 |
1252 | // A few handy aliases for readability.
1253 | var doc = window.document;
1254 | var re = window.RegExp;
1255 | var nav = window.navigator;
1256 |
1257 | function get_browser() {
1258 | var b = {};
1259 | b.isIE = /msie/.test(nav.userAgent.toLowerCase());
1260 | b.isIE_5or6 = /msie 6/.test(nav.userAgent.toLowerCase()) || /msie 5/.test(nav.userAgent.toLowerCase());
1261 | b.isIE_7plus = b.isIE && !b.isIE_5or6;
1262 | b.isOpera = /opera/.test(nav.userAgent.toLowerCase());
1263 | b.isKonqueror = /konqueror/.test(nav.userAgent.toLowerCase());
1264 | return b;
1265 | }
1266 |
1267 | // Used to work around some browser bugs where we can't use feature testing.
1268 | var browser = get_browser();
1269 |
1270 | var wmdBase = function (wmd, wmd_options) { // {{{
1271 | // Some namespaces.
1272 | //wmd.Util = {};
1273 | //wmd.Position = {};
1274 | wmd.Command = {};
1275 | wmd.Global = {};
1276 | wmd.buttons = {};
1277 |
1278 | wmd.showdown = window.Attacklab && window.Attacklab.showdown;
1279 |
1280 | var util = WMDEditor.util;
1281 | var position = WMDEditor.position;
1282 | var command = wmd.Command;
1283 |
1284 | // Internet explorer has problems with CSS sprite buttons that use HTML
1285 | // lists. When you click on the background image "button", IE will
1286 | // select the non-existent link text and discard the selection in the
1287 | // textarea. The solution to this is to cache the textarea selection
1288 | // on the button's mousedown event and set a flag. In the part of the
1289 | // code where we need to grab the selection, we check for the flag
1290 | // and, if it's set, use the cached area instead of querying the
1291 | // textarea.
1292 | //
1293 | // This ONLY affects Internet Explorer (tested on versions 6, 7
1294 | // and 8) and ONLY on button clicks. Keyboard shortcuts work
1295 | // normally since the focus never leaves the textarea.
1296 | wmd.ieCachedRange = null; // cached textarea selection
1297 | wmd.ieRetardedClick = false; // flag
1298 | // I think my understanding of how the buttons and callbacks are stored in the array is incomplete.
1299 | wmd.editor = function (previewRefreshCallback) { // {{{
1300 | if (!previewRefreshCallback) {
1301 | previewRefreshCallback = function () {};
1302 | }
1303 |
1304 | var inputBox = wmd.panels.input;
1305 |
1306 | var offsetHeight = 0;
1307 |
1308 | var editObj = this;
1309 |
1310 | var mainDiv;
1311 | var mainSpan;
1312 |
1313 | var div; // This name is pretty ambiguous. I should rename this.
1314 | // Used to cancel recurring events from setInterval.
1315 | var creationHandle;
1316 |
1317 | var undoMgr; // The undo manager
1318 | // Perform the button's action.
1319 | var doClick = function (button) {
1320 |
1321 | inputBox.focus();
1322 |
1323 | if (button.textOp) {
1324 |
1325 | if (undoMgr) {
1326 | undoMgr.setCommandMode();
1327 | }
1328 |
1329 | var state = new TextareaState(wmd.panels.input, wmd);
1330 |
1331 | if (!state) {
1332 | return;
1333 | }
1334 |
1335 | var chunks = state.getChunks();
1336 |
1337 | // Some commands launch a "modal" prompt dialog. Javascript
1338 | // can't really make a modal dialog box and the WMD code
1339 | // will continue to execute while the dialog is displayed.
1340 | // This prevents the dialog pattern I'm used to and means
1341 | // I can't do something like this:
1342 | //
1343 | // var link = CreateLinkDialog();
1344 | // makeMarkdownLink(link);
1345 | //
1346 | // Instead of this straightforward method of handling a
1347 | // dialog I have to pass any code which would execute
1348 | // after the dialog is dismissed (e.g. link creation)
1349 | // in a function parameter.
1350 | //
1351 | // Yes this is awkward and I think it sucks, but there's
1352 | // no real workaround. Only the image and link code
1353 | // create dialogs and require the function pointers.
1354 | var fixupInputArea = function () {
1355 |
1356 | inputBox.focus();
1357 |
1358 | if (chunks) {
1359 | state.setChunks(chunks);
1360 | }
1361 |
1362 | state.restore();
1363 | previewRefreshCallback();
1364 | };
1365 |
1366 | var useDefaultText = true;
1367 | var noCleanup = button.textOp(chunks, fixupInputArea, useDefaultText);
1368 |
1369 | if (!noCleanup) {
1370 | fixupInputArea();
1371 | }
1372 |
1373 | }
1374 |
1375 | if (button.execute) {
1376 | button.execute(editObj);
1377 | }
1378 | };
1379 |
1380 | var setUndoRedoButtonStates = function () {
1381 | if (undoMgr) {
1382 | setupButton(wmd.buttons["wmd-undo-button"], undoMgr.canUndo());
1383 | setupButton(wmd.buttons["wmd-redo-button"], undoMgr.canRedo());
1384 | }
1385 | };
1386 |
1387 | var setupButton = function (button, isEnabled) {
1388 |
1389 | var normalYShift = "0px";
1390 | var disabledYShift = "-20px";
1391 | var highlightYShift = "-40px";
1392 |
1393 | if (isEnabled) {
1394 | button.style.backgroundPosition = button.XShift + " " + normalYShift;
1395 | button.onmouseover = function () {
1396 | this.style.backgroundPosition = this.XShift + " " + highlightYShift;
1397 | };
1398 |
1399 | button.onmouseout = function () {
1400 | this.style.backgroundPosition = this.XShift + " " + normalYShift;
1401 | };
1402 |
1403 | // IE tries to select the background image "button" text (it's
1404 | // implemented in a list item) so we have to cache the selection
1405 | // on mousedown.
1406 | if (browser.isIE) {
1407 | button.onmousedown = function () {
1408 | wmd.ieRetardedClick = true;
1409 | wmd.ieCachedRange = document.selection.createRange();
1410 | };
1411 | }
1412 |
1413 | if (!button.isHelp) {
1414 | button.onclick = function () {
1415 | if (this.onmouseout) {
1416 | this.onmouseout();
1417 | }
1418 | doClick(this);
1419 | return false;
1420 | };
1421 | }
1422 | }
1423 | else {
1424 | button.style.backgroundPosition = button.XShift + " " + disabledYShift;
1425 | button.onmouseover = button.onmouseout = button.onclick = function () {};
1426 | }
1427 | };
1428 |
1429 | var makeSpritedButtonRow = function () {
1430 |
1431 | var buttonBar = (typeof wmd_options.button_bar == 'string') ? document.getElementById(wmd_options.button_bar || "wmd-button-bar") : wmd_options.button_bar;
1432 |
1433 | var normalYShift = "0px";
1434 | var disabledYShift = "-20px";
1435 | var highlightYShift = "-40px";
1436 |
1437 | var buttonRow = document.createElement("ul");
1438 | buttonRow.className = "wmd-button-row";
1439 | buttonRow = buttonBar.appendChild(buttonRow);
1440 |
1441 | var xoffset = 0;
1442 |
1443 | function createButton(name, title, textOp) {
1444 | var button = document.createElement("li");
1445 | wmd.buttons[name] = button;
1446 | button.className = "wmd-button " + name;
1447 | button.XShift = xoffset + "px";
1448 | xoffset -= 20;
1449 |
1450 | if (title) button.title = title;
1451 |
1452 | if (textOp) button.textOp = textOp;
1453 |
1454 | return button;
1455 | }
1456 |
1457 | function addButton(name, title, textOp) {
1458 | var button = createButton(name, title, textOp);
1459 |
1460 | setupButton(button, true);
1461 | buttonRow.appendChild(button);
1462 | return button;
1463 | }
1464 |
1465 | function addSpacer() {
1466 | var spacer = document.createElement("li");
1467 | spacer.className = "wmd-spacer";
1468 | buttonRow.appendChild(spacer);
1469 | return spacer;
1470 | }
1471 |
1472 | var boldButton = addButton("wmd-bold-button", "Strong Ctrl+B", command.doBold);
1473 | var italicButton = addButton("wmd-italic-button", "Emphasis Ctrl+I", command.doItalic);
1474 | var spacer1 = addSpacer();
1475 |
1476 | var linkButton = addButton("wmd-link-button", "Hyperlink Ctrl+L", function (chunk, postProcessing, useDefaultText) {
1477 | return command.doLinkOrImage(chunk, postProcessing, false);
1478 | });
1479 | var quoteButton = addButton("wmd-quote-button", "Blockquote
Ctrl+Q", command.doBlockquote);
1480 | var codeButton = addButton("wmd-code-button", "Code Sample
Ctrl+K", command.doCode);
1481 | var imageButton = addButton("wmd-image-button", "Image
Ctrl+G", function (chunk, postProcessing, useDefaultText) {
1482 | return command.doLinkOrImage(chunk, postProcessing, true);
1483 | });
1484 |
1485 | var spacer2 = addSpacer();
1486 |
1487 | var olistButton = addButton("wmd-olist-button", "Numbered List
Ctrl+O", function (chunk, postProcessing, useDefaultText) {
1488 | command.doList(chunk, postProcessing, true, useDefaultText);
1489 | });
1490 | var ulistButton = addButton("wmd-ulist-button", "Bulleted List
Ctrl+U", function (chunk, postProcessing, useDefaultText) {
1491 | command.doList(chunk, postProcessing, false, useDefaultText);
1492 | });
1493 | var headingButton = addButton("wmd-heading-button", "Heading
/
Ctrl+H", command.doHeading);
1494 | var hrButton = addButton("wmd-hr-button", "Horizontal Rule
5 |
Ctrl+R", command.doHorizontalRule);
1495 | var spacer3 = addSpacer();
1496 |
1497 | var undoButton = addButton("wmd-undo-button", "Undo - Ctrl+Z");
1498 | undoButton.execute = function (manager) {
1499 | manager.undo();
1500 | };
1501 |
1502 | var redo_title = null;
1503 |
1504 | var redoButton = addButton("wmd-redo-button", "Redo - Ctrl+Y");
1505 | if (/win/.test(nav.platform.toLowerCase())) {
1506 | redoButton.title = "Redo - Ctrl+Y";
1507 | }
1508 | else {
1509 | // mac and other non-Windows platforms
1510 | redoButton.title = "Redo - Ctrl+Shift+Z";
1511 | }
1512 | redoButton.execute = function (manager) {
1513 | manager.redo();
1514 | };
1515 |
1516 | var helpButton = createButton("wmd-help-button");
1517 | helpButton.isHelp = true;
1518 | setupButton(helpButton, true);
1519 | buttonRow.appendChild(helpButton);
1520 |
1521 | var helpAnchor = document.createElement("a");
1522 | helpAnchor.href = wmd_options.helpLink;
1523 | helpAnchor.target = wmd_options.helpTarget;
1524 | helpAnchor.title = wmd_options.helpHoverTitle;
1525 | helpButton.appendChild(helpAnchor);
1526 |
1527 | setUndoRedoButtonStates();
1528 | };
1529 |
1530 | var setupEditor = function () {
1531 |
1532 | if (/\?noundo/.test(document.location.href)) {
1533 | wmd.nativeUndo = true;
1534 | }
1535 |
1536 | if (!wmd.nativeUndo) {
1537 | undoMgr = new UndoManager(wmd, wmd.panels.input, wmd.options.pastePollInterval, function () {
1538 | previewRefreshCallback();
1539 | setUndoRedoButtonStates();
1540 | });
1541 | }
1542 |
1543 | makeSpritedButtonRow();
1544 |
1545 |
1546 | var keyEvent = "keydown";
1547 | if (browser.isOpera) {
1548 | keyEvent = "keypress";
1549 | }
1550 |
1551 | util.addEvent(inputBox, keyEvent, function (key) {
1552 |
1553 | // Check to see if we have a button key and, if so execute the callback.
1554 | if (key.ctrlKey || key.metaKey) {
1555 |
1556 | var keyCode = key.charCode || key.keyCode;
1557 | var keyCodeStr = String.fromCharCode(keyCode).toLowerCase();
1558 |
1559 | switch (keyCodeStr) {
1560 | case "b":
1561 | doClick(wmd.buttons["wmd-bold-button"]);
1562 | break;
1563 | case "i":
1564 | doClick(wmd.buttons["wmd-italic-button"]);
1565 | break;
1566 | case "l":
1567 | doClick(wmd.buttons["wmd-link-button"]);
1568 | break;
1569 | case "q":
1570 | doClick(wmd.buttons["wmd-quote-button"]);
1571 | break;
1572 | case "k":
1573 | doClick(wmd.buttons["wmd-code-button"]);
1574 | break;
1575 | case "g":
1576 | doClick(wmd.buttons["wmd-image-button"]);
1577 | break;
1578 | case "o":
1579 | doClick(wmd.buttons["wmd-olist-button"]);
1580 | break;
1581 | case "u":
1582 | doClick(wmd.buttons["wmd-ulist-button"]);
1583 | break;
1584 | case "h":
1585 | doClick(wmd.buttons["wmd-heading-button"]);
1586 | break;
1587 | case "r":
1588 | doClick(wmd.buttons["wmd-hr-button"]);
1589 | break;
1590 | case "y":
1591 | doClick(wmd.buttons["wmd-redo-button"]);
1592 | break;
1593 | case "z":
1594 | if (key.shiftKey) {
1595 | doClick(wmd.buttons["wmd-redo-button"]);
1596 | }
1597 | else {
1598 | doClick(wmd.buttons["wmd-undo-button"]);
1599 | }
1600 | break;
1601 | default:
1602 | return;
1603 | }
1604 |
1605 |
1606 | if (key.preventDefault) {
1607 | key.preventDefault();
1608 | }
1609 |
1610 | if (window.event) {
1611 | window.event.returnValue = false;
1612 | }
1613 | }
1614 | });
1615 |
1616 | // Auto-continue lists, code blocks and block quotes when
1617 | // the enter key is pressed.
1618 | util.addEvent(inputBox, "keyup", function (key) {
1619 | if (!key.shiftKey && !key.ctrlKey && !key.metaKey) {
1620 | var keyCode = key.charCode || key.keyCode;
1621 | // Key code 13 is Enter
1622 | if (keyCode === 13) {
1623 | fakeButton = {};
1624 | fakeButton.textOp = command.doAutoindent;
1625 | doClick(fakeButton);
1626 | }
1627 | }
1628 | });
1629 |
1630 | // Disable ESC clearing the input textarea on IE
1631 | if (browser.isIE) {
1632 | util.addEvent(inputBox, "keydown", function (key) {
1633 | var code = key.keyCode;
1634 | // Key code 27 is ESC
1635 | if (code === 27) {
1636 | return false;
1637 | }
1638 | });
1639 | }
1640 | };
1641 |
1642 |
1643 | this.undo = function () {
1644 | if (undoMgr) {
1645 | undoMgr.undo();
1646 | }
1647 | };
1648 |
1649 | this.redo = function () {
1650 | if (undoMgr) {
1651 | undoMgr.redo();
1652 | }
1653 | };
1654 |
1655 | // This is pretty useless. The setupEditor function contents
1656 | // should just be copied here.
1657 | var init = function () {
1658 | setupEditor();
1659 | };
1660 |
1661 | this.destroy = function () {
1662 | if (undoMgr) {
1663 | undoMgr.destroy();
1664 | }
1665 | if (div.parentNode) {
1666 | div.parentNode.removeChild(div);
1667 | }
1668 | if (inputBox) {
1669 | inputBox.style.marginTop = "";
1670 | }
1671 | window.clearInterval(creationHandle);
1672 | };
1673 |
1674 | init();
1675 | }; // }}}
1676 | // command {{{
1677 | // The markdown symbols - 4 spaces = code, > = blockquote, etc.
1678 | command.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)";
1679 |
1680 | // Remove markdown symbols from the chunk selection.
1681 | command.unwrap = function (chunk) {
1682 | var txt = new re("([^\\n])\\n(?!(\\n|" + command.prefixes + "))", "g");
1683 | chunk.selection = chunk.selection.replace(txt, "$1 $2");
1684 | };
1685 |
1686 | command.wrap = function (chunk, len) {
1687 | command.unwrap(chunk);
1688 | var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm");
1689 |
1690 | chunk.selection = chunk.selection.replace(regex, function (line, marked) {
1691 | if (new re("^" + command.prefixes, "").test(line)) {
1692 | return line;
1693 | }
1694 | return marked + "\n";
1695 | });
1696 |
1697 | chunk.selection = chunk.selection.replace(/\s+$/, "");
1698 | };
1699 |
1700 | command.doBold = function (chunk, postProcessing, useDefaultText) {
1701 | return command.doBorI(chunk, 2, "strong text");
1702 | };
1703 |
1704 | command.doItalic = function (chunk, postProcessing, useDefaultText) {
1705 | return command.doBorI(chunk, 1, "emphasized text");
1706 | };
1707 |
1708 | // chunk: The selected region that will be enclosed with */**
1709 | // nStars: 1 for italics, 2 for bold
1710 | // insertText: If you just click the button without highlighting text, this gets inserted
1711 | command.doBorI = function (chunk, nStars, insertText) {
1712 |
1713 | // Get rid of whitespace and fixup newlines.
1714 | chunk.trimWhitespace();
1715 | chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n");
1716 |
1717 | // Look for stars before and after. Is the chunk already marked up?
1718 | chunk.before.search(/(\**$)/);
1719 | var starsBefore = re.$1;
1720 |
1721 | chunk.after.search(/(^\**)/);
1722 | var starsAfter = re.$1;
1723 |
1724 | var prevStars = Math.min(starsBefore.length, starsAfter.length);
1725 |
1726 | // Remove stars if we have to since the button acts as a toggle.
1727 | if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) {
1728 | chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), "");
1729 | chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), "");
1730 | }
1731 | else if (!chunk.selection && starsAfter) {
1732 | // It's not really clear why this code is necessary. It just moves
1733 | // some arbitrary stuff around.
1734 | chunk.after = chunk.after.replace(/^([*_]*)/, "");
1735 | chunk.before = chunk.before.replace(/(\s?)$/, "");
1736 | var whitespace = re.$1;
1737 | chunk.before = chunk.before + starsAfter + whitespace;
1738 | }
1739 | else {
1740 |
1741 | // In most cases, if you don't have any selected text and click the button
1742 | // you'll get a selected, marked up region with the default text inserted.
1743 | if (!chunk.selection && !starsAfter) {
1744 | chunk.selection = insertText;
1745 | }
1746 |
1747 | // Add the true markup.
1748 | var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ?
1749 | chunk.before = chunk.before + markup;
1750 | chunk.after = markup + chunk.after;
1751 | }
1752 |
1753 | return;
1754 | };
1755 |
1756 | command.stripLinkDefs = function (text, defsToAdd) {
1757 |
1758 | text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm, function (totalMatch, id, link, newlines, title) {
1759 | defsToAdd[id] = totalMatch.replace(/\s*$/, "");
1760 | if (newlines) {
1761 | // Strip the title and return that separately.
1762 | defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, "");
1763 | return newlines + title;
1764 | }
1765 | return "";
1766 | });
1767 |
1768 | return text;
1769 | };
1770 |
1771 | command.addLinkDef = function (chunk, linkDef) {
1772 |
1773 | var refNumber = 0; // The current reference number
1774 | var defsToAdd = {}; //
1775 | // Start with a clean slate by removing all previous link definitions.
1776 | chunk.before = command.stripLinkDefs(chunk.before, defsToAdd);
1777 | chunk.selection = command.stripLinkDefs(chunk.selection, defsToAdd);
1778 | chunk.after = command.stripLinkDefs(chunk.after, defsToAdd);
1779 |
1780 | var defs = "";
1781 | var regex = /(\[(?:\[[^\]]*\]|[^\[\]])*\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g;
1782 |
1783 | var addDefNumber = function (def) {
1784 | refNumber++;
1785 | def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, " [" + refNumber + "]:");
1786 | defs += "\n" + def;
1787 | };
1788 |
1789 | var getLink = function (wholeMatch, link, id, end) {
1790 |
1791 | if (defsToAdd[id]) {
1792 | addDefNumber(defsToAdd[id]);
1793 | return link + refNumber + end;
1794 |
1795 | }
1796 | return wholeMatch;
1797 | };
1798 |
1799 | chunk.before = chunk.before.replace(regex, getLink);
1800 |
1801 | if (linkDef) {
1802 | addDefNumber(linkDef);
1803 | }
1804 | else {
1805 | chunk.selection = chunk.selection.replace(regex, getLink);
1806 | }
1807 |
1808 | var refOut = refNumber;
1809 |
1810 | chunk.after = chunk.after.replace(regex, getLink);
1811 |
1812 | if (chunk.after) {
1813 | chunk.after = chunk.after.replace(/\n*$/, "");
1814 | }
1815 | if (!chunk.after) {
1816 | chunk.selection = chunk.selection.replace(/\n*$/, "");
1817 | }
1818 |
1819 | chunk.after += "\n\n" + defs;
1820 |
1821 | return refOut;
1822 | };
1823 |
1824 | command.doLinkOrImage = function (chunk, postProcessing, isImage) {
1825 |
1826 | chunk.trimWhitespace();
1827 | chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/);
1828 |
1829 | if (chunk.endTag.length > 1) {
1830 |
1831 | chunk.startTag = chunk.startTag.replace(/!?\[/, "");
1832 | chunk.endTag = "";
1833 | command.addLinkDef(chunk, null);
1834 |
1835 | }
1836 | else {
1837 |
1838 | if (/\n\n/.test(chunk.selection)) {
1839 | command.addLinkDef(chunk, null);
1840 | return;
1841 | }
1842 |
1843 | // The function to be executed when you enter a link and press OK or Cancel.
1844 | // Marks up the link and adds the ref.
1845 | var makeLinkMarkdown = function (link) {
1846 |
1847 | if (link !== null) {
1848 |
1849 | chunk.startTag = chunk.endTag = "";
1850 | var linkDef = " [999]: " + link;
1851 |
1852 | var num = command.addLinkDef(chunk, linkDef);
1853 | chunk.startTag = isImage ? "![" : "[";
1854 | chunk.endTag = "][" + num + "]";
1855 |
1856 | if (!chunk.selection) {
1857 | if (isImage) {
1858 | chunk.selection = "alt text";
1859 | }
1860 | else {
1861 | chunk.selection = "link text";
1862 | }
1863 | }
1864 | }
1865 | postProcessing();
1866 | };
1867 |
1868 | if (isImage) {
1869 | util.prompt(wmd_options.imageDialogText, wmd_options.imageDefaultText, makeLinkMarkdown);
1870 | }
1871 | else {
1872 | util.prompt(wmd_options.linkDialogText, wmd_options.linkDefaultText, makeLinkMarkdown);
1873 | }
1874 | return true;
1875 | }
1876 | };
1877 |
1878 | // Moves the cursor to the next line and continues lists, quotes and code.
1879 | command.doAutoindent = function (chunk, postProcessing, useDefaultText) {
1880 |
1881 | chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");
1882 | chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");
1883 | chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");
1884 |
1885 | useDefaultText = false;
1886 |
1887 | if (/(\n|^)[ ]{0,3}([*+-])[ \t]+.*\n$/.test(chunk.before)) {
1888 | if (command.doList) {
1889 | command.doList(chunk, postProcessing, false, true);
1890 | }
1891 | }
1892 | if (/(\n|^)[ ]{0,3}(\d+[.])[ \t]+.*\n$/.test(chunk.before)) {
1893 | if (command.doList) {
1894 | command.doList(chunk, postProcessing, true, true);
1895 | }
1896 | }
1897 | if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) {
1898 | if (command.doBlockquote) {
1899 | command.doBlockquote(chunk, postProcessing, useDefaultText);
1900 | }
1901 | }
1902 | if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
1903 | if (command.doCode) {
1904 | command.doCode(chunk, postProcessing, useDefaultText);
1905 | }
1906 | }
1907 | };
1908 |
1909 | command.doBlockquote = function (chunk, postProcessing, useDefaultText) {
1910 |
1911 | chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/, function (totalMatch, newlinesBefore, text, newlinesAfter) {
1912 | chunk.before += newlinesBefore;
1913 | chunk.after = newlinesAfter + chunk.after;
1914 | return text;
1915 | });
1916 |
1917 | chunk.before = chunk.before.replace(/(>[ \t]*)$/, function (totalMatch, blankLine) {
1918 | chunk.selection = blankLine + chunk.selection;
1919 | return "";
1920 | });
1921 |
1922 | var defaultText = useDefaultText ? "Blockquote" : "";
1923 | chunk.selection = chunk.selection.replace(/^(\s|>)+$/, "");
1924 | chunk.selection = chunk.selection || defaultText;
1925 |
1926 | if (chunk.before) {
1927 | chunk.before = chunk.before.replace(/\n?$/, "\n");
1928 | }
1929 | if (chunk.after) {
1930 | chunk.after = chunk.after.replace(/^\n?/, "\n");
1931 | }
1932 |
1933 | chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/, function (totalMatch) {
1934 | chunk.startTag = totalMatch;
1935 | return "";
1936 | });
1937 |
1938 | chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/, function (totalMatch) {
1939 | chunk.endTag = totalMatch;
1940 | return "";
1941 | });
1942 |
1943 | var replaceBlanksInTags = function (useBracket) {
1944 |
1945 | var replacement = useBracket ? "> " : "";
1946 |
1947 | if (chunk.startTag) {
1948 | chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/, function (totalMatch, markdown) {
1949 | return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
1950 | });
1951 | }
1952 | if (chunk.endTag) {
1953 | chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/, function (totalMatch, markdown) {
1954 | return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n";
1955 | });
1956 | }
1957 | };
1958 |
1959 | if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) {
1960 | command.wrap(chunk, wmd_options.lineLength - 2);
1961 | chunk.selection = chunk.selection.replace(/^/gm, "> ");
1962 | replaceBlanksInTags(true);
1963 | chunk.addBlankLines();
1964 | }
1965 | else {
1966 | chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, "");
1967 | command.unwrap(chunk);
1968 | replaceBlanksInTags(false);
1969 |
1970 | if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) {
1971 | chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n");
1972 | }
1973 |
1974 | if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) {
1975 | chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n");
1976 | }
1977 | }
1978 |
1979 | if (!/\n/.test(chunk.selection)) {
1980 | chunk.selection = chunk.selection.replace(/^(> *)/, function (wholeMatch, blanks) {
1981 | chunk.startTag += blanks;
1982 | return "";
1983 | });
1984 | }
1985 | };
1986 |
1987 | command.doCode = function (chunk, postProcessing, useDefaultText) {
1988 |
1989 | var hasTextBefore = /\S[ ]*$/.test(chunk.before);
1990 | var hasTextAfter = /^[ ]*\S/.test(chunk.after);
1991 |
1992 | // Use 'four space' markdown if the selection is on its own
1993 | // line or is multiline.
1994 | if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) {
1995 |
1996 | chunk.before = chunk.before.replace(/[ ]{4}$/, function (totalMatch) {
1997 | chunk.selection = totalMatch + chunk.selection;
1998 | return "";
1999 | });
2000 |
2001 | var nLinesBefore = 1;
2002 | var nLinesAfter = 1;
2003 |
2004 |
2005 | if (/\n(\t|[ ]{4,}).*\n$/.test(chunk.before) || chunk.after === "") {
2006 | nLinesBefore = 0;
2007 | }
2008 | if (/^\n(\t|[ ]{4,})/.test(chunk.after)) {
2009 | nLinesAfter = 0; // This needs to happen on line 1
2010 | }
2011 |
2012 | chunk.addBlankLines(nLinesBefore, nLinesAfter);
2013 |
2014 | if (!chunk.selection) {
2015 | chunk.startTag = " ";
2016 | chunk.selection = useDefaultText ? "enter code here" : "";
2017 | }
2018 | else {
2019 | if (/^[ ]{0,3}\S/m.test(chunk.selection)) {
2020 | chunk.selection = chunk.selection.replace(/^/gm, " ");
2021 | }
2022 | else {
2023 | chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, "");
2024 | }
2025 | }
2026 | }
2027 | else {
2028 | // Use backticks (`) to delimit the code block.
2029 | chunk.trimWhitespace();
2030 | chunk.findTags(/`/, /`/);
2031 |
2032 | if (!chunk.startTag && !chunk.endTag) {
2033 | chunk.startTag = chunk.endTag = "`";
2034 | if (!chunk.selection) {
2035 | chunk.selection = useDefaultText ? "enter code here" : "";
2036 | }
2037 | }
2038 | else if (chunk.endTag && !chunk.startTag) {
2039 | chunk.before += chunk.endTag;
2040 | chunk.endTag = "";
2041 | }
2042 | else {
2043 | chunk.startTag = chunk.endTag = "";
2044 | }
2045 | }
2046 | };
2047 |
2048 | command.doList = function (chunk, postProcessing, isNumberedList, useDefaultText) {
2049 |
2050 | // These are identical except at the very beginning and end.
2051 | // Should probably use the regex extension function to make this clearer.
2052 | var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/;
2053 | var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/;
2054 |
2055 | // The default bullet is a dash but others are possible.
2056 | // This has nothing to do with the particular HTML bullet,
2057 | // it's just a markdown bullet.
2058 | var bullet = "-";
2059 |
2060 | // The number in a numbered list.
2061 | var num = 1;
2062 |
2063 | // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list.
2064 | var getItemPrefix = function () {
2065 | var prefix;
2066 | if (isNumberedList) {
2067 | prefix = " " + num + ". ";
2068 | num++;
2069 | }
2070 | else {
2071 | prefix = " " + bullet + " ";
2072 | }
2073 | return prefix;
2074 | };
2075 |
2076 | // Fixes the prefixes of the other list items.
2077 | var getPrefixedItem = function (itemText) {
2078 |
2079 | // The numbering flag is unset when called by autoindent.
2080 | if (isNumberedList === undefined) {
2081 | isNumberedList = /^\s*\d/.test(itemText);
2082 | }
2083 |
2084 | // Renumber/bullet the list element.
2085 | itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm, function (_) {
2086 | return getItemPrefix();
2087 | });
2088 |
2089 | return itemText;
2090 | };
2091 |
2092 | chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null);
2093 |
2094 | if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) {
2095 | chunk.before += chunk.startTag;
2096 | chunk.startTag = "";
2097 | }
2098 |
2099 | if (chunk.startTag) {
2100 |
2101 | var hasDigits = /\d+[.]/.test(chunk.startTag);
2102 | chunk.startTag = "";
2103 | chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n");
2104 | command.unwrap(chunk);
2105 | chunk.addBlankLines();
2106 |
2107 | if (hasDigits) {
2108 | // Have to renumber the bullet points if this is a numbered list.
2109 | chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem);
2110 | }
2111 | if (isNumberedList == hasDigits) {
2112 | return;
2113 | }
2114 | }
2115 |
2116 | var nLinesBefore = 1;
2117 |
2118 | chunk.before = chunk.before.replace(previousItemsRegex, function (itemText) {
2119 | if (/^\s*([*+-])/.test(itemText)) {
2120 | bullet = re.$1;
2121 | }
2122 | nLinesBefore = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
2123 | return getPrefixedItem(itemText);
2124 | });
2125 |
2126 | if (!chunk.selection) {
2127 | chunk.selection = useDefaultText ? "List item" : " ";
2128 | }
2129 |
2130 | var prefix = getItemPrefix();
2131 |
2132 | var nLinesAfter = 1;
2133 |
2134 | chunk.after = chunk.after.replace(nextItemsRegex, function (itemText) {
2135 | nLinesAfter = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0;
2136 | return getPrefixedItem(itemText);
2137 | });
2138 |
2139 | chunk.trimWhitespace(true);
2140 | chunk.addBlankLines(nLinesBefore, nLinesAfter, true);
2141 | chunk.startTag = prefix;
2142 | var spaces = prefix.replace(/./g, " ");
2143 | command.wrap(chunk, wmd_options.lineLength - spaces.length);
2144 | chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces);
2145 |
2146 | };
2147 |
2148 | command.doHeading = function (chunk, postProcessing, useDefaultText) {
2149 |
2150 | // Remove leading/trailing whitespace and reduce internal spaces to single spaces.
2151 | chunk.selection = chunk.selection.replace(/\s+/g, " ");
2152 | chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, "");
2153 |
2154 | // If we clicked the button with no selected text, we just
2155 | // make a level 2 hash header around some default text.
2156 | if (!chunk.selection) {
2157 | chunk.startTag = "## ";
2158 | chunk.selection = "Heading";
2159 | chunk.endTag = " ##";
2160 | return;
2161 | }
2162 |
2163 | var headerLevel = 0; // The existing header level of the selected text.
2164 | // Remove any existing hash heading markdown and save the header level.
2165 | chunk.findTags(/#+[ ]*/, /[ ]*#+/);
2166 | if (/#+/.test(chunk.startTag)) {
2167 | headerLevel = re.lastMatch.length;
2168 | }
2169 | chunk.startTag = chunk.endTag = "";
2170 |
2171 | // Try to get the current header level by looking for - and = in the line
2172 | // below the selection.
2173 | chunk.findTags(null, /\s?(-+|=+)/);
2174 | if (/=+/.test(chunk.endTag)) {
2175 | headerLevel = 1;
2176 | }
2177 | if (/-+/.test(chunk.endTag)) {
2178 | headerLevel = 2;
2179 | }
2180 |
2181 | // Skip to the next line so we can create the header markdown.
2182 | chunk.startTag = chunk.endTag = "";
2183 | chunk.addBlankLines(1, 1);
2184 |
2185 | // We make a level 2 header if there is no current header.
2186 | // If there is a header level, we substract one from the header level.
2187 | // If it's already a level 1 header, it's removed.
2188 | var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1;
2189 |
2190 | if (headerLevelToCreate > 0) {
2191 |
2192 | // The button only creates level 1 and 2 underline headers.
2193 | // Why not have it iterate over hash header levels? Wouldn't that be easier and cleaner?
2194 | var headerChar = headerLevelToCreate >= 2 ? "-" : "=";
2195 | var len = chunk.selection.length;
2196 | if (len > wmd_options.lineLength) {
2197 | len = wmd_options.lineLength;
2198 | }
2199 | chunk.endTag = "\n";
2200 | while (len--) {
2201 | chunk.endTag += headerChar;
2202 | }
2203 | }
2204 | };
2205 |
2206 | command.doHorizontalRule = function (chunk, postProcessing, useDefaultText) {
2207 | chunk.startTag = "----------\n";
2208 | chunk.selection = "";
2209 | chunk.addBlankLines(2, 1, true);
2210 | };
2211 | // }}}
2212 | }; // }}}
2213 | })();
2214 |
2215 | // For backward compatibility
2216 |
2217 | function setup_wmd(options) {
2218 | return new WMDEditor(options);
2219 | }
--------------------------------------------------------------------------------
/templates/_comments.html:
--------------------------------------------------------------------------------
1 | {% macro comments_render(type, model, xsrf_form_html) %}
2 |
17 | {% endmacro %}
18 |
--------------------------------------------------------------------------------
/templates/ask.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block styles %}
3 |
4 | {% endblock %}
5 | {% block scripts %}
6 |
7 |
8 | {% endblock %}
9 | {% block body %}
10 | {{ ask.title|e }}
86 | 回答
140 | {{ notice_message }}
141 |
152 |
160 |
29 |
31 | {{ user.name }}
5 | 个人设置
3 |
5 |