1'''
2Position Map Extension for Python-Markdown
3==========================================
4
5This extension adds data-posmap attributes to the generated HTML elements that
6can be used to relate HTML elements to the corresponding lines in the markdown
7input file.
8
9Note: the line number stored in the data-posmap attribute corresponds to the
10      empty line *after* the markdown block that the HTML was generated from.
11
12Copyright 2016 [Maurice van der Pot](griffon26@kfk4ever.com)
13
14License: [BSD](http://www.opensource.org/licenses/bsd-license.php)
15
16'''
17
18from __future__ import unicode_literals
19
20import re
21from xml.etree.ElementTree import SubElement
22
23from markdown.blockprocessors import BlockProcessor
24from markdown.extensions import Extension
25from markdown.extensions.codehilite import CodeHilite
26from markdown.preprocessors import Preprocessor
27from markdown.util import HTML_PLACEHOLDER_RE
28try:
29    from pymdownx.highlight import Highlight
30except ImportError:
31    Highlight = None
32
33
34POSMAP_MARKER_RE = re.compile(r'__posmapmarker__\d+\n\n')
35
36
37class PosMapExtension(Extension):
38    """ Position Map Extension for Python-Markdown. """
39
40    def extendMarkdown(self, md):
41        """ Insert the PosMapExtension blockprocessor before any other
42            extensions to make sure our own markers, inserted by the
43            preprocessor, are removed before any other extensions get confused
44            by them.
45        """
46        md.preprocessors.register(PosMapMarkPreprocessor(md), 'posmap_mark', 50)
47        md.preprocessors.register(PosMapCleanPreprocessor(md), 'posmap_clean', 5)
48        md.parser.blockprocessors.register(PosMapBlockProcessor(md.parser), 'posmap', 150)
49
50        # Monkey patch CodeHilite constructor to remove the posmap markers from
51        # text before highlighting it
52        orig_codehilite_init = CodeHilite.__init__
53
54        def new_codehilite_init(self, src=None, *args, **kwargs):
55            src = POSMAP_MARKER_RE.sub('', src)
56            orig_codehilite_init(self, src=src, *args, **kwargs)
57        CodeHilite.__init__ = new_codehilite_init
58
59        # Same for PyMdown Extensions if it is available
60        if Highlight is not None:
61            orig_highlight_highlight = Highlight.highlight
62
63            def new_highlight_highlight(self, src, *args, **kwargs):
64                src = POSMAP_MARKER_RE.sub('', src)
65                return orig_highlight_highlight(self, src, *args, **kwargs)
66            Highlight.highlight = new_highlight_highlight
67
68
69class PosMapMarkPreprocessor(Preprocessor):
70    """ PosMapMarkPreprocessor - insert __posmapmarker__linenr entries at each empty line """
71
72    def run(self, lines):
73        new_text = []
74        for i, line in enumerate(lines):
75            new_text.append(line)
76            if line == '':
77                new_text.append('__posmapmarker__%d' % i)
78                new_text.append('')
79        return new_text
80
81class PosMapCleanPreprocessor(Preprocessor):
82    """ PosMapCleanPreprocessor - remove __posmapmarker__linenr entries that
83        accidentally ended up in the htmlStash. This could have happened
84        because they were inside html tags or a fenced code block.
85    """
86
87    def run(self, lines):
88
89        for i in range(self.md.htmlStash.html_counter):
90            block = self.md.htmlStash.rawHtmlBlocks[i]
91            block = re.sub(POSMAP_MARKER_RE, '', block)
92            self.md.htmlStash.rawHtmlBlocks[i] = block
93
94        return lines
95
96
97class PosMapBlockProcessor(BlockProcessor):
98    """ PosMapBlockProcessor - remove each marker and add a data-posmap
99        attribute to the previous HTML element
100    """
101
102    def test(self, parent, block):
103        return block.startswith('__posmapmarker__')
104
105    def run(self, parent, blocks):
106        block = blocks.pop(0)
107        line_nr = block.split('__')[2]
108        last_child = self.lastChild(parent)
109        if last_child != None:
110            # Avoid setting the attribute on HTML placeholders, because it
111            # would interfere with later replacement with literal HTML
112            # fragments. In this case just add an empty <p> with the attribute.
113            if last_child.text and re.match(HTML_PLACEHOLDER_RE, last_child.text):
114                last_child = SubElement(parent, 'p')
115            last_child.set('data-posmap', line_nr)
116
117def makeExtension(*args, **kwargs):
118    return PosMapExtension(*args, **kwargs)
119
120