|
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
|
|
|
|