├── .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 | // 8 | // 9 | // The full source distribution is at: 10 | // 11 | // A A L 12 | // T C A 13 | // T K B 14 | // 15 | // 16 | // 17 | // 18 | // Wherever possible, Showdown is a straight, line-by-line port 19 | // of the Perl version of Markdown. 20 | // 21 | // This is not a normal parser design; it's basically just a 22 | // series of string substitutions. It's hard to read and 23 | // maintain this way, but keeping Showdown close to the original 24 | // design makes it easier to port new features. 25 | // 26 | // More importantly, Showdown behaves like markdown.pl in most 27 | // edge cases. So web applications can do client-side preview 28 | // in Javascript, and then build identical HTML on the server. 29 | // 30 | // This port needs the new RegExp functionality of ECMA 262, 31 | // 3rd Edition (i.e. Javascript 1.5). Most modern web browsers 32 | // should do fine. Even with the new regular expression features, 33 | // We do a lot of work to emulate Perl's regex functionality. 34 | // The tricky changes in this file mostly have the "attacklab:" 35 | // label. Major or self-explanatory changes don't. 36 | // 37 | // Smart diff tools like Araxis Merge will be able to match up 38 | // this file with markdown.pl in a useful way. A little tweaking 39 | // helps: in a copy of markdown.pl, replace "#" with "//" and 40 | // replace "$text" with "text". Be sure to ignore whitespace 41 | // and line endings. 42 | // 43 | 44 | // 45 | // Showdown usage: 46 | // 47 | // var text = "Markdown *rocks*."; 48 | // 49 | // var converter = new Attacklab.showdown.converter(); 50 | // var html = converter.makeHtml(text); 51 | // 52 | // alert(html); 53 | // 54 | // Note: move the sample code to the bottom of this 55 | // file before uncommenting it. 56 | // 57 | 58 | // 59 | // Attacklab namespace 60 | // 61 | var Attacklab = Attacklab || {}; 62 | 63 | // 64 | // Showdown namespace 65 | // 66 | Attacklab.showdown = Attacklab.showdown || {}; 67 | 68 | // 69 | // converter 70 | // 71 | // Wraps all "globals" so that the only thing 72 | // exposed is makeHtml(). 73 | // 74 | Attacklab.showdown.converter = function () { 75 | 76 | // 77 | // Globals: 78 | // 79 | // Global hashes, used by various utility routines 80 | var g_urls; 81 | var g_titles; 82 | var g_html_blocks; 83 | 84 | // Used to track when we're inside an ordered or unordered list 85 | // (see _ProcessListItems() for details): 86 | var g_list_level = 0; 87 | 88 | 89 | this.makeHtml = function (text) { 90 | // 91 | // Main function. The order in which other subs are called here is 92 | // essential. Link and image substitutions need to happen before 93 | // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the 94 | // and 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 | ? // 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]*?[ \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 | //

221 | //
222 | // tags for inner block must be indented. 223 | //
224 | //
225 | // 226 | // The outermost tags must start at the left margin for this to match, and 227 | // the inner nested divs must be indented. 228 | // We need to do this before the next, more liberal match, because the next 229 | // match will start at the first `
` and stop at the first `
`. 230 | // attacklab: This regex can be expensive when it fails. 231 | /* 232 | var text = text.replace(/ 233 | ( // save in $1 234 | ^ // start of line (with /m) 235 | <($block_tags_a) // start tag = $2 236 | \b // word break 237 | // attacklab: hack around khtml/pcre bug... 238 | [^\r]*?\n // any number of lines, minimally matching 239 | // the matching end tag 240 | [ \t]* // trailing spaces/tabs 241 | (?=\n+) // followed by a newline 242 | ) // attacklab: there are sentinel newlines at end of document 243 | /gm,function(){...}}; 244 | */ 245 | text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm, hashElement); 246 | 247 | // 248 | // Now match more liberally, simply from `\n` to `\n` 249 | // 250 | /* 251 | var text = text.replace(/ 252 | ( // save in $1 253 | ^ // start of line (with /m) 254 | <($block_tags_b) // start tag = $2 255 | \b // word break 256 | // attacklab: hack around khtml/pcre bug... 257 | [^\r]*? // any number of lines, minimally matching 258 | .* // the matching end tag 259 | [ \t]* // trailing spaces/tabs 260 | (?=\n+) // followed by a newline 261 | ) // attacklab: there are sentinel newlines at end of document 262 | /gm,function(){...}}; 263 | */ 264 | text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm, hashElement); 265 | 266 | // Special case just for
. It was easier to make a special case than 267 | // to make the other regex more complicated. 268 | /* 269 | text = text.replace(/ 270 | ( // save in $1 271 | \n\n // Starting after a blank line 272 | [ ]{0,3} 273 | (<(hr) // start tag = $2 274 | \b // word break 275 | ([^<>])*? // 276 | \/?>) // the matching end tag 277 | [ \t]* 278 | (?=\n{2,}) // followed by a blank line 279 | ) 280 | /g,hashElement); 281 | */ 282 | text = text.replace(/(\n[ ]{0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, hashElement); 283 | 284 | // Special case for standalone HTML comments: 285 | /* 286 | text = text.replace(/ 287 | ( // save in $1 288 | \n\n // Starting after a blank line 289 | [ ]{0,3} // attacklab: g_tab_width - 1 290 | 293 | [ \t]* 294 | (?=\n{2,}) // followed by a blank line 295 | ) 296 | /g,hashElement); 297 | */ 298 | text = text.replace(/(\n\n[ ]{0,3}[ \t]*(?=\n{2,}))/g, hashElement); 299 | 300 | // PHP and ASP-style processor instructions ( and <%...%>) 301 | /* 302 | text = text.replace(/ 303 | (?: 304 | \n\n // Starting after a blank line 305 | ) 306 | ( // save in $1 307 | [ ]{0,3} // attacklab: g_tab_width - 1 308 | (?: 309 | <([?%]) // $2 310 | [^\r]*? 311 | \2> 312 | ) 313 | [ \t]* 314 | (?=\n{2,}) // followed by a blank line 315 | ) 316 | /g,hashElement); 317 | */ 318 | text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g, hashElement); 319 | 320 | // attacklab: Undo double lines (see comment at top of this function) 321 | text = text.replace(/\n\n/g, "\n"); 322 | return text; 323 | } 324 | 325 | var hashElement = function (wholeMatch, m1) { 326 | var blockText = m1; 327 | 328 | // Undo double lines 329 | blockText = blockText.replace(/\n\n/g, "\n"); 330 | blockText = blockText.replace(/^\n/, ""); 331 | 332 | // strip trailing blank lines 333 | blockText = blockText.replace(/\n+$/g, ""); 334 | 335 | // Replace the element text with a marker ("~KxK" where x is its key) 336 | blockText = "\n\n~K" + (g_html_blocks.push(blockText) - 1) + "K\n\n"; 337 | 338 | return blockText; 339 | }; 340 | 341 | var _RunBlockGamut = function (text) { 342 | // 343 | // These are all the transformations that form block-level 344 | // tags like paragraphs, headers, and list items. 345 | // 346 | text = _DoHeaders(text); 347 | 348 | // Do Horizontal Rules: 349 | var key = hashBlock("
"); 350 | text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm, key); 351 | text = text.replace(/^[ ]{0,2}([ ]?\-[ ]?){3,}[ \t]*$/gm, key); 352 | text = text.replace(/^[ ]{0,2}([ ]?\_[ ]?){3,}[ \t]*$/gm, key); 353 | 354 | text = _DoLists(text); 355 | text = _DoCodeBlocks(text); 356 | text = _DoBlockQuotes(text); 357 | 358 | // We already ran _HashHTMLBlocks() before, in Markdown(), but that 359 | // was to escape raw HTML in the original Markdown source. This time, 360 | // we're escaping the markup we've just created, so that we don't wrap 361 | //

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 `` 384 | // Must come after _DoAnchors(), because you can use < and > 385 | // delimiters in inline links like [this](). 386 | text = _DoAutoLinks(text); 387 | text = _EncodeAmpsAndAngles(text); 388 | text = _DoItalicsAndBold(text); 389 | 390 | // Do hard breaks: 391 | text = text.replace(/ +\n/g, "
\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 = ""; 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: ![alt text](url "optional title") 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 | ? // 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]*()?[ \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 = "\""" + _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("" + _RunSpanGamut(m2) + ""); 694 | }); 695 | 696 | return text; 697 | } 698 | 699 | // This declaration keeps Dojo compressor from outputting garbage: 700 | var _ProcessListItems; 701 | 702 | var _DoLists = function (text) { 703 | // 704 | // Form HTML ordered (numbered) and unordered (bulleted) lists. 705 | // 706 | // attacklab: add sentinel to hack around khtml/safari bug: 707 | // http://bugs.webkit.org/show_bug.cgi?id=11231 708 | text += "~0"; 709 | 710 | // Re-usable pattern to match any entirel ul or ol list: 711 | /* 712 | var whole_list = / 713 | ( // $1 = whole list 714 | ( // $2 715 | [ ]{0,3} // attacklab: g_tab_width - 1 716 | ([*+-]|\d+[.]) // $3 = first list item marker 717 | [ \t]+ 718 | ) 719 | [^\r]+? 720 | ( // $4 721 | ~0 // sentinel for workaround; should be $ 722 | | 723 | \n{2,} 724 | (?=\S) 725 | (?! // Negative lookahead for another list item marker 726 | [ \t]* 727 | (?:[*+-]|\d+[.])[ \t]+ 728 | ) 729 | ) 730 | )/g 731 | */ 732 | var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; 733 | 734 | if (g_list_level) { 735 | text = text.replace(whole_list, function (wholeMatch, m1, m2) { 736 | var list = m1; 737 | var list_type = (m2.search(/[*+-]/g) > -1) ? "ul" : "ol"; 738 | 739 | // Turn double returns into triple returns, so that we can make a 740 | // paragraph for the last item in a list, if necessary: 741 | list = list.replace(/\n{2,}/g, "\n\n\n"); 742 | var result = _ProcessListItems(list); 743 | 744 | // Trim any trailing whitespace, to put the closing `` 745 | // up on the preceding line, to get it past the current stupid 746 | // HTML block parser. This is a hack to work around the terrible 747 | // hack that is the HTML block parser. 748 | result = result.replace(/\s+$/, ""); 749 | result = "<" + list_type + ">" + result + "\n"; 750 | return result; 751 | }); 752 | } else { 753 | whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g; 754 | text = text.replace(whole_list, function (wholeMatch, m1, m2, m3) { 755 | var runup = m1; 756 | var list = m2; 757 | 758 | var list_type = (m3.search(/[*+-]/g) > -1) ? "ul" : "ol"; 759 | // Turn double returns into triple returns, so that we can make a 760 | // paragraph for the last item in a list, if necessary: 761 | var list = list.replace(/\n{2,}/g, "\n\n\n"); 762 | var result = _ProcessListItems(list); 763 | result = runup + "<" + list_type + ">\n" + result + "\n"; 764 | return result; 765 | }); 766 | } 767 | 768 | // attacklab: strip sentinel 769 | text = text.replace(/~0/, ""); 770 | 771 | return text; 772 | } 773 | 774 | _ProcessListItems = function (list_str) { 775 | // 776 | // Process the contents of a single ordered or unordered list, splitting it 777 | // into individual list items. 778 | // 779 | // The $g_list_level global keeps track of when we're inside a list. 780 | // Each time we enter a list, we increment it; when we leave a list, 781 | // we decrement. If it's zero, we're not in a list anymore. 782 | // 783 | // We do this because when we're not inside a list, we want to treat 784 | // something like this: 785 | // 786 | // I recommend upgrading to version 787 | // 8. Oops, now this line is treated 788 | // as a sub-list. 789 | // 790 | // As a single paragraph, despite the fact that the second line starts 791 | // with a digit-period-space sequence. 792 | // 793 | // Whereas when we're inside a list (or sub-list), that line will be 794 | // treated as the start of a sub-list. What a kludge, huh? This is 795 | // an aspect of Markdown's syntax that's hard to parse perfectly 796 | // without resorting to mind-reading. Perhaps the solution is to 797 | // change the syntax rules such that sub-lists must start with a 798 | // starting cardinal number; e.g. "1." or "a.". 799 | g_list_level++; 800 | 801 | // trim trailing blank lines: 802 | list_str = list_str.replace(/\n{2,}$/, "\n"); 803 | 804 | // attacklab: add sentinel to emulate \z 805 | list_str += "~0"; 806 | 807 | /* 808 | list_str = list_str.replace(/ 809 | (\n)? // leading line = $1 810 | (^[ \t]*) // leading whitespace = $2 811 | ([*+-]|\d+[.]) [ \t]+ // list marker = $3 812 | ([^\r]+? // list item text = $4 813 | (\n{1,2})) 814 | (?= \n* (~0 | \2 ([*+-]|\d+[.]) [ \t]+)) 815 | /gm, function(){...}); 816 | */ 817 | list_str = list_str.replace(/(\n)?(^[ \t]*)([*+-]|\d+[.])[ \t]+([^\r]+?(\n{1,2}))(?=\n*(~0|\2([*+-]|\d+[.])[ \t]+))/gm, function (wholeMatch, m1, m2, m3, m4) { 818 | var item = m4; 819 | var leading_line = m1; 820 | var leading_space = m2; 821 | 822 | if (leading_line || (item.search(/\n{2,}/) > -1)) { 823 | item = _RunBlockGamut(_Outdent(item)); 824 | } 825 | else { 826 | // Recursion for sub-lists: 827 | item = _DoLists(_Outdent(item)); 828 | item = item.replace(/\n$/, ""); // chomp(item) 829 | item = _RunSpanGamut(item); 830 | } 831 | 832 | return "
  • " + item + "
  • \n"; 833 | }); 834 | 835 | // attacklab: strip sentinel 836 | list_str = list_str.replace(/~0/g, ""); 837 | 838 | g_list_level--; 839 | return list_str; 840 | } 841 | 842 | 843 | var _DoCodeBlocks = function (text) { 844 | // 845 | // Process Markdown `
    ` 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 = "
    " + codeblock + "\n
    "; 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 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 | //

    Just type foo `bar` baz at the prompt.

    900 | // 901 | // There's no arbitrary limit to the number of backticks you 902 | // can use as delimters. If you need three consecutive backticks 903 | // in your code, use four for delimiters, etc. 904 | // 905 | // * You can use spaces to get literal backticks at the edges: 906 | // 907 | // ... type `` `bar` `` ... 908 | // 909 | // Turns to: 910 | // 911 | // ... type `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

    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, "
    "); // ** GFM ** 1053 | str = str.replace(/^([ \t]*)/g, "

    "); 1054 | str += "

    "; 1055 | grafsOut.push(str); 1056 | } 1057 | 1058 | } 1059 | 1060 | // 1061 | // Unhashify HTML blocks 1062 | // 1063 | end = grafsOut.length; 1064 | for (var i = 0; i < end; i++) { 1065 | // if this is a marker for an html block... 1066 | while (grafsOut[i].search(/~K(\d+)K/) >= 0) { 1067 | var blockText = g_html_blocks[RegExp.$1]; 1068 | blockText = blockText.replace(/\$/g, "$$$$"); // Escape any dollar signs 1069 | grafsOut[i] = grafsOut[i].replace(/~K\d+K/, blockText); 1070 | } 1071 | } 1072 | 1073 | return grafsOut.join("\n\n"); 1074 | } 1075 | 1076 | 1077 | var _EncodeAmpsAndAngles = function (text) { 1078 | // Smart processing for ampersands and angle brackets that need to be encoded. 1079 | // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: 1080 | // http://bumppo.net/projects/amputator/ 1081 | text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&"); 1082 | 1083 | // Encode naked <'s 1084 | text = text.replace(/<(?![a-z\/?\$!])/gi, "<"); 1085 | 1086 | // Encode "smart" quotes 1087 | text = text.replace(/‘/g, "‘"); 1088 | text = text.replace(/’/g, "’"); 1089 | text = text.replace(/“/g, "“"); 1090 | text = text.replace(/”/g, "”"); 1091 | text = text.replace(/–/g, "—"); 1092 | 1093 | 1094 | return text; 1095 | } 1096 | 1097 | 1098 | var _EncodeBackslashEscapes = function (text) { 1099 | // 1100 | // Parameter: String. 1101 | // Returns: The string, with after processing the following backslash 1102 | // escape sequences. 1103 | // 1104 | // attacklab: The polite way to do this is with the new 1105 | // escapeCharacters() function: 1106 | // 1107 | // text = escapeCharacters(text,"\\",true); 1108 | // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); 1109 | // 1110 | // ...but we're sidestepping its use of the (slow) RegExp constructor 1111 | // as an optimization for Firefox. This function gets called a LOT. 1112 | text = text.replace(/\\(\\)/g, escapeCharacters_callback); 1113 | text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, escapeCharacters_callback); 1114 | return text; 1115 | } 1116 | 1117 | 1118 | var _DoAutoLinks = function (text) { 1119 | 1120 | text = text.replace(/(?:")<((https?|ftp|dict):[^'">\s]+)>/gi, "
    $1"); 1121 | 1122 | // Email addresses: 1123 | /* 1124 | text = text.replace(/ 1125 | < 1126 | (?:mailto:)? 1127 | ( 1128 | [-.\w]+ 1129 | \@ 1130 | [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ 1131 | ) 1132 | > 1133 | /gi, _DoAutoLinks_callback()); 1134 | */ 1135 | text = text.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi, function (wholeMatch, m1) { 1136 | return _EncodeEmailAddress(_UnescapeSpecialChars(m1)); 1137 | }); 1138 | 1139 | return text; 1140 | } 1141 | 1142 | 1143 | var _EncodeEmailAddress = function (addr) { 1144 | // 1145 | // Input: an email address, e.g. "foo@example.com" 1146 | // 1147 | // Output: the email address as a mailto link, with each character 1148 | // of the address encoded as either a decimal or hex entity, in 1149 | // the hopes of foiling most address harvesting spam bots. E.g.: 1150 | // 1151 | // foo 1153 | // @example.com 1154 | // 1155 | // Based on a filter by Matthew Wickline, posted to the BBEdit-Talk 1156 | // mailing list: 1157 | // 1158 | // attacklab: why can't javascript speak hex? 1159 | 1160 | 1161 | function char2hex(ch) { 1162 | var hexDigits = '0123456789ABCDEF'; 1163 | var dec = ch.charCodeAt(0); 1164 | return (hexDigits.charAt(dec >> 4) + hexDigits.charAt(dec & 15)); 1165 | } 1166 | 1167 | var encode = [ 1168 | function (ch) { 1169 | return "&#" + ch.charCodeAt(0) + ";";}, 1170 | function (ch) { 1171 | return "&#x" + char2hex(ch) + ";";}, 1172 | function (ch) { 1173 | return ch;} 1174 | ]; 1175 | 1176 | addr = "mailto:" + addr; 1177 | 1178 | addr = addr.replace(/./g, function (ch) { 1179 | if (ch == "@") { 1180 | // this *must* be encoded. I insist. 1181 | ch = encode[Math.floor(Math.random() * 2)](ch); 1182 | } else if (ch != ":") { 1183 | // leave ':' alone (to spot mailto: later) 1184 | var r = Math.random(); 1185 | // roughly 10% raw, 45% hex, 45% dec 1186 | ch = ( 1187 | r > .9 ? encode[2](ch) : r > .45 ? encode[1](ch) : encode[0](ch)); 1188 | } 1189 | return ch; 1190 | }); 1191 | 1192 | addr = "" + addr + ""; 1193 | addr = addr.replace(/">.+:/g, "\">"); // strip the mailto: from the visible part 1194 | return addr; 1195 | } 1196 | 1197 | 1198 | var _UnescapeSpecialChars = function (text) { 1199 | // 1200 | // Swap back in all the special characters we've hidden. 1201 | // 1202 | text = text.replace(/~E(\d+)E/g, function (wholeMatch, m1) { 1203 | var charCodeToReplace = parseInt(m1); 1204 | return String.fromCharCode(charCodeToReplace); 1205 | }); 1206 | return text; 1207 | } 1208 | 1209 | 1210 | var _Outdent = function (text) { 1211 | // 1212 | // Remove one level of line-leading tabs or spaces 1213 | // 1214 | // attacklab: hack around Konqueror 3.5.4 bug: 1215 | // "----------bug".replace(/^-/g,"") == "bug" 1216 | text = text.replace(/^(\t|[ ]{1,4})/gm, "~0"); // attacklab: g_tab_width 1217 | // attacklab: clean up hack 1218 | text = text.replace(/~0/g, "") 1219 | 1220 | return text; 1221 | } 1222 | 1223 | var _Detab = function (text) { 1224 | // attacklab: Detab's completely rewritten for speed. 1225 | // In perl we could fix it by anchoring the regexp with \G. 1226 | // In javascript we're less fortunate. 1227 | // expand first n-1 tabs 1228 | text = text.replace(/\t(?=\t)/g, " "); // attacklab: g_tab_width 1229 | // replace the nth with two sentinels 1230 | text = text.replace(/\t/g, "~A~B"); 1231 | 1232 | // use the sentinel to anchor our regex so it doesn't explode 1233 | text = text.replace(/~B(.+?)~A/g, function (wholeMatch, m1, m2) { 1234 | var leadingText = m1; 1235 | var numSpaces = 4 - leadingText.length % 4; // attacklab: g_tab_width 1236 | // there *must* be a better way to do this: 1237 | for (var i = 0; i < numSpaces; i++) leadingText += " "; 1238 | 1239 | return leadingText; 1240 | }); 1241 | 1242 | // clean up sentinels 1243 | text = text.replace(/~A/g, " "); // attacklab: g_tab_width 1244 | text = text.replace(/~B/g, ""); 1245 | 1246 | return text; 1247 | } 1248 | 1249 | 1250 | // 1251 | // attacklab: Utility functions 1252 | // 1253 | 1254 | var escapeCharacters = function (text, charsToEscape, afterBackslash) { 1255 | // First we have to escape the escape characters so that 1256 | // we can build a character class out of them 1257 | var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g, "\\$1") + "])"; 1258 | 1259 | if (afterBackslash) { 1260 | regexString = "\\\\" + regexString; 1261 | } 1262 | 1263 | var regex = new RegExp(regexString, "g"); 1264 | text = text.replace(regex, escapeCharacters_callback); 1265 | 1266 | return text; 1267 | } 1268 | 1269 | 1270 | var escapeCharacters_callback = function (wholeMatch, m1) { 1271 | var charCodeToEscape = m1.charCodeAt(0); 1272 | return "~E" + charCodeToEscape + "E"; 1273 | } 1274 | 1275 | } // end of Attacklab.showdown.converter 1276 | 1277 | // Version 0.9 used the Showdown namespace instead of Attacklab.showdown 1278 | // The old namespace is deprecated, but we'll support it for now: 1279 | var Showdown = Attacklab.showdown; 1280 | 1281 | // If anyone's interested, tell the world that this file's been loaded 1282 | if (Attacklab.fileLoaded) { 1283 | Attacklab.fileLoaded("showdown.js"); 1284 | } -------------------------------------------------------------------------------- /static/js/wmd.js: -------------------------------------------------------------------------------- 1 | ; 2 | (function () { 3 | 4 | WMDEditor = function (options) { 5 | this.options = WMDEditor.util.extend({}, WMDEditor.defaults, options || {}); 6 | wmdBase(this, this.options); 7 | 8 | this.startEditor(); 9 | }; 10 | window.WMDEditor = WMDEditor; 11 | 12 | WMDEditor.defaults = { // {{{ 13 | version: 2.0, 14 | output_format: "markdown", 15 | lineLength: 40, 16 | 17 | button_bar: "wmd-button-bar", 18 | preview: "wmd-preview", 19 | output: "wmd-output", 20 | input: "wmd-input", 21 | 22 | // The text that appears on the upper part of the dialog box when 23 | // entering links. 24 | imageDialogText: "

    Enter the image URL.

    " + "

    You can also add a title, which will be displayed as a tool tip.

    " + "

    Example:
    http://i.imgur.com/1cZl4.jpg \"Optional title\"

    ", 25 | linkDialogText: "

    Enter the web address.

    " + "

    You can also add a title, which will be displayed as a tool tip.

    " + "

    Example:
    http://www.google.com/ \"Optional title\"

    ", 26 | 27 | // The default text that appears in the dialog input box when entering 28 | // links. 29 | imageDefaultText: "http://", 30 | linkDefaultText: "http://", 31 | imageDirectory: "images/", 32 | 33 | // The link and title for the help button 34 | helpLink: "/wmd/markdownhelp.html", 35 | helpHoverTitle: "Markdown Syntax", 36 | helpTarget: "_blank", 37 | 38 | // Some intervals in ms. These can be adjusted to reduce the control's load. 39 | previewPollInterval: 500, 40 | pastePollInterval: 100, 41 | 42 | buttons: "bold italic link blockquote code image ol ul heading hr", 43 | 44 | tagFilter: { 45 | enabled: true, 46 | allowedTags: /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i, 47 | patternLink: /^(]+")?\s?>|<\/a>)$/i, 48 | patternImage: /^(]*")?(\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
    
     940 | 				else {
     941 | 					var newText = text.replace(/&/g, "&");
     942 | 					newText = newText.replace(/
    "; 944 | } 945 | } 946 | 947 | if (wmd.panels.preview) { 948 | // original WMD code allowed javascript injection, like this: 949 | // 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