1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
import codecs |
3
|
|
|
import inspect |
4
|
|
|
import os |
5
|
|
|
import re |
6
|
|
|
import shutil |
7
|
|
|
import sys |
8
|
|
|
|
9
|
|
|
import jinja2 |
10
|
|
|
from six import binary_type |
11
|
|
|
from six import string_types |
12
|
|
|
from six.moves import configparser |
13
|
|
|
|
14
|
|
|
from . import macro as macro_module |
15
|
|
|
from . import __version__ |
16
|
|
|
from . import utils |
17
|
|
|
from .parser import Parser |
18
|
|
|
|
19
|
|
|
BASE_DIR = os.path.dirname(__file__) |
20
|
|
|
THEMES_DIR = os.path.join(BASE_DIR, 'themes') |
21
|
|
|
VALID_LINENOS = ('no', 'inline', 'table') |
22
|
|
|
|
23
|
|
|
|
24
|
|
|
class Generator(object): |
25
|
|
|
"""The Generator class takes and processes presentation source as a file, a |
26
|
|
|
folder or a configuration file and provides methods to render them as a |
27
|
|
|
presentation. |
28
|
|
|
""" |
29
|
|
|
default_macros = ( |
30
|
|
|
macro_module.CodeHighlightingMacro, |
31
|
|
|
macro_module.EmbedImagesMacro, |
32
|
|
|
macro_module.FixImagePathsMacro, |
33
|
|
|
macro_module.FxMacro, |
34
|
|
|
macro_module.NotesMacro, |
35
|
|
|
macro_module.QRMacro, |
36
|
|
|
macro_module.FooterMacro, |
37
|
|
|
) |
38
|
|
|
|
39
|
|
|
def __init__(self, source, **kwargs): |
40
|
|
|
""" Configures this generator. Available ``args`` are: |
41
|
|
|
- ``source``: source file or directory path |
42
|
|
|
Available ``kwargs`` are: |
43
|
|
|
- ``copy_theme``: copy theme directory and files into presentation |
44
|
|
|
one |
45
|
|
|
- ``destination_file``: path to html destination file |
46
|
|
|
- ``direct``: enables direct rendering presentation to stdout |
47
|
|
|
- ``debug``: enables debug mode |
48
|
|
|
- ``embed``: generates a standalone document, with embedded assets |
49
|
|
|
- ``encoding``: the encoding to use for this presentation |
50
|
|
|
- ``extensions``: Comma separated list of markdown extensions |
51
|
|
|
- ``logger``: a logger lambda to use for logging |
52
|
|
|
- ``maxtoclevel``: the maximum level to include in toc |
53
|
|
|
- ``presenter_notes``: enable presenter notes |
54
|
|
|
- ``relative``: enable relative asset urls |
55
|
|
|
- ``theme``: path to the theme to use for this presentation |
56
|
|
|
- ``verbose``: enables verbose output |
57
|
|
|
""" |
58
|
|
|
self.user_css = [] |
59
|
|
|
self.user_js = [] |
60
|
|
|
self.copy_theme = kwargs.get('copy_theme', False) |
61
|
|
|
self.debug = kwargs.get('debug', False) |
62
|
|
|
self.destination_file = kwargs.get('destination_file', |
63
|
|
|
'presentation.html') |
64
|
|
|
self.direct = kwargs.get('direct', False) |
65
|
|
|
self.embed = kwargs.get('embed', False) |
66
|
|
|
self.encoding = kwargs.get('encoding', 'utf8') |
67
|
|
|
self.extensions = kwargs.get('extensions', None) |
68
|
|
|
self.logger = kwargs.get('logger', None) |
69
|
|
|
self.maxtoclevel = kwargs.get('maxtoclevel', 2) |
70
|
|
|
self.presenter_notes = kwargs.get('presenter_notes', True) |
71
|
|
|
self.relative = kwargs.get('relative', False) |
72
|
|
|
self.theme = kwargs.get('theme', 'default') |
73
|
|
|
self.verbose = kwargs.get('verbose', False) |
74
|
|
|
self.linenos = self.linenos_check(kwargs.get('linenos')) |
75
|
|
|
self.watch = kwargs.get('watch', False) |
76
|
|
|
self.num_slides = 0 |
77
|
|
|
self.__toc = [] |
78
|
|
|
|
79
|
|
|
if self.direct: |
80
|
|
|
# Only output html in direct output mode, not log messages |
81
|
|
|
self.verbose = False |
82
|
|
|
|
83
|
|
|
if not source or not os.path.exists(source): |
84
|
|
|
raise IOError("Source file/directory %s does not exist" % source) |
85
|
|
|
|
86
|
|
|
if source.endswith('.cfg'): |
87
|
|
|
self.work_dir = os.path.dirname(source) |
88
|
|
|
config = self.parse_config(source) |
89
|
|
|
self.source = config.get('source') |
90
|
|
|
if not self.source: |
91
|
|
|
raise IOError('unable to fetch a valid source from config') |
92
|
|
|
source_abspath = os.path.abspath(self.source[0]) |
93
|
|
|
self.destination_file = config.get('destination', self.destination_file) |
94
|
|
|
self.embed = config.get('embed', self.embed) |
95
|
|
|
self.relative = config.get('relative', self.relative) |
96
|
|
|
self.copy_theme = config.get('copy_theme', self.copy_theme) |
97
|
|
|
self.extensions = config.get('extensions', self.extensions) |
98
|
|
|
self.maxtoclevel = config.get('max-toc-level', self.maxtoclevel) |
99
|
|
|
self.theme = config.get('theme', self.theme) |
100
|
|
|
self.destination_dir = os.path.dirname(self.destination_file) |
101
|
|
|
self.add_user_css(config.get('css', [])) |
102
|
|
|
self.add_user_js(config.get('js', [])) |
103
|
|
|
self.linenos = self.linenos_check(config.get('linenos', self.linenos)) |
104
|
|
|
else: |
105
|
|
|
self.source = source |
106
|
|
|
self.work_dir = '.' |
107
|
|
|
self.destination_dir = os.path.dirname(self.destination_file) |
108
|
|
|
|
109
|
|
|
source_abspath = os.path.abspath(source) |
110
|
|
|
|
111
|
|
|
if not os.path.isdir(source_abspath): |
112
|
|
|
source_abspath = os.path.dirname(source_abspath) |
113
|
|
|
|
114
|
|
|
self.watch_dir = source_abspath |
115
|
|
|
|
116
|
|
|
if os.path.exists(self.destination_file) and not os.path.isfile(self.destination_file): |
117
|
|
|
raise IOError("Destination %s exists and is not a file" % self.destination_file) |
118
|
|
|
|
119
|
|
|
self.theme_dir = self.find_theme_dir(self.theme, self.copy_theme) |
120
|
|
|
self.template_file = self.get_template_file() |
121
|
|
|
|
122
|
|
|
# macros registering |
123
|
|
|
self.macros = [] |
124
|
|
|
self.register_macro(*self.default_macros) |
125
|
|
|
|
126
|
|
|
def add_user_css(self, css_list): |
127
|
|
|
""" Adds supplementary user css files to the presentation. The |
128
|
|
|
``css_list`` arg can be either a ``list`` or a string. |
129
|
|
|
""" |
130
|
|
|
if isinstance(css_list, string_types): |
131
|
|
|
css_list = [css_list] |
132
|
|
|
|
133
|
|
|
for css_path in css_list: |
134
|
|
|
if css_path and css_path not in self.user_css: |
135
|
|
|
if not os.path.exists(css_path): |
136
|
|
|
raise IOError('%s user css file not found' % (css_path,)) |
137
|
|
|
with codecs.open(css_path, encoding=self.encoding) as css_file: |
138
|
|
View Code Duplication |
self.user_css.append({ |
|
|
|
|
139
|
|
|
'path_url': utils.get_path_url(css_path, |
140
|
|
|
self.relative and self.destination_dir), |
141
|
|
|
'contents': css_file.read(), |
142
|
|
|
}) |
143
|
|
|
|
144
|
|
|
def add_user_js(self, js_list): |
145
|
|
|
""" Adds supplementary user javascript files to the presentation. The |
146
|
|
|
``js_list`` arg can be either a ``list`` or a string. |
147
|
|
|
""" |
148
|
|
|
if isinstance(js_list, string_types): |
149
|
|
|
js_list = [js_list] |
150
|
|
|
for js_path in js_list: |
151
|
|
|
if js_path and js_path not in self.user_js: |
152
|
|
|
if js_path.startswith("http:"): |
153
|
|
|
self.user_js.append({ |
154
|
|
|
'path_url': js_path, |
155
|
|
|
'contents': '', |
156
|
|
View Code Duplication |
}) |
|
|
|
|
157
|
|
|
elif not os.path.exists(js_path): |
158
|
|
|
raise IOError('%s user js file not found' % (js_path,)) |
159
|
|
|
else: |
160
|
|
|
with codecs.open(js_path, |
161
|
|
|
encoding=self.encoding) as js_file: |
162
|
|
|
self.user_js.append({ |
163
|
|
|
'path_url': utils.get_path_url(js_path, |
164
|
|
|
self.relative and self.destination_dir), |
165
|
|
|
'contents': js_file.read(), |
166
|
|
|
}) |
167
|
|
|
|
168
|
|
|
def add_toc_entry(self, title, level, slide_number): |
169
|
|
|
""" Adds a new entry to current presentation Table of Contents. |
170
|
|
|
""" |
171
|
|
|
self.__toc.append({'title': title, 'number': slide_number, |
172
|
|
|
'level': level}) |
173
|
|
|
|
174
|
|
|
@property |
175
|
|
|
def toc(self): |
176
|
|
|
""" Smart getter for Table of Content list. |
177
|
|
|
""" |
178
|
|
|
toc = [] |
179
|
|
|
stack = [toc] |
180
|
|
|
for entry in self.__toc: |
181
|
|
|
entry['sub'] = [] |
182
|
|
|
while entry['level'] < len(stack): |
183
|
|
|
stack.pop() |
184
|
|
|
while entry['level'] > len(stack): |
185
|
|
|
stack.append(stack[-1][-1]['sub']) |
186
|
|
|
stack[-1].append(entry) |
187
|
|
|
return toc |
188
|
|
|
|
189
|
|
|
def execute(self): |
190
|
|
|
""" Execute this generator regarding its current configuration. |
191
|
|
|
""" |
192
|
|
|
if self.direct: |
193
|
|
|
out = getattr(sys.stdout, 'buffer', sys.stdout) |
194
|
|
|
out.write(self.render().encode(self.encoding)) |
195
|
|
|
else: |
196
|
|
|
self.write_and_log() |
197
|
|
|
|
198
|
|
|
if self.watch: |
199
|
|
|
from .watcher import watch |
200
|
|
|
|
201
|
|
|
self.log(u"Watching %s\n" % self.watch_dir) |
202
|
|
|
|
203
|
|
|
watch(self.watch_dir, self.write_and_log) |
204
|
|
|
|
205
|
|
|
def write_and_log(self): |
206
|
|
|
self.watch_files = [] |
207
|
|
|
self.num_slides = 0 |
208
|
|
|
self.__toc = [] |
209
|
|
|
self.write() |
210
|
|
|
self.log(u"Generated file: %s" % self.destination_file) |
211
|
|
|
|
212
|
|
|
def get_template_file(self): |
213
|
|
|
""" Retrieves Jinja2 template file path. |
214
|
|
|
""" |
215
|
|
|
if os.path.exists(os.path.join(self.theme_dir, 'base.html')): |
216
|
|
|
return os.path.join(self.theme_dir, 'base.html') |
217
|
|
|
default_dir = os.path.join(THEMES_DIR, 'default') |
218
|
|
|
if not os.path.exists(os.path.join(default_dir, 'base.html')): |
219
|
|
|
raise IOError("Cannot find base.html in default theme") |
220
|
|
|
return os.path.join(default_dir, 'base.html') |
221
|
|
|
|
222
|
|
|
def fetch_contents(self, source, work_dir): |
223
|
|
|
""" Recursively fetches Markdown contents from a single file or |
224
|
|
|
directory containing itself Markdown/RST files. |
225
|
|
|
""" |
226
|
|
|
slides = [] |
227
|
|
|
|
228
|
|
|
if type(source) is list: |
229
|
|
|
for entry in source: |
230
|
|
|
slides.extend(self.fetch_contents(entry, work_dir)) |
231
|
|
|
else: |
232
|
|
|
source = os.path.normpath(os.path.join(work_dir, source)) |
233
|
|
|
if os.path.isdir(source): |
234
|
|
|
self.log(u"Entering %r" % source) |
235
|
|
|
entries = os.listdir(source) |
236
|
|
|
entries.sort() |
237
|
|
|
for entry in entries: |
238
|
|
|
slides.extend(self.fetch_contents(entry, source)) |
239
|
|
|
else: |
240
|
|
|
try: |
241
|
|
|
parser = Parser(os.path.splitext(source)[1], self.encoding, self.extensions) |
242
|
|
|
except NotImplementedError as exc: |
243
|
|
|
self.log(u"Failed %r: %r" % (source, exc)) |
244
|
|
|
return slides |
245
|
|
|
|
246
|
|
|
self.log(u"Adding %r (%s)" % (source, parser.format)) |
247
|
|
|
|
248
|
|
|
try: |
249
|
|
|
with codecs.open(source, encoding=self.encoding) as file: |
250
|
|
|
file_contents = file.read() |
251
|
|
|
except UnicodeDecodeError: |
252
|
|
|
self.log(u"Unable to decode source %r: skipping" % source, |
253
|
|
|
'warning') |
254
|
|
|
else: |
255
|
|
|
inner_slides = re.split(r'<hr.+>', parser.parse(file_contents)) |
256
|
|
|
for inner_slide in inner_slides: |
257
|
|
|
slides.append(self.get_slide_vars(inner_slide, source)) |
258
|
|
|
|
259
|
|
|
if not slides: |
260
|
|
|
self.log(u"Exiting %r: no contents found" % source, 'notice') |
261
|
|
|
|
262
|
|
|
return slides |
263
|
|
|
|
264
|
|
|
def find_theme_dir(self, theme, copy_theme=False): |
265
|
|
|
""" Finds them dir path from its name. |
266
|
|
|
""" |
267
|
|
|
if os.path.exists(theme): |
268
|
|
|
self.theme_dir = theme |
269
|
|
|
elif os.path.exists(os.path.join(THEMES_DIR, theme)): |
270
|
|
|
self.theme_dir = os.path.join(THEMES_DIR, theme) |
271
|
|
|
else: |
272
|
|
|
raise IOError("Theme %s not found or invalid" % theme) |
273
|
|
|
target_theme_dir = os.path.join(os.getcwd(), 'theme') |
274
|
|
|
if copy_theme or os.path.exists(target_theme_dir): |
275
|
|
|
self.log(u'Copying %s theme directory to %s' |
276
|
|
|
% (theme, target_theme_dir)) |
277
|
|
|
if not os.path.exists(target_theme_dir): |
278
|
|
|
try: |
279
|
|
|
shutil.copytree(self.theme_dir, target_theme_dir) |
280
|
|
|
except Exception as e: |
281
|
|
|
self.log(u"Skipped copy of theme folder: %s" % e) |
282
|
|
|
pass |
283
|
|
|
self.theme_dir = target_theme_dir |
284
|
|
|
return self.theme_dir |
285
|
|
|
|
286
|
|
|
def get_css(self): |
287
|
|
|
""" Fetches and returns stylesheet file path or contents, for both |
288
|
|
|
print and screen contexts, depending if we want a standalone |
289
|
|
|
presentation or not. |
290
|
|
|
""" |
291
|
|
|
css = {} |
292
|
|
|
|
293
|
|
|
base_css = os.path.join(self.theme_dir, 'css', 'base.css') |
294
|
|
|
if not os.path.exists(base_css): |
295
|
|
|
base_css = os.path.join(THEMES_DIR, 'default', 'css', 'base.css') |
296
|
|
|
if not os.path.exists(base_css): |
297
|
|
|
raise IOError(u"Cannot find base.css in default theme") |
298
|
|
|
with codecs.open(base_css, encoding=self.encoding) as css_file: |
299
|
|
|
css['base'] = { |
300
|
|
|
'path_url': utils.get_path_url(base_css, self.relative and self.destination_dir), |
301
|
|
|
'contents': css_file.read(), |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
print_css = os.path.join(self.theme_dir, 'css', 'print.css') |
305
|
|
|
if not os.path.exists(print_css): |
306
|
|
|
print_css = os.path.join(THEMES_DIR, 'default', 'css', 'print.css') |
307
|
|
|
if not os.path.exists(print_css): |
308
|
|
|
raise IOError(u"Cannot find print.css in default theme") |
309
|
|
|
with codecs.open(print_css, encoding=self.encoding) as css_file: |
310
|
|
|
css['print'] = { |
311
|
|
|
'path_url': utils.get_path_url(print_css, self.relative and self.destination_dir), |
312
|
|
|
'contents': css_file.read(), |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
screen_css = os.path.join(self.theme_dir, 'css', 'screen.css') |
316
|
|
|
if not os.path.exists(screen_css): |
317
|
|
|
screen_css = os.path.join(THEMES_DIR, 'default', 'css', 'screen.css') |
318
|
|
|
if not os.path.exists(screen_css): |
319
|
|
|
raise IOError(u"Cannot find screen.css in default theme") |
320
|
|
|
with codecs.open(screen_css, encoding=self.encoding) as css_file: |
321
|
|
|
css['screen'] = { |
322
|
|
|
'path_url': utils.get_path_url(screen_css, self.relative and self.destination_dir), |
323
|
|
|
'contents': css_file.read(), |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
theme_css = os.path.join(self.theme_dir, 'css', 'theme.css') |
327
|
|
|
if not os.path.exists(theme_css): |
328
|
|
|
theme_css = os.path.join(THEMES_DIR, 'default', 'css', 'theme.css') |
329
|
|
|
if not os.path.exists(theme_css): |
330
|
|
|
raise IOError(u"Cannot find theme.css in default theme") |
331
|
|
|
with codecs.open(theme_css, encoding=self.encoding) as css_file: |
332
|
|
|
css['theme'] = { |
333
|
|
|
'path_url': utils.get_path_url(theme_css, self.relative and self.destination_dir), |
334
|
|
|
'contents': css_file.read(), |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
return css |
338
|
|
|
|
339
|
|
|
def get_js(self): |
340
|
|
|
""" Fetches and returns javascript file path or contents, depending if |
341
|
|
|
we want a standalone presentation or not. |
342
|
|
|
""" |
343
|
|
|
js_file = os.path.join(self.theme_dir, 'js', 'slides.js') |
344
|
|
|
|
345
|
|
|
if not os.path.exists(js_file): |
346
|
|
|
js_file = os.path.join(THEMES_DIR, 'default', 'js', 'slides.js') |
347
|
|
|
|
348
|
|
|
if not os.path.exists(js_file): |
349
|
|
|
raise IOError(u"Cannot find slides.js in default theme") |
350
|
|
|
with codecs.open(js_file, encoding=self.encoding) as js_file_obj: |
351
|
|
|
return { |
352
|
|
|
'path_url': utils.get_path_url(js_file, self.relative and self.destination_dir), |
353
|
|
|
'contents': js_file_obj.read(), |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
def get_slide_vars(self, slide_src, source, |
357
|
|
|
_presenter_notes_re=re.compile(r'<h\d[^>]*>presenter notes</h\d>', |
358
|
|
|
re.DOTALL | re.UNICODE | re.IGNORECASE), |
359
|
|
|
_slide_title_re=re.compile(r'(<h(\d+?).*?>(.+?)</h\d>)\s?(.+)?', re.DOTALL | re.UNICODE)): |
360
|
|
|
""" Computes a single slide template vars from its html source code. |
361
|
|
|
Also extracts slide information for the table of contents. |
362
|
|
|
""" |
363
|
|
|
presenter_notes = None |
364
|
|
|
|
365
|
|
|
find = _presenter_notes_re.search(slide_src) |
366
|
|
|
|
367
|
|
|
if find: |
368
|
|
|
if self.presenter_notes: |
369
|
|
|
presenter_notes = slide_src[find.end():].strip() |
370
|
|
|
|
371
|
|
|
slide_src = slide_src[:find.start()] |
372
|
|
|
|
373
|
|
|
find = _slide_title_re.search(slide_src) |
374
|
|
|
|
375
|
|
|
if not find: |
376
|
|
|
header = level = title = None |
377
|
|
|
content = slide_src.strip() |
378
|
|
|
else: |
379
|
|
|
header = find.group(1) |
380
|
|
|
level = int(find.group(2)) |
381
|
|
|
title = find.group(3) |
382
|
|
|
content = find.group(4).strip() if find.group(4) else find.group(4) |
383
|
|
|
|
384
|
|
|
slide_classes = [] |
385
|
|
|
context = {} |
386
|
|
|
|
387
|
|
|
if header: |
388
|
|
|
header, _ = self.process_macros(header, source, context) |
389
|
|
|
|
390
|
|
|
if content: |
391
|
|
|
content, slide_classes = self.process_macros(content, source, context) |
392
|
|
|
|
393
|
|
|
source_dict = {} |
394
|
|
|
|
395
|
|
|
if source: |
396
|
|
|
source_dict = { |
397
|
|
|
'rel_path': source.decode(sys.getfilesystemencoding(), 'ignore') if isinstance(source, |
398
|
|
|
binary_type) else source, |
399
|
|
|
'abs_path': os.path.abspath(source) |
400
|
|
|
} |
401
|
|
|
|
402
|
|
|
if header or content: |
403
|
|
|
context.update( |
404
|
|
|
content=content, |
405
|
|
|
classes=slide_classes, |
406
|
|
|
header=header, |
407
|
|
|
level=level, |
408
|
|
|
presenter_notes=presenter_notes, |
409
|
|
|
source=source_dict, |
410
|
|
|
title=title, |
411
|
|
|
) |
412
|
|
|
return context |
413
|
|
|
|
414
|
|
|
def get_template_vars(self, slides): |
415
|
|
|
""" Computes template vars from slides html source code. |
416
|
|
|
""" |
417
|
|
|
try: |
418
|
|
|
head_title = slides[0]['title'] |
419
|
|
|
except (IndexError, TypeError): |
420
|
|
|
head_title = "Untitled Presentation" |
421
|
|
|
|
422
|
|
|
for slide_index, slide_vars in enumerate(slides): |
423
|
|
|
if not slide_vars: |
424
|
|
|
continue |
425
|
|
|
self.num_slides += 1 |
426
|
|
|
slide_number = slide_vars['number'] = self.num_slides |
427
|
|
|
if slide_vars['level'] and slide_vars['level'] <= self.maxtoclevel: |
428
|
|
|
# only show slides that have a title and lever is not too deep |
429
|
|
|
self.add_toc_entry(slide_vars['title'], slide_vars['level'], slide_number) |
430
|
|
|
|
431
|
|
|
return {'head_title': head_title, 'num_slides': str(self.num_slides), |
432
|
|
|
'slides': slides, 'toc': self.toc, 'embed': self.embed, |
433
|
|
|
'css': self.get_css(), 'js': self.get_js(), |
434
|
|
|
'user_css': self.user_css, 'user_js': self.user_js, |
435
|
|
|
'version': __version__} |
436
|
|
|
|
437
|
|
|
def linenos_check(self, value): |
438
|
|
|
""" Checks and returns a valid value for the ``linenos`` option. |
439
|
|
|
""" |
440
|
|
|
return value if value in VALID_LINENOS else 'inline' |
441
|
|
|
|
442
|
|
|
def log(self, message, type='notice'): |
443
|
|
|
""" Logs a message (eventually, override to do something more clever). |
444
|
|
|
""" |
445
|
|
|
if self.logger and not callable(self.logger): |
446
|
|
|
raise ValueError(u"Invalid logger set, must be a callable") |
447
|
|
|
if self.verbose and self.logger: |
448
|
|
|
self.logger(message, type) |
449
|
|
|
|
450
|
|
|
def parse_config(self, config_source): |
451
|
|
|
""" Parses a landslide configuration file and returns a normalized |
452
|
|
|
python dict. |
453
|
|
|
""" |
454
|
|
|
self.log(u"Config %s" % config_source) |
455
|
|
|
try: |
456
|
|
|
raw_config = configparser.RawConfigParser() |
457
|
|
|
raw_config.read(config_source) |
458
|
|
|
except Exception as e: |
459
|
|
|
raise RuntimeError(u"Invalid configuration file: %s" % e) |
460
|
|
|
section_name = 'landslide' if raw_config.has_section('landslide') else 'darkslide' |
461
|
|
|
config = { |
462
|
|
|
'source': raw_config.get(section_name, 'source').replace('\r', '').split('\n') |
463
|
|
|
} |
464
|
|
|
if raw_config.has_option(section_name, 'theme'): |
465
|
|
|
config['theme'] = raw_config.get(section_name, 'theme') |
466
|
|
|
self.log(u"Using configured theme %s" % config['theme']) |
467
|
|
|
if raw_config.has_option(section_name, 'destination'): |
468
|
|
|
config['destination'] = raw_config.get(section_name, 'destination') |
469
|
|
|
if raw_config.has_option(section_name, 'linenos'): |
470
|
|
|
config['linenos'] = raw_config.get(section_name, 'linenos') |
471
|
|
|
if raw_config.has_option(section_name, 'max-toc-level'): |
472
|
|
|
config['max-toc-level'] = int(raw_config.get(section_name, 'max-toc-level')) |
473
|
|
|
for boolopt in ('embed', 'relative', 'copy_theme'): |
474
|
|
|
if raw_config.has_option(section_name, boolopt): |
475
|
|
|
config[boolopt] = raw_config.getboolean(section_name, boolopt) |
476
|
|
|
if raw_config.has_option(section_name, 'extensions'): |
477
|
|
|
config['extensions'] = ",".join(raw_config.get(section_name, 'extensions').replace('\r', '').split('\n')) |
478
|
|
|
if raw_config.has_option(section_name, 'css'): |
479
|
|
|
config['css'] = raw_config.get(section_name, 'css').replace('\r', '').split('\n') |
480
|
|
|
if raw_config.has_option(section_name, 'js'): |
481
|
|
|
config['js'] = raw_config.get(section_name, 'js').replace('\r', '').split('\n') |
482
|
|
|
return config |
483
|
|
|
|
484
|
|
|
def process_macros(self, content, source, context): |
485
|
|
|
""" Processed all macros. |
486
|
|
|
""" |
487
|
|
|
classes = [] |
488
|
|
|
for macro in self.macros: |
489
|
|
|
content, add_classes = macro.process(content, source, context) |
490
|
|
|
if add_classes: |
491
|
|
|
classes += add_classes |
492
|
|
|
return content, classes |
493
|
|
|
|
494
|
|
|
def register_macro(self, *macros): |
495
|
|
|
""" Registers macro classes passed a method arguments. |
496
|
|
|
""" |
497
|
|
|
macro_options = {'relative': self.relative, 'linenos': self.linenos, 'destination_dir': self.destination_dir} |
498
|
|
|
for m in macros: |
499
|
|
|
if inspect.isclass(m) and issubclass(m, macro_module.Macro): |
500
|
|
|
self.macros.append(m(logger=self.logger, embed=self.embed, options=macro_options)) |
501
|
|
|
else: |
502
|
|
|
raise TypeError("Couldn't register macro; a macro must inherit" |
503
|
|
|
" from macro.Macro") |
504
|
|
|
|
505
|
|
|
def render(self): |
506
|
|
|
""" Returns generated html code. |
507
|
|
|
""" |
508
|
|
|
with codecs.open(self.template_file, encoding=self.encoding) as template_src: |
509
|
|
|
template = jinja2.Template(template_src.read()) |
510
|
|
|
slides = self.fetch_contents(self.source, self.work_dir) |
511
|
|
|
context = self.get_template_vars(slides) |
512
|
|
|
|
513
|
|
|
html = template.render(context) |
514
|
|
|
|
515
|
|
|
if self.embed: |
516
|
|
|
images = re.findall(r'url\(["\']?(.*?\.(?:jpe?g|gif|png|svg)[\'"]?)\)', html, re.DOTALL | re.UNICODE) |
517
|
|
|
|
518
|
|
|
for img_url in images: |
519
|
|
|
img_url = img_url.replace('"', '').replace("'", '') |
520
|
|
|
if self.theme_dir: |
521
|
|
|
source = os.path.join(self.theme_dir, 'css') |
522
|
|
|
else: |
523
|
|
|
source = os.path.join(THEMES_DIR, self.theme, 'css') |
524
|
|
|
|
525
|
|
|
encoded_url = utils.encode_image_from_url(img_url, source) |
526
|
|
|
if encoded_url: |
527
|
|
|
html = html.replace(img_url, encoded_url, 1) |
528
|
|
|
self.log("Embedded theme image %s from theme directory %s" % (img_url, source)) |
529
|
|
|
else: |
530
|
|
|
# Missing file in theme directory. Try user_css folders |
531
|
|
|
found = False |
532
|
|
|
for css_entry in context['user_css']: |
533
|
|
|
directory = os.path.dirname(css_entry['path_url']) |
534
|
|
|
if not directory: |
535
|
|
|
directory = "." |
536
|
|
|
|
537
|
|
|
encoded_url = utils.encode_image_from_url(img_url, directory) |
538
|
|
|
|
539
|
|
|
if encoded_url: |
540
|
|
|
found = True |
541
|
|
|
html = html.replace(img_url, encoded_url, 1) |
542
|
|
|
self.log("Embedded theme image %s from directory %s" % (img_url, directory)) |
543
|
|
|
|
544
|
|
|
if not found: |
545
|
|
|
# Missing image file, etc... |
546
|
|
|
self.log(u"Failed to embed theme image %s" % img_url) |
547
|
|
|
|
548
|
|
|
return html |
549
|
|
|
|
550
|
|
|
def write(self): |
551
|
|
|
""" Writes generated presentation code into the destination file. |
552
|
|
|
""" |
553
|
|
|
html = self.render() |
554
|
|
|
dirname = os.path.dirname(self.destination_file) |
555
|
|
|
if dirname and not os.path.exists(dirname): |
556
|
|
|
os.makedirs(dirname) |
557
|
|
|
with codecs.open(self.destination_file, 'w', |
558
|
|
|
encoding='utf_8') as outfile: |
559
|
|
|
outfile.write(html) |
560
|
|
|
|