%s
' % s.replace('&', '&').replace('<', '<').replace('>', '>'), filter(lambda s: s.strip() != '', text.split('\n'))) + return ''.join(lines) + +@asyncio.coroutine +def cookie2user(cookie_str): + ''' + Parse cookie and load user if cookie is valid. + ''' + if not cookie_str: + return None + try: + L = cookie_str.split('-') + if len(L) != 3: + return None + uid, expires, sha1 = L + if int(expires) < time.time(): + return None + user = yield from User.find(uid) + if user is None: + return None + s = '%s-%s-%s-%s' % (uid, user.passwd, expires, _COOKIE_KEY) + if sha1 != hashlib.sha1(s.encode('utf-8')).hexdigest(): + logging.info('invalid sha1') + return None + user.passwd = '******' + return user + except Exception as e: + logging.exception(e) + return None + +@get('/') +def index(*, page='1'): + page_index = get_page_index(page) + num = yield from Blog.findNumber('count(id)') + page = Page(num) + if num == 0: + blogs = [] + else: + blogs = yield from Blog.findAll(orderBy='created_at desc', limit=(page.offset, page.limit)) + return { + '__template__': 'blogs.html', + 'page': page, + 'blogs': blogs + } + +@get('/blog/{id}') +def get_blog(id): + blog = yield from Blog.find(id) + comments = yield from Comment.findAll('blog_id=?', [id], orderBy='created_at desc') + for c in comments: + c.html_content = text2html(c.content) + blog.html_content = markdown2.markdown(blog.content) + return { + '__template__': 'blog.html', + 'blog': blog, + 'comments': comments + } + +@get('/register') +def register(): + return { + '__template__': 'register.html' + } + +@get('/signin') +def signin(): + return { + '__template__': 'signin.html' + } + +@post('/api/authenticate') +def authenticate(*, email, passwd): + if not email: + raise APIValueError('email', 'Invalid email.') + if not passwd: + raise APIValueError('passwd', 'Invalid password.') + users = yield from User.findAll('email=?', [email]) + if len(users) == 0: + raise APIValueError('email', 'Email not exist.') + user = users[0] + # check passwd: + sha1 = hashlib.sha1() + sha1.update(user.id.encode('utf-8')) + sha1.update(b':') + sha1.update(passwd.encode('utf-8')) + if user.passwd != sha1.hexdigest(): + raise APIValueError('passwd', 'Invalid password.') + # authenticate ok, set cookie: + r = web.Response() + r.set_cookie(COOKIE_NAME, user2cookie(user, 86400), max_age=86400, httponly=True) + user.passwd = '******' + r.content_type = 'application/json' + r.body = json.dumps(user, ensure_ascii=False).encode('utf-8') + return r + +@get('/signout') +def signout(request): + referer = request.headers.get('Referer') + r = web.HTTPFound(referer or '/') + r.set_cookie(COOKIE_NAME, '-deleted-', max_age=0, httponly=True) + logging.info('user signed out.') + return r + +@get('/manage/') +def manage(): + return 'redirect:/manage/comments' + +@get('/manage/comments') +def manage_comments(*, page='1'): + return { + '__template__': 'manage_comments.html', + 'page_index': get_page_index(page) + } + +@get('/manage/blogs') +def manage_blogs(*, page='1'): + return { + '__template__': 'manage_blogs.html', + 'page_index': get_page_index(page) + } + +@get('/manage/blogs/create') +def manage_create_blog(): + return { + '__template__': 'manage_blog_edit.html', + 'id': '', + 'action': '/api/blogs' + } + +@get('/manage/blogs/edit') +def manage_edit_blog(*, id): + return { + '__template__': 'manage_blog_edit.html', + 'id': id, + 'action': '/api/blogs/%s' % id + } + +@get('/manage/users') +def manage_users(*, page='1'): + return { + '__template__': 'manage_users.html', + 'page_index': get_page_index(page) + } + +@get('/api/comments') +def api_comments(*, page='1'): + page_index = get_page_index(page) + num = yield from Comment.findNumber('count(id)') + p = Page(num, page_index) + if num == 0: + return dict(page=p, comments=()) + comments = yield from Comment.findAll(orderBy='created_at desc', limit=(p.offset, p.limit)) + return dict(page=p, comments=comments) + +@post('/api/blogs/{id}/comments') +def api_create_comment(id, request, *, content): + user = request.__user__ + if user is None: + raise APIPermissionError('Please signin first.') + if not content or not content.strip(): + raise APIValueError('content') + blog = yield from Blog.find(id) + if blog is None: + raise APIResourceNotFoundError('Blog') + comment = Comment(blog_id=blog.id, user_id=user.id, user_name=user.name, user_image=user.image, content=content.strip()) + yield from comment.save() + return comment + +@post('/api/comments/{id}/delete') +def api_delete_comments(id, request): + check_admin(request) + c = yield from Comment.find(id) + if c is None: + raise APIResourceNotFoundError('Comment') + yield from c.remove() + return dict(id=id) + +@get('/api/users') +def api_get_users(*, page='1'): + page_index = get_page_index(page) + num = yield from User.findNumber('count(id)') + p = Page(num, page_index) + if num == 0: + return dict(page=p, users=()) + users = yield from User.findAll(orderBy='created_at desc', limit=(p.offset, p.limit)) + for u in users: + u.passwd = '******' + return dict(page=p, users=users) + +_RE_EMAIL = re.compile(r'^[a-z0-9\.\-\_]+\@[a-z0-9\-\_]+(\.[a-z0-9\-\_]+){1,4}$') +_RE_SHA1 = re.compile(r'^[0-9a-f]{40}$') + +@post('/api/users') +def api_register_user(*, email, name, passwd): + if not name or not name.strip(): + raise APIValueError('name') + if not email or not _RE_EMAIL.match(email): + raise APIValueError('email') + if not passwd or not _RE_SHA1.match(passwd): + raise APIValueError('passwd') + users = yield from User.findAll('email=?', [email]) + if len(users) > 0: + raise APIError('register:failed', 'email', 'Email is already in use.') + uid = next_id() + sha1_passwd = '%s:%s' % (uid, passwd) + user = User(id=uid, name=name.strip(), email=email, passwd=hashlib.sha1(sha1_passwd.encode('utf-8')).hexdigest(), image='http://www.gravatar.com/avatar/%s?d=mm&s=120' % hashlib.md5(email.encode('utf-8')).hexdigest()) + yield from user.save() + # make session cookie: + r = web.Response() + r.set_cookie(COOKIE_NAME, user2cookie(user, 86400), max_age=86400, httponly=True) + user.passwd = '******' + r.content_type = 'application/json' + r.body = json.dumps(user, ensure_ascii=False).encode('utf-8') + return r + +@get('/api/blogs') +def api_blogs(*, page='1'): + page_index = get_page_index(page) + num = yield from Blog.findNumber('count(id)') + p = Page(num, page_index) + if num == 0: + return dict(page=p, blogs=()) + blogs = yield from Blog.findAll(orderBy='created_at desc', limit=(p.offset, p.limit)) + return dict(page=p, blogs=blogs) + +@get('/api/blogs/{id}') +def api_get_blog(*, id): + blog = yield from Blog.find(id) + return blog + +@post('/api/blogs') +def api_create_blog(request, *, name, summary, content): + check_admin(request) + if not name or not name.strip(): + raise APIValueError('name', 'name cannot be empty.') + if not summary or not summary.strip(): + raise APIValueError('summary', 'summary cannot be empty.') + if not content or not content.strip(): + raise APIValueError('content', 'content cannot be empty.') + blog = Blog(user_id=request.__user__.id, user_name=request.__user__.name, user_image=request.__user__.image, name=name.strip(), summary=summary.strip(), content=content.strip()) + yield from blog.save() + return blog + +@post('/api/blogs/{id}') +def api_update_blog(id, request, *, name, summary, content): + check_admin(request) + blog = yield from Blog.find(id) + if not name or not name.strip(): + raise APIValueError('name', 'name cannot be empty.') + if not summary or not summary.strip(): + raise APIValueError('summary', 'summary cannot be empty.') + if not content or not content.strip(): + raise APIValueError('content', 'content cannot be empty.') + blog.name = name.strip() + blog.summary = summary.strip() + blog.content = content.strip() + yield from blog.update() + return blog + +@post('/api/blogs/{id}/delete') +def api_delete_blog(request, *, id): + check_admin(request) + blog = yield from Blog.find(id) + yield from blog.remove() + return dict(id=id) diff --git a/pythonweb/www/markdown2.py b/pythonweb/www/markdown2.py new file mode 100644 index 0000000..7b7307b --- /dev/null +++ b/pythonweb/www/markdown2.py @@ -0,0 +1,2440 @@ +# python3 by 121 +# Copyright (c) 2012 Trent Mick. +# Copyright (c) 2007-2008 ActiveState Corp. +# License: MIT (http://www.opensource.org/licenses/mit-license.php) + +from __future__ import generators + +r"""A fast and complete Python implementation of Markdown. + +[from http://daringfireball.net/projects/markdown/] +> Markdown is a text-to-HTML filter; it translates an easy-to-read / +> easy-to-write structured text format into HTML. Markdown's text +> format is most similar to that of plain text email, and supports +> features such as headers, *emphasis*, code blocks, blockquotes, and +> links. +> +> Markdown's syntax is designed not as a generic markup language, but +> specifically to serve as a front-end to (X)HTML. You can use span-level +> HTML tags anywhere in a Markdown document, and you can use block level +> HTML tags (like tags.
+ """
+ yield 0, ""
+ for tup in inner:
+ yield tup
+ yield 0, "
"
+
+ def wrap(self, source, outfile):
+ """Return the source with a code, pre, and div."""
+ return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
+
+ formatter_opts.setdefault("cssclass", "codehilite")
+ formatter = HtmlCodeFormatter(**formatter_opts)
+ return pygments.highlight(codeblock, lexer, formatter)
+
+ def _code_block_sub(self, match, is_fenced_code_block=False):
+ lexer_name = None
+ if is_fenced_code_block:
+ lexer_name = match.group(1)
+ if lexer_name:
+ formatter_opts = self.extras['fenced-code-blocks'] or {}
+ codeblock = match.group(2)
+ codeblock = codeblock[:-1] # drop one trailing newline
+ else:
+ codeblock = match.group(1)
+ codeblock = self._outdent(codeblock)
+ codeblock = self._detab(codeblock)
+ codeblock = codeblock.lstrip('\n') # trim leading newlines
+ codeblock = codeblock.rstrip() # trim trailing whitespace
+
+ # Note: "code-color" extra is DEPRECATED.
+ if "code-color" in self.extras and codeblock.startswith(":::"):
+ lexer_name, rest = codeblock.split('\n', 1)
+ lexer_name = lexer_name[3:].strip()
+ codeblock = rest.lstrip("\n") # Remove lexer declaration line.
+ formatter_opts = self.extras['code-color'] or {}
+
+ if lexer_name:
+ def unhash_code( codeblock ):
+ for key, sanitized in list(self.html_spans.items()):
+ codeblock = codeblock.replace(key, sanitized)
+ replacements = [
+ ("&", "&"),
+ ("<", "<"),
+ (">", ">")
+ ]
+ for old, new in replacements:
+ codeblock = codeblock.replace(old, new)
+ return codeblock
+ lexer = self._get_pygments_lexer(lexer_name)
+ if lexer:
+ codeblock = unhash_code( codeblock )
+ colored = self._color_with_pygments(codeblock, lexer,
+ **formatter_opts)
+ return "\n\n%s\n\n" % colored
+
+ codeblock = self._encode_code(codeblock)
+ pre_class_str = self._html_class_str_from_tag("pre")
+ code_class_str = self._html_class_str_from_tag("code")
+ return "\n\n%s\n
\n\n" % (
+ pre_class_str, code_class_str, codeblock)
+
+ def _html_class_str_from_tag(self, tag):
+ """Get the appropriate ' class="..."' string (note the leading
+ space), if any, for the given tag.
+ """
+ if "html-classes" not in self.extras:
+ return ""
+ try:
+ html_classes_from_tag = self.extras["html-classes"]
+ except TypeError:
+ return ""
+ else:
+ if tag in html_classes_from_tag:
+ return ' class="%s"' % html_classes_from_tag[tag]
+ return ""
+
+ def _do_code_blocks(self, text):
+ """Process Markdown `` blocks."""
+ code_block_re = re.compile(r'''
+ (?:\n\n|\A\n?)
+ ( # $1 = the code block -- one or more lines, starting with a space/tab
+ (?:
+ (?:[ ]{%d} | \t) # Lines must start with a tab or a tab-width of spaces
+ .*\n+
+ )+
+ )
+ ((?=^[ ]{0,%d}\S)|\Z) # Lookahead for non-space at line-start, or end of doc
+ # Lookahead to make sure this block isn't already in a code block.
+ # Needed when syntax highlighting is being used.
+ (?![^<]*\
)
+ ''' % (self.tab_width, self.tab_width),
+ re.M | re.X)
+ return code_block_re.sub(self._code_block_sub, text)
+
+ _fenced_code_block_re = re.compile(r'''
+ (?:\n\n|\A\n?)
+ ^```([\w+-]+)?[ \t]*\n # opening fence, $1 = optional lang
+ (.*?) # $2 = code block content
+ ^```[ \t]*\n # closing fence
+ ''', re.M | re.X | re.S)
+
+ def _fenced_code_block_sub(self, match):
+ return self._code_block_sub(match, is_fenced_code_block=True);
+
+ def _do_fenced_code_blocks(self, text):
+ """Process ```-fenced unindented code blocks ('fenced-code-blocks' extra)."""
+ return self._fenced_code_block_re.sub(self._fenced_code_block_sub, text)
+
+ # Rules for a code span:
+ # - backslash escapes are not interpreted in a code span
+ # - to include one or or a run of more backticks the delimiters must
+ # be a longer run of backticks
+ # - cannot start or end a code span with a backtick; pad with a
+ # space and that space will be removed in the emitted HTML
+ # See `test/tm-cases/escapes.text` for a number of edge-case
+ # examples.
+ _code_span_re = re.compile(r'''
+ (?%s
" % c
+
+ def _do_code_spans(self, text):
+ # * Backtick quotes are used for
spans.
+ #
+ # * You can use multiple backticks as the delimiters if you want to
+ # include literal backticks in the code span. So, this input:
+ #
+ # Just type ``foo `bar` baz`` at the prompt.
+ #
+ # Will translate to:
+ #
+ # Just type foo `bar` baz
at the prompt.
`bar`
...
+ return self._code_span_re.sub(self._code_span_sub, text)
+
+ def _encode_code(self, text):
+ """Encode/escape certain characters inside Markdown code runs.
+ The point is that in code, these characters are literals,
+ and lose their special Markdown meanings.
+ """
+ replacements = [
+ # Encode all ampersands; HTML entities are not
+ # entities within a Markdown code span.
+ ('&', '&'),
+ # Do the angle bracket song and dance:
+ ('<', '<'),
+ ('>', '>'),
+ ]
+ for before, after in replacements:
+ text = text.replace(before, after)
+ hashed = _hash_text(text)
+ self._escape_table[text] = hashed
+ return hashed
+
+ _strong_re = re.compile(r"(\*\*|__)(?=\S)(.+?[*_]*)(?<=\S)\1", re.S)
+ _em_re = re.compile(r"(\*|_)(?=\S)(.+?)(?<=\S)\1", re.S)
+ _code_friendly_strong_re = re.compile(r"\*\*(?=\S)(.+?[*_]*)(?<=\S)\*\*", re.S)
+ _code_friendly_em_re = re.compile(r"\*(?=\S)(.+?)(?<=\S)\*", re.S)
+ def _do_italics_and_bold(self, text):
+ # must go first:
+ if "code-friendly" in self.extras:
+ text = self._code_friendly_strong_re.sub(r"\1", text)
+ text = self._code_friendly_em_re.sub(r"\1", text)
+ else:
+ text = self._strong_re.sub(r"\2", text)
+ text = self._em_re.sub(r"\2", text)
+ return text
+
+ # "smarty-pants" extra: Very liberal in interpreting a single prime as an
+ # apostrophe; e.g. ignores the fact that "round", "bout", "twer", and
+ # "twixt" can be written without an initial apostrophe. This is fine because
+ # using scare quotes (single quotation marks) is rare.
+ _apostrophe_year_re = re.compile(r"'(\d\d)(?=(\s|,|;|\.|\?|!|$))")
+ _contractions = ["tis", "twas", "twer", "neath", "o", "n",
+ "round", "bout", "twixt", "nuff", "fraid", "sup"]
+ def _do_smart_contractions(self, text):
+ text = self._apostrophe_year_re.sub(r"’\1", text)
+ for c in self._contractions:
+ text = text.replace("'%s" % c, "’%s" % c)
+ text = text.replace("'%s" % c.capitalize(),
+ "’%s" % c.capitalize())
+ return text
+
+ # Substitute double-quotes before single-quotes.
+ _opening_single_quote_re = re.compile(r"(?
+ See "test/tm-cases/smarty_pants.text" for a full discussion of the
+ support here and
+ .+?)', re.S) + def _dedent_two_spaces_sub(self, match): + return re.sub(r'(?m)^ ', '', match.group(1)) + + def _block_quote_sub(self, match): + bq = match.group(1) + bq = self._bq_one_level_re.sub('', bq) # trim one level of quoting + bq = self._ws_only_line_re.sub('', bq) # trim whitespace-only lines + bq = self._run_block_gamut(bq) # recurse + + bq = re.sub('(?m)^', ' ', bq) + # These leading spaces screw with
content, so we need to fix that: + bq = self._html_pre_block_re.sub(self._dedent_two_spaces_sub, bq) + + return "\n%s\n\n\n" % bq + + def _do_block_quotes(self, text): + if '>' not in text: + return text + return self._block_quote_re.sub(self._block_quote_sub, text) + + def _form_paragraphs(self, text): + # Strip leading and trailing lines: + text = text.strip('\n') + + # Wraptags. + grafs = [] + for i, graf in enumerate(re.split(r"\n{2,}", text)): + if graf in self.html_blocks: + # Unhashify HTML blocks + grafs.append(self.html_blocks[graf]) + else: + cuddled_list = None + if "cuddled-lists" in self.extras: + # Need to put back trailing '\n' for `_list_item_re` + # match at the end of the paragraph. + li = self._list_item_re.search(graf + '\n') + # Two of the same list marker in this paragraph: a likely + # candidate for a list cuddled to preceding paragraph + # text (issue 33). Note the `[-1]` is a quick way to + # consider numeric bullets (e.g. "1." and "2.") to be + # equal. + if (li and len(li.group(2)) <= 3 and li.group("next_marker") + and li.group("marker")[-1] == li.group("next_marker")[-1]): + start = li.start() + cuddled_list = self._do_lists(graf[start:]).rstrip("\n") + assert cuddled_list.startswith("
tags. + graf = self._run_span_gamut(graf) + grafs.append("
" + graf.lstrip(" \t") + "
") + + if cuddled_list: + grafs.append(cuddled_list) + + return "\n\n".join(grafs) + + def _add_footnotes(self, text): + if self.footnotes: + footer = [ + '%s
" % backlink) + footer.append('vA9XxDLyUk5nmBs6~80?xA;He-^DJ8RN^C1NybWMO6ExxOV&s>OP-SKlxQUu
zNxCEtRJdwMgQQb(MDmQ}tmIiqujCEMHOY0!H &+03P}IdMxon^wJ+EegJG^7B0Xxyc%CLKZ^bQ;6Uhr6Dl5U
z*PMIqT+i`;$Qlk-w;v`8L*z602~b(lJVNvDvqSXW2=x9Z55$h2lomT!MMg D!^kV3b{%$a5Pj}W>TLSREi+|z+V9Zm`XGsJRsdT*M=Y9`QpK>
zGvpy0%tpYX>9{W*C<9C$!EYJTYomDNxjK=7O=OH(cw0=>GoV^1E(|Wrsf?ChnbAl)
z4+a-1JOaH|k`s$*qe`2&aNAOFFaeOEj=Mtj1rmFKATL9vT!#%fb36t-f-K!nW=@Bx
zQv&X)Cb|1|XFuvZq>JsB#)PveQe{;jxBiN^8{5K0jUrRqVzDg~18#Ciz@>FQUv
zymy!
z&*Od810Fl&u{>a&NYRqnoKmjF>yBohOh1`&!vECeGZ#-?l2ulhSKE~}#We+0>ac&U
zetlbytST=DEOI$HMPT2?V*?FMarLpa{zkN(ZYfS}NLFDp%px7`o%{7?Ua9oUL)qyK{_Ai_VIOP#S7N&Z?ckpe>SiZNU9u
zm_q=i4bJZ5(sVGj!PB!f7mo=XL{82L5inMgk&7V{T*SK~8Nwgw=%`(Z+g00lwVjUA
zU=<3WUD{k?Dq6tekKu^y$hJ1`S7AGt=)v}92iHh2woB0rmiQX{&
!A_yRcap%RV1Aj6_&7Kx;2d?
BieX4oF0#rORpS2BDwoUT1t*y&<5l|L
z6PbO#Ve63PCayBPXnBxIzSa7(#u8(Wjs~D7LLN(dTgeQ}LdC^eMi4!E1ZxtAm`1
z1~D4Fj@;$=bBFla`kMM3JAZz-nY`Nap_FK
g6hQci^?554dATb{-)j(lvyL)qjwGIrcmNyA&2j9QlLX#>zGk0YGw8Y0t7}
z+PSpKrBzXR^BU&X&u^5LYzx}8W!6yo_5yY2rrM%#o=*P_5TfpV$aHB!P1v68r^wsi
zT~yTvH^kL(o6l@H7j!ncBI0PIU5a>aR+@U_l(_iK{L;vv`C;!$gXTofeoHlI-^ltA
zT-B`Yb9QUn=r{!HR+Diroen%7dND$}<<__Be^h^bp}gTdf2j6ML*-FvabwA+ds(pZ
zfy~tgkh^zYV6#uF7?F{H%UG1<8ZS
$HTEJEHKdNz_1?2GmfhJ^ib)KLJIiLyuCzkL(
zNJ1tz%g!(R$I_4<46OoeLv98Vp<>1+hC$Vq&FDtl4!uQ5EAy})F6=!V^wt0GqI6g8gRupETL01|9su9kc>Vt>5EXVy`rPy
zlCwhc#r6}eH&jf|89ZbMQX=52G-E#<7J;4Y672$jH&vWR-#sN2Tn++KO1pN2hA~ng
z!2X)%?>CPX?q((GEuc^A($1B2wlHl)qWfF9-O=K$1n#XnJ;Pg6dIn>smvW3TkGmVY
zwhqIj3lqXqdiwvm(f`lauV9u$W2kQR6=J%Hm?%2Iy8y_T(VLlj;e>k;1NVaU_Pp$S
zhET$!PZU3Sfq!Jde|H=NY3bxaAlkP#f93HOf)IPwzAlrei5iH5xe0E@%JC5T?*qFC
zuriYZ0ARO63Sa>IsRWr^2KV}DnLJ~P;Ap^rLvKJV53NV009CDMGom8!j5>LH1^_kO
z5zicfD2!JXf-Oy$jO5NrL}Nz&9gWGh0o!V2(HI~3pC_$3`8l?1DH)2>$?PClWC~}1
zQT7ocuJE3kmDn2^X6$;RtstXsTIz|;{CUz7o(T(!TDnPv%VuZD9xM`K+7q-Q1pDz2
z+fbI>6R7dNCMYxjwF;-hyI^7j9q=4$Fg*m^XMM!nAmF(2KlLBU@UDuzf}yDExE=A)
zV?~dk2bu;kMh=;9+}{7VB?H(k*(xDz?3N6|n+6YkJgWhdr6b7mKhZXHX9CXhM*IO-
zGApZrHn(uJt%2%VL^B{tgjxOynWh;4(!F>_Pz$m)@*8+bwL~WxAPx$GJZ3`>QKU+!
zHe7TNHgLEol`4XQs$>m8B6;I|F%G5^L2Wt!dt+V{-$!dxnFLdt2=8?*q^&^&p^2=9
zEDuN?7fp8!D=&bsi2}Z6{Kl+t>dDZXWK|;e=@8w){xni2SO=8nsg)_PX)V&MEkRHS20c_`fo_Jhp&y!+(n|
z+GdW_`$p&!Bf?d%AHxeHs`Ol?zRp};gte*Fr?eoiyix@fa2<@m$Ee}s(k_+ZpXRZa
zrR>mEcKb!c