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