#!/usr/bin/env python
"""
Python Markdown
===============
Python Markdown converts Markdown to HTML and can be used as a library or
called from the command line.
## Basic usage as a module:
import markdown
md = Markdown()
html = md.convert(your_text_string)
## Basic use from the command line:
python markdown.py source.txt > destination.html
Run "python markdown.py --help" to see more options.
## Extensions
See
around it - append the paragraph bits directly # onto parentElem el = parentElem else: # Otherwise make a "p" element el = etree.SubElement(parentElem, "p") dump = [] # Searching for hr or header for line in paragraph: # it's hr if CORE_RE["isline3"].match(line): el.text = "\n".join(dump) self.__processHR(el) dump = [] # it's header elif line.startswith("#"): el.text = "\n".join(dump) self.__processHeader(parentElem, [line]) dump = [] else: dump.append(line) if dump: text = "\n".join(dump) el.text = text def __processUList(self, parentElem, lines, inList): self.__processList(parentElem, lines, inList, listexpr='ul', tag='ul') def __processOList(self, parentElem, lines, inList): self.__processList(parentElem, lines, inList, listexpr='ol', tag='ol') def __processList(self, parentElem, lines, inList, listexpr, tag): """ Given a list of document lines starting with a list item, finds the end of the list, breaks it up, and recursively processes each list item and the remainder of the text file. Keyword arguments: * parentElem: A ElementTree element to which the content will be added * lines: a list of lines * inList: a level Returns: None """ ul = etree.SubElement(parentElem, tag) # ul might actually be '
") and recursively processes the
the remainder of the text file.
Keyword arguments:
* parentElem: ElementTree element to which the content will be added
* lines: a list of lines
* inList: a level
Returns: None
"""
detabbed, theRest = self.detectTabbed(lines)
pre = etree.SubElement(parentElem, "pre")
code = etree.SubElement(pre, "code")
text = "\n".join(detabbed).rstrip()+"\n"
code.text = AtomicString(text)
self.parseChunk(parentElem, theRest, inList)
def detectTabbed(self, lines):
""" Find indented text and remove indent before further proccesing.
Keyword arguments:
* lines: an array of strings
* fn: a function that returns a substring of a string
if the string matches the necessary criteria
Returns: a list of post processes items and the unused
remainder of the original list
"""
items = []
item = -1
i = 0 # to keep track of where we are
def detab(line):
match = CORE_RE['tabbed'].match(line)
if match:
return match.group(4)
for line in lines:
if line.strip(): # Non-blank line
line = detab(line)
if line:
items.append(line)
i += 1
continue
else:
return items, lines[i:]
else: # Blank line: _maybe_ we are done.
i += 1 # advance
# Find the next non-blank line
for j in range(i, len(lines)):
if lines[j].strip():
next_line = lines[j]; break
else:
break # There is no more text; we are done.
# Check if the next non-blank line is tabbed
if detab(next_line): # Yes, more work to do.
items.append("")
continue
else:
break # No, we are done.
else:
i += 1
return items, lines[i:]
"""
INLINE PROCESSOR
=============================================================================
This class handles basic Markdown parsing. It doesn't concern itself with
inline elements such as **bold** or *italics*, but rather just catches blocks,
lists, quotes, etc.
"""
class InlineProcessor:
"""
An auxiliary class to traverse a Markdown tree, applying inline patterns.
"""
def __init__ (self, patterns):
self.__inlinePatterns = patterns
self.__placeholder_prefix = INLINE_PLACEHOLDER_PREFIX
self.__placeholder_suffix = ETX
self.__placeholder_length = 4 + len(self.__placeholder_prefix) \
+ len(self.__placeholder_suffix)
self.__placeholder_re = re.compile(INLINE_PLACEHOLDER % r'([0-9]{4})')
def __makePlaceholder(self, type):
""" Generate a placeholder """
id = "%04d" % len(self.stashed_nodes)
hash = INLINE_PLACEHOLDER % id
return hash, id
def __findPlaceholder(self, data, index):
"""
Extract id from data string, start from index
Keyword arguments:
* data: string
* index: index, from which we start search
Returns: placeholder id and string index, after the found placeholder.
"""
m = self.__placeholder_re.search(data, index)
if m:
return m.group(1), m.end()
else:
return None, index + 1
def __stashNode(self, node, type):
""" Add node to stash """
placeholder, id = self.__makePlaceholder(type)
self.stashed_nodes[id] = node
return placeholder
def __handleInline(self, data, patternIndex=0):
"""
Process string with inline patterns and replace it
with placeholders
Keyword arguments:
* data: A line of Markdown text
* patternIndex: The index of the inlinePattern to start with
Returns: String with placeholders.
"""
if not isinstance(data, AtomicString):
startIndex = 0
while patternIndex < len(self.__inlinePatterns):
data, matched, startIndex = self.__applyPattern(
self.__inlinePatterns[patternIndex],
data, patternIndex, startIndex)
if not matched:
patternIndex += 1
return data
def __processElementText(self, node, subnode, isText=True):
"""
Process placeholders in Element.text or Element.tail
of Elements popped from self.stashed_nodes.
Keywords arguments:
* node: parent node
* subnode: processing node
* isText: bool variable, True - it's text, False - it's tail
Returns: None
"""
if isText:
text = subnode.text
subnode.text = None
else:
text = subnode.tail
subnode.tail = None
childResult = self.__processPlaceholders(text, subnode)
if not isText and node is not subnode:
pos = node.getchildren().index(subnode)
node.remove(subnode)
else:
pos = 0
childResult.reverse()
for newChild in childResult:
node.insert(pos, newChild)
def __processPlaceholders(self, data, parent):
"""
Process string with placeholders and generate ElementTree tree.
Keyword arguments:
* data: string with placeholders instead of ElementTree elements.
* parent: Element, which contains processing inline data
Returns: list with ElementTree elements with applied inline patterns.
"""
def linkText(text):
if text:
if result:
if result[-1].tail:
result[-1].tail += text
else:
result[-1].tail = text
else:
if parent.text:
parent.text += text
else:
parent.text = text
result = []
strartIndex = 0
while data:
index = data.find(self.__placeholder_prefix, strartIndex)
if index != -1:
id, phEndIndex = self.__findPlaceholder(data, index)
if self.stashed_nodes.has_key(id):
node = self.stashed_nodes.get(id)
if index > 0:
text = data[strartIndex:index]
linkText(text)
if not isString(node): # it's Element
for child in [node] + node.getchildren():
if child.tail:
if child.tail.strip():
self.__processElementText(node, child, False)
if child.text:
if child.text.strip():
self.__processElementText(child, child)
else: # it's just a string
linkText(node)
strartIndex = phEndIndex
continue
strartIndex = phEndIndex
result.append(node)
else: # wrong placeholder
end = index + len(prefix)
linkText(data[strartIndex:end])
strartIndex = end
else:
text = data[strartIndex:]
linkText(text)
data = ""
return result
def __applyPattern(self, pattern, data, patternIndex, startIndex=0):
"""
Check if the line fits the pattern, create the necessary
elements, add it to stashed_nodes.
Keyword arguments:
* data: the text to be processed
* pattern: the pattern to be checked
* patternIndex: index of current pattern
* startIndex: string index, from which we starting search
Returns: String with placeholders instead of ElementTree elements.
"""
match = pattern.getCompiledRegExp().match(data[startIndex:])
leftData = data[:startIndex]
if not match:
return data, False, 0
node = pattern.handleMatch(match)
if node is None:
return data, True, len(leftData) + match.span(len(match.groups()))[0]
if not isString(node):
if not isinstance(node.text, AtomicString):
# We need to process current node too
for child in [node] + node.getchildren():
if not isString(node):
if child.text:
child.text = self.__handleInline(child.text,
patternIndex + 1)
if child.tail:
child.tail = self.__handleInline(child.tail,
patternIndex)
placeholder = self.__stashNode(node, pattern.type())
return "%s%s%s%s" % (leftData,
match.group(1),
placeholder, match.groups()[-1]), True, 0
def applyInlinePatterns(self, markdownTree):
"""Apply inline patterns to a parsed Markdown tree.
Iterate over ElementTree, find elements with inline tag, apply inline
patterns and append newly created Elements to tree. If you don't
want process your data with inline paterns, instead of normal string,
use subclass AtomicString:
node.text = AtomicString("data won't be processed with inline patterns")
Arguments:
* markdownTree: ElementTree object, representing Markdown tree.
Returns: ElementTree object with applied inline patterns.
"""
self.stashed_nodes = {}
stack = [markdownTree.getroot()]
while stack:
currElement = stack.pop()
insertQueue = []
for child in currElement.getchildren():
if child.text and not isinstance(child.text, AtomicString):
text = child.text
child.text = None
lst = self.__processPlaceholders(self.__handleInline(
text), child)
stack += lst
insertQueue.append((child, lst))
if child.getchildren():
stack.append(child)
for element, lst in insertQueue:
if element.text:
element.text = handleAttributes(element.text, element)
i = 0
for newChild in lst:
# Processing attributes
if newChild.tail:
newChild.tail = handleAttributes(newChild.tail,
element)
if newChild.text:
newChild.text = handleAttributes(newChild.text,
newChild)
element.insert(i, newChild)
i += 1
return markdownTree
"""
PRE-PROCESSORS
=============================================================================
Preprocessors work on source text before we start doing anything too
complicated. There are two types of preprocessors: TextPreprocessor and
Preprocessor.
"""
class Processor:
def __init__(self, markdown_instance=None):
if markdown_instance:
self.markdown = markdown_instance
class TextPreprocessor (Processor):
"""
TextPreprocessors are run before the text is broken into lines.
Each TextPreprocessor implements a "run" method that takes a pointer to a
text string of the document, modifies it as necessary and returns
either the same pointer or a pointer to a new string.
TextPreprocessors must extend markdown.TextPreprocessor.
"""
def run(self, text):
"""
Each subclass of TextPreprocessor should override the `run` method,
which takes the document text as a single string and returns the
(possibly modified) document as a single string.
"""
pass
class Preprocessor (Processor):
"""
Preprocessors are run after the text is broken into lines.
Each preprocessor implements a "run" method that takes a pointer to a
list of lines of the document, modifies it as necessary and returns
either the same pointer or a pointer to a new list.
Preprocessors must extend markdown.Preprocessor.
"""
def run(self, lines):
"""
Each subclass of Preprocessor should override the `run` method, which
takes the document as a list of strings split by newlines and returns
the (possibly modified) list of lines.
"""
pass
class HtmlBlockPreprocessor(TextPreprocessor):
"""Remove html blocks from the text and store them for later retrieval."""
right_tag_patterns = ["%s>", "%s>"]
def _get_left_tag(self, block):
return block[1:].replace(">", " ", 1).split()[0].lower()
def _get_right_tag(self, left_tag, block):
for p in self.right_tag_patterns:
tag = p % left_tag
i = block.rfind(tag)
if i > 2:
return tag.lstrip("<").rstrip(">"), i + len(p)-2 + len(left_tag)
return block.rstrip()[-len(left_tag)-2:-1].lower(), len(block)
def _equal_tags(self, left_tag, right_tag):
if left_tag == 'div' or left_tag[0] in ['?', '@', '%']: # handle PHP, etc.
return True
if ("/" + left_tag) == right_tag:
return True
if (right_tag == "--" and left_tag == "--"):
return True
elif left_tag == right_tag[1:] \
and right_tag[0] != "<":
return True
else:
return False
def _is_oneliner(self, tag):
return (tag in ['hr', 'hr/'])
def run(self, text):
new_blocks = []
text = text.split("\n\n")
items = []
left_tag = ''
right_tag = ''
in_tag = False # flag
while text:
block = text[0]
if block.startswith("\n"):
block = block[1:]
text = text[1:]
if block.startswith("\n"):
block = block[1:]
if not in_tag:
if block.startswith("<"):
left_tag = self._get_left_tag(block)
right_tag, data_index = self._get_right_tag(left_tag, block)
if data_index < len(block):
text.insert(0, block[data_index:])
block = block[:data_index]
if not (isBlockLevel(left_tag) \
or block[1] in ["!", "?", "@", "%"]):
new_blocks.append(block)
continue
if self._is_oneliner(left_tag):
new_blocks.append(block.strip())
continue
if block[1] == "!":
# is a comment block
left_tag = "--"
right_tag, data_index = self._get_right_tag(left_tag, block)
# keep checking conditions below and maybe just append
if block.rstrip().endswith(">") \
and self._equal_tags(left_tag, right_tag):
new_blocks.append(
self.markdown.htmlStash.store(block.strip()))
continue
else: #if not block[1] == "!":
# if is block level tag and is not complete
if isBlockLevel(left_tag) or left_tag == "--" \
and not block.rstrip().endswith(">"):
items.append(block.strip())
in_tag = True
else:
new_blocks.append(
self.markdown.htmlStash.store(block.strip()))
continue
new_blocks.append(block)
else:
items.append(block.strip())
right_tag, data_index = self._get_right_tag(left_tag, block)
if self._equal_tags(left_tag, right_tag):
# if find closing tag
in_tag = False
new_blocks.append(
self.markdown.htmlStash.store('\n\n'.join(items)))
items = []
if items:
new_blocks.append(self.markdown.htmlStash.store('\n\n'.join(items)))
new_blocks.append('\n')
return "\n\n".join(new_blocks)
class HeaderPreprocessor(Preprocessor):
"""Replace underlined headers with hashed headers.
(To avoid the need for lookahead later.)
"""
def run (self, lines):
i = -1
while i+1 < len(lines):
i = i+1
if not lines[i].strip():
continue
if lines[i].startswith("#"):
lines.insert(i+1, "\n")
if (i+1 <= len(lines)
and lines[i+1]
and lines[i+1][0] in ['-', '=']):
underline = lines[i+1].strip()
if underline == "="*len(underline):
lines[i] = "# " + lines[i].strip()
lines[i+1] = ""
elif underline == "-"*len(underline):
lines[i] = "## " + lines[i].strip()
lines[i+1] = ""
return lines
class LinePreprocessor(Preprocessor):
"""Convert HR lines to "___" format."""
blockquote_re = re.compile(r'^(> )+')
def run (self, lines):
for i in range(len(lines)):
prefix = ''
m = self.blockquote_re.search(lines[i])
if m:
prefix = m.group(0)
if self._isLine(lines[i][len(prefix):]):
lines[i] = prefix + "___"
return lines
def _isLine(self, block):
"""Determine if a block should be replaced with an
"""
if block.startswith(" "):
return False # a code block
text = "".join([x for x in block if not x.isspace()])
if len(text) <= 2:
return False
for pattern in ['isline1', 'isline2', 'isline3']:
m = CORE_RE[pattern].match(text)
if (m and m.group(1)):
return True
else:
return False
class ReferencePreprocessor(Preprocessor):
"""Remove reference definitions from the text and store them for later use."""
def run (self, lines):
new_text = [];
for line in lines:
m = CORE_RE['reference-def'].match(line)
if m:
id = m.group(2).strip().lower()
t = m.group(4).strip() # potential title
if not t:
self.markdown.references[id] = (m.group(3), t)
elif (len(t) >= 2
and (t[0] == t[-1] == "\""
or t[0] == t[-1] == "\'"
or (t[0] == "(" and t[-1] == ")") ) ):
self.markdown.references[id] = (m.group(3), t[1:-1])
else:
new_text.append(line)
else:
new_text.append(line)
return new_text #+ "\n"
"""
INLINE PATTERNS
=============================================================================
Inline patterns such as *emphasis* are handled by means of auxiliary
objects, one per pattern. Pattern objects must be instances of classes
that extend markdown.Pattern. Each pattern object uses a single regular
expression and needs support the following methods:
pattern.getCompiledRegExp() # returns a regular expression
pattern.handleMatch(m) # takes a match object and returns
# an ElementTree element or just plain text
All of python markdown's built-in patterns subclass from Pattern,
but you can add additional patterns that don't.
Also note that all the regular expressions used by inline must
capture the whole block. For this reason, they all start with
'^(.*)' and end with '(.*)!'. In case with built-in expression
Pattern takes care of adding the "^(.*)" and "(.*)!".
Finally, the order in which regular expressions are applied is very
important - e.g. if we first replace http://.../ links with tags
and _then_ try to replace inline html, we would end up with a mess.
So, we apply the expressions in the following order:
* escape and backticks have to go before everything else, so
that we can preempt any markdown patterns by escaping them.
* then we handle auto-links (must be done before inline html)
* then we handle inline HTML. At this point we will simply
replace all inline HTML strings with a placeholder and add
the actual HTML to a hash.
* then inline images (must be done before links)
* then bracketed links, first regular then reference-style
* finally we apply strong and emphasis
"""
"""
The actual regular expressions for patterns
-----------------------------------------------------------------------------
"""
NOBRACKET = r'[^\]\[]*'
BRK = ( r'\[('
+ (NOBRACKET + r'(\[')*6
+ (NOBRACKET+ r'\])*')*6
+ NOBRACKET + r')\]' )
NOIMG = r'(?|((?:(?:\(.*?\))|[^\(\)]))*?)\s*((['"])(.*)\12)?\)'''
# [text](url) or [text]()
IMAGE_LINK_RE = r'\!' + BRK + r'\s*\((<.*?>|([^\)]*))\)'
# ![alttxt](http://x.com/) or ![alttxt]( )
REFERENCE_RE = NOIMG + BRK+ r'\s*\[([^\]]*)\]' # [Google][3]
IMAGE_REFERENCE_RE = r'\!' + BRK + '\s*\[([^\]]*)\]' # ![alt text][2]
NOT_STRONG_RE = r'( \* )' # stand-alone * or _
AUTOLINK_RE = r'<((?:f|ht)tps?://[^>]*)>' #
AUTOMAIL_RE = r'<([^> \!]*@[^> ]*)>' #
HTML_RE = r'(\<([a-zA-Z/][^\>]*?|\!--.*?--)\>)' # <...>
ENTITY_RE = r'(&[\#a-zA-Z0-9]*;)' # &
LINE_BREAK_RE = r' \n' # two spaces at end of line
LINE_BREAK_2_RE = r' $' # two spaces at end of text
"""
The pattern classes
-----------------------------------------------------------------------------
"""
class Pattern:
"""Base class that inline patterns subclass. """
def __init__ (self, pattern, markdown_instance=None):
"""
Create an instant of an inline pattern.
Keyword arguments:
* pattern: A regular expression that matches a pattern
"""
self.pattern = pattern
self.compiled_re = re.compile("^(.*?)%s(.*?)$" % pattern, re.DOTALL)
# Api for Markdown to pass safe_mode into instance
self.safe_mode = False
if markdown_instance:
self.markdown = markdown_instance
def getCompiledRegExp (self):
""" Return a compiled regular expression. """
return self.compiled_re
def handleMatch(self, m):
"""Return a ElementTree element from the given match.
Subclasses should override this method.
Keyword arguments:
* m: A re match object containing a match of the pattern.
"""
pass
def type(self):
""" Return class name, to define pattern type """
return self.__class__.__name__
BasePattern = Pattern # for backward compatibility
class SimpleTextPattern (Pattern):
""" Return a simple text of group(2) of a Pattern. """
def handleMatch(self, m):
text = m.group(2)
if text == INLINE_PLACEHOLDER_PREFIX:
return None
return text
class SimpleTagPattern (Pattern):
"""
Return element of type `tag` with a text attribute of group(3)
of a Pattern.
"""
def __init__ (self, pattern, tag):
Pattern.__init__(self, pattern)
self.tag = tag
def handleMatch(self, m):
el = etree.Element(self.tag)
el.text = m.group(3)
return el
class SubstituteTagPattern (SimpleTagPattern):
""" Return a eLement of type `tag` with no children. """
def handleMatch (self, m):
return etree.Element(self.tag)
class BacktickPattern (Pattern):
""" Return a `` element containing the matching text. """
def __init__ (self, pattern):
Pattern.__init__(self, pattern)
self.tag = "code"
def handleMatch(self, m):
el = etree.Element(self.tag)
el.text = AtomicString(m.group(3).strip())
return el
class DoubleTagPattern (SimpleTagPattern):
"""Return a ElementTree element nested in tag2 nested in tag1.
Useful for strong emphasis etc.
"""
def handleMatch(self, m):
tag1, tag2 = self.tag.split(",")
el1 = etree.Element(tag1)
el2 = etree.SubElement(el1, tag2)
el2.text = m.group(3)
return el1
class HtmlPattern (Pattern):
""" Store raw inline html and return a placeholder. """
def handleMatch (self, m):
rawhtml = m.group(2)
inline = True
place_holder = self.markdown.htmlStash.store(rawhtml)
return place_holder
class LinkPattern (Pattern):
""" Return a link element from the given match. """
def handleMatch(self, m):
el = etree.Element("a")
el.text = m.group(2)
title = m.group(11)
href = m.group(9)
if href:
if href[0] == "<":
href = href[1:-1]
el.set("href", self.sanitize_url(href.strip()))
else:
el.set("href", "")
if title:
title = dequote(title) #.replace('"', """)
el.set("title", title)
return el
def sanitize_url(self, url):
"""
Sanitize a url against xss attacks in "safe_mode".
Rather than specifically blacklisting `javascript:alert("XSS")` and all
its aliases (see ), we whitelist known
safe url formats. Most urls contain a network location, however some
are known not to (i.e.: mailto links). Script urls do not contain a
location. Additionally, for `javascript:...`, the scheme would be
"javascript" but some aliases will appear to `urlparse()` to have no
scheme. On top of that relative links (i.e.: "foo/bar.html") have no
scheme. Therefore we must check "path", "parameters", "query" and
"fragment" for any literal colons. We don't check "scheme" for colons
because it *should* never have any and "netloc" must allow the form:
`username:password@host:port`.
"""
locless_schemes = ['', 'mailto', 'news']
scheme, netloc, path, params, query, fragment = url = urlparse(url)
safe_url = False
if netloc != '' or scheme in locless_schemes:
safe_url = True
for part in url[2:]:
if ":" in part:
safe_url = False
if self.safe_mode and not safe_url:
return ''
else:
return urlunparse(url)
class ImagePattern(LinkPattern):
""" Return a img element from the given match. """
def handleMatch(self, m):
el = etree.Element("img")
src_parts = m.group(9).split()
if src_parts:
src = src_parts[0]
if src[0] == "<" and src[-1] == ">":
src = src[1:-1]
el.set('src', self.sanitize_url(src))
else:
el.set('src', "")
if len(src_parts) > 1:
el.set('title', dequote(" ".join(src_parts[1:])))
if ENABLE_ATTRIBUTES:
truealt = handleAttributes(m.group(2), el)
else:
truealt = m.group(2)
el.set('alt', truealt)
return el
class ReferencePattern(LinkPattern):
""" Match to a stored reference and return link element. """
def handleMatch(self, m):
if m.group(9):
id = m.group(9).lower()
else:
# if we got something like "[Google][]"
# we'll use "google" as the id
id = m.group(2).lower()
if not self.markdown.references.has_key(id): # ignore undefined refs
return None
href, title = self.markdown.references[id]
text = m.group(2)
return self.makeTag(href, title, text)
def makeTag(self, href, title, text):
el = etree.Element('a')
el.set('href', self.sanitize_url(href))
if title:
el.set('title', title)
el.text = text
return el
class ImageReferencePattern (ReferencePattern):
""" Match to a stored reference and return img element. """
def makeTag(self, href, title, text):
el = etree.Element("img")
el.set("src", self.sanitize_url(href))
if title:
el.set("title", title)
el.set("alt", text)
return el
class AutolinkPattern (Pattern):
""" Return a link Element given an autolink (``). """
def handleMatch(self, m):
el = etree.Element("a")
el.set('href', m.group(2))
el.text = AtomicString(m.group(2))
return el
class AutomailPattern (Pattern):
"""
Return a mailto link Element given an automail link (``).
"""
def handleMatch(self, m):
el = etree.Element('a')
email = m.group(2)
if email.startswith("mailto:"):
email = email[len("mailto:"):]
def codepoint2name(code):
"""Return entity definition by code, or the code if not defined."""
entity = htmlentitydefs.codepoint2name.get(code)
if entity:
return "%s%s;" % (AMP_SUBSTITUTE, entity)
else:
return "%s#%d;" % (AMP_SUBSTITUTE, code)
letters = [codepoint2name(ord(letter)) for letter in email]
el.text = AtomicString(''.join(letters))
mailto = "mailto:" + email
mailto = "".join([AMP_SUBSTITUTE + '#%d;' %
ord(letter) for letter in mailto])
el.set('href', mailto)
return el
"""
POST-PROCESSORS
=============================================================================
Markdown also allows post-processors, which are similar to preprocessors in
that they need to implement a "run" method. However, they are run after core
processing.
There are two types of post-processors: Postprocessor and TextPostprocessor
"""
class Postprocessor (Processor):
"""
Postprocessors are run before the ElementTree serialization.
Each Postprocessor implements a "run" method that takes a pointer to a
ElementTree, modifies it as necessary and returns a ElementTree
document.
Postprocessors must extend markdown.Postprocessor.
"""
def run(self, root):
"""
Subclasses of Postprocessor should implement a `run` method, which
takes a root Element. Method can return another Element, and global
root Element will be replaced, or just modify current and return None.
"""
pass
class TextPostprocessor (Processor):
"""
TextPostprocessors are run after the ElementTree it converted back into text.
Each TextPostprocessor implements a "run" method that takes a pointer to a
text string, modifies it as necessary and returns a text string.
TextPostprocessors must extend markdown.TextPostprocessor.
"""
def run(self, text):
"""
Subclasses of TextPostprocessor should implement a `run` method, which
takes the html document as a single text string and returns a
(possibly modified) string.
"""
pass
class PrettifyPostprocessor(Postprocessor):
"""Add linebreaks to the html document."""
def _prettifyETree(self, elem):
"""Recursively add linebreaks to ElementTree children."""
i = "\n"
if isBlockLevel(elem.tag) and elem.tag not in ['code', 'pre']:
if (not elem.text or not elem.text.strip()) \
and len(elem) and isBlockLevel(elem[0].tag):
elem.text = i
for e in elem:
if isBlockLevel(e.tag):
self._prettifyETree(e)
if not elem.tail or not elem.tail.strip():
elem.tail = i
if not elem.tail or not elem.tail.strip():
elem.tail = i
def run(self, root):
""".Add linebreaks to ElementTree root object."""
self._prettifyETree(root)
# Do
's seperately as they are often in the middle of
# inline content and missed by _prettifyETree.
brs = root.getiterator('br')
for br in brs:
if not br.tail or not br.tail.strip():
br.tail = '\n'
else:
br.tail = '\n%s' % br.tail
class RawHtmlTextPostprocessor(TextPostprocessor):
""" Restore raw html to the document. """
def run(self, text):
""" Iterate over html stash and restore "safe" html. """
for i in range(self.markdown.htmlStash.html_counter):
html, safe = self.markdown.htmlStash.rawHtmlBlocks[i]
if self.markdown.safeMode and not safe:
if str(self.markdown.safeMode).lower() == 'escape':
html = self.escape(html)
elif str(self.markdown.safeMode).lower() == 'remove':
html = ''
else:
html = HTML_REMOVED_TEXT
if safe or not self.markdown.safeMode:
text = text.replace("%s
" % (HTML_PLACEHOLDER % i),
html + "\n")
text = text.replace(HTML_PLACEHOLDER % i, html)
return text
def escape(self, html):
""" Basic html escaping """
html = html.replace('&', '&')
html = html.replace('<', '<')
html = html.replace('>', '>')
return html.replace('"', '"')
class AndSubstitutePostprocessor(TextPostprocessor):
""" Restore valid entities """
def __init__(self):
pass
def run(self, text):
text = text.replace(AMP_SUBSTITUTE, "&")
return text
"""
MISC AUXILIARY CLASSES
=============================================================================
"""
class AtomicString(unicode):
"""A string which should not be further processed."""
pass
class HtmlStash:
"""
This class is used for stashing HTML objects that we extract
in the beginning and replace with place-holders.
"""
def __init__ (self):
""" Create a HtmlStash. """
self.html_counter = 0 # for counting inline html segments
self.rawHtmlBlocks=[]
def store(self, html, safe=False):
"""
Saves an HTML segment for later reinsertion. Returns a
placeholder string that needs to be inserted into the
document.
Keyword arguments:
* html: an html segment
* safe: label an html segment as safe for safemode
Returns : a placeholder string
"""
self.rawHtmlBlocks.append((html, safe))
placeholder = HTML_PLACEHOLDER % self.html_counter
self.html_counter += 1
return placeholder
def reset(self):
self.html_counter = 0
self.rawHtmlBlocks = []
from operator import itemgetter
class Treap(dict):
"""Extends dict to allow assignment of priority.
A treap is a binary search tree that orders the nodes by adding a priority
attribute to a node, as well as a key. The nodes are ordered so that the
keys form a binary search tree and the priorities obey the min heap order
property. The name treap is a composition of tree and heap.
The priority determines the node's location in the heap, which allows
extracting the dictionary's nodes in a prioritized order. Each new node
entry causes the heap to re-balance.
Keyword Argument:
default -- begin/end, determines where unprioritized dict entry is ordered. (Default: begin.)
"""
_r_BRACE = re.compile(r'^([<>])?(.+)')
def __init__(self, default='end'):
self.default = '_'+('begin',default)[default in ('begin','end')]
self.priority = None
self._tree = {
'_begin' : {'heap':('_begin','B'),'kids' : {},'prio' : 'B'}
,'_end' : {'heap':('_end','E'),'kids' : {},'prio' : 'E'}
}
self._reset()
dict.__setitem__(self, '_begin', None)
dict.__setitem__(self, '_end', None)
def __delitem__(self, key):
# Remove item from hash-tree and linking its kids to its parent
parn, brace = self._prior(self._tree[key]['prio'])
self._tree[parn]['kids'].pop(key, None)
for k, b in self._tree[key]['kids'].items():
self.link(k, b + parn)
del self._tree[key]
dict.__delitem__(self, key)
def __setitem__(self, key, val, *args):
if key not in self._tree:
if len(args):
prio = args[0]
else:
prio = (self.default, self.priority)[self.priority != None]
self._tree.setdefault(key, {'kids' : {},'prio' : prio})
self.link(key, prio)
self._reset()
dict.__setitem__(self, key, val)
def add(self, k, v, p):
"""Adds key/value to dict and sets priority."""
self.__setitem__(k, v, p)
def _reset(self): self._heap=[];self._keys=[];self._vals=[];self._items=[];
def _prior(self, p):
m = self._r_BRACE.match(p)
if m.group(1) is None: b = '='
else: b = m.group(1)
return m.group(2), b
def link(self, key, priority):
"""Sets priority for already-existing key/value pair."""
self._reset()
parn, brace = self._prior(priority)
#if parn in self._tree:
try:
self._tree[parn]['kids'][key] = brace
if 'heap' in self._tree[parn]:
self._tree[key]['heap']=(
key,
self._tree[parn]['heap'][1]+brace
)
for k,v in self._tree[key]['kids'].iteritems():
self.link(k, v+key)
self._tree[key]['prio'] = priority
except:
message(CRITICAL,
"Key (%s)'s parent (%s) missing." % (key, priority))
def items(self):
"""Returns list of unsorted key/value tuples."""
if not len(self._items):
dic = dict.copy(self)
del dic['_begin']
del dic['_end']
self._items = dic.items()
return self._items
def values(self):
"""Returns list of unsorted values."""
if not len(self._vals):
dic = dict.copy(self)
del dic['_begin']
del dic['_end']
self._vals = dic.values()
return self._vals
def keys(self):
"""Returns list of unsorted keys."""
if not len(self._keys):
self._keys = dict.keys(self)
self._keys.remove('_begin')
self._keys.remove('_end')
return self._keys
def heapsorted(self, keys=0, items=0):
"""Do heap sort and return list. (Default returns values.)
Keyword Arguments:
keys -- when true, returns heap-sorted list of keys. (default: false)
items -- when true, returns heap-sorted list of key/value tuples. (default: false)
(if both set, items have precedent.)
"""
if not len(self._heap):
self._heap = [
(k, dict.__getitem__(self,k)) for k in [
s[0] for s in sorted(
[v['heap'] for v in self._tree.values()]
,key=itemgetter(1)
)
]
]
for h in self._heap:
if h[0] in ('_begin','_end'):
self._heap.remove(h)
if items:
return self._heap
elif keys:
return [ h[0] for h in self._heap ]
return [ h[1] for h in self._heap ]
"""
Markdown
=============================================================================
"""
class Markdown:
"""Convert Markdown to HTML."""
def __init__(self,
extensions=[],
extension_configs={},
safe_mode = False):
"""
Creates a new Markdown instance.
Keyword arguments:
* extensions: A list of extensions.
If they are of type string, the module mdx_name.py will be loaded.
If they are a subclass of markdown.Extension, they will be used
as-is.
* extension-configs: Configuration setting for extensions.
* safe_mode: Disallow raw html. One of "remove", "replace" or "escape".
"""
self.parser = MarkdownParser()
self.safeMode = safe_mode
self.registeredExtensions = []
self.docType = ""
self.stripTopLevelTags = True
self.textPreprocessors = Treap()
self.textPreprocessors.add("html_block",
HtmlBlockPreprocessor(self), "_begin")
self.preprocessors = Treap()
self.preprocessors.add("header", HeaderPreprocessor(self), "_begin")
self.preprocessors.add("line", LinePreprocessor(self), ">header")
self.preprocessors.add("reference", ReferencePreprocessor(self),
">line")
# footnote preprocessor will be inserted with "raw_html")
# footnote postprocessor will be inserted with ">amp_substitute"
self.prePatterns = []
self.inlinePatterns = Treap()
self.inlinePatterns.add("backtick", BacktickPattern(BACKTICK_RE),
"_begin")
self.inlinePatterns.add("escape", SimpleTextPattern(ESCAPE_RE),
">backtick")
self.inlinePatterns.add("reference",
ReferencePattern(REFERENCE_RE, self), ">escape")
self.inlinePatterns.add("link", LinkPattern(LINK_RE), ">reference")
self.inlinePatterns.add("image_link", ImagePattern(IMAGE_LINK_RE),
">link")
self.inlinePatterns.add("image_reference",
ImageReferencePattern(IMAGE_REFERENCE_RE, self),
">image_link")
self.inlinePatterns.add("autolink", AutolinkPattern(AUTOLINK_RE),
">image_reference")
self.inlinePatterns.add("automail", AutomailPattern(AUTOMAIL_RE),
">autolink")
self.inlinePatterns.add("linebreak2",
SubstituteTagPattern(LINE_BREAK_2_RE, 'br'),
">automail")
self.inlinePatterns.add("linebreak",
SubstituteTagPattern(LINE_BREAK_RE, 'br'),
">linebreak2")
self.inlinePatterns.add("html", HtmlPattern(HTML_RE, self),
">linebreak")
self.inlinePatterns.add("entity", HtmlPattern(ENTITY_RE, self),
">html")
self.inlinePatterns.add("not_strong", SimpleTextPattern(NOT_STRONG_RE),
">entity")
self.inlinePatterns.add("strong_em",
DoubleTagPattern(STRONG_EM_RE, 'strong,em'),
">not_strong")
self.inlinePatterns.add("strong", SimpleTagPattern(STRONG_RE, 'strong'),
">strong_em")
self.inlinePatterns.add("emphasis", SimpleTagPattern(EMPHASIS_RE, 'em'),
">strong")
self.inlinePatterns.add("emphasis2",
SimpleTagPattern(EMPHASIS_2_RE, 'em'),
">emphasis")
# The order of the handlers matters!!!
self.references = {}
self.htmlStash = HtmlStash()
self.registerExtensions(extensions = extensions,
configs = extension_configs)
self.reset()
def registerExtensions(self, extensions, configs):
"""
Register extensions with this instance of Markdown.
Keyword aurguments:
* extensions: A list of extensions, which can either
be strings or objects. See the docstring on Markdown.
* configs: A dictionary mapping module names to config options.
"""
for ext in extensions:
if isinstance(ext, basestring):
ext = load_extension(ext, configs.get(ext, []))
elif hasattr(ext, 'extendMarkdown'):
# Looks like an Extension.
# Nothing to do here.
pass
else:
message(ERROR, "Incorrect type! Extension '%s' is "
"neither a string or an Extension." %(repr(ext)))
continue
ext.extendMarkdown(self, globals())
def registerExtension(self, extension):
""" This gets called by the extension """
self.registeredExtensions.append(extension)
def reset(self):
"""
Resets all state variables so that we can start with a new text.
"""
self.htmlStash.reset()
self.references.clear()
#HTML_BLOCK_PREPROCESSOR.stash = self.htmlStash
#LINE_PREPROCESSOR.stash = self.htmlStash
#REFERENCE_PREPROCESSOR.references = self.references
#HTML_PATTERN.stash = self.htmlStash
#ENTITY_PATTERN.stash = self.htmlStash
#REFERENCE_PATTERN.references = self.references
#IMAGE_REFERENCE_PATTERN.references = self.references
#RAWHTMLTEXTPOSTPROCESSOR.stash = self.htmlStash
#RAWHTMLTEXTPOSTPROCESSOR.safeMode = self.safeMode
for extension in self.registeredExtensions:
extension.reset()
def convert (self, source):
"""Convert markdown to serialized XHTML."""
# Fixup the source text
if not source:
return u"" # a blank unicode string
try:
source = unicode(source)
except UnicodeDecodeError:
message(CRITICAL, 'UnicodeDecodeError: Markdown only accepts unicode or ascii input.')
return u""
source = source.replace(STX, "").replace(ETX, "")
source = source.replace("\r\n", "\n").replace("\r", "\n") + "\n\n"
source = source.expandtabs(TAB_LENGTH)
# Run the text preprocessors
for pp in self.textPreprocessors.heapsorted():
source = pp.run(source)
# Split into lines and run the line preprocessors.
self.lines = source.split("\n")
for prep in self.preprocessors.heapsorted():
self.lines = prep.run(self.lines)
# Parse the high-level elements.
tree = self.parser.parseDocument(self.lines)
# Apply inline patterns
inlineProcessor = InlineProcessor(self.inlinePatterns.heapsorted())
root = inlineProcessor.applyInlinePatterns(tree).getroot()
# Run the post-processors
for postprocessor in self.postprocessors.heapsorted():
newRoot = postprocessor.run(root)
if newRoot:
root = newRoot
# Serialize _properly_. Strip top-level tags.
xml, length = codecs.utf_8_decode(etree.tostring(root, encoding="utf8"))
if self.stripTopLevelTags:
xml = xml.strip()[44:-7] + "\n"
# Run the text post-processors
for pp in self.textPostprocessors.heapsorted():
xml = pp.run(xml)
return xml.strip()
def convertFile(self, input = None, output = None, encoding = None):
"""Converts a markdown file and returns the HTML as a unicode string.
Decodes the file using the provided encoding (defaults to utf-8),
passes the file content to markdown, and outputs the html to either
the provided stream or the file with provided name, using the same
encoding as the source file.
**Note:** This is the only place that decoding and encoding of unicode
takes place in Python-Markdown. (All other code is unicode-in /
unicode-out.)
Keyword arguments:
* input: Name of source text file.
* output: Name of output file. Writes to stdout if `None`.
* extensions: A list of extension names (may contain config args).
* encoding: Encoding of input and output files. Defaults to utf-8.
* safe_mode: Disallow raw html. One of "remove", "replace" or "escape".
"""
encoding = encoding or "utf-8"
# Read the source
input_file = codecs.open(input, mode="r", encoding=encoding)
text = input_file.read()
input_file.close()
text = text.lstrip(u'\ufeff') # remove the byte-order mark
# Convert
html = self.convert(text)
# Write to file or stdout
if type(output) == type("string"):
output_file = codecs.open(output, "w", encoding=encoding)
output_file.write(html)
output_file.close()
else:
output.write(html.encode(encoding))
"""
Extensions
-----------------------------------------------------------------------------
"""
class Extension:
""" Base class for extensions to subclass. """
def __init__(self, configs = {}):
"""Create an instance of an Extention.
Keyword arguments:
* configs: A dict of configuration setting used by an Extension.
"""
self.config = configs
def getConfig(self, key):
""" Return a setting for the given key or an empty string. """
if self.config.has_key(key):
return self.config[key][0]
else:
return ""
def getConfigInfo(self):
""" Return all config settings as a list of tuples. """
return [(key, self.config[key][1]) for key in self.config.keys()]
def setConfig(self, key, value):
""" Set a config setting for `key` with the given `value`. """
self.config[key][0] = value
def extendMarkdown(self, md, md_globals):
"""
Add the various proccesors and patterns to the Markdown Instance.
This method must be overriden by every extension.
Keyword arguments:
* md: The Markdown instance.
* md_globals: Global variables in the markdown module namespace.
"""
pass
def load_extension(ext_name, configs = []):
"""Load extension by name, then return the module.
The extension name may contain arguments as part of the string in the
following format: "extname(key1=value1,key2=value2)"
"""
# Parse extensions config params (ignore the order)
configs = dict(configs)
pos = ext_name.find("(") # find the first "("
if pos > 0:
ext_args = ext_name[pos+1:-1]
ext_name = ext_name[:pos]
pairs = [x.split("=") for x in ext_args.split(",")]
configs.update([(x.strip(), y.strip()) for (x, y) in pairs])
# Setup the module names
ext_module = 'markdown_extensions'
module_name_new_style = '.'.join([ext_module, ext_name])
module_name_old_style = '_'.join(['mdx', ext_name])
# Try loading the extention first from one place, then another
try: # New style (markdown_extensons.)
module = __import__(module_name_new_style, {}, {}, [ext_module])
except ImportError:
try: # Old style (mdx.)
module = __import__(module_name_old_style)
except ImportError:
pass
if module :
# If the module is loaded successfully, we expect it to define a
# function called makeExtension()
try:
return module.makeExtension(configs.items())
except:
message(CRITICAL, "Failed to instantiate extension '%s'" % ext_name)
else:
message(CRITICAL, "Failed loading extension '%s' from '%s' or '%s'"
% (ext_name, module_name_new_style, module_name_old_style))
def load_extensions(ext_names):
"""Loads multiple extensions"""
extensions = []
for ext_name in ext_names:
extension = load_extension(ext_name)
if extension:
extensions.append(extension)
return extensions
# Extensions should use "markdown.etree" instead of "etree" (or do `from
# markdown import etree`). Do not import it by yourself.
etree = importETree()
"""
EXPORTED FUNCTIONS
=============================================================================
Those are the two functions we really mean to export: markdown() and
markdownFromFile().
"""
def markdown(text,
extensions = [],
safe_mode = False):
"""Convert a markdown string to HTML and return HTML as a unicode string.
This is a shortcut function for `Markdown` class to cover the most
basic use case. It initializes an instance of Markdown, loads the
necessary extensions and runs the parser on the given text.
Keyword arguments:
* text: Markdown formatted text as Unicode or ASCII string.
* extensions: A list of extensions or extension names (may contain config args).
* safe_mode: Disallow raw html. One of "remove", "replace" or "escape".
Returns: An HTML document as a string.
"""
md = Markdown(extensions=load_extensions(extensions),
safe_mode = safe_mode)
return md.convert(text)
def markdownFromFile(input = None,
output = None,
extensions = [],
encoding = None,
safe = False):
"""Read markdown code from a file and write it to a file or a stream."""
md = Markdown(extensions=load_extensions(extensions), safe_mode = safe)
md.convertFile(input, output, encoding)
"""
COMMAND-LINE SPECIFIC STUFF
=============================================================================
The rest of the code is specifically for handling the case where Python
Markdown is called from the command line.
"""
OPTPARSE_WARNING = """
Python 2.3 or higher required for advanced command line options.
For lower versions of Python use:
%s INPUT_FILE > OUTPUT_FILE
""" % EXECUTABLE_NAME_FOR_USAGE
def parse_options():
"""
Define and parse `optparse` options for command-line usage.
"""
try:
optparse = __import__("optparse")
except:
if len(sys.argv) == 2:
return {'input': sys.argv[1],
'output': None,
'safe': False,
'extensions': [],
'encoding': None }, CRITICAL
else:
print OPTPARSE_WARNING
return None, None
parser = optparse.OptionParser(usage="%prog INPUTFILE [options]")
parser.add_option("-f", "--file", dest="filename", default=sys.stdout,
help="write output to OUTPUT_FILE",
metavar="OUTPUT_FILE")
parser.add_option("-e", "--encoding", dest="encoding",
help="encoding for input and output files",)
parser.add_option("-q", "--quiet", default = CRITICAL,
action="store_const", const=CRITICAL+10, dest="verbose",
help="suppress all messages")
parser.add_option("-v", "--verbose",
action="store_const", const=INFO, dest="verbose",
help="print info messages")
parser.add_option("-s", "--safe", dest="safe", default=False,
metavar="SAFE_MODE",
help="safe mode ('replace', 'remove' or 'escape' user's HTML tag)")
parser.add_option("--noisy",
action="store_const", const=DEBUG, dest="verbose",
help="print debug messages")
parser.add_option("-x", "--extension", action="append", dest="extensions",
help = "load extension EXTENSION", metavar="EXTENSION")
(options, args) = parser.parse_args()
if not len(args) == 1:
parser.print_help()
return None, None
else:
input_file = args[0]
if not options.extensions:
options.extensions = []
return {'input': input_file,
'output': options.filename,
'safe': options.safe,
'extensions': options.extensions,
'encoding': options.encoding }, options.verbose
def command_line_run():
"""Run Markdown from the command line."""
# Setup a logger manually for compatibility with Python 2.3
logger = logging.getLogger('MARKDOWN')
logger.setLevel(COMMAND_LINE_LOGGING_LEVEL)
logger.addHandler(logging.StreamHandler())
# Parse options and adjust logging level if necessary
options, logging_level = parse_options()
if not options: sys.exit(0)
if logging_level: logging.getLogger('MARKDOWN').setLevel(logging_level)
# Run
markdownFromFile(**options)
if __name__ == '__main__':
command_line_run()