From 23ed372d644bcce90e2104344fba0fa991eb3472 Mon Sep 17 00:00:00 2001 From: endorphant Date: Sun, 15 May 2016 01:19:44 -0400 Subject: [PATCH] incremented to v0.8.0 added markdown parsing for entries --- bin/_ttbp.py | 3 + bin/core.py | 18 +- bin/mistune.py | 1154 ++++++++++++++++++++++++++++++++++++++++++++++++ bin/ttbp.py | 3 + changelog.txt | 4 +- 5 files changed, 1173 insertions(+), 9 deletions(-) create mode 100644 bin/mistune.py diff --git a/bin/_ttbp.py b/bin/_ttbp.py index 596c92c..50200b0 100644 --- a/bin/_ttbp.py +++ b/bin/_ttbp.py @@ -416,6 +416,9 @@ thanks to everyone who reads, listens, writes, and feels.\ def write_entry(entry=os.path.join(DATA, "test.txt")): entered = raw_input(""" +"""+util.hilight("new feature!")+""" you can now use standard markdown in +your entry text! raw html is still valid, and you can mix them together. + feels will be recorded for today, """+time.strftime("%d %B %Y")+""". if you've already started recording feels for this day, you diff --git a/bin/core.py b/bin/core.py index eb6d353..e768474 100644 --- a/bin/core.py +++ b/bin/core.py @@ -4,6 +4,7 @@ import os import time import subprocess import re +import mistune import chatter @@ -50,7 +51,6 @@ def load_files(): FILES = [] for filename in os.listdir(DATA): filename = os.path.join(DATA, filename) - #if os.path.isfile(filename) and os.path.splitext(filename)[1] == ".txt": if os.path.isfile(filename) and valid(filename): FILES.append(filename) @@ -114,8 +114,8 @@ def write_entry(filename): entry = [ "\t\t



\n", "\t\t
\n", - "\t\t\t
"+date[2]+" "+MONTHS[date[1]]+" "+date[0]+"
\n", - "\t\t\t

" + "\t\t\t

"+date[2]+" "+MONTHS[date[1]]+" "+date[0]+"
\n" + #"\t\t\t

" ] raw = [] @@ -125,12 +125,14 @@ def write_entry(filename): raw.append(line) rawfile.close() - for line in raw: - entry.append(line+"\t\t\t") - if line == "\n": - entry.append("

\n\t\t\t

") + entry.append("\t\t\t"+mistune.markdown("".join(raw), escape=False, hard_wrap=False)) - entry.append("

\n") + #for line in raw: + #entry.append(line+"\t\t\t") + #if line == "\n": + # entry.append("

\n\t\t\t

") + + #entry.append("

\n") entry.append("\t\t\t

permalink

\n") entry.append("\n\t\t
\n") diff --git a/bin/mistune.py b/bin/mistune.py new file mode 100644 index 0000000..3c82c8e --- /dev/null +++ b/bin/mistune.py @@ -0,0 +1,1154 @@ +# coding: utf-8 +""" + mistune + ~~~~~~~ + + The fastest markdown parser in pure Python with renderer feature. + + :copyright: (c) 2014 - 2015 by Hsiaoming Yang. +""" + +import re +import inspect + +__version__ = '0.7.2' +__author__ = 'Hsiaoming Yang ' +__all__ = [ + 'BlockGrammar', 'BlockLexer', + 'InlineGrammar', 'InlineLexer', + 'Renderer', 'Markdown', + 'markdown', 'escape', +] + + +_key_pattern = re.compile(r'\s+') +_nonalpha_pattern = re.compile(r'\W') +_escape_pattern = re.compile(r'&(?!#?\w+;)') +_newline_pattern = re.compile(r'\r\n|\r') +_block_quote_leading_pattern = re.compile(r'^ *> ?', flags=re.M) +_block_code_leading_pattern = re.compile(r'^ {4}', re.M) +_inline_tags = [ + 'a', 'em', 'strong', 'small', 's', 'cite', 'q', 'dfn', 'abbr', 'data', + 'time', 'code', 'var', 'samp', 'kbd', 'sub', 'sup', 'i', 'b', 'u', 'mark', + 'ruby', 'rt', 'rp', 'bdi', 'bdo', 'span', 'br', 'wbr', 'ins', 'del', + 'img', 'font', +] +_pre_tags = ['pre', 'script', 'style'] +_valid_end = r'(?!:/|[^\w\s@]*@)\b' +_valid_attr = r'''"[^"]*"|'[^']*'|[^'">]''' +_block_tag = r'(?!(?:%s)\b)\w+%s' % ('|'.join(_inline_tags), _valid_end) +_scheme_blacklist = ('javascript', 'data', 'vbscript') + + +def _pure_pattern(regex): + pattern = regex.pattern + if pattern.startswith('^'): + pattern = pattern[1:] + return pattern + + +def _keyify(key): + return _key_pattern.sub(' ', key.lower()) + + +def escape(text, quote=False, smart_amp=True): + """Replace special characters "&", "<" and ">" to HTML-safe sequences. + + The original cgi.escape will always escape "&", but you can control + this one for a smart escape amp. + + :param quote: if set to True, " and ' will be escaped. + :param smart_amp: if set to False, & will always be escaped. + """ + if smart_amp: + text = _escape_pattern.sub('&', text) + else: + text = text.replace('&', '&') + text = text.replace('<', '<') + text = text.replace('>', '>') + if quote: + text = text.replace('"', '"') + text = text.replace("'", ''') + return text + + +def escape_link(url, **kwargs): + """Remove dangerous URL schemes like javascript: and escape afterwards.""" + if ':' in url: + scheme, _ = url.split(':', 1) + scheme = _nonalpha_pattern.sub('', scheme) + # whitelist would be better but mistune's use case is too general + if scheme.lower() in _scheme_blacklist: + return '' + # escape &entities; to &entities; + kwargs['smart_amp'] = False + return escape(url, **kwargs) + + +def preprocessing(text, tab=4): + text = _newline_pattern.sub('\n', text) + text = text.replace('\t', ' ' * tab) + text = text.replace('\u00a0', ' ') + text = text.replace('\u2424', '\n') + pattern = re.compile(r'^ +$', re.M) + return pattern.sub('', text) + + +class BlockGrammar(object): + """Grammars for block level tokens.""" + + def_links = re.compile( + r'^ *\[([^^\]]+)\]: *' # [key]: + r']+)>?' # or link + r'(?: +["(]([^\n]+)[")])? *(?:\n+|$)' + ) + def_footnotes = re.compile( + r'^\[\^([^\]]+)\]: *(' + r'[^\n]*(?:\n+|$)' # [^key]: + r'(?: {1,}[^\n]*(?:\n+|$))*' + r')' + ) + + newline = re.compile(r'^\n+') + block_code = re.compile(r'^( {4}[^\n]+\n*)+') + fences = re.compile( + r'^ *(`{3,}|~{3,}) *(\S+)? *\n' # ```lang + r'([\s\S]+?)\s*' + r'\1 *(?:\n+|$)' # ``` + ) + hrule = re.compile(r'^ {0,3}[-*_](?: *[-*_]){2,} *(?:\n+|$)') + heading = re.compile(r'^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)') + lheading = re.compile(r'^([^\n]+)\n *(=|-)+ *(?:\n+|$)') + block_quote = re.compile(r'^( *>[^\n]+(\n[^\n]+)*\n*)+') + list_block = re.compile( + r'^( *)([*+-]|\d+\.) [\s\S]+?' + r'(?:' + r'\n+(?=\1?(?:[-*_] *){3,}(?:\n+|$))' # hrule + r'|\n+(?=%s)' # def links + r'|\n+(?=%s)' # def footnotes + r'|\n{2,}' + r'(?! )' + r'(?!\1(?:[*+-]|\d+\.) )\n*' + r'|' + r'\s*$)' % ( + _pure_pattern(def_links), + _pure_pattern(def_footnotes), + ) + ) + list_item = re.compile( + r'^(( *)(?:[*+-]|\d+\.) [^\n]*' + r'(?:\n(?!\2(?:[*+-]|\d+\.) )[^\n]*)*)', + flags=re.M + ) + list_bullet = re.compile(r'^ *(?:[*+-]|\d+\.) +') + paragraph = re.compile( + r'^((?:[^\n]+\n?(?!' + r'%s|%s|%s|%s|%s|%s|%s|%s|%s' + r'))+)\n*' % ( + _pure_pattern(fences).replace(r'\1', r'\2'), + _pure_pattern(list_block).replace(r'\1', r'\3'), + _pure_pattern(hrule), + _pure_pattern(heading), + _pure_pattern(lheading), + _pure_pattern(block_quote), + _pure_pattern(def_links), + _pure_pattern(def_footnotes), + '<' + _block_tag, + ) + ) + block_html = re.compile( + r'^ *(?:%s|%s|%s) *(?:\n{2,}|\s*$)' % ( + r'', + r'<(%s)((?:%s)*?)>([\s\S]+?)<\/\1>' % (_block_tag, _valid_attr), + r'<%s(?:%s)*?>' % (_block_tag, _valid_attr), + ) + ) + table = re.compile( + r'^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*' + ) + nptable = re.compile( + r'^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*' + ) + text = re.compile(r'^[^\n]+') + + +class BlockLexer(object): + """Block level lexer for block grammars.""" + grammar_class = BlockGrammar + + default_rules = [ + 'newline', 'hrule', 'block_code', 'fences', 'heading', + 'nptable', 'lheading', 'block_quote', + 'list_block', 'block_html', 'def_links', + 'def_footnotes', 'table', 'paragraph', 'text' + ] + + list_rules = ( + 'newline', 'block_code', 'fences', 'lheading', 'hrule', + 'block_quote', 'list_block', 'block_html', 'text', + ) + + footnote_rules = ( + 'newline', 'block_code', 'fences', 'heading', + 'nptable', 'lheading', 'hrule', 'block_quote', + 'list_block', 'block_html', 'table', 'paragraph', 'text' + ) + + def __init__(self, rules=None, **kwargs): + self.tokens = [] + self.def_links = {} + self.def_footnotes = {} + + if not rules: + rules = self.grammar_class() + + self.rules = rules + + def __call__(self, text, rules=None): + return self.parse(text, rules) + + def parse(self, text, rules=None): + text = text.rstrip('\n') + + if not rules: + rules = self.default_rules + + def manipulate(text): + for key in rules: + rule = getattr(self.rules, key) + m = rule.match(text) + if not m: + continue + getattr(self, 'parse_%s' % key)(m) + return m + return False # pragma: no cover + + while text: + m = manipulate(text) + if m is not False: + text = text[len(m.group(0)):] + continue + if text: # pragma: no cover + raise RuntimeError('Infinite loop at: %s' % text) + return self.tokens + + def parse_newline(self, m): + length = len(m.group(0)) + if length > 1: + self.tokens.append({'type': 'newline'}) + + def parse_block_code(self, m): + # clean leading whitespace + code = _block_code_leading_pattern.sub('', m.group(0)) + self.tokens.append({ + 'type': 'code', + 'lang': None, + 'text': code, + }) + + def parse_fences(self, m): + self.tokens.append({ + 'type': 'code', + 'lang': m.group(2), + 'text': m.group(3), + }) + + def parse_heading(self, m): + self.tokens.append({ + 'type': 'heading', + 'level': len(m.group(1)), + 'text': m.group(2), + }) + + def parse_lheading(self, m): + """Parse setext heading.""" + self.tokens.append({ + 'type': 'heading', + 'level': 1 if m.group(2) == '=' else 2, + 'text': m.group(1), + }) + + def parse_hrule(self, m): + self.tokens.append({'type': 'hrule'}) + + def parse_list_block(self, m): + bull = m.group(2) + self.tokens.append({ + 'type': 'list_start', + 'ordered': '.' in bull, + }) + cap = m.group(0) + self._process_list_item(cap, bull) + self.tokens.append({'type': 'list_end'}) + + def _process_list_item(self, cap, bull): + cap = self.rules.list_item.findall(cap) + + _next = False + length = len(cap) + + for i in range(length): + item = cap[i][0] + + # remove the bullet + space = len(item) + item = self.rules.list_bullet.sub('', item) + + # outdent + if '\n ' in item: + space = space - len(item) + pattern = re.compile(r'^ {1,%d}' % space, flags=re.M) + item = pattern.sub('', item) + + # determine whether item is loose or not + loose = _next + if not loose and re.search(r'\n\n(?!\s*$)', item): + loose = True + + rest = len(item) + if i != length - 1 and rest: + _next = item[rest-1] == '\n' + if not loose: + loose = _next + + if loose: + t = 'loose_item_start' + else: + t = 'list_item_start' + + self.tokens.append({'type': t}) + # recurse + self.parse(item, self.list_rules) + self.tokens.append({'type': 'list_item_end'}) + + def parse_block_quote(self, m): + self.tokens.append({'type': 'block_quote_start'}) + # clean leading > + cap = _block_quote_leading_pattern.sub('', m.group(0)) + self.parse(cap) + self.tokens.append({'type': 'block_quote_end'}) + + def parse_def_links(self, m): + key = _keyify(m.group(1)) + self.def_links[key] = { + 'link': m.group(2), + 'title': m.group(3), + } + + def parse_def_footnotes(self, m): + key = _keyify(m.group(1)) + if key in self.def_footnotes: + # footnote is already defined + return + + self.def_footnotes[key] = 0 + + self.tokens.append({ + 'type': 'footnote_start', + 'key': key, + }) + + text = m.group(2) + + if '\n' in text: + lines = text.split('\n') + whitespace = None + for line in lines[1:]: + space = len(line) - len(line.lstrip()) + if space and (not whitespace or space < whitespace): + whitespace = space + newlines = [lines[0]] + for line in lines[1:]: + newlines.append(line[whitespace:]) + text = '\n'.join(newlines) + + self.parse(text, self.footnote_rules) + + self.tokens.append({ + 'type': 'footnote_end', + 'key': key, + }) + + def parse_table(self, m): + item = self._process_table(m) + + cells = re.sub(r'(?: *\| *)?\n$', '', m.group(3)) + cells = cells.split('\n') + for i, v in enumerate(cells): + v = re.sub(r'^ *\| *| *\| *$', '', v) + cells[i] = re.split(r' *\| *', v) + + item['cells'] = cells + self.tokens.append(item) + + def parse_nptable(self, m): + item = self._process_table(m) + + cells = re.sub(r'\n$', '', m.group(3)) + cells = cells.split('\n') + for i, v in enumerate(cells): + cells[i] = re.split(r' *\| *', v) + + item['cells'] = cells + self.tokens.append(item) + + def _process_table(self, m): + header = re.sub(r'^ *| *\| *$', '', m.group(1)) + header = re.split(r' *\| *', header) + align = re.sub(r' *|\| *$', '', m.group(2)) + align = re.split(r' *\| *', align) + + for i, v in enumerate(align): + if re.search(r'^ *-+: *$', v): + align[i] = 'right' + elif re.search(r'^ *:-+: *$', v): + align[i] = 'center' + elif re.search(r'^ *:-+ *$', v): + align[i] = 'left' + else: + align[i] = None + + item = { + 'type': 'table', + 'header': header, + 'align': align, + } + return item + + def parse_block_html(self, m): + tag = m.group(1) + if not tag: + text = m.group(0) + self.tokens.append({ + 'type': 'close_html', + 'text': text + }) + else: + attr = m.group(2) + text = m.group(3) + self.tokens.append({ + 'type': 'open_html', + 'tag': tag, + 'extra': attr, + 'text': text + }) + + def parse_paragraph(self, m): + text = m.group(1).rstrip('\n') + self.tokens.append({'type': 'paragraph', 'text': text}) + + def parse_text(self, m): + text = m.group(0) + self.tokens.append({'type': 'text', 'text': text}) + + +class InlineGrammar(object): + """Grammars for inline level tokens.""" + + escape = re.compile(r'^\\([\\`*{}\[\]()#+\-.!_>~|])') # \* \+ \! .... + inline_html = re.compile( + r'^(?:%s|%s|%s)' % ( + r'', + r'<(\w+%s)((?:%s)*?)>([\s\S]*?)<\/\1>' % (_valid_end, _valid_attr), + r'<\w+%s(?:%s)*?>' % (_valid_end, _valid_attr), + ) + ) + autolink = re.compile(r'^<([^ >]+(@|:)[^ >]+)>') + link = re.compile( + r'^!?\[(' + r'(?:\[[^^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*' + r')\]\(' + r'''\s*(<)?([\s\S]*?)(?(2)>)(?:\s+['"]([\s\S]*?)['"])?\s*''' + r'\)' + ) + reflink = re.compile( + r'^!?\[(' + r'(?:\[[^^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*' + r')\]\s*\[([^^\]]*)\]' + ) + nolink = re.compile(r'^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]') + url = re.compile(r'''^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])''') + double_emphasis = re.compile( + r'^_{2}([\s\S]+?)_{2}(?!_)' # __word__ + r'|' + r'^\*{2}([\s\S]+?)\*{2}(?!\*)' # **word** + ) + emphasis = re.compile( + r'^\b_((?:__|[^_])+?)_\b' # _word_ + r'|' + r'^\*((?:\*\*|[^\*])+?)\*(?!\*)' # *word* + ) + code = re.compile(r'^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)') # `code` + linebreak = re.compile(r'^ {2,}\n(?!\s*$)') + strikethrough = re.compile(r'^~~(?=\S)([\s\S]*?\S)~~') # ~~word~~ + footnote = re.compile(r'^\[\^([^\]]+)\]') + text = re.compile(r'^[\s\S]+?(?=[\\%s' % (tag, extra, text, tag) + else: + html = m.group(0) + return self.renderer.inline_html(html) + + def output_footnote(self, m): + key = _keyify(m.group(1)) + if key not in self.footnotes: + return None + if self.footnotes[key]: + return None + self.footnote_index += 1 + self.footnotes[key] = self.footnote_index + return self.renderer.footnote_ref(key, self.footnote_index) + + def output_link(self, m): + return self._process_link(m, m.group(3), m.group(4)) + + def output_reflink(self, m): + key = _keyify(m.group(2) or m.group(1)) + if key not in self.links: + return None + ret = self.links[key] + return self._process_link(m, ret['link'], ret['title']) + + def output_nolink(self, m): + key = _keyify(m.group(1)) + if key not in self.links: + return None + ret = self.links[key] + return self._process_link(m, ret['link'], ret['title']) + + def _process_link(self, m, link, title=None): + line = m.group(0) + text = m.group(1) + if line[0] == '!': + return self.renderer.image(link, title, text) + + self._in_link = True + text = self.output(text) + self._in_link = False + return self.renderer.link(link, title, text) + + def output_double_emphasis(self, m): + text = m.group(2) or m.group(1) + text = self.output(text) + return self.renderer.double_emphasis(text) + + def output_emphasis(self, m): + text = m.group(2) or m.group(1) + text = self.output(text) + return self.renderer.emphasis(text) + + def output_code(self, m): + text = m.group(2) + return self.renderer.codespan(text) + + def output_linebreak(self, m): + return self.renderer.linebreak() + + def output_strikethrough(self, m): + text = self.output(m.group(1)) + return self.renderer.strikethrough(text) + + def output_text(self, m): + text = m.group(0) + return self.renderer.text(text) + + +class Renderer(object): + """The default HTML renderer for rendering Markdown. + """ + + def __init__(self, **kwargs): + self.options = kwargs + + def placeholder(self): + """Returns the default, empty output value for the renderer. + + All renderer methods use the '+=' operator to append to this value. + Default is a string so rendering HTML can build up a result string with + the rendered Markdown. + + Can be overridden by Renderer subclasses to be types like an empty + list, allowing the renderer to create a tree-like structure to + represent the document (which can then be reprocessed later into a + separate format like docx or pdf). + """ + return '' + + def block_code(self, code, lang=None): + """Rendering block level code. ``pre > code``. + + :param code: text content of the code block. + :param lang: language of the given code. + """ + code = code.rstrip('\n') + if not lang: + code = escape(code, smart_amp=False) + return '
%s\n
\n' % code + code = escape(code, quote=True, smart_amp=False) + return '
%s\n
\n' % (lang, code) + + def block_quote(self, text): + """Rendering
with the given text. + + :param text: text content of the blockquote. + """ + return '
%s\n
\n' % text.rstrip('\n') + + def block_html(self, html): + """Rendering block level pure html content. + + :param html: text content of the html snippet. + """ + if self.options.get('skip_style') and \ + html.lower().startswith('`` ``

``. + + :param text: rendered text content for the header. + :param level: a number for the header level, for example: 1. + :param raw: raw text content of the header. + """ + return '%s\n' % (level, text, level) + + def hrule(self): + """Rendering method for ``
`` tag.""" + if self.options.get('use_xhtml'): + return '
\n' + return '
\n' + + def list(self, body, ordered=True): + """Rendering list tags like ``
    `` and ``
      ``. + + :param body: body contents of the list. + :param ordered: whether this list is ordered or not. + """ + tag = 'ul' + if ordered: + tag = 'ol' + return '<%s>\n%s\n' % (tag, body, tag) + + def list_item(self, text): + """Rendering list item snippet. Like ``
    1. ``.""" + return '
    2. %s
    3. \n' % text + + def paragraph(self, text): + """Rendering paragraph tags. Like ``

      ``.""" + return '

      %s

      \n' % text.strip(' ') + + def table(self, header, body): + """Rendering table element. Wrap header and body in it. + + :param header: header part of the table. + :param body: body part of the table. + """ + return ( + '\n%s\n' + '\n%s\n
      \n' + ) % (header, body) + + def table_row(self, content): + """Rendering a table row. Like ````. + + :param content: content of current table row. + """ + return '\n%s\n' % content + + def table_cell(self, content, **flags): + """Rendering a table cell. Like ```` ````. + + :param content: content of current table cell. + :param header: whether this is header or not. + :param align: align of current table cell. + """ + if flags['header']: + tag = 'th' + else: + tag = 'td' + align = flags['align'] + if not align: + return '<%s>%s\n' % (tag, content, tag) + return '<%s style="text-align:%s">%s\n' % ( + tag, align, content, tag + ) + + def double_emphasis(self, text): + """Rendering **strong** text. + + :param text: text content for emphasis. + """ + return '%s' % text + + def emphasis(self, text): + """Rendering *emphasis* text. + + :param text: text content for emphasis. + """ + return '%s' % text + + def codespan(self, text): + """Rendering inline `code` text. + + :param text: text content for inline code. + """ + text = escape(text.rstrip(), smart_amp=False) + return '%s' % text + + def linebreak(self): + """Rendering line break like ``
      ``.""" + if self.options.get('use_xhtml'): + return '
      \n' + return '
      \n' + + def strikethrough(self, text): + """Rendering ~~strikethrough~~ text. + + :param text: text content for strikethrough. + """ + return '%s' % text + + def text(self, text): + """Rendering unformatted text. + + :param text: text content. + """ + return escape(text) + + def autolink(self, link, is_email=False): + """Rendering a given link or email address. + + :param link: link content or email address. + :param is_email: whether this is an email or not. + """ + text = link = escape(link) + if is_email: + link = 'mailto:%s' % link + return '%s' % (link, text) + + def link(self, link, title, text): + """Rendering a given link with content and title. + + :param link: href link for ```` tag. + :param title: title content for `title` attribute. + :param text: text content for description. + """ + link = escape_link(link, quote=True) + if not title: + return '%s' % (link, text) + title = escape(title, quote=True) + return '%s' % (link, title, text) + + def image(self, src, title, text): + """Rendering a image with title and text. + + :param src: source link of the image. + :param title: title text of the image. + :param text: alt text of the image. + """ + src = escape_link(src, quote=True) + text = escape(text, quote=True) + if title: + title = escape(title, quote=True) + html = '%s' % html + return '%s>' % html + + def inline_html(self, html): + """Rendering span level pure html content. + + :param html: text content of the html snippet. + """ + if self.options.get('escape'): + return escape(html) + return html + + def newline(self): + """Rendering newline element.""" + return '' + + def footnote_ref(self, key, index): + """Rendering the ref anchor of a footnote. + + :param key: identity key for the footnote. + :param index: the index count of current footnote. + """ + html = ( + '' + '%d' + ) % (escape(key), escape(key), index) + return html + + def footnote_item(self, key, text): + """Rendering a footnote item. + + :param key: identity key for the footnote. + :param text: text content of the footnote. + """ + back = ( + '' + ) % escape(key) + text = text.rstrip() + if text.endswith('

      '): + text = re.sub(r'<\/p>$', r'%s

      ' % back, text) + else: + text = '%s

      %s

      ' % (text, back) + html = '
    4. %s
    5. \n' % (escape(key), text) + return html + + def footnotes(self, text): + """Wrapper for all footnotes. + + :param text: contents of all footnotes. + """ + html = '
      \n%s
        %s
      \n
      \n' + return html % (self.hrule(), text) + + +class Markdown(object): + """The Markdown parser. + + :param renderer: An instance of ``Renderer``. + :param inline: An inline lexer class or instance. + :param block: A block lexer class or instance. + """ + def __init__(self, renderer=None, inline=None, block=None, **kwargs): + if not renderer: + renderer = Renderer(**kwargs) + else: + kwargs.update(renderer.options) + + self.renderer = renderer + + if inline and inspect.isclass(inline): + inline = inline(renderer, **kwargs) + if block and inspect.isclass(block): + block = block(**kwargs) + + if inline: + self.inline = inline + else: + self.inline = InlineLexer(renderer, **kwargs) + + self.block = block or BlockLexer(BlockGrammar()) + self.footnotes = [] + self.tokens = [] + + # detect if it should parse text in block html + self._parse_block_html = kwargs.get('parse_block_html') + + def __call__(self, text): + return self.parse(text) + + def render(self, text): + """Render the Markdown text. + + :param text: markdown formatted text content. + """ + return self.parse(text) + + def parse(self, text): + out = self.output(preprocessing(text)) + + keys = self.block.def_footnotes + + # reset block + self.block.def_links = {} + self.block.def_footnotes = {} + + # reset inline + self.inline.links = {} + self.inline.footnotes = {} + + if not self.footnotes: + return out + + footnotes = filter(lambda o: keys.get(o['key']), self.footnotes) + self.footnotes = sorted( + footnotes, key=lambda o: keys.get(o['key']), reverse=True + ) + + body = self.renderer.placeholder() + while self.footnotes: + note = self.footnotes.pop() + body += self.renderer.footnote_item( + note['key'], note['text'] + ) + + out += self.renderer.footnotes(body) + return out + + def pop(self): + if not self.tokens: + return None + self.token = self.tokens.pop() + return self.token + + def peek(self): + if self.tokens: + return self.tokens[-1] + return None # pragma: no cover + + def output(self, text, rules=None): + self.tokens = self.block(text, rules) + self.tokens.reverse() + + self.inline.setup(self.block.def_links, self.block.def_footnotes) + + out = self.renderer.placeholder() + while self.pop(): + out += self.tok() + return out + + def tok(self): + t = self.token['type'] + + # sepcial cases + if t.endswith('_start'): + t = t[:-6] + + return getattr(self, 'output_%s' % t)() + + def tok_text(self): + text = self.token['text'] + while self.peek()['type'] == 'text': + text += '\n' + self.pop()['text'] + return self.inline(text) + + def output_newline(self): + return self.renderer.newline() + + def output_hrule(self): + return self.renderer.hrule() + + def output_heading(self): + return self.renderer.header( + self.inline(self.token['text']), + self.token['level'], + self.token['text'], + ) + + def output_code(self): + return self.renderer.block_code( + self.token['text'], self.token['lang'] + ) + + def output_table(self): + aligns = self.token['align'] + aligns_length = len(aligns) + cell = self.renderer.placeholder() + + # header part + header = self.renderer.placeholder() + for i, value in enumerate(self.token['header']): + align = aligns[i] if i < aligns_length else None + flags = {'header': True, 'align': align} + cell += self.renderer.table_cell(self.inline(value), **flags) + + header += self.renderer.table_row(cell) + + # body part + body = self.renderer.placeholder() + for i, row in enumerate(self.token['cells']): + cell = self.renderer.placeholder() + for j, value in enumerate(row): + align = aligns[j] if j < aligns_length else None + flags = {'header': False, 'align': align} + cell += self.renderer.table_cell(self.inline(value), **flags) + body += self.renderer.table_row(cell) + + return self.renderer.table(header, body) + + def output_block_quote(self): + body = self.renderer.placeholder() + while self.pop()['type'] != 'block_quote_end': + body += self.tok() + return self.renderer.block_quote(body) + + def output_list(self): + ordered = self.token['ordered'] + body = self.renderer.placeholder() + while self.pop()['type'] != 'list_end': + body += self.tok() + return self.renderer.list(body, ordered) + + def output_list_item(self): + body = self.renderer.placeholder() + while self.pop()['type'] != 'list_item_end': + if self.token['type'] == 'text': + body += self.tok_text() + else: + body += self.tok() + + return self.renderer.list_item(body) + + def output_loose_item(self): + body = self.renderer.placeholder() + while self.pop()['type'] != 'list_item_end': + body += self.tok() + return self.renderer.list_item(body) + + def output_footnote(self): + self.inline._in_footnote = True + body = self.renderer.placeholder() + key = self.token['key'] + while self.pop()['type'] != 'footnote_end': + body += self.tok() + self.footnotes.append({'key': key, 'text': body}) + self.inline._in_footnote = False + return self.renderer.placeholder() + + def output_close_html(self): + text = self.token['text'] + return self.renderer.block_html(text) + + def output_open_html(self): + text = self.token['text'] + tag = self.token['tag'] + if self._parse_block_html and tag not in _pre_tags: + text = self.inline(text, rules=self.inline.inline_html_rules) + extra = self.token.get('extra') or '' + html = '<%s%s>%s' % (tag, extra, text, tag) + return self.renderer.block_html(html) + + def output_paragraph(self): + return self.renderer.paragraph(self.inline(self.token['text'])) + + def output_text(self): + return self.renderer.paragraph(self.tok_text()) + + +def markdown(text, escape=True, **kwargs): + """Render markdown formatted text to html. + + :param text: markdown formatted text content. + :param escape: if set to False, all html tags will not be escaped. + :param use_xhtml: output with xhtml tags. + :param hard_wrap: if set to True, it will use the GFM line breaks feature. + :param parse_block_html: parse text only in block level html. + :param parse_inline_html: parse text only in inline level html. + """ + return Markdown(escape=escape, **kwargs)(text) diff --git a/bin/ttbp.py b/bin/ttbp.py index bb09b21..fedb730 100644 --- a/bin/ttbp.py +++ b/bin/ttbp.py @@ -415,6 +415,9 @@ thanks to everyone who reads, listens, writes, and feels.\ def write_entry(entry=os.path.join(DATA, "test.txt")): entered = raw_input(""" +"""+util.hilight("new feature!")+""" you can now use standard markdown in +your entry text! raw html is still valid, and you can mix them together. + feels will be recorded for today, """+time.strftime("%d %B %Y")+""". if you've already started recording feels for this day, you diff --git a/changelog.txt b/changelog.txt index 2f8e677..d0ac914 100644 --- a/changelog.txt +++ b/changelog.txt @@ -4,7 +4,6 @@ TO-DO: -show most recent global entries on html -command line flags: -shortcut to most recent feels, writing entry, seeing own entry - -markdown parsing -neighbor view takes you to individual entry listing (goals for phase two) @@ -22,6 +21,9 @@ TO-DO: CHANGELOG: +ver 0.8.0 + -markdown parsing for entries + ver 0.7.5 -COLORFUL BANNER -backend stuff; feedback uses sendmail, some code cleanup