# -*- coding: utf-8 -*-␊ |
#!/usr/bin/env python␊ |
# 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 <div> and <table> as well).␊ |
␊ |
Module usage:␊ |
␊ |
>>> import markdown2␊ |
>>> markdown2.markdown("*boo!*") # or use `html = markdown_path(PATH)`␊ |
u'<p><em>boo!</em></p>\n'␊ |
␊ |
>>> markdowner = Markdown()␊ |
>>> markdowner.convert("*boo!*")␊ |
u'<p><em>boo!</em></p>\n'␊ |
>>> markdowner.convert("**boom!**")␊ |
u'<p><strong>boom!</strong></p>\n'␊ |
␊ |
This implementation of Markdown implements the full "core" syntax plus a␊ |
number of extras (e.g., code syntax coloring, footnotes) as described on␊ |
<https://github.com/trentm/python-markdown2/wiki/Extras>.␊ |
"""␊ |
␊ |
cmdln_desc = """A fast and complete Python implementation of Markdown, a␊ |
text-to-HTML conversion tool for web writers.␊ |
␊ |
Supported extra syntax options (see -x|--extras option below and␊ |
see <https://github.com/trentm/python-markdown2/wiki/Extras> for details):␊ |
␊ |
* code-friendly: Disable _ and __ for em and strong.␊ |
* cuddled-lists: Allow lists to be cuddled to the preceding paragraph.␊ |
* fenced-code-blocks: Allows a code block to not have to be indented␊ |
by fencing it with '```' on a line before and after. Based on␊ |
<http://github.github.com/github-flavored-markdown/> with support for␊ |
syntax highlighting.␊ |
* footnotes: Support footnotes as in use on daringfireball.net and␊ |
implemented in other Markdown processors (tho not in Markdown.pl v1.0.1).␊ |
* header-ids: Adds "id" attributes to headers. The id value is a slug of␊ |
the header text.␊ |
* html-classes: Takes a dict mapping html tag names (lowercase) to a␊ |
string to use for a "class" tag attribute. Currently only supports␊ |
"pre" and "code" tags. Add an issue if you require this for other tags.␊ |
* markdown-in-html: Allow the use of `markdown="1"` in a block HTML tag to␊ |
have markdown processing be done on its contents. Similar to␊ |
<http://michelf.com/projects/php-markdown/extra/#markdown-attr> but with␊ |
some limitations.␊ |
* metadata: Extract metadata from a leading '---'-fenced block.␊ |
See <https://github.com/trentm/python-markdown2/issues/77> for details.␊ |
* nofollow: Add `rel="nofollow"` to add `<a>` tags with an href. See␊ |
<http://en.wikipedia.org/wiki/Nofollow>.␊ |
* pyshell: Treats unindented Python interactive shell sessions as <code>␊ |
blocks.␊ |
* link-patterns: Auto-link given regex patterns in text (e.g. bug number␊ |
references, revision number references).␊ |
* smarty-pants: Replaces ' and " with curly quotation marks or curly␊ |
apostrophes. Replaces --, ---, ..., and . . . with en dashes, em dashes,␊ |
and ellipses.␊ |
* toc: The returned HTML string gets a new "toc_html" attribute which is␊ |
a Table of Contents for the document. (experimental)␊ |
* xml: Passes one-liner processing instructions and namespaced XML tags.␊ |
* wiki-tables: Google Code Wiki-style tables. See␊ |
<http://code.google.com/p/support/wiki/WikiSyntax#Tables>.␊ |
"""␊ |
␊ |
# Dev Notes:␊ |
# - Python's regex syntax doesn't have '\z', so I'm using '\Z'. I'm␊ |
# not yet sure if there implications with this. Compare 'pydoc sre'␊ |
# and 'perldoc perlre'.␊ |
␊ |
__version_info__ = (2, 1, 1)␊ |
__version__ = '.'.join(map(str, __version_info__))␊ |
__author__ = "Trent Mick"␊ |
␊ |
import os␊ |
import sys␊ |
from pprint import pprint␊ |
import re␊ |
import logging␊ |
try:␊ |
from hashlib import md5␊ |
except ImportError:␊ |
from md5 import md5␊ |
import optparse␊ |
from random import random, randint␊ |
import codecs␊ |
␊ |
␊ |
#---- Python version compat␊ |
␊ |
try:␊ |
from urllib.parse import quote # python3␊ |
except ImportError:␊ |
from urllib import quote # python2␊ |
␊ |
if sys.version_info[:2] < (2,4):␊ |
from sets import Set as set␊ |
def reversed(sequence):␊ |
for i in sequence[::-1]:␊ |
yield i␊ |
␊ |
# Use `bytes` for byte strings and `unicode` for unicode strings (str in Py3).␊ |
if sys.version_info[0] <= 2:␊ |
py3 = False␊ |
try:␊ |
bytes␊ |
except NameError:␊ |
bytes = str␊ |
base_string_type = basestring␊ |
elif sys.version_info[0] >= 3:␊ |
py3 = True␊ |
unicode = str␊ |
base_string_type = str␊ |
␊ |
␊ |
␊ |
#---- globals␊ |
␊ |
DEBUG = False␊ |
log = logging.getLogger("markdown")␊ |
␊ |
DEFAULT_TAB_WIDTH = 4␊ |
␊ |
␊ |
SECRET_SALT = bytes(randint(0, 1000000))␊ |
def _hash_text(s):␊ |
return 'md5-' + md5(SECRET_SALT + s.encode("utf-8")).hexdigest()␊ |
␊ |
# Table of hash values for escaped characters:␊ |
g_escape_table = dict([(ch, _hash_text(ch))␊ |
for ch in '\\`*_{}[]()>#+-.!'])␊ |
␊ |
␊ |
␊ |
#---- exceptions␊ |
␊ |
class MarkdownError(Exception):␊ |
pass␊ |
␊ |
␊ |
␊ |
#---- public api␊ |
␊ |
def markdown_path(path, encoding="utf-8",␊ |
html4tags=False, tab_width=DEFAULT_TAB_WIDTH,␊ |
safe_mode=None, extras=None, link_patterns=None,␊ |
use_file_vars=False):␊ |
fp = codecs.open(path, 'r', encoding)␊ |
text = fp.read()␊ |
fp.close()␊ |
return Markdown(html4tags=html4tags, tab_width=tab_width,␊ |
safe_mode=safe_mode, extras=extras,␊ |
link_patterns=link_patterns,␊ |
use_file_vars=use_file_vars).convert(text)␊ |
␊ |
def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH,␊ |
safe_mode=None, extras=None, link_patterns=None,␊ |
use_file_vars=False):␊ |
return Markdown(html4tags=html4tags, tab_width=tab_width,␊ |
safe_mode=safe_mode, extras=extras,␊ |
link_patterns=link_patterns,␊ |
use_file_vars=use_file_vars).convert(text)␊ |
␊ |
class Markdown(object):␊ |
# The dict of "extras" to enable in processing -- a mapping of␊ |
# extra name to argument for the extra. Most extras do not have an␊ |
# argument, in which case the value is None.␊ |
#␊ |
# This can be set via (a) subclassing and (b) the constructor␊ |
# "extras" argument.␊ |
extras = None␊ |
␊ |
urls = None␊ |
titles = None␊ |
html_blocks = None␊ |
html_spans = None␊ |
html_removed_text = "[HTML_REMOVED]" # for compat with markdown.py␊ |
␊ |
# Used to track when we're inside an ordered or unordered list␊ |
# (see _ProcessListItems() for details):␊ |
list_level = 0␊ |
␊ |
_ws_only_line_re = re.compile(r"^[ \t]+$", re.M)␊ |
␊ |
def __init__(self, html4tags=False, tab_width=4, safe_mode=None,␊ |
extras=None, link_patterns=None, use_file_vars=False):␊ |
if html4tags:␊ |
self.empty_element_suffix = ">"␊ |
else:␊ |
self.empty_element_suffix = " />"␊ |
self.tab_width = tab_width␊ |
␊ |
# For compatibility with earlier markdown2.py and with␊ |
# markdown.py's safe_mode being a boolean,␊ |
# safe_mode == True -> "replace"␊ |
if safe_mode is True:␊ |
self.safe_mode = "replace"␊ |
else:␊ |
self.safe_mode = safe_mode␊ |
␊ |
# Massaging and building the "extras" info.␊ |
if self.extras is None:␊ |
self.extras = {}␊ |
elif not isinstance(self.extras, dict):␊ |
self.extras = dict([(e, None) for e in self.extras])␊ |
if extras:␊ |
if not isinstance(extras, dict):␊ |
extras = dict([(e, None) for e in extras])␊ |
self.extras.update(extras)␊ |
assert isinstance(self.extras, dict)␊ |
if "toc" in self.extras and not "header-ids" in self.extras:␊ |
self.extras["header-ids"] = None # "toc" implies "header-ids"␊ |
self._instance_extras = self.extras.copy()␊ |
␊ |
self.link_patterns = link_patterns␊ |
self.use_file_vars = use_file_vars␊ |
self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M)␊ |
␊ |
self._escape_table = g_escape_table.copy()␊ |
if "smarty-pants" in self.extras:␊ |
self._escape_table['"'] = _hash_text('"')␊ |
self._escape_table["'"] = _hash_text("'")␊ |
␊ |
def reset(self):␊ |
self.urls = {}␊ |
self.titles = {}␊ |
self.html_blocks = {}␊ |
self.html_spans = {}␊ |
self.list_level = 0␊ |
self.extras = self._instance_extras.copy()␊ |
if "footnotes" in self.extras:␊ |
self.footnotes = {}␊ |
self.footnote_ids = []␊ |
if "header-ids" in self.extras:␊ |
self._count_from_header_id = {} # no `defaultdict` in Python 2.4␊ |
if "metadata" in self.extras:␊ |
self.metadata = {}␊ |
␊ |
# Per <https://developer.mozilla.org/en-US/docs/HTML/Element/a> "rel"␊ |
# should only be used in <a> tags with an "href" attribute.␊ |
_a_nofollow = re.compile(r"<(a)([^>]*href=)", re.IGNORECASE)␊ |
␊ |
def convert(self, text):␊ |
"""Convert the given text."""␊ |
# Main function. The order in which other subs are called here is␊ |
# essential. Link and image substitutions need to happen before␊ |
# _EscapeSpecialChars(), so that any *'s or _'s in the <a>␊ |
# and <img> tags get encoded.␊ |
␊ |
# Clear the global hashes. If we don't clear these, you get conflicts␊ |
# from other articles when generating a page which contains more than␊ |
# one article (e.g. an index page that shows the N most recent␊ |
# articles):␊ |
self.reset()␊ |
␊ |
if not isinstance(text, unicode):␊ |
#TODO: perhaps shouldn't presume UTF-8 for string input?␊ |
text = unicode(text, 'utf-8')␊ |
␊ |
if self.use_file_vars:␊ |
# Look for emacs-style file variable hints.␊ |
emacs_vars = self._get_emacs_vars(text)␊ |
if "markdown-extras" in emacs_vars:␊ |
splitter = re.compile("[ ,]+")␊ |
for e in splitter.split(emacs_vars["markdown-extras"]):␊ |
if '=' in e:␊ |
ename, earg = e.split('=', 1)␊ |
try:␊ |
earg = int(earg)␊ |
except ValueError:␊ |
pass␊ |
else:␊ |
ename, earg = e, None␊ |
self.extras[ename] = earg␊ |
␊ |
# Standardize line endings:␊ |
text = re.sub("\r\n|\r", "\n", text)␊ |
␊ |
# Make sure $text ends with a couple of newlines:␊ |
text += "\n\n"␊ |
␊ |
# Convert all tabs to spaces.␊ |
text = self._detab(text)␊ |
␊ |
# Strip any lines consisting only of spaces and tabs.␊ |
# This makes subsequent regexen easier to write, because we can␊ |
# match consecutive blank lines with /\n+/ instead of something␊ |
# contorted like /[ \t]*\n+/ .␊ |
text = self._ws_only_line_re.sub("", text)␊ |
␊ |
# strip metadata from head and extract␊ |
if "metadata" in self.extras:␊ |
text = self._extract_metadata(text)␊ |
␊ |
text = self.preprocess(text)␊ |
␊ |
if self.safe_mode:␊ |
text = self._hash_html_spans(text)␊ |
␊ |
# Turn block-level HTML blocks into hash entries␊ |
text = self._hash_html_blocks(text, raw=True)␊ |
␊ |
if "fenced-code-blocks" in self.extras:␊ |
text = self._do_fenced_code_blocks(text)␊ |
␊ |
# Strip link definitions, store in hashes.␊ |
if "footnotes" in self.extras:␊ |
# Must do footnotes first because an unlucky footnote defn␊ |
# looks like a link defn:␊ |
# [^4]: this "looks like a link defn"␊ |
text = self._strip_footnote_definitions(text)␊ |
text = self._strip_link_definitions(text)␊ |
␊ |
text = self._run_block_gamut(text)␊ |
␊ |
if "footnotes" in self.extras:␊ |
text = self._add_footnotes(text)␊ |
␊ |
text = self.postprocess(text)␊ |
␊ |
text = self._unescape_special_chars(text)␊ |
␊ |
if self.safe_mode:␊ |
text = self._unhash_html_spans(text)␊ |
␊ |
if "nofollow" in self.extras:␊ |
text = self._a_nofollow.sub(r'<\1 rel="nofollow"\2', text)␊ |
␊ |
text += "\n"␊ |
␊ |
rv = UnicodeWithAttrs(text)␊ |
if "toc" in self.extras:␊ |
rv._toc = self._toc␊ |
if "metadata" in self.extras:␊ |
rv.metadata = self.metadata␊ |
return rv␊ |
␊ |
def postprocess(self, text):␊ |
"""A hook for subclasses to do some postprocessing of the html, if␊ |
desired. This is called before unescaping of special chars and␊ |
unhashing of raw HTML spans.␊ |
"""␊ |
return text␊ |
␊ |
def preprocess(self, text):␊ |
"""A hook for subclasses to do some preprocessing of the Markdown, if␊ |
desired. This is called after basic formatting of the text, but prior␊ |
to any extras, safe mode, etc. processing.␊ |
"""␊ |
return text␊ |
␊ |
# Is metadata if the content starts with '---'-fenced `key: value`␊ |
# pairs. E.g. (indented for presentation):␊ |
# ---␊ |
# foo: bar␊ |
# another-var: blah blah␊ |
# ---␊ |
_metadata_pat = re.compile("""^---[ \t]*\n((?:[ \t]*[^ \t:]+[ \t]*:[^\n]*\n)+)---[ \t]*\n""")␊ |
␊ |
def _extract_metadata(self, text):␊ |
# fast test␊ |
if not text.startswith("---"):␊ |
return text␊ |
match = self._metadata_pat.match(text)␊ |
if not match:␊ |
return text␊ |
␊ |
tail = text[len(match.group(0)):]␊ |
metadata_str = match.group(1).strip()␊ |
for line in metadata_str.split('\n'):␊ |
key, value = line.split(':', 1)␊ |
self.metadata[key.strip()] = value.strip()␊ |
␊ |
return tail␊ |
␊ |
␊ |
_emacs_oneliner_vars_pat = re.compile(r"-\*-\s*([^\r\n]*?)\s*-\*-", re.UNICODE)␊ |
# This regular expression is intended to match blocks like this:␊ |
# PREFIX Local Variables: SUFFIX␊ |
# PREFIX mode: Tcl SUFFIX␊ |
# PREFIX End: SUFFIX␊ |
# Some notes:␊ |
# - "[ \t]" is used instead of "\s" to specifically exclude newlines␊ |
# - "(\r\n|\n|\r)" is used instead of "$" because the sre engine does␊ |
# not like anything other than Unix-style line terminators.␊ |
_emacs_local_vars_pat = re.compile(r"""^␊ |
(?P<prefix>(?:[^\r\n|\n|\r])*?)␊ |
[\ \t]*Local\ Variables:[\ \t]*␊ |
(?P<suffix>.*?)(?:\r\n|\n|\r)␊ |
(?P<content>.*?\1End:)␊ |
""", re.IGNORECASE | re.MULTILINE | re.DOTALL | re.VERBOSE)␊ |
␊ |
def _get_emacs_vars(self, text):␊ |
"""Return a dictionary of emacs-style local variables.␊ |
␊ |
Parsing is done loosely according to this spec (and according to␊ |
some in-practice deviations from this):␊ |
http://www.gnu.org/software/emacs/manual/html_node/emacs/Specifying-File-Variables.html#Specifying-File-Variables␊ |
"""␊ |
emacs_vars = {}␊ |
SIZE = pow(2, 13) # 8kB␊ |
␊ |
# Search near the start for a '-*-'-style one-liner of variables.␊ |
head = text[:SIZE]␊ |
if "-*-" in head:␊ |
match = self._emacs_oneliner_vars_pat.search(head)␊ |
if match:␊ |
emacs_vars_str = match.group(1)␊ |
assert '\n' not in emacs_vars_str␊ |
emacs_var_strs = [s.strip() for s in emacs_vars_str.split(';')␊ |
if s.strip()]␊ |
if len(emacs_var_strs) == 1 and ':' not in emacs_var_strs[0]:␊ |
# While not in the spec, this form is allowed by emacs:␊ |
# -*- Tcl -*-␊ |
# where the implied "variable" is "mode". This form␊ |
# is only allowed if there are no other variables.␊ |
emacs_vars["mode"] = emacs_var_strs[0].strip()␊ |
else:␊ |
for emacs_var_str in emacs_var_strs:␊ |
try:␊ |
variable, value = emacs_var_str.strip().split(':', 1)␊ |
except ValueError:␊ |
log.debug("emacs variables error: malformed -*- "␊ |
"line: %r", emacs_var_str)␊ |
continue␊ |
# Lowercase the variable name because Emacs allows "Mode"␊ |
# or "mode" or "MoDe", etc.␊ |
emacs_vars[variable.lower()] = value.strip()␊ |
␊ |
tail = text[-SIZE:]␊ |
if "Local Variables" in tail:␊ |
match = self._emacs_local_vars_pat.search(tail)␊ |
if match:␊ |
prefix = match.group("prefix")␊ |
suffix = match.group("suffix")␊ |
lines = match.group("content").splitlines(0)␊ |
#print "prefix=%r, suffix=%r, content=%r, lines: %s"\␊ |
# % (prefix, suffix, match.group("content"), lines)␊ |
␊ |
# Validate the Local Variables block: proper prefix and suffix␊ |
# usage.␊ |
for i, line in enumerate(lines):␊ |
if not line.startswith(prefix):␊ |
log.debug("emacs variables error: line '%s' "␊ |
"does not use proper prefix '%s'"␊ |
% (line, prefix))␊ |
return {}␊ |
# Don't validate suffix on last line. Emacs doesn't care,␊ |
# neither should we.␊ |
if i != len(lines)-1 and not line.endswith(suffix):␊ |
log.debug("emacs variables error: line '%s' "␊ |
"does not use proper suffix '%s'"␊ |
% (line, suffix))␊ |
return {}␊ |
␊ |
# Parse out one emacs var per line.␊ |
continued_for = None␊ |
for line in lines[:-1]: # no var on the last line ("PREFIX End:")␊ |
if prefix: line = line[len(prefix):] # strip prefix␊ |
if suffix: line = line[:-len(suffix)] # strip suffix␊ |
line = line.strip()␊ |
if continued_for:␊ |
variable = continued_for␊ |
if line.endswith('\\'):␊ |
line = line[:-1].rstrip()␊ |
else:␊ |
continued_for = None␊ |
emacs_vars[variable] += ' ' + line␊ |
else:␊ |
try:␊ |
variable, value = line.split(':', 1)␊ |
except ValueError:␊ |
log.debug("local variables error: missing colon "␊ |
"in local variables entry: '%s'" % line)␊ |
continue␊ |
# Do NOT lowercase the variable name, because Emacs only␊ |
# allows "mode" (and not "Mode", "MoDe", etc.) in this block.␊ |
value = value.strip()␊ |
if value.endswith('\\'):␊ |
value = value[:-1].rstrip()␊ |
continued_for = variable␊ |
else:␊ |
continued_for = None␊ |
emacs_vars[variable] = value␊ |
␊ |
# Unquote values.␊ |
for var, val in list(emacs_vars.items()):␊ |
if len(val) > 1 and (val.startswith('"') and val.endswith('"')␊ |
or val.startswith('"') and val.endswith('"')):␊ |
emacs_vars[var] = val[1:-1]␊ |
␊ |
return emacs_vars␊ |
␊ |
# Cribbed from a post by Bart Lateur:␊ |
# <http://www.nntp.perl.org/group/perl.macperl.anyperl/154>␊ |
_detab_re = re.compile(r'(.*?)\t', re.M)␊ |
def _detab_sub(self, match):␊ |
g1 = match.group(1)␊ |
return g1 + (' ' * (self.tab_width - len(g1) % self.tab_width))␊ |
def _detab(self, text):␊ |
r"""Remove (leading?) tabs from a file.␊ |
␊ |
>>> m = Markdown()␊ |
>>> m._detab("\tfoo")␊ |
' foo'␊ |
>>> m._detab(" \tfoo")␊ |
' foo'␊ |
>>> m._detab("\t foo")␊ |
' foo'␊ |
>>> m._detab(" foo")␊ |
' foo'␊ |
>>> m._detab(" foo\n\tbar\tblam")␊ |
' foo\n bar blam'␊ |
"""␊ |
if '\t' not in text:␊ |
return text␊ |
return self._detab_re.subn(self._detab_sub, text)[0]␊ |
␊ |
# I broke out the html5 tags here and add them to _block_tags_a and␊ |
# _block_tags_b. This way html5 tags are easy to keep track of.␊ |
_html5tags = '|article|aside|header|hgroup|footer|nav|section|figure|figcaption'␊ |
␊ |
_block_tags_a = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del'␊ |
_block_tags_a += _html5tags␊ |
␊ |
_strict_tag_block_re = re.compile(r"""␊ |
( # save in \1␊ |
^ # start of line (with re.M)␊ |
<(%s) # start tag = \2␊ |
\b # word break␊ |
(.*\n)*? # any number of lines, minimally matching␊ |
</\2> # the matching end tag␊ |
[ \t]* # trailing spaces/tabs␊ |
(?=\n+|\Z) # followed by a newline or end of document␊ |
)␊ |
""" % _block_tags_a,␊ |
re.X | re.M)␊ |
␊ |
_block_tags_b = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math'␊ |
_block_tags_b += _html5tags␊ |
␊ |
_liberal_tag_block_re = re.compile(r"""␊ |
( # save in \1␊ |
^ # start of line (with re.M)␊ |
<(%s) # start tag = \2␊ |
\b # word break␊ |
(.*\n)*? # any number of lines, minimally matching␊ |
.*</\2> # the matching end tag␊ |
[ \t]* # trailing spaces/tabs␊ |
(?=\n+|\Z) # followed by a newline or end of document␊ |
)␊ |
""" % _block_tags_b,␊ |
re.X | re.M)␊ |
␊ |
_html_markdown_attr_re = re.compile(␊ |
r'''\s+markdown=("1"|'1')''')␊ |
def _hash_html_block_sub(self, match, raw=False):␊ |
html = match.group(1)␊ |
if raw and self.safe_mode:␊ |
html = self._sanitize_html(html)␊ |
elif 'markdown-in-html' in self.extras and 'markdown=' in html:␊ |
first_line = html.split('\n', 1)[0]␊ |
m = self._html_markdown_attr_re.search(first_line)␊ |
if m:␊ |
lines = html.split('\n')␊ |
middle = '\n'.join(lines[1:-1])␊ |
last_line = lines[-1]␊ |
first_line = first_line[:m.start()] + first_line[m.end():]␊ |
f_key = _hash_text(first_line)␊ |
self.html_blocks[f_key] = first_line␊ |
l_key = _hash_text(last_line)␊ |
self.html_blocks[l_key] = last_line␊ |
return ''.join(["\n\n", f_key,␊ |
"\n\n", middle, "\n\n",␊ |
l_key, "\n\n"])␊ |
key = _hash_text(html)␊ |
self.html_blocks[key] = html␊ |
return "\n\n" + key + "\n\n"␊ |
␊ |
def _hash_html_blocks(self, text, raw=False):␊ |
"""Hashify HTML blocks␊ |
␊ |
We only want to do this for block-level HTML tags, such as headers,␊ |
lists, and tables. That's because we still want to wrap <p>s around␊ |
"paragraphs" that are wrapped in non-block-level tags, such as anchors,␊ |
phrase emphasis, and spans. The list of tags we're looking for is␊ |
hard-coded.␊ |
␊ |
@param raw {boolean} indicates if these are raw HTML blocks in␊ |
the original source. It makes a difference in "safe" mode.␊ |
"""␊ |
if '<' not in text:␊ |
return text␊ |
␊ |
# Pass `raw` value into our calls to self._hash_html_block_sub.␊ |
hash_html_block_sub = _curry(self._hash_html_block_sub, raw=raw)␊ |
␊ |
# First, look for nested blocks, e.g.:␊ |
# <div>␊ |
# <div>␊ |
# tags for inner block must be indented.␊ |
# </div>␊ |
# </div>␊ |
#␊ |
# The outermost tags must start at the left margin for this to match, and␊ |
# the inner nested divs must be indented.␊ |
# We need to do this before the next, more liberal match, because the next␊ |
# match will start at the first `<div>` and stop at the first `</div>`.␊ |
text = self._strict_tag_block_re.sub(hash_html_block_sub, text)␊ |
␊ |
# Now match more liberally, simply from `\n<tag>` to `</tag>\n`␊ |
text = self._liberal_tag_block_re.sub(hash_html_block_sub, text)␊ |
␊ |
# Special case just for <hr />. It was easier to make a special␊ |
# case than to make the other regex more complicated.␊ |
if "<hr" in text:␊ |
_hr_tag_re = _hr_tag_re_from_tab_width(self.tab_width)␊ |
text = _hr_tag_re.sub(hash_html_block_sub, text)␊ |
␊ |
# Special case for standalone HTML comments:␊ |
if "<!--" in text:␊ |
start = 0␊ |
while True:␊ |
# Delimiters for next comment block.␊ |
try:␊ |
start_idx = text.index("<!--", start)␊ |
except ValueError:␊ |
break␊ |
try:␊ |
end_idx = text.index("-->", start_idx) + 3␊ |
except ValueError:␊ |
break␊ |
␊ |
# Start position for next comment block search.␊ |
start = end_idx␊ |
␊ |
# Validate whitespace before comment.␊ |
if start_idx:␊ |
# - Up to `tab_width - 1` spaces before start_idx.␊ |
for i in range(self.tab_width - 1):␊ |
if text[start_idx - 1] != ' ':␊ |
break␊ |
start_idx -= 1␊ |
if start_idx == 0:␊ |
break␊ |
# - Must be preceded by 2 newlines or hit the start of␊ |
# the document.␊ |
if start_idx == 0:␊ |
pass␊ |
elif start_idx == 1 and text[0] == '\n':␊ |
start_idx = 0 # to match minute detail of Markdown.pl regex␊ |
elif text[start_idx-2:start_idx] == '\n\n':␊ |
pass␊ |
else:␊ |
break␊ |
␊ |
# Validate whitespace after comment.␊ |
# - Any number of spaces and tabs.␊ |
while end_idx < len(text):␊ |
if text[end_idx] not in ' \t':␊ |
break␊ |
end_idx += 1␊ |
# - Must be following by 2 newlines or hit end of text.␊ |
if text[end_idx:end_idx+2] not in ('', '\n', '\n\n'):␊ |
continue␊ |
␊ |
# Escape and hash (must match `_hash_html_block_sub`).␊ |
html = text[start_idx:end_idx]␊ |
if raw and self.safe_mode:␊ |
html = self._sanitize_html(html)␊ |
key = _hash_text(html)␊ |
self.html_blocks[key] = html␊ |
text = text[:start_idx] + "\n\n" + key + "\n\n" + text[end_idx:]␊ |
␊ |
if "xml" in self.extras:␊ |
# Treat XML processing instructions and namespaced one-liner␊ |
# tags as if they were block HTML tags. E.g., if standalone␊ |
# (i.e. are their own paragraph), the following do not get␊ |
# wrapped in a <p> tag:␊ |
# <?foo bar?>␊ |
#␊ |
# <xi:include xmlns:xi="http://www.w3.org/2001/XInclude" href="chapter_1.md"/>␊ |
_xml_oneliner_re = _xml_oneliner_re_from_tab_width(self.tab_width)␊ |
text = _xml_oneliner_re.sub(hash_html_block_sub, text)␊ |
␊ |
return text␊ |
␊ |
def _strip_link_definitions(self, text):␊ |
# Strips link definitions from text, stores the URLs and titles in␊ |
# hash references.␊ |
less_than_tab = self.tab_width - 1␊ |
␊ |
# Link defs are in the form:␊ |
# [id]: url "optional title"␊ |
_link_def_re = re.compile(r"""␊ |
^[ ]{0,%d}\[(.+)\]: # id = \1␊ |
[ \t]*␊ |
\n? # maybe *one* newline␊ |
[ \t]*␊ |
<?(.+?)>? # url = \2␊ |
[ \t]*␊ |
(?:␊ |
\n? # maybe one newline␊ |
[ \t]*␊ |
(?<=\s) # lookbehind for whitespace␊ |
['"(]␊ |
([^\n]*) # title = \3␊ |
['")]␊ |
[ \t]*␊ |
)? # title is optional␊ |
(?:\n+|\Z)␊ |
""" % less_than_tab, re.X | re.M | re.U)␊ |
return _link_def_re.sub(self._extract_link_def_sub, text)␊ |
␊ |
def _extract_link_def_sub(self, match):␊ |
id, url, title = match.groups()␊ |
key = id.lower() # Link IDs are case-insensitive␊ |
self.urls[key] = self._encode_amps_and_angles(url)␊ |
if title:␊ |
self.titles[key] = title␊ |
return ""␊ |
␊ |
def _extract_footnote_def_sub(self, match):␊ |
id, text = match.groups()␊ |
text = _dedent(text, skip_first_line=not text.startswith('\n')).strip()␊ |
normed_id = re.sub(r'\W', '-', id)␊ |
# Ensure footnote text ends with a couple newlines (for some␊ |
# block gamut matches).␊ |
self.footnotes[normed_id] = text + "\n\n"␊ |
return ""␊ |
␊ |
def _strip_footnote_definitions(self, text):␊ |
"""A footnote definition looks like this:␊ |
␊ |
[^note-id]: Text of the note.␊ |
␊ |
May include one or more indented paragraphs.␊ |
␊ |
Where,␊ |
- The 'note-id' can be pretty much anything, though typically it␊ |
is the number of the footnote.␊ |
- The first paragraph may start on the next line, like so:␊ |
␊ |
[^note-id]:␊ |
Text of the note.␊ |
"""␊ |
less_than_tab = self.tab_width - 1␊ |
footnote_def_re = re.compile(r'''␊ |
^[ ]{0,%d}\[\^(.+)\]: # id = \1␊ |
[ \t]*␊ |
( # footnote text = \2␊ |
# First line need not start with the spaces.␊ |
(?:\s*.*\n+)␊ |
(?:␊ |
(?:[ ]{%d} | \t) # Subsequent lines must be indented.␊ |
.*\n+␊ |
)*␊ |
)␊ |
# Lookahead for non-space at line-start, or end of doc.␊ |
(?:(?=^[ ]{0,%d}\S)|\Z)␊ |
''' % (less_than_tab, self.tab_width, self.tab_width),␊ |
re.X | re.M)␊ |
return footnote_def_re.sub(self._extract_footnote_def_sub, text)␊ |
␊ |
␊ |
_hr_data = [␊ |
('*', re.compile(r"^[ ]{0,3}\*(.*?)$", re.M)),␊ |
('-', re.compile(r"^[ ]{0,3}\-(.*?)$", re.M)),␊ |
('_', re.compile(r"^[ ]{0,3}\_(.*?)$", re.M)),␊ |
]␊ |
␊ |
def _run_block_gamut(self, text):␊ |
# These are all the transformations that form block-level␊ |
# tags like paragraphs, headers, and list items.␊ |
␊ |
if "fenced-code-blocks" in self.extras:␊ |
text = self._do_fenced_code_blocks(text)␊ |
␊ |
text = self._do_headers(text)␊ |
␊ |
# Do Horizontal Rules:␊ |
# On the number of spaces in horizontal rules: The spec is fuzzy: "If␊ |
# you wish, you may use spaces between the hyphens or asterisks."␊ |
# Markdown.pl 1.0.1's hr regexes limit the number of spaces between the␊ |
# hr chars to one or two. We'll reproduce that limit here.␊ |
hr = "\n<hr"+self.empty_element_suffix+"\n"␊ |
for ch, regex in self._hr_data:␊ |
if ch in text:␊ |
for m in reversed(list(regex.finditer(text))):␊ |
tail = m.group(1).rstrip()␊ |
if not tail.strip(ch + ' ') and tail.count(" ") == 0:␊ |
start, end = m.span()␊ |
text = text[:start] + hr + text[end:]␊ |
␊ |
text = self._do_lists(text)␊ |
␊ |
if "pyshell" in self.extras:␊ |
text = self._prepare_pyshell_blocks(text)␊ |
if "wiki-tables" in self.extras:␊ |
text = self._do_wiki_tables(text)␊ |
␊ |
text = self._do_code_blocks(text)␊ |
␊ |
text = self._do_block_quotes(text)␊ |
␊ |
# We already ran _HashHTMLBlocks() before, in Markdown(), but that␊ |
# was to escape raw HTML in the original Markdown source. This time,␊ |
# we're escaping the markup we've just created, so that we don't wrap␊ |
# <p> tags around block-level tags.␊ |
text = self._hash_html_blocks(text)␊ |
␊ |
text = self._form_paragraphs(text)␊ |
␊ |
return text␊ |
␊ |
def _pyshell_block_sub(self, match):␊ |
lines = match.group(0).splitlines(0)␊ |
_dedentlines(lines)␊ |
indent = ' ' * self.tab_width␊ |
s = ('\n' # separate from possible cuddled paragraph␊ |
+ indent + ('\n'+indent).join(lines)␊ |
+ '\n\n')␊ |
return s␊ |
␊ |
def _prepare_pyshell_blocks(self, text):␊ |
"""Ensure that Python interactive shell sessions are put in␊ |
code blocks -- even if not properly indented.␊ |
"""␊ |
if ">>>" not in text:␊ |
return text␊ |
␊ |
less_than_tab = self.tab_width - 1␊ |
_pyshell_block_re = re.compile(r"""␊ |
^([ ]{0,%d})>>>[ ].*\n # first line␊ |
^(\1.*\S+.*\n)* # any number of subsequent lines␊ |
^\n # ends with a blank line␊ |
""" % less_than_tab, re.M | re.X)␊ |
␊ |
return _pyshell_block_re.sub(self._pyshell_block_sub, text)␊ |
␊ |
def _wiki_table_sub(self, match):␊ |
ttext = match.group(0).strip()␊ |
#print 'wiki table: %r' % match.group(0)␊ |
rows = []␊ |
for line in ttext.splitlines(0):␊ |
line = line.strip()[2:-2].strip()␊ |
row = [c.strip() for c in re.split(r'(?<!\\)\|\|', line)]␊ |
rows.append(row)␊ |
#pprint(rows)␊ |
hlines = ['<table>', '<tbody>']␊ |
for row in rows:␊ |
hrow = ['<tr>']␊ |
for cell in row:␊ |
hrow.append('<td>')␊ |
hrow.append(self._run_span_gamut(cell))␊ |
hrow.append('</td>')␊ |
hrow.append('</tr>')␊ |
hlines.append(''.join(hrow))␊ |
hlines += ['</tbody>', '</table>']␊ |
return '\n'.join(hlines) + '\n'␊ |
␊ |
def _do_wiki_tables(self, text):␊ |
# Optimization.␊ |
if "||" not in text:␊ |
return text␊ |
␊ |
less_than_tab = self.tab_width - 1␊ |
wiki_table_re = re.compile(r'''␊ |
(?:(?<=\n\n)|\A\n?) # leading blank line␊ |
^([ ]{0,%d})\|\|.+?\|\|[ ]*\n # first line␊ |
(^\1\|\|.+?\|\|\n)* # any number of subsequent lines␊ |
''' % less_than_tab, re.M | re.X)␊ |
return wiki_table_re.sub(self._wiki_table_sub, text)␊ |
␊ |
def _run_span_gamut(self, text):␊ |
# These are all the transformations that occur *within* block-level␊ |
# tags like paragraphs, headers, and list items.␊ |
␊ |
text = self._do_code_spans(text)␊ |
␊ |
text = self._escape_special_chars(text)␊ |
␊ |
# Process anchor and image tags.␊ |
text = self._do_links(text)␊ |
␊ |
# Make links out of things like `<http://example.com/>`␊ |
# Must come after _do_links(), because you can use < and >␊ |
# delimiters in inline links like [this](<url>).␊ |
text = self._do_auto_links(text)␊ |
␊ |
if "link-patterns" in self.extras:␊ |
text = self._do_link_patterns(text)␊ |
␊ |
text = self._encode_amps_and_angles(text)␊ |
␊ |
text = self._do_italics_and_bold(text)␊ |
␊ |
if "smarty-pants" in self.extras:␊ |
text = self._do_smart_punctuation(text)␊ |
␊ |
# Do hard breaks:␊ |
text = re.sub(r" {2,}\n", " <br%s\n" % self.empty_element_suffix, text)␊ |
␊ |
return text␊ |
␊ |
# "Sorta" because auto-links are identified as "tag" tokens.␊ |
_sorta_html_tokenize_re = re.compile(r"""␊ |
(␊ |
# tag␊ |
</?␊ |
(?:\w+) # tag name␊ |
(?:\s+(?:[\w-]+:)?[\w-]+=(?:".*?"|'.*?'))* # attributes␊ |
\s*/?>␊ |
|␊ |
# auto-link (e.g., <http://www.activestate.com/>)␊ |
<\w+[^>]*>␊ |
|␊ |
<!--.*?--> # comment␊ |
|␊ |
<\?.*?\?> # processing instruction␊ |
)␊ |
""", re.X)␊ |
␊ |
def _escape_special_chars(self, text):␊ |
# Python markdown note: the HTML tokenization here differs from␊ |
# that in Markdown.pl, hence the behaviour for subtle cases can␊ |
# differ (I believe the tokenizer here does a better job because␊ |
# it isn't susceptible to unmatched '<' and '>' in HTML tags).␊ |
# Note, however, that '>' is not allowed in an auto-link URL␊ |
# here.␊ |
escaped = []␊ |
is_html_markup = False␊ |
for token in self._sorta_html_tokenize_re.split(text):␊ |
if is_html_markup:␊ |
# Within tags/HTML-comments/auto-links, encode * and _␊ |
# so they don't conflict with their use in Markdown for␊ |
# italics and strong. We're replacing each such␊ |
# character with its corresponding MD5 checksum value;␊ |
# this is likely overkill, but it should prevent us from␊ |
# colliding with the escape values by accident.␊ |
escaped.append(token.replace('*', self._escape_table['*'])␊ |
.replace('_', self._escape_table['_']))␊ |
else:␊ |
escaped.append(self._encode_backslash_escapes(token))␊ |
is_html_markup = not is_html_markup␊ |
return ''.join(escaped)␊ |
␊ |
def _hash_html_spans(self, text):␊ |
# Used for safe_mode.␊ |
␊ |
def _is_auto_link(s):␊ |
if ':' in s and self._auto_link_re.match(s):␊ |
return True␊ |
elif '@' in s and self._auto_email_link_re.match(s):␊ |
return True␊ |
return False␊ |
␊ |
tokens = []␊ |
is_html_markup = False␊ |
for token in self._sorta_html_tokenize_re.split(text):␊ |
if is_html_markup and not _is_auto_link(token):␊ |
sanitized = self._sanitize_html(token)␊ |
key = _hash_text(sanitized)␊ |
self.html_spans[key] = sanitized␊ |
tokens.append(key)␊ |
else:␊ |
tokens.append(token)␊ |
is_html_markup = not is_html_markup␊ |
return ''.join(tokens)␊ |
␊ |
def _unhash_html_spans(self, text):␊ |
for key, sanitized in list(self.html_spans.items()):␊ |
text = text.replace(key, sanitized)␊ |
return text␊ |
␊ |
def _sanitize_html(self, s):␊ |
if self.safe_mode == "replace":␊ |
return self.html_removed_text␊ |
elif self.safe_mode == "escape":␊ |
replacements = [␊ |
('&', '&'),␊ |
('<', '<'),␊ |
('>', '>'),␊ |
]␊ |
for before, after in replacements:␊ |
s = s.replace(before, after)␊ |
return s␊ |
else:␊ |
raise MarkdownError("invalid value for 'safe_mode': %r (must be "␊ |
"'escape' or 'replace')" % self.safe_mode)␊ |
␊ |
_tail_of_inline_link_re = re.compile(r'''␊ |
# Match tail of: [text](/url/) or [text](/url/ "title")␊ |
\( # literal paren␊ |
[ \t]*␊ |
(?P<url> # \1␊ |
<.*?>␊ |
|␊ |
.*?␊ |
)␊ |
[ \t]*␊ |
( # \2␊ |
(['"]) # quote char = \3␊ |
(?P<title>.*?)␊ |
\3 # matching quote␊ |
)? # title is optional␊ |
\)␊ |
''', re.X | re.S)␊ |
_tail_of_reference_link_re = re.compile(r'''␊ |
# Match tail of: [text][id]␊ |
[ ]? # one optional space␊ |
(?:\n[ ]*)? # one optional newline followed by spaces␊ |
\[␊ |
(?P<id>.*?)␊ |
\]␊ |
''', re.X | re.S)␊ |
␊ |
def _do_links(self, text):␊ |
"""Turn Markdown link shortcuts into XHTML <a> and <img> tags.␊ |
␊ |
This is a combination of Markdown.pl's _DoAnchors() and␊ |
_DoImages(). They are done together because that simplified the␊ |
approach. It was necessary to use a different approach than␊ |
Markdown.pl because of the lack of atomic matching support in␊ |
Python's regex engine used in $g_nested_brackets.␊ |
"""␊ |
MAX_LINK_TEXT_SENTINEL = 3000 # markdown2 issue 24␊ |
␊ |
# `anchor_allowed_pos` is used to support img links inside␊ |
# anchors, but not anchors inside anchors. An anchor's start␊ |
# pos must be `>= anchor_allowed_pos`.␊ |
anchor_allowed_pos = 0␊ |
␊ |
curr_pos = 0␊ |
while True: # Handle the next link.␊ |
# The next '[' is the start of:␊ |
# - an inline anchor: [text](url "title")␊ |
# - a reference anchor: [text][id]␊ |
# - an inline img: ␊ |
# - a reference img: ![text][id]␊ |
# - a footnote ref: [^id]␊ |
# (Only if 'footnotes' extra enabled)␊ |
# - a footnote defn: [^id]: ...␊ |
# (Only if 'footnotes' extra enabled) These have already␊ |
# been stripped in _strip_footnote_definitions() so no␊ |
# need to watch for them.␊ |
# - a link definition: [id]: url "title"␊ |
# These have already been stripped in␊ |
# _strip_link_definitions() so no need to watch for them.␊ |
# - not markup: [...anything else...␊ |
try:␊ |
start_idx = text.index('[', curr_pos)␊ |
except ValueError:␊ |
break␊ |
text_length = len(text)␊ |
␊ |
# Find the matching closing ']'.␊ |
# Markdown.pl allows *matching* brackets in link text so we␊ |
# will here too. Markdown.pl *doesn't* currently allow␊ |
# matching brackets in img alt text -- we'll differ in that␊ |
# regard.␊ |
bracket_depth = 0␊ |
for p in range(start_idx+1, min(start_idx+MAX_LINK_TEXT_SENTINEL,␊ |
text_length)):␊ |
ch = text[p]␊ |
if ch == ']':␊ |
bracket_depth -= 1␊ |
if bracket_depth < 0:␊ |
break␊ |
elif ch == '[':␊ |
bracket_depth += 1␊ |
else:␊ |
# Closing bracket not found within sentinel length.␊ |
# This isn't markup.␊ |
curr_pos = start_idx + 1␊ |
continue␊ |
link_text = text[start_idx+1:p]␊ |
␊ |
# Possibly a footnote ref?␊ |
if "footnotes" in self.extras and link_text.startswith("^"):␊ |
normed_id = re.sub(r'\W', '-', link_text[1:])␊ |
if normed_id in self.footnotes:␊ |
self.footnote_ids.append(normed_id)␊ |
result = '<sup class="footnote-ref" id="fnref-%s">' \␊ |
'<a href="#fn-%s">%s</a></sup>' \␊ |
% (normed_id, normed_id, len(self.footnote_ids))␊ |
text = text[:start_idx] + result + text[p+1:]␊ |
else:␊ |
# This id isn't defined, leave the markup alone.␊ |
curr_pos = p+1␊ |
continue␊ |
␊ |
# Now determine what this is by the remainder.␊ |
p += 1␊ |
if p == text_length:␊ |
return text␊ |
␊ |
# Inline anchor or img?␊ |
if text[p] == '(': # attempt at perf improvement␊ |
match = self._tail_of_inline_link_re.match(text, p)␊ |
if match:␊ |
# Handle an inline anchor or img.␊ |
is_img = start_idx > 0 and text[start_idx-1] == "!"␊ |
if is_img:␊ |
start_idx -= 1␊ |
␊ |
␉␉ is_inline_img = start_idx > 0 and text[start_idx-1] == "#"␊ |
␉␉ if is_inline_img:␊ |
␉␉␉ start_idx -= 1␊ |
␉␉␉ is_img = 1␊ |
␊ |
url, title = match.group("url"), match.group("title")␊ |
if url and url[0] == '<':␊ |
url = url[1:-1] # '<url>' -> 'url'␊ |
# We've got to encode these to avoid conflicting␊ |
# with italics/bold.␊ |
url = url.replace('*', self._escape_table['*']) \␊ |
.replace('_', self._escape_table['_'])␊ |
if title:␊ |
title_str = ' title="%s"' % (␊ |
_xml_escape_attr(title)␊ |
.replace('*', self._escape_table['*'])␊ |
.replace('_', self._escape_table['_']))␊ |
else:␊ |
title_str = ''␊ |
if is_img:␊ |
if is_inline_img:␊ |
␉␉␉␉result = '<img class="inlineimage" src="%s" alt="%s"%s%s' \␊ |
␉␉␉␉ % (url.replace('"', '"'),␊ |
␉␉␉␉ _xml_escape_attr(link_text),␊ |
␉␉␉␉ title_str, self.empty_element_suffix)␊ |
else:␊ |
result = '<img src="%s" alt="%s"%s%s' \␊ |
␉␉␉␉% (url.replace('"', '"'),␊ |
␉␉␉␉ _xml_escape_attr(link_text),␊ |
␉␉␉␉ title_str, self.empty_element_suffix)␊ |
if "smarty-pants" in self.extras:␊ |
result = result.replace('"', self._escape_table['"'])␊ |
curr_pos = start_idx + len(result)␊ |
text = text[:start_idx] + result + text[match.end():]␊ |
elif start_idx >= anchor_allowed_pos:␊ |
result_head = '<a href="%s"%s>' % (url, title_str)␊ |
result = '%s%s</a>' % (result_head, link_text)␊ |
if "smarty-pants" in self.extras:␊ |
result = result.replace('"', self._escape_table['"'])␊ |
# <img> allowed from curr_pos on, <a> from␊ |
# anchor_allowed_pos on.␊ |
curr_pos = start_idx + len(result_head)␊ |
anchor_allowed_pos = start_idx + len(result)␊ |
text = text[:start_idx] + result + text[match.end():]␊ |
else:␊ |
# Anchor not allowed here.␊ |
curr_pos = start_idx + 1␊ |
continue␊ |
␊ |
# Reference anchor or img?␊ |
else:␊ |
match = self._tail_of_reference_link_re.match(text, p)␊ |
if match:␊ |
# Handle a reference-style anchor or img.␊ |
is_img = start_idx > 0 and text[start_idx-1] == "!"␊ |
if is_img:␊ |
start_idx -= 1␊ |
link_id = match.group("id").lower()␊ |
if not link_id:␊ |
link_id = link_text.lower() # for links like [this][]␊ |
if link_id in self.urls:␊ |
url = self.urls[link_id]␊ |
# We've got to encode these to avoid conflicting␊ |
# with italics/bold.␊ |
url = url.replace('*', self._escape_table['*']) \␊ |
.replace('_', self._escape_table['_'])␊ |
title = self.titles.get(link_id)␊ |
if title:␊ |
before = title␊ |
title = _xml_escape_attr(title) \␊ |
.replace('*', self._escape_table['*']) \␊ |
.replace('_', self._escape_table['_'])␊ |
title_str = ' title="%s"' % title␊ |
else:␊ |
title_str = ''␊ |
if is_img:␊ |
result = '<img src="%s" alt="%s"%s%s' \␊ |
% (url.replace('"', '"'),␊ |
link_text.replace('"', '"'),␊ |
title_str, self.empty_element_suffix)␊ |
if "smarty-pants" in self.extras:␊ |
result = result.replace('"', self._escape_table['"'])␊ |
curr_pos = start_idx + len(result)␊ |
text = text[:start_idx] + result + text[match.end():]␊ |
elif start_idx >= anchor_allowed_pos:␊ |
result = '<a href="%s"%s>%s</a>' \␊ |
% (url, title_str, link_text)␊ |
result_head = '<a href="%s"%s>' % (url, title_str)␊ |
result = '%s%s</a>' % (result_head, link_text)␊ |
if "smarty-pants" in self.extras:␊ |
result = result.replace('"', self._escape_table['"'])␊ |
# <img> allowed from curr_pos on, <a> from␊ |
# anchor_allowed_pos on.␊ |
curr_pos = start_idx + len(result_head)␊ |
anchor_allowed_pos = start_idx + len(result)␊ |
text = text[:start_idx] + result + text[match.end():]␊ |
else:␊ |
# Anchor not allowed here.␊ |
curr_pos = start_idx + 1␊ |
else:␊ |
# This id isn't defined, leave the markup alone.␊ |
curr_pos = match.end()␊ |
continue␊ |
␊ |
# Otherwise, it isn't markup.␊ |
curr_pos = start_idx + 1␊ |
␊ |
return text␊ |
␊ |
def header_id_from_text(self, text, prefix, n):␊ |
"""Generate a header id attribute value from the given header␊ |
HTML content.␊ |
␊ |
This is only called if the "header-ids" extra is enabled.␊ |
Subclasses may override this for different header ids.␊ |
␊ |
@param text {str} The text of the header tag␊ |
@param prefix {str} The requested prefix for header ids. This is the␊ |
value of the "header-ids" extra key, if any. Otherwise, None.␊ |
@param n {int} The <hN> tag number, i.e. `1` for an <h1> tag.␊ |
@returns {str} The value for the header tag's "id" attribute. Return␊ |
None to not have an id attribute and to exclude this header from␊ |
the TOC (if the "toc" extra is specified).␊ |
"""␊ |
header_id = _slugify(text)␊ |
if prefix and isinstance(prefix, base_string_type):␊ |
header_id = prefix + '-' + header_id␊ |
if header_id in self._count_from_header_id:␊ |
self._count_from_header_id[header_id] += 1␊ |
header_id += '-%s' % self._count_from_header_id[header_id]␊ |
else:␊ |
self._count_from_header_id[header_id] = 1␊ |
return header_id␊ |
␊ |
_toc = None␊ |
def _toc_add_entry(self, level, id, name):␊ |
if self._toc is None:␊ |
self._toc = []␊ |
self._toc.append((level, id, self._unescape_special_chars(name)))␊ |
␊ |
_setext_h_re = re.compile(r'^(.+)[ \t]*\n(=+|-+)[ \t]*\n+', re.M)␊ |
def _setext_h_sub(self, match):␊ |
n = {"=": 1, "-": 2}[match.group(2)[0]]␊ |
demote_headers = self.extras.get("demote-headers")␊ |
if demote_headers:␊ |
n = min(n + demote_headers, 6)␊ |
header_id_attr = ""␊ |
if "header-ids" in self.extras:␊ |
header_id = self.header_id_from_text(match.group(1),␊ |
self.extras["header-ids"], n)␊ |
if header_id:␊ |
header_id_attr = ' id="%s"' % header_id␊ |
html = self._run_span_gamut(match.group(1))␊ |
if "toc" in self.extras and header_id:␊ |
self._toc_add_entry(n, header_id, html)␊ |
return "<h%d%s>%s</h%d>\n\n" % (n, header_id_attr, html, n)␊ |
␊ |
_atx_h_re = re.compile(r'''␊ |
^(\#{1,6}) # \1 = string of #'s␊ |
[ \t]+␊ |
(.+?) # \2 = Header text␊ |
[ \t]*␊ |
(?<!\\) # ensure not an escaped trailing '#'␊ |
\#* # optional closing #'s (not counted)␊ |
\n+␊ |
''', re.X | re.M)␊ |
def _atx_h_sub(self, match):␊ |
n = len(match.group(1))␊ |
demote_headers = self.extras.get("demote-headers")␊ |
if demote_headers:␊ |
n = min(n + demote_headers, 6)␊ |
header_id_attr = ""␊ |
if "header-ids" in self.extras:␊ |
header_id = self.header_id_from_text(match.group(2),␊ |
self.extras["header-ids"], n)␊ |
if header_id:␊ |
header_id_attr = ' id="%s"' % header_id␊ |
html = self._run_span_gamut(match.group(2))␊ |
if "toc" in self.extras and header_id:␊ |
self._toc_add_entry(n, header_id, html)␊ |
return "<h%d%s>%s</h%d>\n\n" % (n, header_id_attr, html, n)␊ |
␊ |
def _do_headers(self, text):␊ |
# Setext-style headers:␊ |
# Header 1␊ |
# ========␊ |
#␊ |
# Header 2␊ |
# --------␊ |
text = self._setext_h_re.sub(self._setext_h_sub, text)␊ |
␊ |
# atx-style headers:␊ |
# # Header 1␊ |
# ## Header 2␊ |
# ## Header 2 with closing hashes ##␊ |
# ...␊ |
# ###### Header 6␊ |
text = self._atx_h_re.sub(self._atx_h_sub, text)␊ |
␊ |
return text␊ |
␊ |
␊ |
_marker_ul_chars = '*+-'␊ |
_marker_any = r'(?:[%s]|\d+\.)' % _marker_ul_chars␊ |
_marker_ul = '(?:[%s])' % _marker_ul_chars␊ |
_marker_ol = r'(?:\d+\.)'␊ |
␊ |
def _list_sub(self, match):␊ |
lst = match.group(1)␊ |
lst_type = match.group(3) in self._marker_ul_chars and "ul" or "ol"␊ |
result = self._process_list_items(lst)␊ |
if self.list_level:␊ |
return "<%s>\n%s</%s>\n" % (lst_type, result, lst_type)␊ |
else:␊ |
return "<%s>\n%s</%s>\n\n" % (lst_type, result, lst_type)␊ |
␊ |
def _do_lists(self, text):␊ |
# Form HTML ordered (numbered) and unordered (bulleted) lists.␊ |
␊ |
# Iterate over each *non-overlapping* list match.␊ |
pos = 0␊ |
while True:␊ |
# Find the *first* hit for either list style (ul or ol). We␊ |
# match ul and ol separately to avoid adjacent lists of different␊ |
# types running into each other (see issue #16).␊ |
hits = []␊ |
for marker_pat in (self._marker_ul, self._marker_ol):␊ |
less_than_tab = self.tab_width - 1␊ |
whole_list = r'''␊ |
( # \1 = whole list␊ |
( # \2␊ |
[ ]{0,%d}␊ |
(%s) # \3 = first list item marker␊ |
[ \t]+␊ |
(?!\ *\3\ ) # '- - - ...' isn't a list. See 'not_quite_a_list' test case.␊ |
)␊ |
(?:.+?)␊ |
( # \4␊ |
\Z␊ |
|␊ |
\n{2,}␊ |
(?=\S)␊ |
(?! # Negative lookahead for another list item marker␊ |
[ \t]*␊ |
%s[ \t]+␊ |
)␊ |
)␊ |
)␊ |
''' % (less_than_tab, marker_pat, marker_pat)␊ |
if self.list_level: # sub-list␊ |
list_re = re.compile("^"+whole_list, re.X | re.M | re.S)␊ |
else:␊ |
list_re = re.compile(r"(?:(?<=\n\n)|\A\n?)"+whole_list,␊ |
re.X | re.M | re.S)␊ |
match = list_re.search(text, pos)␊ |
if match:␊ |
hits.append((match.start(), match))␊ |
if not hits:␊ |
break␊ |
hits.sort()␊ |
match = hits[0][1]␊ |
start, end = match.span()␊ |
text = text[:start] + self._list_sub(match) + text[end:]␊ |
pos = end␊ |
␊ |
return text␊ |
␊ |
_list_item_re = re.compile(r'''␊ |
(\n)? # leading line = \1␊ |
(^[ \t]*) # leading whitespace = \2␊ |
(?P<marker>%s) [ \t]+ # list marker = \3␊ |
((?:.+?) # list item text = \4␊ |
(\n{1,2})) # eols = \5␊ |
(?= \n* (\Z | \2 (?P<next_marker>%s) [ \t]+))␊ |
''' % (_marker_any, _marker_any),␊ |
re.M | re.X | re.S)␊ |
␊ |
_last_li_endswith_two_eols = False␊ |
def _list_item_sub(self, match):␊ |
item = match.group(4)␊ |
leading_line = match.group(1)␊ |
leading_space = match.group(2)␊ |
if leading_line or "\n\n" in item or self._last_li_endswith_two_eols:␊ |
item = self._run_block_gamut(self._outdent(item))␊ |
else:␊ |
# Recursion for sub-lists:␊ |
item = self._do_lists(self._outdent(item))␊ |
if item.endswith('\n'):␊ |
item = item[:-1]␊ |
item = self._run_span_gamut(item)␊ |
self._last_li_endswith_two_eols = (len(match.group(5)) == 2)␊ |
return "<li>%s</li>\n" % item␊ |
␊ |
def _process_list_items(self, list_str):␊ |
# Process the contents of a single ordered or unordered list,␊ |
# splitting it into individual list items.␊ |
␊ |
# The $g_list_level global keeps track of when we're inside a list.␊ |
# Each time we enter a list, we increment it; when we leave a list,␊ |
# we decrement. If it's zero, we're not in a list anymore.␊ |
#␊ |
# We do this because when we're not inside a list, we want to treat␊ |
# something like this:␊ |
#␊ |
# I recommend upgrading to version␊ |
# 8. Oops, now this line is treated␊ |
# as a sub-list.␊ |
#␊ |
# As a single paragraph, despite the fact that the second line starts␊ |
# with a digit-period-space sequence.␊ |
#␊ |
# Whereas when we're inside a list (or sub-list), that line will be␊ |
# treated as the start of a sub-list. What a kludge, huh? This is␊ |
# an aspect of Markdown's syntax that's hard to parse perfectly␊ |
# without resorting to mind-reading. Perhaps the solution is to␊ |
# change the syntax rules such that sub-lists must start with a␊ |
# starting cardinal number; e.g. "1." or "a.".␊ |
self.list_level += 1␊ |
self._last_li_endswith_two_eols = False␊ |
list_str = list_str.rstrip('\n') + '\n'␊ |
list_str = self._list_item_re.sub(self._list_item_sub, list_str)␊ |
self.list_level -= 1␊ |
return list_str␊ |
␊ |
def _get_pygments_lexer(self, lexer_name):␊ |
try:␊ |
from pygments import lexers, util␊ |
except ImportError:␊ |
return None␊ |
try:␊ |
return lexers.get_lexer_by_name(lexer_name)␊ |
except util.ClassNotFound:␊ |
return None␊ |
␊ |
def _color_with_pygments(self, codeblock, lexer, **formatter_opts):␊ |
import pygments␊ |
import pygments.formatters␊ |
␊ |
class HtmlCodeFormatter(pygments.formatters.HtmlFormatter):␊ |
def _wrap_code(self, inner):␊ |
"""A function for use in a Pygments Formatter which␊ |
wraps in <code> tags.␊ |
"""␊ |
yield 0, "<code>"␊ |
for tup in inner:␊ |
yield tup␊ |
yield 0, "</code>"␊ |
␊ |
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:␊ |
lexer = self._get_pygments_lexer(lexer_name)␊ |
if lexer:␊ |
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<pre%s><code%s>%s\n</code></pre>\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 `<pre><code>` 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␊ |
''' % (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'''␊ |
(?<!\\)␊ |
(`+) # \1 = Opening run of `␊ |
(?!`) # See Note A test/tm-cases/escapes.text␊ |
(.+?) # \2 = The code block␊ |
(?<!`)␊ |
\1 # Matching closer␊ |
(?!`)␊ |
''', re.X | re.S)␊ |
␊ |
def _code_span_sub(self, match):␊ |
c = match.group(2).strip(" \t")␊ |
c = self._encode_code(c)␊ |
return "<code>%s</code>" % c␊ |
␊ |
def _do_code_spans(self, text):␊ |
# * Backtick quotes are used for <code></code> 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:␊ |
#␊ |
# <p>Just type <code>foo `bar` baz</code> at the prompt.</p>␊ |
#␊ |
# There's no arbitrary limit to the number of backticks you␊ |
# can use as delimters. If you need three consecutive backticks␊ |
# in your code, use four for delimiters, etc.␊ |
#␊ |
# * You can use spaces to get literal backticks at the edges:␊ |
#␊ |
# ... type `` `bar` `` ...␊ |
#␊ |
# Turns to:␊ |
#␊ |
# ... type <code>`bar`</code> ...␊ |
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)␊ |
_code_friendly_line_re = re.compile(r"\~\~(?=\S)(.+?)(?<=\S)\~\~", re.S)␊ |
_code_friendly_underline_re = re.compile(r"\~(?=\S)(.+?)(?<=\S)\~", re.S)␊ |
def _do_italics_and_bold(self, text):␊ |
# <strong> must go first:␊ |
if "code-friendly" in self.extras:␊ |
text = self._code_friendly_strong_re.sub(r"<strong>\1</strong>", text)␊ |
text = self._code_friendly_em_re.sub(r"<em>\1</em>", text)␊ |
␉ text = self._code_friendly_line_re.sub(r"<span style='text-decoration:line-through'>\1</span>", text)␊ |
␉ text = self._code_friendly_underline_re.sub(r"<span style='text-decoration:underline'>\1</span>", text)␊ |
else:␊ |
text = self._strong_re.sub(r"<strong>\2</strong>", text)␊ |
text = self._em_re.sub(r"<em>\2</em>", text)␊ |
␉ text = self._code_friendly_line_re.sub(r"<span style='text-decoration:line-through'>\1</span>", text)␊ |
␉ text = self._code_friendly_underline_re.sub(r"<span style='text-decoration:underline'>\1</span>", 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"(?<!\S)'(?=\S)")␊ |
_opening_double_quote_re = re.compile(r'(?<!\S)"(?=\S)')␊ |
_closing_single_quote_re = re.compile(r"(?<=\S)'")␊ |
_closing_double_quote_re = re.compile(r'(?<=\S)"(?=(\s|,|;|\.|\?|!|$))')␊ |
def _do_smart_punctuation(self, text):␊ |
"""Fancifies 'single quotes', "double quotes", and apostrophes.␊ |
Converts --, ---, and ... into en dashes, em dashes, and ellipses.␊ |
␊ |
Inspiration is: <http://daringfireball.net/projects/smartypants/>␊ |
See "test/tm-cases/smarty_pants.text" for a full discussion of the␊ |
support here and␊ |
<http://code.google.com/p/python-markdown2/issues/detail?id=42> for a␊ |
discussion of some diversion from the original SmartyPants.␊ |
"""␊ |
if "'" in text: # guard for perf␊ |
text = self._do_smart_contractions(text)␊ |
text = self._opening_single_quote_re.sub("‘", text)␊ |
text = self._closing_single_quote_re.sub("’", text)␊ |
␊ |
if '"' in text: # guard for perf␊ |
text = self._opening_double_quote_re.sub("“", text)␊ |
text = self._closing_double_quote_re.sub("”", text)␊ |
␊ |
text = text.replace("---", "—")␊ |
text = text.replace("--", "–")␊ |
text = text.replace("...", "…")␊ |
text = text.replace(" . . . ", "…")␊ |
text = text.replace(". . .", "…")␊ |
return text␊ |
␊ |
_block_quote_re = re.compile(r'''␊ |
( # Wrap whole match in \1␊ |
(␊ |
^[ \t]*>[ \t]? # '>' at the start of a line␊ |
.+\n # rest of the first line␊ |
(.+\n)* # subsequent consecutive lines␊ |
\n* # blanks␊ |
)+␊ |
)␊ |
''', re.M | re.X)␊ |
_bq_one_level_re = re.compile('^[ \t]*>[ \t]?', re.M);␊ |
␊ |
_html_pre_block_re = re.compile(r'(\s*<pre>.+?</pre>)', 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 <pre> content, so we need to fix that:␊ |
bq = self._html_pre_block_re.sub(self._dedent_two_spaces_sub, bq)␊ |
␊ |
return "<blockquote>\n%s\n</blockquote>\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')␊ |
␊ |
# Wrap <p> tags.␊ |
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("<ul>") or cuddled_list.startswith("<ol>")␊ |
graf = graf[:start]␊ |
␊ |
# Wrap <p> tags.␊ |
graf = self._run_span_gamut(graf)␊ |
grafs.append("<p>" + graf.lstrip(" \t") + "</p>")␊ |
␊ |
if cuddled_list:␊ |
grafs.append(cuddled_list)␊ |
␊ |
return "\n\n".join(grafs)␊ |
␊ |
def _add_footnotes(self, text):␊ |
if self.footnotes:␊ |
footer = [␊ |
'<div class="footnotes">',␊ |
'<hr' + self.empty_element_suffix,␊ |
'<ol>',␊ |
]␊ |
for i, id in enumerate(self.footnote_ids):␊ |
if i != 0:␊ |
footer.append('')␊ |
footer.append('<li id="fn-%s">' % id)␊ |
footer.append(self._run_block_gamut(self.footnotes[id]))␊ |
backlink = ('<a href="#fnref-%s" '␊ |
'class="footnoteBackLink" '␊ |
'title="Jump back to footnote %d in the text.">'␊ |
'↩</a>' % (id, i+1))␊ |
if footer[-1].endswith("</p>"):␊ |
footer[-1] = footer[-1][:-len("</p>")] \␊ |
+ ' ' + backlink + "</p>"␊ |
else:␊ |
footer.append("\n<p>%s</p>" % backlink)␊ |
footer.append('</li>')␊ |
footer.append('</ol>')␊ |
footer.append('</div>')␊ |
return text + '\n\n' + '\n'.join(footer)␊ |
else:␊ |
return text␊ |
␊ |
# Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:␊ |
# http://bumppo.net/projects/amputator/␊ |
_ampersand_re = re.compile(r'&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)')␊ |
_naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I)␊ |
_naked_gt_re = re.compile(r'''(?<![a-z0-9?!/'"-])>''', re.I)␊ |
␊ |
def _encode_amps_and_angles(self, text):␊ |
# Smart processing for ampersands and angle brackets that need␊ |
# to be encoded.␊ |
text = self._ampersand_re.sub('&', text)␊ |
␊ |
# Encode naked <'s␊ |
text = self._naked_lt_re.sub('<', text)␊ |
␊ |
# Encode naked >'s␊ |
# Note: Other markdown implementations (e.g. Markdown.pl, PHP␊ |
# Markdown) don't do this.␊ |
text = self._naked_gt_re.sub('>', text)␊ |
return text␊ |
␊ |
def _encode_backslash_escapes(self, text):␊ |
for ch, escape in list(self._escape_table.items()):␊ |
text = text.replace("\\"+ch, escape)␊ |
return text␊ |
␊ |
_auto_link_re = re.compile(r'<((https?|ftp):[^\'">\s]+)>', re.I)␊ |
def _auto_link_sub(self, match):␊ |
g1 = match.group(1)␊ |
return '<a href="%s">%s</a>' % (g1, g1)␊ |
␊ |
_auto_email_link_re = re.compile(r"""␊ |
<␊ |
(?:mailto:)?␊ |
(␊ |
[-.\w]+␊ |
\@␊ |
[-\w]+(\.[-\w]+)*\.[a-z]+␊ |
)␊ |
>␊ |
""", re.I | re.X | re.U)␊ |
def _auto_email_link_sub(self, match):␊ |
return self._encode_email_address(␊ |
self._unescape_special_chars(match.group(1)))␊ |
␊ |
def _do_auto_links(self, text):␊ |
text = self._auto_link_re.sub(self._auto_link_sub, text)␊ |
text = self._auto_email_link_re.sub(self._auto_email_link_sub, text)␊ |
return text␊ |
␊ |
def _encode_email_address(self, addr):␊ |
# Input: an email address, e.g. "foo@example.com"␊ |
#␊ |
# Output: the email address as a mailto link, with each character␊ |
# of the address encoded as either a decimal or hex entity, in␊ |
# the hopes of foiling most address harvesting spam bots. E.g.:␊ |
#␊ |
# <a href="mailto:foo@e␊ |
# xample.com">foo␊ |
# @example.com</a>␊ |
#␊ |
# Based on a filter by Matthew Wickline, posted to the BBEdit-Talk␊ |
# mailing list: <http://tinyurl.com/yu7ue>␊ |
chars = [_xml_encode_email_char_at_random(ch)␊ |
for ch in "mailto:" + addr]␊ |
# Strip the mailto: from the visible part.␊ |
addr = '<a href="%s">%s</a>' \␊ |
% (''.join(chars), ''.join(chars[7:]))␊ |
return addr␊ |
␊ |
def _do_link_patterns(self, text):␊ |
"""Caveat emptor: there isn't much guarding against link␊ |
patterns being formed inside other standard Markdown links, e.g.␊ |
inside a [link def][like this].␊ |
␊ |
Dev Notes: *Could* consider prefixing regexes with a negative␊ |
lookbehind assertion to attempt to guard against this.␊ |
"""␊ |
link_from_hash = {}␊ |
for regex, repl in self.link_patterns:␊ |
replacements = []␊ |
for match in regex.finditer(text):␊ |
if hasattr(repl, "__call__"):␊ |
href = repl(match)␊ |
else:␊ |
href = match.expand(repl)␊ |
replacements.append((match.span(), href))␊ |
for (start, end), href in reversed(replacements):␊ |
escaped_href = (␊ |
href.replace('"', '"') # b/c of attr quote␊ |
# To avoid markdown <em> and <strong>:␊ |
.replace('*', self._escape_table['*'])␊ |
.replace('_', self._escape_table['_']))␊ |
link = '<a href="%s">%s</a>' % (escaped_href, text[start:end])␊ |
hash = _hash_text(link)␊ |
link_from_hash[hash] = link␊ |
text = text[:start] + hash + text[end:]␊ |
for hash, link in list(link_from_hash.items()):␊ |
text = text.replace(hash, link)␊ |
return text␊ |
␊ |
def _unescape_special_chars(self, text):␊ |
# Swap back in all the special characters we've hidden.␊ |
for ch, hash in list(self._escape_table.items()):␊ |
text = text.replace(hash, ch)␊ |
return text␊ |
␊ |
def _outdent(self, text):␊ |
# Remove one level of line-leading tabs or spaces␊ |
return self._outdent_re.sub('', text)␊ |
␊ |
␊ |
class MarkdownWithExtras(Markdown):␊ |
"""A markdowner class that enables most extras:␊ |
␊ |
- footnotes␊ |
- code-color (only has effect if 'pygments' Python module on path)␊ |
␊ |
These are not included:␊ |
- pyshell (specific to Python-related documenting)␊ |
- code-friendly (because it *disables* part of the syntax)␊ |
- link-patterns (because you need to specify some actual␊ |
link-patterns anyway)␊ |
"""␊ |
extras = ["footnotes", "code-color"]␊ |
␊ |
␊ |
#---- internal support functions␊ |
␊ |
class UnicodeWithAttrs(unicode):␊ |
"""A subclass of unicode used for the return value of conversion to␊ |
possibly attach some attributes. E.g. the "toc_html" attribute when␊ |
the "toc" extra is used.␊ |
"""␊ |
metadata = None␊ |
_toc = None␊ |
def toc_html(self):␊ |
"""Return the HTML for the current TOC.␊ |
␊ |
This expects the `_toc` attribute to have been set on this instance.␊ |
"""␊ |
if self._toc is None:␊ |
return None␊ |
␊ |
def indent():␊ |
return ' ' * (len(h_stack) - 1)␊ |
lines = []␊ |
h_stack = [0] # stack of header-level numbers␊ |
for level, id, name in self._toc:␊ |
if level > h_stack[-1]:␊ |
lines.append("%s<ul>" % indent())␊ |
h_stack.append(level)␊ |
elif level == h_stack[-1]:␊ |
lines[-1] += "</li>"␊ |
else:␊ |
while level < h_stack[-1]:␊ |
h_stack.pop()␊ |
if not lines[-1].endswith("</li>"):␊ |
lines[-1] += "</li>"␊ |
lines.append("%s</ul></li>" % indent())␊ |
lines.append('%s<li><a href="#%s">%s</a>' % (␊ |
indent(), id, name))␊ |
while len(h_stack) > 1:␊ |
h_stack.pop()␊ |
if not lines[-1].endswith("</li>"):␊ |
lines[-1] += "</li>"␊ |
lines.append("%s</ul>" % indent())␊ |
return '\n'.join(lines) + '\n'␊ |
toc_html = property(toc_html)␊ |
␊ |
## {{{ http://code.activestate.com/recipes/577257/ (r1)␊ |
_slugify_strip_re = re.compile(r'[^\w\s-]')␊ |
_slugify_hyphenate_re = re.compile(r'[-\s]+')␊ |
def _slugify(value):␊ |
"""␊ |
Normalizes string, converts to lowercase, removes non-alpha characters,␊ |
and converts spaces to hyphens.␊ |
␊ |
From Django's "django/template/defaultfilters.py".␊ |
"""␊ |
import unicodedata␊ |
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode()␊ |
value = _slugify_strip_re.sub('', value).strip().lower()␊ |
return _slugify_hyphenate_re.sub('-', value)␊ |
## end of http://code.activestate.com/recipes/577257/ }}}␊ |
␊ |
␊ |
# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549␊ |
def _curry(*args, **kwargs):␊ |
function, args = args[0], args[1:]␊ |
def result(*rest, **kwrest):␊ |
combined = kwargs.copy()␊ |
combined.update(kwrest)␊ |
return function(*args + rest, **combined)␊ |
return result␊ |
␊ |
# Recipe: regex_from_encoded_pattern (1.0)␊ |
def _regex_from_encoded_pattern(s):␊ |
"""'foo' -> re.compile(re.escape('foo'))␊ |
'/foo/' -> re.compile('foo')␊ |
'/foo/i' -> re.compile('foo', re.I)␊ |
"""␊ |
if s.startswith('/') and s.rfind('/') != 0:␊ |
# Parse it: /PATTERN/FLAGS␊ |
idx = s.rfind('/')␊ |
pattern, flags_str = s[1:idx], s[idx+1:]␊ |
flag_from_char = {␊ |
"i": re.IGNORECASE,␊ |
"l": re.LOCALE,␊ |
"s": re.DOTALL,␊ |
"m": re.MULTILINE,␊ |
"u": re.UNICODE,␊ |
}␊ |
flags = 0␊ |
for char in flags_str:␊ |
try:␊ |
flags |= flag_from_char[char]␊ |
except KeyError:␊ |
raise ValueError("unsupported regex flag: '%s' in '%s' "␊ |
"(must be one of '%s')"␊ |
% (char, s, ''.join(list(flag_from_char.keys()))))␊ |
return re.compile(s[1:idx], flags)␊ |
else: # not an encoded regex␊ |
return re.compile(re.escape(s))␊ |
␊ |
# Recipe: dedent (0.1.2)␊ |
def _dedentlines(lines, tabsize=8, skip_first_line=False):␊ |
"""_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines␊ |
␊ |
"lines" is a list of lines to dedent.␊ |
"tabsize" is the tab width to use for indent width calculations.␊ |
"skip_first_line" is a boolean indicating if the first line should␊ |
be skipped for calculating the indent width and for dedenting.␊ |
This is sometimes useful for docstrings and similar.␊ |
␊ |
Same as dedent() except operates on a sequence of lines. Note: the␊ |
lines list is modified **in-place**.␊ |
"""␊ |
DEBUG = False␊ |
if DEBUG:␊ |
print("dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\␊ |
% (tabsize, skip_first_line))␊ |
indents = []␊ |
margin = None␊ |
for i, line in enumerate(lines):␊ |
if i == 0 and skip_first_line: continue␊ |
indent = 0␊ |
for ch in line:␊ |
if ch == ' ':␊ |
indent += 1␊ |
elif ch == '\t':␊ |
indent += tabsize - (indent % tabsize)␊ |
elif ch in '\r\n':␊ |
continue # skip all-whitespace lines␊ |
else:␊ |
break␊ |
else:␊ |
continue # skip all-whitespace lines␊ |
if DEBUG: print("dedent: indent=%d: %r" % (indent, line))␊ |
if margin is None:␊ |
margin = indent␊ |
else:␊ |
margin = min(margin, indent)␊ |
if DEBUG: print("dedent: margin=%r" % margin)␊ |
␊ |
if margin is not None and margin > 0:␊ |
for i, line in enumerate(lines):␊ |
if i == 0 and skip_first_line: continue␊ |
removed = 0␊ |
for j, ch in enumerate(line):␊ |
if ch == ' ':␊ |
removed += 1␊ |
elif ch == '\t':␊ |
removed += tabsize - (removed % tabsize)␊ |
elif ch in '\r\n':␊ |
if DEBUG: print("dedent: %r: EOL -> strip up to EOL" % line)␊ |
lines[i] = lines[i][j:]␊ |
break␊ |
else:␊ |
raise ValueError("unexpected non-whitespace char %r in "␊ |
"line %r while removing %d-space margin"␊ |
% (ch, line, margin))␊ |
if DEBUG:␊ |
print("dedent: %r: %r -> removed %d/%d"\␊ |
% (line, ch, removed, margin))␊ |
if removed == margin:␊ |
lines[i] = lines[i][j+1:]␊ |
break␊ |
elif removed > margin:␊ |
lines[i] = ' '*(removed-margin) + lines[i][j+1:]␊ |
break␊ |
else:␊ |
if removed:␊ |
lines[i] = lines[i][removed:]␊ |
return lines␊ |
␊ |
def _dedent(text, tabsize=8, skip_first_line=False):␊ |
"""_dedent(text, tabsize=8, skip_first_line=False) -> dedented text␊ |
␊ |
"text" is the text to dedent.␊ |
"tabsize" is the tab width to use for indent width calculations.␊ |
"skip_first_line" is a boolean indicating if the first line should␊ |
be skipped for calculating the indent width and for dedenting.␊ |
This is sometimes useful for docstrings and similar.␊ |
␊ |
textwrap.dedent(s), but don't expand tabs to spaces␊ |
"""␊ |
lines = text.splitlines(1)␊ |
_dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line)␊ |
return ''.join(lines)␊ |
␊ |
␊ |
class _memoized(object):␊ |
"""Decorator that caches a function's return value each time it is called.␊ |
If called later with the same arguments, the cached value is returned, and␊ |
not re-evaluated.␊ |
␊ |
http://wiki.python.org/moin/PythonDecoratorLibrary␊ |
"""␊ |
def __init__(self, func):␊ |
self.func = func␊ |
self.cache = {}␊ |
def __call__(self, *args):␊ |
try:␊ |
return self.cache[args]␊ |
except KeyError:␊ |
self.cache[args] = value = self.func(*args)␊ |
return value␊ |
except TypeError:␊ |
# uncachable -- for instance, passing a list as an argument.␊ |
# Better to not cache than to blow up entirely.␊ |
return self.func(*args)␊ |
def __repr__(self):␊ |
"""Return the function's docstring."""␊ |
return self.func.__doc__␊ |
␊ |
␊ |
def _xml_oneliner_re_from_tab_width(tab_width):␊ |
"""Standalone XML processing instruction regex."""␊ |
return re.compile(r"""␊ |
(?:␊ |
(?<=\n\n) # Starting after a blank line␊ |
| # or␊ |
\A\n? # the beginning of the doc␊ |
)␊ |
( # save in $1␊ |
[ ]{0,%d}␊ |
(?:␊ |
<\?\w+\b\s+.*?\?> # XML processing instruction␊ |
|␊ |
<\w+:\w+\b\s+.*?/> # namespaced single tag␊ |
)␊ |
[ \t]*␊ |
(?=\n{2,}|\Z) # followed by a blank line or end of document␊ |
)␊ |
""" % (tab_width - 1), re.X)␊ |
_xml_oneliner_re_from_tab_width = _memoized(_xml_oneliner_re_from_tab_width)␊ |
␊ |
def _hr_tag_re_from_tab_width(tab_width):␊ |
return re.compile(r"""␊ |
(?:␊ |
(?<=\n\n) # Starting after a blank line␊ |
| # or␊ |
\A\n? # the beginning of the doc␊ |
)␊ |
( # save in \1␊ |
[ ]{0,%d}␊ |
<(hr) # start tag = \2␊ |
\b # word break␊ |
([^<>])*? #␊ |
/?> # the matching end tag␊ |
[ \t]*␊ |
(?=\n{2,}|\Z) # followed by a blank line or end of document␊ |
)␊ |
""" % (tab_width - 1), re.X)␊ |
_hr_tag_re_from_tab_width = _memoized(_hr_tag_re_from_tab_width)␊ |
␊ |
␊ |
def _xml_escape_attr(attr, skip_single_quote=True):␊ |
"""Escape the given string for use in an HTML/XML tag attribute.␊ |
␊ |
By default this doesn't bother with escaping `'` to `'`, presuming that␊ |
the tag attribute is surrounded by double quotes.␊ |
"""␊ |
escaped = (attr␊ |
.replace('&', '&')␊ |
.replace('"', '"')␊ |
.replace('<', '<')␊ |
.replace('>', '>'))␊ |
if not skip_single_quote:␊ |
escaped = escaped.replace("'", "'")␊ |
return escaped␊ |
␊ |
␊ |
def _xml_encode_email_char_at_random(ch):␊ |
r = random()␊ |
# Roughly 10% raw, 45% hex, 45% dec.␊ |
# '@' *must* be encoded. I [John Gruber] insist.␊ |
# Issue 26: '_' must be encoded.␊ |
if r > 0.9 and ch not in "@_":␊ |
return ch␊ |
elif r < 0.45:␊ |
# The [1:] is to drop leading '0': 0x63 -> x63␊ |
return '&#%s;' % hex(ord(ch))[1:]␊ |
else:␊ |
return '&#%s;' % ord(ch)␊ |
␊ |
␊ |
␊ |
#---- mainline␊ |
␊ |
class _NoReflowFormatter(optparse.IndentedHelpFormatter):␊ |
"""An optparse formatter that does NOT reflow the description."""␊ |
def format_description(self, description):␊ |
return description or ""␊ |
␊ |
def _test():␊ |
import doctest␊ |
doctest.testmod()␊ |
␊ |
def main(argv=None):␊ |
if argv is None:␊ |
argv = sys.argv␊ |
if not logging.root.handlers:␊ |
logging.basicConfig()␊ |
␊ |
usage = "usage: %prog [PATHS...]"␊ |
version = "%prog "+__version__␊ |
parser = optparse.OptionParser(prog="markdown2", usage=usage,␊ |
version=version, description=cmdln_desc,␊ |
formatter=_NoReflowFormatter())␊ |
parser.add_option("-v", "--verbose", dest="log_level",␊ |
action="store_const", const=logging.DEBUG,␊ |
help="more verbose output")␊ |
parser.add_option("--encoding",␊ |
help="specify encoding of text content")␊ |
parser.add_option("--html4tags", action="store_true", default=False,␊ |
help="use HTML 4 style for empty element tags")␊ |
parser.add_option("-s", "--safe", metavar="MODE", dest="safe_mode",␊ |
help="sanitize literal HTML: 'escape' escapes "␊ |
"HTML meta chars, 'replace' replaces with an "␊ |
"[HTML_REMOVED] note")␊ |
parser.add_option("-x", "--extras", action="append",␊ |
help="Turn on specific extra features (not part of "␊ |
"the core Markdown spec). See above.")␊ |
parser.add_option("--use-file-vars",␊ |
help="Look for and use Emacs-style 'markdown-extras' "␊ |
"file var to turn on extras. See "␊ |
"<https://github.com/trentm/python-markdown2/wiki/Extras>")␊ |
parser.add_option("--link-patterns-file",␊ |
help="path to a link pattern file")␊ |
parser.add_option("--self-test", action="store_true",␊ |
help="run internal self-tests (some doctests)")␊ |
parser.add_option("--compare", action="store_true",␊ |
help="run against Markdown.pl as well (for testing)")␊ |
parser.set_defaults(log_level=logging.INFO, compare=False,␊ |
encoding="utf-8", safe_mode=None, use_file_vars=False)␊ |
opts, paths = parser.parse_args()␊ |
log.setLevel(opts.log_level)␊ |
␊ |
if opts.self_test:␊ |
return _test()␊ |
␊ |
if opts.extras:␊ |
extras = {}␊ |
for s in opts.extras:␊ |
splitter = re.compile("[,;: ]+")␊ |
for e in splitter.split(s):␊ |
if '=' in e:␊ |
ename, earg = e.split('=', 1)␊ |
try:␊ |
earg = int(earg)␊ |
except ValueError:␊ |
pass␊ |
else:␊ |
ename, earg = e, None␊ |
extras[ename] = earg␊ |
else:␊ |
extras = None␊ |
␊ |
if opts.link_patterns_file:␊ |
link_patterns = []␊ |
f = open(opts.link_patterns_file)␊ |
try:␊ |
for i, line in enumerate(f.readlines()):␊ |
if not line.strip(): continue␊ |
if line.lstrip().startswith("#"): continue␊ |
try:␊ |
pat, href = line.rstrip().rsplit(None, 1)␊ |
except ValueError:␊ |
raise MarkdownError("%s:%d: invalid link pattern line: %r"␊ |
% (opts.link_patterns_file, i+1, line))␊ |
link_patterns.append(␊ |
(_regex_from_encoded_pattern(pat), href))␊ |
finally:␊ |
f.close()␊ |
else:␊ |
link_patterns = None␊ |
␊ |
from os.path import join, dirname, abspath, exists␊ |
markdown_pl = join(dirname(dirname(abspath(__file__))), "test",␊ |
"Markdown.pl")␊ |
if not paths:␊ |
paths = ['-']␊ |
for path in paths:␊ |
if path == '-':␊ |
text = sys.stdin.read()␊ |
else:␊ |
fp = codecs.open(path, 'r', opts.encoding)␊ |
text = fp.read()␊ |
fp.close()␊ |
if opts.compare:␊ |
from subprocess import Popen, PIPE␊ |
print("==== Markdown.pl ====")␊ |
p = Popen('perl %s' % markdown_pl, shell=True, stdin=PIPE, stdout=PIPE, close_fds=True)␊ |
p.stdin.write(text.encode('utf-8'))␊ |
p.stdin.close()␊ |
perl_html = p.stdout.read().decode('utf-8')␊ |
if py3:␊ |
sys.stdout.write(perl_html)␊ |
else:␊ |
sys.stdout.write(perl_html.encode(␊ |
sys.stdout.encoding or "utf-8", 'xmlcharrefreplace'))␊ |
print("==== markdown2.py ====")␊ |
html = markdown(text,␊ |
html4tags=opts.html4tags,␊ |
safe_mode=opts.safe_mode,␊ |
extras=extras, link_patterns=link_patterns,␊ |
use_file_vars=opts.use_file_vars)␊ |
if py3:␊ |
sys.stdout.write(html)␊ |
else:␊ |
sys.stdout.write(html.encode(␊ |
sys.stdout.encoding or "utf-8", 'xmlcharrefreplace'))␊ |
if extras and "toc" in extras:␊ |
log.debug("toc_html: " +␊ |
html.toc_html.encode(sys.stdout.encoding or "utf-8", 'xmlcharrefreplace'))␊ |
if opts.compare:␊ |
test_dir = join(dirname(dirname(abspath(__file__))), "test")␊ |
if exists(join(test_dir, "test_markdown2.py")):␊ |
sys.path.insert(0, test_dir)␊ |
from test_markdown2 import norm_html_from_html␊ |
norm_html = norm_html_from_html(html)␊ |
norm_perl_html = norm_html_from_html(perl_html)␊ |
else:␊ |
norm_html = html␊ |
norm_perl_html = perl_html␊ |
print("==== match? %r ====" % (norm_perl_html == norm_html))␊ |
␊ |
␊ |
if __name__ == "__main__":␊ |
sys.exit( main(sys.argv) )␊ |