Completed
Push — master ( 986bdb...20d7bd )
by Ionel Cristian
59s
created

src.darkslide.CodeHighlightingMacro.descape()   A

Complexity

Conditions 4

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 4
dl 0
loc 12
rs 9.2

1 Method

Rating   Name   Duplication   Size   Complexity  
A src.darkslide.CodeHighlightingMacro.replacer() 0 5 2
1
# -*- coding: utf-8 -*-
2
import os
3
import re
4
import sys
5
try:
6
    from io import BytesIO as StringIO
7
except ImportError:
8
    from StringIO import StringIO
9
10
import pygments
11
import qrcode
12
from pygments.formatters import HtmlFormatter
13
from pygments.lexers import get_lexer_by_name
14
from qrcode.image.svg import SvgPathImage
15
from six.moves import html_entities
16
17
from . import utils
18
19
20
class Macro(object):
21
    """Base class for altering slide HTML during presentation generation"""
22
23
    def __init__(self, logger=sys.stdout, embed=False, options=None):
24
        self.logger = logger
25
        self.embed = embed
26
        if options:
27
            if not isinstance(options, dict):
28
                raise ValueError(u'Macro options must be a dict instance')
29
            self.options = options
30
        else:
31
            self.options = {}
32
33
    def process(self, content, source=None, context=None):
34
        """Generic processor (does actually nothing)"""
35
        return content, []
36
37
38
class CodeHighlightingMacro(Macro):
39
    """Performs syntax coloration in slide code blocks using Pygments"""
40
41
    macro_re = re.compile(
42
        r'(<pre.+?>(<code>)?\s?!(\S+?)\n(.*?)(</code>)?</pre>)',
43
        re.UNICODE | re.MULTILINE | re.DOTALL)
44
45
    html_entity_re = re.compile('&(\w+?);')
46
47
    def descape(self, string, defs=None):
48
        """Decodes html entities from a given string"""
49
        if defs is None:
50
            defs = html_entities.entitydefs
51
52
        def replacer(m):
53
            if len(m.groups()) > 0:
54
                return defs[m.group(1)]
55
            else:
56
                return m.group(0)
57
58
        return self.html_entity_re.sub(replacer, string)
59
60
    def process(self, content, source=None, context=None):
61
        code_blocks = self.macro_re.findall(content)
62
        if not code_blocks:
63
            return content, []
64
65
        classes = []
66
        for block, void1, lang, code, void2 in code_blocks:
67
            try:
68
                lexer = get_lexer_by_name(lang, startinline=True)
69
            except Exception:
70
                self.logger(u"Unknown pygment lexer \"%s\", skipping"
71
                            % lang, 'warning')
72
                return content, classes
73
74
            if 'linenos' not in self.options or self.options['linenos'] == 'no':
75
                self.options['linenos'] = False
76
77
            formatter = HtmlFormatter(linenos=self.options['linenos'],
78
                                      nobackground=True)
79
            pretty_code = pygments.highlight(self.descape(code), lexer,
80
                                             formatter)
81
            content = content.replace(block, pretty_code, 1)
82
83
        return content, [u'has_code']
84
85
86
class EmbedImagesMacro(Macro):
87
    """Encodes images in base64 for embedding in image:data"""
88
    macro_re = re.compile(
89
        r'<img\s.*?src="(.+?)"\s?.*?/?>|<object[^<>]+?data="(.*?)"[^<>]+?type="image/svg\+xml"',
90
        re.DOTALL | re.UNICODE)
91
92
    def process(self, content, source=None, context=None):
93
        classes = []
94
95
        if not self.embed:
96
            return content, classes
97
98
        images = self.macro_re.findall(content)
99
100
        source_dir = os.path.dirname(source)
101
102
        for image_url, data_url in images:
103
            encoded_url = utils.encode_image_from_url(image_url or data_url, source_dir)
104
105
            if not encoded_url:
106
                self.logger(u"Failed to embed image \"%s\"" % image_url, 'warning')
107
                return content, classes
108
109
            if image_url:
110
                content = content.replace(u"src=\"" + image_url,
111
                                          u"src=\"" + encoded_url, 1)
112
            else:
113
                content = content.replace(u"data=\"" + data_url,
114
                                          u"data=\"" + encoded_url, 1)
115
116
            self.logger(u"Embedded image %s" % (image_url or data_url), 'notice')
117
118
        return content, classes
119
120
121
class FixImagePathsMacro(Macro):
122
    """Replaces html image paths with fully qualified absolute urls"""
123
124
    macro_re = re.compile(
125
        r'<img.*?src="(?!https?://|file://)(.*?)"'
126
        r'|<object[^<>]+?data="(?!http://)(.*?)"[^<>]+?type="image/svg\+xml"',
127
        re.DOTALL | re.UNICODE
128
    )
129
130
    def process(self, content, source=None, context=None):
131
        classes = []
132
133
        if self.embed:
134
            return content, classes
135
136
        base_path = utils.get_path_url(source, self.options['relative'] and self.options['destination_dir'])
137
        base_url = os.path.split(base_path)[0]
138
139
        images = self.macro_re.findall(content)
140
141
        for matches in images:
142
            for image in matches:
143
                if image:
144
                    full_path = '"%s"' % os.path.join(base_url, image)
145
                    image = '"%s"' % image
146
                    content = content.replace(image, full_path)
147
148
        return content, classes
149
150
151
class FxMacro(Macro):
152
    """Adds custom CSS class to slides"""
153
    macro_re = re.compile(r'(<p>\.fx:\s?(.*?)</p>\n?)',
154
                          re.DOTALL | re.UNICODE)
155
156
    def process(self, content, source=None, context=None):
157
        classes = []
158
159
        fx_match = self.macro_re.search(content)
160
        if fx_match:
161
            classes = fx_match.group(2).split(u' ')
162
            content = content.replace(fx_match.group(1), '', 1)
163
164
        return content, classes
165
166
167
class NotesMacro(Macro):
168
    """Adds toggleable notes to slides"""
169
    macro_re = re.compile(r'<p>\.notes:\s?(.*?)</p>')
170
171
    def process(self, content, source=None, context=None):
172
        classes = []
173
174
        new_content = self.macro_re.sub(r'<p class="notes">\1</p>', content)
175
176
        if content != new_content:
177
            classes.append(u'has_notes')
178
179
        return new_content, classes
180
181
182
class QRMacro(Macro):
183
    """Generates a QR code in a slide"""
184
    macro_re = re.compile(r'<p>\.qr:\s?(.*?)</p>')
185
186
    def process(self, content, source=None, context=None):
187
        classes = []
188
189
        def encoder(match):
190
            qr = qrcode.QRCode(1, error_correction=qrcode.ERROR_CORRECT_L, box_size=40)
191
            qr.add_data(match.group(1))
192
            buff = StringIO()
193
            qr.make_image(image_factory=SvgPathImage).save(buff)
194
            return '<p class="qr">%s</p>' % buff.getvalue().decode('utf-8')
195
196
        new_content = self.macro_re.sub(encoder, content)
197
198
        if content != new_content:
199
            classes.append(u'has_qr')
200
201
        return new_content, classes
202
203
204
class FooterMacro(Macro):
205
    """Add footer in slides"""
206
    footer = ''
207
    macro_re = re.compile(r'<p>\.footer:\s?(.*?)</p>')
208
209
    def process(self, content, source=None, context=None):
210
        classes = []
211
212
        def save(match):
213
            self.footer = match.group(1)
214
            return ''
215
216
        content = self.macro_re.sub(save, content)
217
218
        if self.footer:
219
            classes.append(u'has_footer')
220
            context['footer'] = self.footer
221
222
        return content, classes
223