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