1
# Author: Chris Liechti
2
# Contact: cliechti@gmx.net
3
# Author: David Goodger
4
# Contact: goodger@python.org
5
# Revision: $Revision: 4223 $
6
# Date: $Date: 2005-12-18 00:52:30 +0100 (Sun, 18 Dec 2005) $
7
# Copyright: This module has been placed in the public domain.
10
S5/HTML Slideshow Writer.
13
__docformat__ = 'reStructuredText'
20
from docutils import frontend, nodes, utils
21
from docutils.writers import html4css1
22
from docutils.parsers.rst import directives
24
themes_dir_path = utils.relative_path(
25
os.path.join(os.getcwd(), 'dummy'),
26
os.path.join(os.path.dirname(__file__), 'themes'))
29
# Where else to look for a theme?
30
# Check working dir? Destination dir? Config dir? Plugins dir?
31
path = os.path.join(themes_dir_path, name)
32
if not os.path.isdir(path):
33
raise docutils.ApplicationError(
34
'Theme directory not found: %r (path: %r)' % (name, path))
38
class Writer(html4css1.Writer):
40
settings_spec = html4css1.Writer.settings_spec + (
41
'S5 Slideshow Specific Options',
42
'For the S5/HTML writer, the --no-toc-backlinks option '
43
'(defined in General Docutils Options above) is the default, '
44
'and should not be changed.',
45
(('Specify an installed S5 theme by name. Overrides --theme-url. '
46
'The default theme name is "default". The theme files will be '
47
'copied into a "ui/<theme>" directory, in the same directory as the '
48
'destination file (output HTML). Note that existing theme files '
49
'will not be overwritten (unless --overwrite-theme-files is used).',
51
{'default': 'default', 'metavar': '<name>',
52
'overrides': 'theme_url'}),
53
('Specify an S5 theme URL. The destination file (output HTML) will '
54
'link to this theme; nothing will be copied. Overrides --theme.',
56
{'metavar': '<URL>', 'overrides': 'theme'}),
57
('Allow existing theme files in the ``ui/<theme>`` directory to be '
58
'overwritten. The default is not to overwrite theme files.',
59
['--overwrite-theme-files'],
60
{'action': 'store_true'}),
61
('Keep existing theme files in the ``ui/<theme>`` directory; do not '
62
'overwrite any. This is the default.',
63
['--keep-theme-files'],
64
{'dest': 'overwrite_theme_files', 'action': 'store_false'}),
65
('Enable the current slide indicator ("1 / 15"). '
66
'The default is to disable it.',
68
{'action': 'store_true'}),
69
('Disable the current slide indicator. This is the default.',
70
['--no-current-slide'],
71
{'dest': 'current_slide', 'action': 'store_false'}),))
73
settings_default_overrides = {'toc_backlinks': 0}
75
config_section = 's5_html writer'
76
config_section_dependencies = ('writers', 'html4css1 writer')
79
html4css1.Writer.__init__(self)
80
self.translator_class = S5HTMLTranslator
83
class S5HTMLTranslator(html4css1.HTMLTranslator):
86
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
87
' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
89
s5_stylesheet_template = """\
90
<!-- configuration parameters -->
91
<meta name="defaultView" content="slideshow" />
92
<meta name="controlVis" content="hidden" />
93
<!-- style sheet links -->
94
<script src="%(path)s/slides.js" type="text/javascript"></script>
95
<link rel="stylesheet" href="%(path)s/slides.css"
96
type="text/css" media="projection" id="slideProj" />
97
<link rel="stylesheet" href="%(path)s/outline.css"
98
type="text/css" media="screen" id="outlineStyle" />
99
<link rel="stylesheet" href="%(path)s/print.css"
100
type="text/css" media="print" id="slidePrint" />
101
<link rel="stylesheet" href="%(path)s/opera.css"
102
type="text/css" media="projection" id="operaFix" />\n"""
103
# The script element must go in front of the link elements to
104
# avoid a flash of unstyled content (FOUC), reproducible with
107
disable_current_slide = """
108
<style type="text/css">
109
#currentSlide {display: none;}
112
layout_template = """\
114
<div id="controls"></div>
115
<div id="currentSlide"></div>
123
# <div class="topleft"></div>
124
# <div class="topright"></div>
125
# <div class="bottomleft"></div>
126
# <div class="bottomright"></div>
128
default_theme = 'default'
129
"""Name of the default theme."""
131
base_theme_file = '__base__'
132
"""Name of the file containing the name of the base theme."""
134
direct_theme_files = (
135
'slides.css', 'outline.css', 'print.css', 'opera.css', 'slides.js')
136
"""Names of theme files directly linked to in the output HTML"""
138
indirect_theme_files = (
139
's5-core.css', 'framing.css', 'pretty.css', 'blank.gif', 'iepngfix.htc')
140
"""Names of files used indirectly; imported or used by files in
141
`direct_theme_files`."""
143
required_theme_files = indirect_theme_files + direct_theme_files
144
"""Names of mandatory theme files."""
146
def __init__(self, *args):
147
html4css1.HTMLTranslator.__init__(self, *args)
148
#insert S5-specific stylesheet and script stuff:
149
self.theme_file_path = None
151
self.stylesheet.append(self.s5_stylesheet_template
152
% {'path': self.theme_file_path})
153
if not self.document.settings.current_slide:
154
self.stylesheet.append(self.disable_current_slide)
155
self.add_meta('<meta name="version" content="S5 1.1" />\n')
158
self.section_count = 0
159
self.theme_files_copied = None
161
def setup_theme(self):
162
if self.document.settings.theme:
164
elif self.document.settings.theme_url:
165
self.theme_file_path = self.document.settings.theme_url
167
raise docutils.ApplicationError(
168
'No theme specified for S5/HTML writer.')
170
def copy_theme(self):
172
Locate & copy theme files.
174
A theme may be explicitly based on another theme via a '__base__'
175
file. The default base theme is 'default'. Files are accumulated
176
from the specified theme, any base themes, and 'default'.
178
settings = self.document.settings
179
path = find_theme(settings.theme)
181
self.theme_files_copied = {}
182
required_files_copied = {}
183
# This is a link (URL) in HTML, so we use "/", not os.sep:
184
self.theme_file_path = '%s/%s' % ('ui', settings.theme)
185
if settings._destination:
187
os.path.dirname(settings._destination), 'ui', settings.theme)
188
if not os.path.isdir(dest):
191
# no destination, so we can't copy the theme
195
for f in os.listdir(path): # copy all files from each theme
196
if f == self.base_theme_file:
197
continue # ... except the "__base__" file
198
if ( self.copy_file(f, path, dest)
199
and f in self.required_theme_files):
200
required_files_copied[f] = 1
202
break # "default" theme has no base theme
203
# Find the "__base__" file in theme directory:
204
base_theme_file = os.path.join(path, self.base_theme_file)
205
# If it exists, read it and record the theme path:
206
if os.path.isfile(base_theme_file):
207
lines = open(base_theme_file).readlines()
210
if line and not line.startswith('#'):
211
path = find_theme(line)
212
if path in theme_paths: # check for duplicates (cycles)
213
path = None # if found, use default base
215
theme_paths.append(path)
217
else: # no theme name found
218
path = None # use default base
219
else: # no base theme file found
220
path = None # use default base
222
path = find_theme(self.default_theme)
223
theme_paths.append(path)
225
if len(required_files_copied) != len(self.required_theme_files):
226
# Some required files weren't found & couldn't be copied.
227
required = list(self.required_theme_files)
228
for f in required_files_copied.keys():
230
raise docutils.ApplicationError(
231
'Theme files not found: %s'
232
% ', '.join(['%r' % f for f in required]))
234
files_to_skip_pattern = re.compile(r'~$|\.bak$|#$|\.cvsignore$')
236
def copy_file(self, name, source_dir, dest_dir):
238
Copy file `name` from `source_dir` to `dest_dir`.
239
Return 1 if the file exists in either `source_dir` or `dest_dir`.
241
source = os.path.join(source_dir, name)
242
dest = os.path.join(dest_dir, name)
243
if self.theme_files_copied.has_key(dest):
246
self.theme_files_copied[dest] = 1
247
if os.path.isfile(source):
248
if self.files_to_skip_pattern.search(source):
250
settings = self.document.settings
251
if os.path.exists(dest) and not settings.overwrite_theme_files:
252
settings.record_dependencies.add(dest)
254
src_file = open(source, 'rb')
255
src_data = src_file.read()
257
dest_file = open(dest, 'wb')
258
dest_dir = dest_dir.replace(os.sep, '/')
259
dest_file.write(src_data.replace(
260
'ui/default', dest_dir[dest_dir.rfind('ui/'):]))
262
settings.record_dependencies.add(source)
264
if os.path.isfile(dest):
267
def depart_document(self, node):
268
header = ''.join(self.s5_header)
269
footer = ''.join(self.s5_footer)
270
title = ''.join(self.html_title).replace('<h1 class="title">', '<h1>')
271
layout = self.layout_template % {'header': header,
274
self.fragment.extend(self.body)
275
self.body_prefix.extend(layout)
276
self.body_prefix.append('<div class="presentation">\n')
277
self.body_prefix.append(
278
self.starttag({'classes': ['slide'], 'ids': ['slide0']}, 'div'))
279
if not self.section_count:
280
self.body.append('</div>\n')
281
self.body_suffix.insert(0, '</div>\n')
282
# skip content-type meta tag with interpolated charset value:
283
self.html_head.extend(self.head[1:])
284
self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
285
+ self.docinfo + self.body
286
+ self.body_suffix[:-1])
288
def depart_footer(self, node):
289
start = self.context.pop()
290
self.s5_footer.append('<h2>')
291
self.s5_footer.extend(self.body[start:])
292
self.s5_footer.append('</h2>')
293
del self.body[start:]
295
def depart_header(self, node):
296
start = self.context.pop()
297
header = ['<div id="header">\n']
298
header.extend(self.body[start:])
299
header.append('\n</div>\n')
300
del self.body[start:]
301
self.s5_header.extend(header)
303
def visit_section(self, node):
304
if not self.section_count:
305
self.body.append('\n</div>\n')
306
self.section_count += 1
307
self.section_level += 1
308
if self.section_level > 1:
309
# dummy for matching div's
310
self.body.append(self.starttag(node, 'div', CLASS='section'))
312
self.body.append(self.starttag(node, 'div', CLASS='slide'))
314
def visit_subtitle(self, node):
315
if isinstance(node.parent, nodes.section):
316
level = self.section_level + self.initial_header_level - 1
320
self.body.append(self.starttag(node, tag, ''))
321
self.context.append('</%s>\n' % tag)
323
html4css1.HTMLTranslator.visit_subtitle(self, node)
325
def visit_title(self, node, move_ids=0):
326
html4css1.HTMLTranslator.visit_title(self, node, move_ids=move_ids)