diff options
Diffstat (limited to 'markdown/extensions')
-rw-r--r-- | markdown/extensions/__init__.py | 0 | ||||
-rw-r--r-- | markdown/extensions/abbr.py | 95 | ||||
-rw-r--r-- | markdown/extensions/codehilite.py | 224 | ||||
-rw-r--r-- | markdown/extensions/def_list.py | 104 | ||||
-rw-r--r-- | markdown/extensions/extra.py | 49 | ||||
-rw-r--r-- | markdown/extensions/fenced_code.py | 117 | ||||
-rw-r--r-- | markdown/extensions/footnotes.py | 307 | ||||
-rw-r--r-- | markdown/extensions/headerid.py | 195 | ||||
-rw-r--r-- | markdown/extensions/html_tidy.py | 62 | ||||
-rw-r--r-- | markdown/extensions/imagelinks.py | 119 | ||||
-rw-r--r-- | markdown/extensions/meta.py | 90 | ||||
-rw-r--r-- | markdown/extensions/rss.py | 114 | ||||
-rw-r--r-- | markdown/extensions/tables.py | 97 | ||||
-rw-r--r-- | markdown/extensions/toc.py | 136 | ||||
-rw-r--r-- | markdown/extensions/wikilinks.py | 155 |
15 files changed, 1864 insertions, 0 deletions
diff --git a/markdown/extensions/__init__.py b/markdown/extensions/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/markdown/extensions/__init__.py diff --git a/markdown/extensions/abbr.py b/markdown/extensions/abbr.py new file mode 100644 index 0000000..783220e --- /dev/null +++ b/markdown/extensions/abbr.py @@ -0,0 +1,95 @@ +''' +Abbreviation Extension for Python-Markdown +========================================== + +This extension adds abbreviation handling to Python-Markdown. + +Simple Usage: + + >>> import markdown + >>> text = """ + ... Some text with an ABBR and a REF. Ignore REFERENCE and ref. + ... + ... *[ABBR]: Abbreviation + ... *[REF]: Abbreviation Reference + ... """ + >>> markdown.markdown(text, ['abbr']) + u'<p>Some text with an <abbr title="Abbreviation">ABBR</abbr> and a <abbr title="Abbreviation Reference">REF</abbr>. Ignore REFERENCE and ref.</p>' + +Copyright 2007-2008 +* [Waylan Limberg](http://achinghead.com/) +* [Seemant Kulleen](http://www.kulleen.org/) + + +''' + +import markdown, re +from markdown import etree + +# Global Vars +ABBR_REF_RE = re.compile(r'[*]\[(?P<abbr>[^\]]*)\][ ]?:\s*(?P<title>.*)') + +class AbbrExtension(markdown.Extension): + """ Abbreviation Extension for Python-Markdown. """ + + def extendMarkdown(self, md, md_globals): + """ Insert AbbrPreprocessor before ReferencePreprocessor. """ + md.preprocessors.add('abbr', AbbrPreprocessor(md), '<reference') + + +class AbbrPreprocessor(markdown.preprocessors.Preprocessor): + """ Abbreviation Preprocessor - parse text for abbr references. """ + + def run(self, lines): + ''' + Find and remove all Abbreviation references from the text. + Each reference is set as a new AbbrPattern in the markdown instance. + + ''' + new_text = [] + for line in lines: + m = ABBR_REF_RE.match(line) + if m: + abbr = m.group('abbr').strip() + title = m.group('title').strip() + self.markdown.inlinePatterns['abbr-%s'%abbr] = \ + AbbrPattern(self._generate_pattern(abbr), title) + else: + new_text.append(line) + return new_text + + def _generate_pattern(self, text): + ''' + Given a string, returns an regex pattern to match that string. + + 'HTML' -> r'(?P<abbr>[H][T][M][L])' + + Note: we force each char as a literal match (in brackets) as we don't + know what they will be beforehand. + + ''' + chars = list(text) + for i in range(len(chars)): + chars[i] = r'[%s]' % chars[i] + return r'(?P<abbr>\b%s\b)' % (r''.join(chars)) + + +class AbbrPattern(markdown.inlinepatterns.Pattern): + """ Abbreviation inline pattern. """ + + def __init__(self, pattern, title): + markdown.inlinepatterns.Pattern.__init__(self, pattern) + self.title = title + + def handleMatch(self, m): + abbr = etree.Element('abbr') + abbr.text = m.group('abbr') + abbr.set('title', self.title) + return abbr + +def makeExtension(configs=None): + return AbbrExtension(configs=configs) + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/markdown/extensions/codehilite.py b/markdown/extensions/codehilite.py new file mode 100644 index 0000000..c5d496b --- /dev/null +++ b/markdown/extensions/codehilite.py @@ -0,0 +1,224 @@ +#!/usr/bin/python + +""" +CodeHilite Extension for Python-Markdown +======================================== + +Adds code/syntax highlighting to standard Python-Markdown code blocks. + +Copyright 2006-2008 [Waylan Limberg](http://achinghead.com/). + +Project website: <http://www.freewisdom.org/project/python-markdown/CodeHilite> +Contact: markdown@freewisdom.org + +License: BSD (see ../docs/LICENSE for details) + +Dependencies: +* [Python 2.3+](http://python.org/) +* [Markdown 2.0+](http://www.freewisdom.org/projects/python-markdown/) +* [Pygments](http://pygments.org/) + +""" + +import markdown + +# --------------- CONSTANTS YOU MIGHT WANT TO MODIFY ----------------- + +try: + TAB_LENGTH = markdown.TAB_LENGTH +except AttributeError: + TAB_LENGTH = 4 + + +# ------------------ The Main CodeHilite Class ---------------------- +class CodeHilite: + """ + Determine language of source code, and pass it into the pygments hilighter. + + Basic Usage: + >>> code = CodeHilite(src = 'some text') + >>> html = code.hilite() + + * src: Source string or any object with a .readline attribute. + + * linenos: (Boolen) Turn line numbering 'on' or 'off' (off by default). + + * css_class: Set class name of wrapper div ('codehilite' by default). + + Low Level Usage: + >>> code = CodeHilite() + >>> code.src = 'some text' # String or anything with a .readline attr. + >>> code.linenos = True # True or False; Turns line numbering on or of. + >>> html = code.hilite() + + """ + + def __init__(self, src=None, linenos=False, css_class="codehilite"): + self.src = src + self.lang = None + self.linenos = linenos + self.css_class = css_class + + def hilite(self): + """ + Pass code to the [Pygments](http://pygments.pocoo.org/) highliter with + optional line numbers. The output should then be styled with css to + your liking. No styles are applied by default - only styling hooks + (i.e.: <span class="k">). + + returns : A string of html. + + """ + + self.src = self.src.strip('\n') + + self._getLang() + + try: + from pygments import highlight + from pygments.lexers import get_lexer_by_name, guess_lexer, \ + TextLexer + from pygments.formatters import HtmlFormatter + except ImportError: + # just escape and pass through + txt = self._escape(self.src) + if self.linenos: + txt = self._number(txt) + else : + txt = '<div class="%s"><pre>%s</pre></div>\n'% \ + (self.css_class, txt) + return txt + else: + try: + lexer = get_lexer_by_name(self.lang) + except ValueError: + try: + lexer = guess_lexer(self.src) + except ValueError: + lexer = TextLexer() + formatter = HtmlFormatter(linenos=self.linenos, + cssclass=self.css_class) + return highlight(self.src, lexer, formatter) + + def _escape(self, txt): + """ basic html escaping """ + txt = txt.replace('&', '&') + txt = txt.replace('<', '<') + txt = txt.replace('>', '>') + txt = txt.replace('"', '"') + return txt + + def _number(self, txt): + """ Use <ol> for line numbering """ + # Fix Whitespace + txt = txt.replace('\t', ' '*TAB_LENGTH) + txt = txt.replace(" "*4, " ") + txt = txt.replace(" "*3, " ") + txt = txt.replace(" "*2, " ") + + # Add line numbers + lines = txt.splitlines() + txt = '<div class="codehilite"><pre><ol>\n' + for line in lines: + txt += '\t<li>%s</li>\n'% line + txt += '</ol></pre></div>\n' + return txt + + + def _getLang(self): + """ + Determines language of a code block from shebang lines and whether said + line should be removed or left in place. If the sheband line contains a + path (even a single /) then it is assumed to be a real shebang lines and + left alone. However, if no path is given (e.i.: #!python or :::python) + then it is assumed to be a mock shebang for language identifitation of a + code fragment and removed from the code block prior to processing for + code highlighting. When a mock shebang (e.i: #!python) is found, line + numbering is turned on. When colons are found in place of a shebang + (e.i.: :::python), line numbering is left in the current state - off + by default. + + """ + + import re + + #split text into lines + lines = self.src.split("\n") + #pull first line to examine + fl = lines.pop(0) + + c = re.compile(r''' + (?:(?:::+)|(?P<shebang>[#]!)) # Shebang or 2 or more colons. + (?P<path>(?:/\w+)*[/ ])? # Zero or 1 path + (?P<lang>[\w+-]*) # The language + ''', re.VERBOSE) + # search first line for shebang + m = c.search(fl) + if m: + # we have a match + try: + self.lang = m.group('lang').lower() + except IndexError: + self.lang = None + if m.group('path'): + # path exists - restore first line + lines.insert(0, fl) + if m.group('shebang'): + # shebang exists - use line numbers + self.linenos = True + else: + # No match + lines.insert(0, fl) + + self.src = "\n".join(lines).strip("\n") + + + +# ------------------ The Markdown Extension ------------------------------- +class HiliteTreeprocessor(markdown.treeprocessors.Treeprocessor): + """ Hilight source code in code blocks. """ + + def run(self, root): + """ Find code blocks and store in htmlStash. """ + blocks = root.getiterator('pre') + for block in blocks: + children = block.getchildren() + if len(children) == 1 and children[0].tag == 'code': + code = CodeHilite(children[0].text, + linenos=self.config['force_linenos'][0], + css_class=self.config['css_class'][0]) + placeholder = self.markdown.htmlStash.store(code.hilite(), + safe=True) + # Clear codeblock in etree instance + block.clear() + # Change to p element which will later + # be removed when inserting raw html + block.tag = 'p' + block.text = placeholder + + +class CodeHiliteExtension(markdown.Extension): + """ Add source code hilighting to markdown codeblocks. """ + + def __init__(self, configs): + # define default configs + self.config = { + 'force_linenos' : [False, "Force line numbers - Default: False"], + 'css_class' : ["codehilite", + "Set class name for wrapper <div> - Default: codehilite"], + } + + # Override defaults with user settings + for key, value in configs: + self.setConfig(key, value) + + def extendMarkdown(self, md, md_globals): + """ Add HilitePostprocessor to Markdown instance. """ + hiliter = HiliteTreeprocessor(md) + hiliter.config = self.config + md.treeprocessors.add("hilite", hiliter, "_begin") + + +def makeExtension(configs={}): + return CodeHiliteExtension(configs=configs) + diff --git a/markdown/extensions/def_list.py b/markdown/extensions/def_list.py new file mode 100644 index 0000000..73a1c85 --- /dev/null +++ b/markdown/extensions/def_list.py @@ -0,0 +1,104 @@ +#!/usr/bin/env Python +""" +Definition List Extension for Python-Markdown +============================================= + +Added parsing of Definition Lists to Python-Markdown. + +A simple example: + + Apple + : Pomaceous fruit of plants of the genus Malus in + the family Rosaceae. + : An american computer company. + + Orange + : The fruit of an evergreen tree of the genus Citrus. + +Copyright 2008 - [Waylan Limberg](http://achinghead.com) + +""" + +import markdown, re +from markdown import etree + + +class DefListProcessor(markdown.blockprocessors.BlockProcessor): + """ Process Definition Lists. """ + + RE = re.compile(r'(^|\n)[ ]{0,3}:[ ]{1,3}(.*?)(\n|$)') + + def test(self, parent, block): + return bool(self.RE.search(block)) + + def run(self, parent, blocks): + block = blocks.pop(0) + m = self.RE.search(block) + terms = [l.strip() for l in block[:m.start()].split('\n') if l.strip()] + d, theRest = self.detab(block[m.end():]) + if d: + d = '%s\n%s' % (m.group(2), d) + else: + d = m.group(2) + #import ipdb; ipdb.set_trace() + sibling = self.lastChild(parent) + if not terms and sibling.tag == 'p': + # The previous paragraph contains the terms + state = 'looselist' + terms = sibling.text.split('\n') + parent.remove(sibling) + # Aquire new sibling + sibling = self.lastChild(parent) + else: + state = 'list' + + if sibling and sibling.tag == 'dl': + # This is another item on an existing list + dl = sibling + if len(dl) and dl[-1].tag == 'dd' and len(dl[-1]): + state = 'looselist' + else: + # This is a new list + dl = etree.SubElement(parent, 'dl') + # Add terms + for term in terms: + dt = etree.SubElement(dl, 'dt') + dt.text = term + # Add definition + self.parser.state.set(state) + dd = etree.SubElement(dl, 'dd') + self.parser.parseBlocks(dd, [d]) + self.parser.state.reset() + + if theRest: + blocks.insert(0, theRest) + +class DefListIndentProcessor(markdown.blockprocessors.ListIndentProcessor): + """ Process indented children of definition list items. """ + + ITEM_TYPES = ['dd'] + LIST_TYPES = ['dl'] + + def create_item(parent, block): + """ Create a new dd and parse the block with it as the parent. """ + dd = markdown.etree.SubElement(parent, 'dd') + self.parser.parseBlocks(dd, [block]) + + + +class DefListExtension(markdown.Extension): + """ Add definition lists to Markdown. """ + + def extendMarkdown(self, md, md_globals): + """ Add an instance of DefListProcessor to BlockParser. """ + md.parser.blockprocessors.add('defindent', + DefListIndentProcessor(md.parser), + '>indent') + md.parser.blockprocessors.add('deflist', + DefListProcessor(md.parser), + '>ulist') + + +def makeExtension(configs={}): + return DefListExtension(configs=configs) + diff --git a/markdown/extensions/extra.py b/markdown/extensions/extra.py new file mode 100644 index 0000000..4a2ffbf --- /dev/null +++ b/markdown/extensions/extra.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +""" +Python-Markdown Extra Extension +=============================== + +A compilation of various Python-Markdown extensions that imitates +[PHP Markdown Extra](http://michelf.com/projects/php-markdown/extra/). + +Note that each of the individual extensions still need to be available +on your PYTHONPATH. This extension simply wraps them all up as a +convenience so that only one extension needs to be listed when +initiating Markdown. See the documentation for each individual +extension for specifics about that extension. + +In the event that one or more of the supported extensions are not +available for import, Markdown will issue a warning and simply continue +without that extension. + +There may be additional extensions that are distributed with +Python-Markdown that are not included here in Extra. Those extensions +are not part of PHP Markdown Extra, and therefore, not part of +Python-Markdown Extra. If you really would like Extra to include +additional extensions, we suggest creating your own clone of Extra +under a differant name. You could also edit the `extensions` global +variable defined below, but be aware that such changes may be lost +when you upgrade to any future version of Python-Markdown. + +""" + +import markdown + +extensions = ['fenced_code', + 'footnotes', + 'headerid', + 'def_list', + 'tables', + 'abbr', + ] + + +class ExtraExtension(markdown.Extension): + """ Add various extensions to Markdown class.""" + + def extendMarkdown(self, md, md_globals): + """ Register extension instances. """ + md.registerExtensions(extensions, self.config) + +def makeExtension(configs={}): + return ExtraExtension(configs=dict(configs)) diff --git a/markdown/extensions/fenced_code.py b/markdown/extensions/fenced_code.py new file mode 100644 index 0000000..307b1dc --- /dev/null +++ b/markdown/extensions/fenced_code.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +""" +Fenced Code Extension for Python Markdown +========================================= + +This extension adds Fenced Code Blocks to Python-Markdown. + + >>> import markdown + >>> text = ''' + ... A paragraph before a fenced code block: + ... + ... ~~~ + ... Fenced code block + ... ~~~ + ... ''' + >>> html = markdown.markdown(text, extensions=['fenced_code']) + >>> html + u'<p>A paragraph before a fenced code block:</p>\\n<pre><code>Fenced code block\\n</code></pre>' + +Works with safe_mode also (we check this because we are using the HtmlStash): + + >>> markdown.markdown(text, extensions=['fenced_code'], safe_mode='replace') + u'<p>A paragraph before a fenced code block:</p>\\n<pre><code>Fenced code block\\n</code></pre>' + +Include tilde's in a code block and wrap with blank lines: + + >>> text = ''' + ... ~~~~~~~~ + ... + ... ~~~~ + ... + ... ~~~~~~~~''' + >>> markdown.markdown(text, extensions=['fenced_code']) + u'<pre><code>\\n~~~~\\n\\n</code></pre>' + +Multiple blocks and language tags: + + >>> text = ''' + ... ~~~~{.python} + ... block one + ... ~~~~ + ... + ... ~~~~.html + ... <p>block two</p> + ... ~~~~''' + >>> markdown.markdown(text, extensions=['fenced_code']) + u'<pre><code class="python">block one\\n</code></pre>\\n\\n<pre><code class="html"><p>block two</p>\\n</code></pre>' + +Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/). + +Project website: <http://www.freewisdom.org/project/python-markdown/Fenced__Code__Blocks> +Contact: markdown@freewisdom.org + +License: BSD (see ../docs/LICENSE for details) + +Dependencies: +* [Python 2.3+](http://python.org) +* [Markdown 2.0+](http://www.freewisdom.org/projects/python-markdown/) + +""" + +import markdown, re + +# Global vars +FENCED_BLOCK_RE = re.compile( \ + r'(?P<fence>^~{3,})[ ]*(\{?\.(?P<lang>[a-zA-Z0-9_-]*)\}?)?[ ]*\n(?P<code>.*?)(?P=fence)[ ]*$', + re.MULTILINE|re.DOTALL + ) +CODE_WRAP = '<pre><code%s>%s</code></pre>' +LANG_TAG = ' class="%s"' + + +class FencedCodeExtension(markdown.Extension): + + def extendMarkdown(self, md, md_globals): + """ Add FencedBlockPreprocessor to the Markdown instance. """ + + md.preprocessors.add('fenced_code_block', + FencedBlockPreprocessor(md), + "_begin") + + +class FencedBlockPreprocessor(markdown.preprocessors.Preprocessor): + + def run(self, lines): + """ Match and store Fenced Code Blocks in the HtmlStash. """ + text = "\n".join(lines) + while 1: + m = FENCED_BLOCK_RE.search(text) + if m: + lang = '' + if m.group('lang'): + lang = LANG_TAG % m.group('lang') + code = CODE_WRAP % (lang, self._escape(m.group('code'))) + placeholder = self.markdown.htmlStash.store(code, safe=True) + text = '%s\n%s\n%s'% (text[:m.start()], placeholder, text[m.end():]) + else: + break + return text.split("\n") + + def _escape(self, txt): + """ basic html escaping """ + txt = txt.replace('&', '&') + txt = txt.replace('<', '<') + txt = txt.replace('>', '>') + txt = txt.replace('"', '"') + return txt + + +def makeExtension(configs=None): + return FencedCodeExtension() + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/markdown/extensions/footnotes.py b/markdown/extensions/footnotes.py new file mode 100644 index 0000000..e1a9cda --- /dev/null +++ b/markdown/extensions/footnotes.py @@ -0,0 +1,307 @@ +""" +========================= FOOTNOTES ================================= + +This section adds footnote handling to markdown. It can be used as +an example for extending python-markdown with relatively complex +functionality. While in this case the extension is included inside +the module itself, it could just as easily be added from outside the +module. Not that all markdown classes above are ignorant about +footnotes. All footnote functionality is provided separately and +then added to the markdown instance at the run time. + +Footnote functionality is attached by calling extendMarkdown() +method of FootnoteExtension. The method also registers the +extension to allow it's state to be reset by a call to reset() +method. + +Example: + Footnotes[^1] have a label[^label] and a definition[^!DEF]. + + [^1]: This is a footnote + [^label]: A footnote on "label" + [^!DEF]: The footnote for definition + +""" + +import re, markdown +from markdown import etree + +FN_BACKLINK_TEXT = "zz1337820767766393qq" +NBSP_PLACEHOLDER = "qq3936677670287331zz" +DEF_RE = re.compile(r'(\ ?\ ?\ ?)\[\^([^\]]*)\]:\s*(.*)') +TABBED_RE = re.compile(r'((\t)|( ))(.*)') + +class FootnoteExtension(markdown.Extension): + """ Footnote Extension. """ + + def __init__ (self, configs): + """ Setup configs. """ + self.config = {'PLACE_MARKER': + ["///Footnotes Go Here///", + "The text string that marks where the footnotes go"], + 'UNIQUE_IDS': + [False, + "Avoid name collisions across " + "multiple calls to reset()."]} + + for key, value in configs: + self.config[key][0] = value + + # In multiple invocations, emit links that don't get tangled. + self.unique_prefix = 0 + + self.reset() + + def extendMarkdown(self, md, md_globals): + """ Add pieces to Markdown. """ + md.registerExtension(self) + self.parser = md.parser + # Insert a preprocessor before ReferencePreprocessor + md.preprocessors.add("footnote", FootnotePreprocessor(self), + "<reference") + # Insert an inline pattern before ImageReferencePattern + FOOTNOTE_RE = r'\[\^([^\]]*)\]' # blah blah [^1] blah + md.inlinePatterns.add("footnote", FootnotePattern(FOOTNOTE_RE, self), + "<reference") + # Insert a tree-processor that would actually add the footnote div + # This must be before the inline treeprocessor so inline patterns + # run on the contents of the div. + md.treeprocessors.add("footnote", FootnoteTreeprocessor(self), + "<inline") + # Insert a postprocessor after amp_substitute oricessor + md.postprocessors.add("footnote", FootnotePostprocessor(self), + ">amp_substitute") + + def reset(self): + """ Clear the footnotes on reset, and prepare for a distinct document. """ + self.footnotes = markdown.odict.OrderedDict() + self.unique_prefix += 1 + + def findFootnotesPlaceholder(self, root): + """ Return ElementTree Element that contains Footnote placeholder. """ + def finder(element): + for child in element: + if child.text: + if child.text.find(self.getConfig("PLACE_MARKER")) > -1: + return child, True + if child.tail: + if child.tail.find(self.getConfig("PLACE_MARKER")) > -1: + return (child, element), False + finder(child) + return None + + res = finder(root) + return res + + def setFootnote(self, id, text): + """ Store a footnote for later retrieval. """ + self.footnotes[id] = text + + def makeFootnoteId(self, id): + """ Return footnote link id. """ + if self.getConfig("UNIQUE_IDS"): + return 'fn:%d-%s' % (self.unique_prefix, id) + else: + return 'fn:%s' % id + + def makeFootnoteRefId(self, id): + """ Return footnote back-link id. """ + if self.getConfig("UNIQUE_IDS"): + return 'fnref:%d-%s' % (self.unique_prefix, id) + else: + return 'fnref:%s' % id + + def makeFootnotesDiv(self, root): + """ Return div of footnotes as et Element. """ + + if not self.footnotes.keys(): + return None + + div = etree.Element("div") + div.set('class', 'footnote') + hr = etree.SubElement(div, "hr") + ol = etree.SubElement(div, "ol") + + for id in self.footnotes.keys(): + li = etree.SubElement(ol, "li") + li.set("id", self.makeFootnoteId(id)) + self.parser.parseChunk(li, self.footnotes[id]) + backlink = etree.Element("a") + backlink.set("href", "#" + self.makeFootnoteRefId(id)) + backlink.set("rev", "footnote") + backlink.set("title", "Jump back to footnote %d in the text" % \ + (self.footnotes.index(id)+1)) + backlink.text = FN_BACKLINK_TEXT + + if li.getchildren(): + node = li[-1] + if node.tag == "p": + node.text = node.text + NBSP_PLACEHOLDER + node.append(backlink) + else: + p = etree.SubElement(li, "p") + p.append(backlink) + return div + + +class FootnotePreprocessor(markdown.preprocessors.Preprocessor): + """ Find all footnote references and store for later use. """ + + def __init__ (self, footnotes): + self.footnotes = footnotes + + def run(self, lines): + lines = self._handleFootnoteDefinitions(lines) + text = "\n".join(lines) + return text.split("\n") + + def _handleFootnoteDefinitions(self, lines): + """ + Recursively find all footnote definitions in lines. + + Keywords: + + * lines: A list of lines of text + + Return: A list of lines with footnote definitions removed. + + """ + i, id, footnote = self._findFootnoteDefinition(lines) + + if id : + plain = lines[:i] + detabbed, theRest = self.detectTabbed(lines[i+1:]) + self.footnotes.setFootnote(id, + footnote + "\n" + + "\n".join(detabbed)) + more_plain = self._handleFootnoteDefinitions(theRest) + return plain + [""] + more_plain + else : + return lines + + def _findFootnoteDefinition(self, lines): + """ + Find the parts of a footnote definition. + + Keywords: + + * lines: A list of lines of text. + + Return: A three item tuple containing the index of the first line of a + footnote definition, the id of the definition and the body of the + definition. + + """ + counter = 0 + for line in lines: + m = DEF_RE.match(line) + if m: + return counter, m.group(2), m.group(3) + counter += 1 + return counter, None, None + + def detectTabbed(self, lines): + """ Find indented text and remove indent before further proccesing. + + Keyword arguments: + + * lines: an array of strings + + Returns: a list of post processed 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 = TABBED_RE.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:] + + +class FootnotePattern(markdown.inlinepatterns.Pattern): + """ InlinePattern for footnote markers in a document's body text. """ + + def __init__(self, pattern, footnotes): + markdown.inlinepatterns.Pattern.__init__(self, pattern) + self.footnotes = footnotes + + def handleMatch(self, m): + sup = etree.Element("sup") + a = etree.SubElement(sup, "a") + id = m.group(2) + sup.set('id', self.footnotes.makeFootnoteRefId(id)) + a.set('href', '#' + self.footnotes.makeFootnoteId(id)) + a.set('rel', 'footnote') + a.text = str(self.footnotes.footnotes.index(id) + 1) + return sup + + +class FootnoteTreeprocessor(markdown.treeprocessors.Treeprocessor): + """ Build and append footnote div to end of document. """ + + def __init__ (self, footnotes): + self.footnotes = footnotes + + def run(self, root): + footnotesDiv = self.footnotes.makeFootnotesDiv(root) + if footnotesDiv: + result = self.footnotes.findFootnotesPlaceholder(root) + if result: + node, isText = result + if isText: + node.text = None + node.getchildren().insert(0, footnotesDiv) + else: + child, element = node + ind = element.getchildren().find(child) + element.getchildren().insert(ind + 1, footnotesDiv) + child.tail = None + fnPlaceholder.parent.replaceChild(fnPlaceholder, footnotesDiv) + else: + root.append(footnotesDiv) + +class FootnotePostprocessor(markdown.postprocessors.Postprocessor): + """ Replace placeholders with html entities. """ + + def run(self, text): + text = text.replace(FN_BACKLINK_TEXT, "↩") + return text.replace(NBSP_PLACEHOLDER, " ") + +def makeExtension(configs=[]): + """ Return an instance of the FootnoteExtension """ + return FootnoteExtension(configs=configs) + diff --git a/markdown/extensions/headerid.py b/markdown/extensions/headerid.py new file mode 100644 index 0000000..f70a7a9 --- /dev/null +++ b/markdown/extensions/headerid.py @@ -0,0 +1,195 @@ +#!/usr/bin/python + +""" +HeaderID Extension for Python-Markdown +====================================== + +Adds ability to set HTML IDs for headers. + +Basic usage: + + >>> import markdown + >>> text = "# Some Header # {#some_id}" + >>> md = markdown.markdown(text, ['headerid']) + >>> md + u'<h1 id="some_id">Some Header</h1>' + +All header IDs are unique: + + >>> text = ''' + ... #Header + ... #Another Header {#header} + ... #Third Header {#header}''' + >>> md = markdown.markdown(text, ['headerid']) + >>> md + u'<h1 id="header">Header</h1>\\n<h1 id="header_1">Another Header</h1>\\n<h1 id="header_2">Third Header</h1>' + +To fit within a html template's hierarchy, set the header base level: + + >>> text = ''' + ... #Some Header + ... ## Next Level''' + >>> md = markdown.markdown(text, ['headerid(level=3)']) + >>> md + u'<h3 id="some_header">Some Header</h3>\\n<h4 id="next_level">Next Level</h4>' + +Turn off auto generated IDs: + + >>> text = ''' + ... # Some Header + ... # Header with ID # { #foo }''' + >>> md = markdown.markdown(text, ['headerid(forceid=False)']) + >>> md + u'<h1>Some Header</h1>\\n<h1 id="foo">Header with ID</h1>' + +Use with MetaData extension: + + >>> text = '''header_level: 2 + ... header_forceid: Off + ... + ... # A Header''' + >>> md = markdown.markdown(text, ['headerid', 'meta']) + >>> md + u'<h2>A Header</h2>' + +Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/). + +Project website: <http://www.freewisdom.org/project/python-markdown/HeaderId> +Contact: markdown@freewisdom.org + +License: BSD (see ../docs/LICENSE for details) + +Dependencies: +* [Python 2.3+](http://python.org) +* [Markdown 2.0+](http://www.freewisdom.org/projects/python-markdown/) + +""" + +import markdown +from markdown import etree +import re +from string import ascii_lowercase, digits, punctuation + +ID_CHARS = ascii_lowercase + digits + '-_' +IDCOUNT_RE = re.compile(r'^(.*)_([0-9]+)$') + + +class HeaderIdProcessor(markdown.blockprocessors.BlockProcessor): + """ Replacement BlockProcessor for Header IDs. """ + + # Detect a header at start of any line in block + RE = re.compile(r"""(^|\n) + (?P<level>\#{1,6}) # group('level') = string of hashes + (?P<header>.*?) # group('header') = Header text + \#* # optional closing hashes + (?:[ \t]*\{[ \t]*\#(?P<id>[-_:a-zA-Z0-9]+)[ \t]*\})? + (\n|$) # ^^ group('id') = id attribute + """, + re.VERBOSE) + + IDs = [] + + def test(self, parent, block): + return bool(self.RE.search(block)) + + def run(self, parent, blocks): + block = blocks.pop(0) + m = self.RE.search(block) + if m: + before = block[:m.start()] # All lines before header + after = block[m.end():] # All lines after header + if before: + # As the header was not the first line of the block and the + # lines before the header must be parsed first, + # recursively parse this lines as a block. + self.parser.parseBlocks(parent, [before]) + # Create header using named groups from RE + start_level, force_id = self._get_meta() + level = len(m.group('level')) + start_level + if level > 6: + level = 6 + h = markdown.etree.SubElement(parent, 'h%d' % level) + h.text = m.group('header').strip() + if m.group('id'): + h.set('id', self._unique_id(m.group('id'))) + elif force_id: + h.set('id', self._create_id(m.group('header').strip())) + if after: + # Insert remaining lines as first block for future parsing. + blocks.insert(0, after) + else: + # This should never happen, but just in case... + message(CRITICAL, "We've got a problem header!") + + def _get_meta(self): + """ Return meta data suported by this ext as a tuple """ + level = int(self.config['level'][0]) - 1 + force = self._str2bool(self.config['forceid'][0]) + if hasattr(self.md, 'Meta'): + if self.md.Meta.has_key('header_level'): + level = int(self.md.Meta['header_level'][0]) - 1 + if self.md.Meta.has_key('header_forceid'): + force = self._str2bool(self.md.Meta['header_forceid'][0]) + return level, force + + def _str2bool(self, s, default=False): + """ Convert a string to a booleen value. """ + s = str(s) + if s.lower() in ['0', 'f', 'false', 'off', 'no', 'n']: + return False + elif s.lower() in ['1', 't', 'true', 'on', 'yes', 'y']: + return True + return default + + def _unique_id(self, id): + """ Ensure ID is unique. Append '_1', '_2'... if not """ + while id in self.IDs: + m = IDCOUNT_RE.match(id) + if m: + id = '%s_%d'% (m.group(1), int(m.group(2))+1) + else: + id = '%s_%d'% (id, 1) + self.IDs.append(id) + return id + + def _create_id(self, header): + """ Return ID from Header text. """ + h = '' + for c in header.lower().replace(' ', '_'): + if c in ID_CHARS: + h += c + elif c not in punctuation: + h += '+' + return self._unique_id(h) + + +class HeaderIdExtension (markdown.Extension): + def __init__(self, configs): + # set defaults + self.config = { + 'level' : ['1', 'Base level for headers.'], + 'forceid' : ['True', 'Force all headers to have an id.'] + } + + for key, value in configs: + self.setConfig(key, value) + + def extendMarkdown(self, md, md_globals): + md.registerExtension(self) + self.processor = HeaderIdProcessor(md.parser) + self.processor.md = md + self.processor.config = self.config + # Replace existing hasheader in place. + md.parser.blockprocessors['hashheader'] = self.processor + + def reset(self): + self.processor.IDs = [] + + +def makeExtension(configs=None): + return HeaderIdExtension(configs=configs) + +if __name__ == "__main__": + import doctest + doctest.testmod() + diff --git a/markdown/extensions/html_tidy.py b/markdown/extensions/html_tidy.py new file mode 100644 index 0000000..5105e33 --- /dev/null +++ b/markdown/extensions/html_tidy.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +""" +HTML Tidy Extension for Python-Markdown +======================================= + +Runs [HTML Tidy][] on the output of Python-Markdown using the [uTidylib][] +Python wrapper. Both libtidy and uTidylib must be installed on your system. + +Note than any Tidy [options][] can be passed in as extension configs. So, +for example, to output HTML rather than XHTML, set ``output_xhtml=0``. To +indent the output, set ``indent=auto`` and to have Tidy wrap the output in +``<html>`` and ``<body>`` tags, set ``show_body_only=0``. + +[HTML Tidy]: http://tidy.sourceforge.net/ +[uTidylib]: http://utidylib.berlios.de/ +[options]: http://tidy.sourceforge.net/docs/quickref.html + +Copyright (c)2008 [Waylan Limberg](http://achinghead.com) + +License: [BSD](http://www.opensource.org/licenses/bsd-license.php) + +Dependencies: +* [Python2.3+](http://python.org) +* [Markdown 2.0+](http://www.freewisdom.org/projects/python-markdown/) +* [HTML Tidy](http://utidylib.berlios.de/) +* [uTidylib](http://utidylib.berlios.de/) + +""" + +import markdown +import tidy + +class TidyExtension(markdown.Extension): + + def __init__(self, configs): + # Set defaults to match typical markdown behavior. + self.config = dict(output_xhtml=1, + show_body_only=1, + ) + # Merge in user defined configs overriding any present if nessecary. + for c in configs: + self.config[c[0]] = c[1] + + def extendMarkdown(self, md, md_globals): + # Save options to markdown instance + md.tidy_options = self.config + # Add TidyProcessor to postprocessors + md.postprocessors['tidy'] = TidyProcessor(md) + + +class TidyProcessor(markdown.postprocessors.Postprocessor): + + def run(self, text): + # Pass text to Tidy. As Tidy does not accept unicode we need to encode + # it and decode its return value. + return unicode(tidy.parseString(text.encode('utf-8'), + **self.markdown.tidy_options)) + + +def makeExtension(configs=None): + return TidyExtension(configs=configs) diff --git a/markdown/extensions/imagelinks.py b/markdown/extensions/imagelinks.py new file mode 100644 index 0000000..ee0b708 --- /dev/null +++ b/markdown/extensions/imagelinks.py @@ -0,0 +1,119 @@ +""" +========================= IMAGE LINKS ================================= + + +Turns paragraphs like + +<~~~~~~~~~~~~~~~~~~~~~~~~ +dir/subdir +dir/subdir +dir/subdir +~~~~~~~~~~~~~~ +dir/subdir +dir/subdir +dir/subdir +~~~~~~~~~~~~~~~~~~~> + +Into mini-photo galleries. + +""" + +import re, markdown +import url_manager + + +IMAGE_LINK = """<a href="%s"><img src="%s" title="%s"/></a>""" +SLIDESHOW_LINK = """<a href="%s" target="_blank">[slideshow]</a>""" +ALBUM_LINK = """ <a href="%s">[%s]</a>""" + + +class ImageLinksExtension(markdown.Extension): + + def extendMarkdown(self, md, md_globals): + + md.preprocessors.add("imagelink", ImageLinkPreprocessor(md), "_begin") + + +class ImageLinkPreprocessor(markdown.preprocessors.Preprocessor): + + def run(self, lines): + + url = url_manager.BlogEntryUrl(url_manager.BlogUrl("all"), + "2006/08/29/the_rest_of_our") + + + all_images = [] + blocks = [] + in_image_block = False + + new_lines = [] + + for line in lines: + + if line.startswith("<~~~~~~~"): + albums = [] + rows = [] + in_image_block = True + + if not in_image_block: + + new_lines.append(line) + + else: + + line = line.strip() + + if line.endswith("~~~~~~>") or not line: + in_image_block = False + new_block = "<div><br/><center><span class='image-links'>\n" + + album_url_hash = {} + + for row in rows: + for photo_url, title in row: + new_block += " " + new_block += IMAGE_LINK % (photo_url, + photo_url.get_thumbnail(), + title) + + album_url_hash[str(photo_url.get_album())] = 1 + + new_block += "<br/>" + + new_block += "</span>" + new_block += SLIDESHOW_LINK % url.get_slideshow() + + album_urls = album_url_hash.keys() + album_urls.sort() + + if len(album_urls) == 1: + new_block += ALBUM_LINK % (album_urls[0], "complete album") + else : + for i in range(len(album_urls)) : + new_block += ALBUM_LINK % (album_urls[i], + "album %d" % (i + 1) ) + + new_lines.append(new_block + "</center><br/></div>") + + elif line[1:6] == "~~~~~" : + rows.append([]) # start a new row + else : + parts = line.split() + line = parts[0] + title = " ".join(parts[1:]) + + album, photo = line.split("/") + photo_url = url.get_photo(album, photo, + len(all_images)+1) + all_images.append(photo_url) + rows[-1].append((photo_url, title)) + + if not album in albums : + albums.append(album) + + return new_lines + + +def makeExtension(configs): + return ImageLinksExtension(configs) + diff --git a/markdown/extensions/meta.py b/markdown/extensions/meta.py new file mode 100644 index 0000000..1b555b2 --- /dev/null +++ b/markdown/extensions/meta.py @@ -0,0 +1,90 @@ +#!usr/bin/python + +""" +Meta Data Extension for Python-Markdown +======================================= + +This extension adds Meta Data handling to markdown. + +Basic Usage: + + >>> import markdown + >>> text = '''Title: A Test Doc. + ... Author: Waylan Limberg + ... John Doe + ... Blank_Data: + ... + ... The body. This is paragraph one. + ... ''' + >>> md = markdown.Markdown(['meta']) + >>> md.convert(text) + u'<p>The body. This is paragraph one.</p>' + >>> md.Meta + {u'blank_data': [u''], u'author': [u'Waylan Limberg', u'John Doe'], u'title': [u'A Test Doc.']} + +Make sure text without Meta Data still works (markdown < 1.6b returns a <p>). + + >>> text = ' Some Code - not extra lines of meta data.' + >>> md = markdown.Markdown(['meta']) + >>> md.convert(text) + u'<pre><code>Some Code - not extra lines of meta data.\\n</code></pre>' + >>> md.Meta + {} + +Copyright 2007-2008 [Waylan Limberg](http://achinghead.com). + +Project website: <http://www.freewisdom.org/project/python-markdown/Meta-Data> +Contact: markdown@freewisdom.org + +License: BSD (see ../docs/LICENSE for details) + +""" + +import markdown, re + +# Global Vars +META_RE = re.compile(r'^[ ]{0,3}(?P<key>[A-Za-z0-9_-]+):\s*(?P<value>.*)') +META_MORE_RE = re.compile(r'^[ ]{4,}(?P<value>.*)') + +class MetaExtension (markdown.Extension): + """ Meta-Data extension for Python-Markdown. """ + + def extendMarkdown(self, md, md_globals): + """ Add MetaPreprocessor to Markdown instance. """ + + md.preprocessors.add("meta", MetaPreprocessor(md), "_begin") + + +class MetaPreprocessor(markdown.preprocessors.Preprocessor): + """ Get Meta-Data. """ + + def run(self, lines): + """ Parse Meta-Data and store in Markdown.Meta. """ + meta = {} + key = None + while 1: + line = lines.pop(0) + if line.strip() == '': + break # blank line - done + m1 = META_RE.match(line) + if m1: + key = m1.group('key').lower().strip() + meta[key] = [m1.group('value').strip()] + else: + m2 = META_MORE_RE.match(line) + if m2 and key: + # Add another line to existing key + meta[key].append(m2.group('value').strip()) + else: + lines.insert(0, line) + break # no meta data - done + self.markdown.Meta = meta + return lines + + +def makeExtension(configs={}): + return MetaExtension(configs=configs) + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/markdown/extensions/rss.py b/markdown/extensions/rss.py new file mode 100644 index 0000000..1274da2 --- /dev/null +++ b/markdown/extensions/rss.py @@ -0,0 +1,114 @@ +import markdown +from markdown import etree + +DEFAULT_URL = "http://www.freewisdom.org/projects/python-markdown/" +DEFAULT_CREATOR = "Yuri Takhteyev" +DEFAULT_TITLE = "Markdown in Python" +GENERATOR = "http://www.freewisdom.org/projects/python-markdown/markdown2rss" + +month_map = { "Jan" : "01", + "Feb" : "02", + "March" : "03", + "April" : "04", + "May" : "05", + "June" : "06", + "July" : "07", + "August" : "08", + "September" : "09", + "October" : "10", + "November" : "11", + "December" : "12" } + +def get_time(heading): + + heading = heading.split("-")[0] + heading = heading.strip().replace(",", " ").replace(".", " ") + + month, date, year = heading.split() + month = month_map[month] + + return rdftime(" ".join((month, date, year, "12:00:00 AM"))) + +def rdftime(time): + + time = time.replace(":", " ") + time = time.replace("/", " ") + time = time.split() + return "%s-%s-%sT%s:%s:%s-08:00" % (time[0], time[1], time[2], + time[3], time[4], time[5]) + + +def get_date(text): + return "date" + +class RssExtension (markdown.Extension): + + def extendMarkdown(self, md, md_globals): + + self.config = { 'URL' : [DEFAULT_URL, "Main URL"], + 'CREATOR' : [DEFAULT_CREATOR, "Feed creator's name"], + 'TITLE' : [DEFAULT_TITLE, "Feed title"] } + + md.xml_mode = True + + # Insert a tree-processor that would actually add the title tag + treeprocessor = RssTreeProcessor(md) + treeprocessor.ext = self + md.treeprocessors['rss'] = treeprocessor + md.stripTopLevelTags = 0 + md.docType = '<?xml version="1.0" encoding="utf-8"?>\n' + +class RssTreeProcessor(markdown.treeprocessors.Treeprocessor): + + def run (self, root): + + rss = etree.Element("rss") + rss.set("version", "2.0") + + channel = etree.SubElement(rss, "channel") + + for tag, text in (("title", self.ext.getConfig("TITLE")), + ("link", self.ext.getConfig("URL")), + ("description", None)): + + element = etree.SubElement(channel, tag) + element.text = text + + for child in root: + + if child.tag in ["h1", "h2", "h3", "h4", "h5"]: + + heading = child.text.strip() + item = etree.SubElement(channel, "item") + link = etree.SubElement(item, "link") + link.text = self.ext.getConfig("URL") + title = etree.SubElement(item, "title") + title.text = heading + + guid = ''.join([x for x in heading if x.isalnum()]) + guidElem = etree.SubElement(item, "guid") + guidElem.text = guid + guidElem.set("isPermaLink", "false") + + elif child.tag in ["p"]: + try: + description = etree.SubElement(item, "description") + except UnboundLocalError: + # Item not defined - moving on + pass + else: + if len(child): + content = "\n".join([etree.tostring(node) + for node in child]) + else: + content = child.text + pholder = self.markdown.htmlStash.store( + "<![CDATA[ %s]]>" % content) + description.text = pholder + + return rss + + +def makeExtension(configs): + + return RssExtension(configs) diff --git a/markdown/extensions/tables.py b/markdown/extensions/tables.py new file mode 100644 index 0000000..1d3c920 --- /dev/null +++ b/markdown/extensions/tables.py @@ -0,0 +1,97 @@ +#!/usr/bin/env Python +""" +Tables Extension for Python-Markdown +==================================== + +Added parsing of tables to Python-Markdown. + +A simple example: + + First Header | Second Header + ------------- | ------------- + Content Cell | Content Cell + Content Cell | Content Cell + +Copyright 2009 - [Waylan Limberg](http://achinghead.com) +""" +import markdown +from markdown import etree + + +class TableProcessor(markdown.blockprocessors.BlockProcessor): + """ Process Tables. """ + + def test(self, parent, block): + rows = block.split('\n') + return (len(rows) > 2 and '|' in rows[0] and + '|' in rows[1] and '-' in rows[1] and + rows[1][0] in ['|', ':', '-']) + + def run(self, parent, blocks): + """ Parse a table block and build table. """ + block = blocks.pop(0).split('\n') + header = block[:2] + rows = block[2:] + # Get format type (bordered by pipes or not) + border = False + if header[0].startswith('|'): + border = True + # Get alignment of columns + align = [] + for c in self._split_row(header[1], border): + if c.startswith(':') and c.endswith(':'): + align.append('center') + elif c.startswith(':'): + align.append('left') + elif c.endswith(':'): + align.append('right') + else: + align.append(None) + # Build table + table = etree.SubElement(parent, 'table') + thead = etree.SubElement(table, 'thead') + self._build_row(header[0], thead, align, border) + tbody = etree.SubElement(table, 'tbody') + for row in rows: + self._build_row(row, tbody, align, border) + + def _build_row(self, row, parent, align, border): + """ Given a row of text, build table cells. """ + tr = etree.SubElement(parent, 'tr') + tag = 'td' + if parent.tag == 'thead': + tag = 'th' + cells = self._split_row(row, border) + # We use align here rather than cells to ensure every row + # contains the same number of columns. + for i, a in enumerate(align): + c = etree.SubElement(tr, tag) + try: + c.text = cells[i].strip() + except IndexError: + c.text = "" + if a: + c.set('align', a) + + def _split_row(self, row, border): + """ split a row of text into list of cells. """ + if border: + if row.startswith('|'): + row = row[1:] + if row.endswith('|'): + row = row[:-1] + return row.split('|') + + +class TableExtension(markdown.Extension): + """ Add tables to Markdown. """ + + def extendMarkdown(self, md, md_globals): + """ Add an instance of TableProcessor to BlockParser. """ + md.parser.blockprocessors.add('table', + TableProcessor(md.parser), + '<hashheader') + + +def makeExtension(configs={}): + return TableExtension(configs=configs) diff --git a/markdown/extensions/toc.py b/markdown/extensions/toc.py new file mode 100644 index 0000000..1d9489c --- /dev/null +++ b/markdown/extensions/toc.py @@ -0,0 +1,136 @@ +""" +Table of Contents Extension for Python-Markdown +* * * + +(c) 2008 [Jack Miller](http://codezen.org) + +Dependencies: +* [Markdown 2.0+](http://www.freewisdom.org/projects/python-markdown/) + +""" +import markdown +from markdown import etree +import re + +class TocTreeprocessor(markdown.treeprocessors.Treeprocessor): + # Iterator wrapper to get parent and child all at once + def iterparent(self, root): + for parent in root.getiterator(): + for child in parent: + yield parent, child + + def run(self, doc): + div = etree.Element("div") + div.attrib["class"] = "toc" + last_li = None + + # Add title to the div + if self.config["title"][0]: + header = etree.SubElement(div, "span") + header.attrib["class"] = "toctitle" + header.text = self.config["title"][0] + + level = 0 + list_stack=[div] + header_rgx = re.compile("[Hh][123456]") + + # Get a list of id attributes + used_ids = [] + for c in doc.getiterator(): + if "id" in c.attrib: + used_ids.append(c.attrib["id"]) + + for (p, c) in self.iterparent(doc): + if not c.text: + continue + + # To keep the output from screwing up the + # validation by putting a <div> inside of a <p> + # we actually replace the <p> in its entirety. + # We do not allow the marker inside a header as that + # would causes an enless loop of placing a new TOC + # inside previously generated TOC. + + if c.text.find(self.config["marker"][0]) > -1 and not header_rgx.match(c.tag): + for i in range(len(p)): + if p[i] == c: + p[i] = div + break + + if header_rgx.match(c.tag): + tag_level = int(c.tag[-1]) + + while tag_level < level: + list_stack.pop() + level -= 1 + + if tag_level > level: + newlist = etree.Element("ul") + if last_li: + last_li.append(newlist) + else: + list_stack[-1].append(newlist) + list_stack.append(newlist) + level += 1 + + # Do not override pre-existing ids + if not "id" in c.attrib: + id = self.config["slugify"][0](c.text) + if id in used_ids: + ctr = 1 + while "%s_%d" % (id, ctr) in used_ids: + ctr += 1 + id = "%s_%d" % (id, ctr) + used_ids.append(id) + c.attrib["id"] = id + else: + id = c.attrib["id"] + + # List item link, to be inserted into the toc div + last_li = etree.Element("li") + link = etree.SubElement(last_li, "a") + link.text = c.text + link.attrib["href"] = '#' + id + + if int(self.config["anchorlink"][0]): + anchor = etree.SubElement(c, "a") + anchor.text = c.text + anchor.attrib["href"] = "#" + id + anchor.attrib["class"] = "toclink" + c.text = "" + + list_stack[-1].append(last_li) + +class TocExtension(markdown.Extension): + def __init__(self, configs): + self.config = { "marker" : ["[TOC]", + "Text to find and replace with Table of Contents -" + "Defaults to \"[TOC]\""], + "slugify" : [self.slugify, + "Function to generate anchors based on header text-" + "Defaults to a built in slugify function."], + "title" : [None, + "Title to insert into TOC <div> - " + "Defaults to None"], + "anchorlink" : [0, + "1 if header should be a self link" + "Defaults to 0"]} + + for key, value in configs: + self.setConfig(key, value) + + # This is exactly the same as Django's slugify + def slugify(self, value): + """ Slugify a string, to make it URL friendly. """ + import unicodedata + value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore') + value = unicode(re.sub('[^\w\s-]', '', value).strip().lower()) + return re.sub('[-\s]+','-',value) + + def extendMarkdown(self, md, md_globals): + tocext = TocTreeprocessor(md) + tocext.config = self.config + md.treeprocessors.add("toc", tocext, "_begin") + +def makeExtension(configs={}): + return TocExtension(configs=configs) diff --git a/markdown/extensions/wikilinks.py b/markdown/extensions/wikilinks.py new file mode 100644 index 0000000..df44e1c --- /dev/null +++ b/markdown/extensions/wikilinks.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python + +''' +WikiLinks Extension for Python-Markdown +====================================== + +Converts [[WikiLinks]] to relative links. Requires Python-Markdown 2.0+ + +Basic usage: + + >>> import markdown + >>> text = "Some text with a [[WikiLink]]." + >>> html = markdown.markdown(text, ['wikilinks']) + >>> html + u'<p>Some text with a <a class="wikilink" href="/WikiLink/">WikiLink</a>.</p>' + +Whitespace behavior: + + >>> markdown.markdown('[[ foo bar_baz ]]', ['wikilinks']) + u'<p><a class="wikilink" href="/foo_bar_baz/">foo bar_baz</a></p>' + >>> markdown.markdown('foo [[ ]] bar', ['wikilinks']) + u'<p>foo bar</p>' + +To define custom settings the simple way: + + >>> markdown.markdown(text, + ... ['wikilinks(base_url=/wiki/,end_url=.html,html_class=foo)'] + ... ) + u'<p>Some text with a <a class="foo" href="/wiki/WikiLink.html">WikiLink</a>.</p>' + +Custom settings the complex way: + + >>> md = markdown.Markdown( + ... extensions = ['wikilinks'], + ... extension_configs = {'wikilinks': [ + ... ('base_url', 'http://example.com/'), + ... ('end_url', '.html'), + ... ('html_class', '') ]}, + ... safe_mode = True) + >>> md.convert(text) + u'<p>Some text with a <a href="http://example.com/WikiLink.html">WikiLink</a>.</p>' + +Use MetaData with mdx_meta.py (Note the blank html_class in MetaData): + + >>> text = """wiki_base_url: http://example.com/ + ... wiki_end_url: .html + ... wiki_html_class: + ... + ... Some text with a [[WikiLink]].""" + >>> md = markdown.Markdown(extensions=['meta', 'wikilinks']) + >>> md.convert(text) + u'<p>Some text with a <a href="http://example.com/WikiLink.html">WikiLink</a>.</p>' + +MetaData should not carry over to next document: + + >>> md.convert("No [[MetaData]] here.") + u'<p>No <a class="wikilink" href="/MetaData/">MetaData</a> here.</p>' + +Define a custom URL builder: + + >>> def my_url_builder(label, base, end): + ... return '/bar/' + >>> md = markdown.Markdown(extensions=['wikilinks'], + ... extension_configs={'wikilinks' : [('build_url', my_url_builder)]}) + >>> md.convert('[[foo]]') + u'<p><a class="wikilink" href="/bar/">foo</a></p>' + +From the command line: + + python markdown.py -x wikilinks(base_url=http://example.com/,end_url=.html,html_class=foo) src.txt + +By [Waylan Limberg](http://achinghead.com/). + +License: [BSD](http://www.opensource.org/licenses/bsd-license.php) + +Dependencies: +* [Python 2.3+](http://python.org) +* [Markdown 2.0+](http://www.freewisdom.org/projects/python-markdown/) +''' + +import markdown +import re + +def build_url(label, base, end): + """ Build a url from the label, a base, and an end. """ + clean_label = re.sub(r'([ ]+_)|(_[ ]+)|([ ]+)', '_', label) + return '%s%s%s'% (base, clean_label, end) + + +class WikiLinkExtension(markdown.Extension): + def __init__(self, configs): + # set extension defaults + self.config = { + 'base_url' : ['/', 'String to append to beginning or URL.'], + 'end_url' : ['/', 'String to append to end of URL.'], + 'html_class' : ['wikilink', 'CSS hook. Leave blank for none.'], + 'build_url' : [build_url, 'Callable formats URL from label.'], + } + + # Override defaults with user settings + for key, value in configs : + self.setConfig(key, value) + + def extendMarkdown(self, md, md_globals): + self.md = md + + # append to end of inline patterns + WIKILINK_RE = r'\[\[([A-Za-z0-9_ -]+)\]\]' + wikilinkPattern = WikiLinks(WIKILINK_RE, self.config) + wikilinkPattern.md = md + md.inlinePatterns.add('wikilink', wikilinkPattern, "<not_strong") + + +class WikiLinks(markdown.inlinepatterns.Pattern): + def __init__(self, pattern, config): + markdown.inlinepatterns.Pattern.__init__(self, pattern) + self.config = config + + def handleMatch(self, m): + if m.group(2).strip(): + base_url, end_url, html_class = self._getMeta() + label = m.group(2).strip() + url = self.config['build_url'][0](label, base_url, end_url) + a = markdown.etree.Element('a') + a.text = label + a.set('href', url) + if html_class: + a.set('class', html_class) + else: + a = '' + return a + + def _getMeta(self): + """ Return meta data or config data. """ + base_url = self.config['base_url'][0] + end_url = self.config['end_url'][0] + html_class = self.config['html_class'][0] + if hasattr(self.md, 'Meta'): + if self.md.Meta.has_key('wiki_base_url'): + base_url = self.md.Meta['wiki_base_url'][0] + if self.md.Meta.has_key('wiki_end_url'): + end_url = self.md.Meta['wiki_end_url'][0] + if self.md.Meta.has_key('wiki_html_class'): + html_class = self.md.Meta['wiki_html_class'][0] + return base_url, end_url, html_class + + +def makeExtension(configs=None) : + return WikiLinkExtension(configs=configs) + + +if __name__ == "__main__": + import doctest + doctest.testmod() + |