Cleanup: Use True/False for boolean values
[docutils.git] / docutils / writers / s5_html / __init__.py
blob37bfa0d905c857320e48676713d4d1e186030fd3
1 # $Id$
2 # Authors: Chris Liechti <cliechti@gmx.net>;
3 # David Goodger <goodger@python.org>
4 # Copyright: This module has been placed in the public domain.
6 """
7 S5/HTML Slideshow Writer.
8 """
10 __docformat__ = 'reStructuredText'
13 import sys
14 import os
15 import re
16 import docutils
17 from docutils import frontend, nodes, utils
18 from docutils.writers import html4css1
19 from docutils.parsers.rst import directives
20 from docutils._compat import b
22 themes_dir_path = utils.relative_path(
23 os.path.join(os.getcwd(), 'dummy'),
24 os.path.join(os.path.dirname(__file__), 'themes'))
26 def find_theme(name):
27 # Where else to look for a theme?
28 # Check working dir? Destination dir? Config dir? Plugins dir?
29 path = os.path.join(themes_dir_path, name)
30 if not os.path.isdir(path):
31 raise docutils.ApplicationError(
32 'Theme directory not found: %r (path: %r)' % (name, path))
33 return path
36 class Writer(html4css1.Writer):
38 settings_spec = html4css1.Writer.settings_spec + (
39 'S5 Slideshow Specific Options',
40 'For the S5/HTML writer, the --no-toc-backlinks option '
41 '(defined in General Docutils Options above) is the default, '
42 'and should not be changed.',
43 (('Specify an installed S5 theme by name. Overrides --theme-url. '
44 'The default theme name is "default". The theme files will be '
45 'copied into a "ui/<theme>" directory, in the same directory as the '
46 'destination file (output HTML). Note that existing theme files '
47 'will not be overwritten (unless --overwrite-theme-files is used).',
48 ['--theme'],
49 {'default': 'default', 'metavar': '<name>',
50 'overrides': 'theme_url'}),
51 ('Specify an S5 theme URL. The destination file (output HTML) will '
52 'link to this theme; nothing will be copied. Overrides --theme.',
53 ['--theme-url'],
54 {'metavar': '<URL>', 'overrides': 'theme'}),
55 ('Allow existing theme files in the ``ui/<theme>`` directory to be '
56 'overwritten. The default is not to overwrite theme files.',
57 ['--overwrite-theme-files'],
58 {'action': 'store_true', 'validator': frontend.validate_boolean}),
59 ('Keep existing theme files in the ``ui/<theme>`` directory; do not '
60 'overwrite any. This is the default.',
61 ['--keep-theme-files'],
62 {'dest': 'overwrite_theme_files', 'action': 'store_false'}),
63 ('Set the initial view mode to "slideshow" [default] or "outline".',
64 ['--view-mode'],
65 {'choices': ['slideshow', 'outline'], 'default': 'slideshow',
66 'metavar': '<mode>'}),
67 ('Normally hide the presentation controls in slideshow mode. '
68 'This is the default.',
69 ['--hidden-controls'],
70 {'action': 'store_true', 'default': True,
71 'validator': frontend.validate_boolean}),
72 ('Always show the presentation controls in slideshow mode. '
73 'The default is to hide the controls.',
74 ['--visible-controls'],
75 {'dest': 'hidden_controls', 'action': 'store_false'}),
76 ('Enable the current slide indicator ("1 / 15"). '
77 'The default is to disable it.',
78 ['--current-slide'],
79 {'action': 'store_true', 'validator': frontend.validate_boolean}),
80 ('Disable the current slide indicator. This is the default.',
81 ['--no-current-slide'],
82 {'dest': 'current_slide', 'action': 'store_false'}),))
84 settings_default_overrides = {'toc_backlinks': 0}
86 config_section = 's5_html writer'
87 config_section_dependencies = ('writers', 'html4css1 writer')
89 def __init__(self):
90 html4css1.Writer.__init__(self)
91 self.translator_class = S5HTMLTranslator
94 class S5HTMLTranslator(html4css1.HTMLTranslator):
96 s5_stylesheet_template = """\
97 <!-- configuration parameters -->
98 <meta name="defaultView" content="%(view_mode)s" />
99 <meta name="controlVis" content="%(control_visibility)s" />
100 <!-- style sheet links -->
101 <script src="%(path)s/slides.js" type="text/javascript"></script>
102 <link rel="stylesheet" href="%(path)s/slides.css"
103 type="text/css" media="projection" id="slideProj" />
104 <link rel="stylesheet" href="%(path)s/outline.css"
105 type="text/css" media="screen" id="outlineStyle" />
106 <link rel="stylesheet" href="%(path)s/print.css"
107 type="text/css" media="print" id="slidePrint" />
108 <link rel="stylesheet" href="%(path)s/opera.css"
109 type="text/css" media="projection" id="operaFix" />\n"""
110 # The script element must go in front of the link elements to
111 # avoid a flash of unstyled content (FOUC), reproducible with
112 # Firefox.
114 disable_current_slide = """
115 <style type="text/css">
116 #currentSlide {display: none;}
117 </style>\n"""
119 layout_template = """\
120 <div class="layout">
121 <div id="controls"></div>
122 <div id="currentSlide"></div>
123 <div id="header">
124 %(header)s
125 </div>
126 <div id="footer">
127 %(title)s%(footer)s
128 </div>
129 </div>\n"""
130 # <div class="topleft"></div>
131 # <div class="topright"></div>
132 # <div class="bottomleft"></div>
133 # <div class="bottomright"></div>
135 default_theme = 'default'
136 """Name of the default theme."""
138 base_theme_file = '__base__'
139 """Name of the file containing the name of the base theme."""
141 direct_theme_files = (
142 'slides.css', 'outline.css', 'print.css', 'opera.css', 'slides.js')
143 """Names of theme files directly linked to in the output HTML"""
145 indirect_theme_files = (
146 's5-core.css', 'framing.css', 'pretty.css', 'blank.gif', 'iepngfix.htc')
147 """Names of files used indirectly; imported or used by files in
148 `direct_theme_files`."""
150 required_theme_files = indirect_theme_files + direct_theme_files
151 """Names of mandatory theme files."""
153 def __init__(self, *args):
154 html4css1.HTMLTranslator.__init__(self, *args)
155 #insert S5-specific stylesheet and script stuff:
156 self.theme_file_path = None
157 self.setup_theme()
158 view_mode = self.document.settings.view_mode
159 control_visibility = ('visible', 'hidden')[self.document.settings
160 .hidden_controls]
161 self.stylesheet.append(self.s5_stylesheet_template
162 % {'path': self.theme_file_path,
163 'view_mode': view_mode,
164 'control_visibility': control_visibility})
165 if not self.document.settings.current_slide:
166 self.stylesheet.append(self.disable_current_slide)
167 self.add_meta('<meta name="version" content="S5 1.1" />\n')
168 self.s5_footer = []
169 self.s5_header = []
170 self.section_count = 0
171 self.theme_files_copied = None
173 def setup_theme(self):
174 if self.document.settings.theme:
175 self.copy_theme()
176 elif self.document.settings.theme_url:
177 self.theme_file_path = self.document.settings.theme_url
178 else:
179 raise docutils.ApplicationError(
180 'No theme specified for S5/HTML writer.')
182 def copy_theme(self):
184 Locate & copy theme files.
186 A theme may be explicitly based on another theme via a '__base__'
187 file. The default base theme is 'default'. Files are accumulated
188 from the specified theme, any base themes, and 'default'.
190 settings = self.document.settings
191 path = find_theme(settings.theme)
192 theme_paths = [path]
193 self.theme_files_copied = {}
194 required_files_copied = {}
195 # This is a link (URL) in HTML, so we use "/", not os.sep:
196 self.theme_file_path = '%s/%s' % ('ui', settings.theme)
197 if settings._destination:
198 dest = os.path.join(
199 os.path.dirname(settings._destination), 'ui', settings.theme)
200 if not os.path.isdir(dest):
201 os.makedirs(dest)
202 else:
203 # no destination, so we can't copy the theme
204 return
205 default = False
206 while path:
207 for f in os.listdir(path): # copy all files from each theme
208 if f == self.base_theme_file:
209 continue # ... except the "__base__" file
210 if ( self.copy_file(f, path, dest)
211 and f in self.required_theme_files):
212 required_files_copied[f] = 1
213 if default:
214 break # "default" theme has no base theme
215 # Find the "__base__" file in theme directory:
216 base_theme_file = os.path.join(path, self.base_theme_file)
217 # If it exists, read it and record the theme path:
218 if os.path.isfile(base_theme_file):
219 lines = open(base_theme_file).readlines()
220 for line in lines:
221 line = line.strip()
222 if line and not line.startswith('#'):
223 path = find_theme(line)
224 if path in theme_paths: # check for duplicates (cycles)
225 path = None # if found, use default base
226 else:
227 theme_paths.append(path)
228 break
229 else: # no theme name found
230 path = None # use default base
231 else: # no base theme file found
232 path = None # use default base
233 if not path:
234 path = find_theme(self.default_theme)
235 theme_paths.append(path)
236 default = True
237 if len(required_files_copied) != len(self.required_theme_files):
238 # Some required files weren't found & couldn't be copied.
239 required = list(self.required_theme_files)
240 for f in required_files_copied.keys():
241 required.remove(f)
242 raise docutils.ApplicationError(
243 'Theme files not found: %s'
244 % ', '.join(['%r' % f for f in required]))
246 files_to_skip_pattern = re.compile(r'~$|\.bak$|#$|\.cvsignore$')
248 def copy_file(self, name, source_dir, dest_dir):
250 Copy file `name` from `source_dir` to `dest_dir`.
251 Return 1 if the file exists in either `source_dir` or `dest_dir`.
253 source = os.path.join(source_dir, name)
254 dest = os.path.join(dest_dir, name)
255 if dest in self.theme_files_copied:
256 return 1
257 else:
258 self.theme_files_copied[dest] = 1
259 if os.path.isfile(source):
260 if self.files_to_skip_pattern.search(source):
261 return None
262 settings = self.document.settings
263 if os.path.exists(dest) and not settings.overwrite_theme_files:
264 settings.record_dependencies.add(dest)
265 else:
266 src_file = open(source, 'rb')
267 src_data = src_file.read()
268 src_file.close()
269 dest_file = open(dest, 'wb')
270 dest_dir = dest_dir.replace(os.sep, '/')
271 dest_file.write(src_data.replace(
272 b('ui/default'),
273 dest_dir[dest_dir.rfind('ui/'):].encode(
274 sys.getfilesystemencoding())))
275 dest_file.close()
276 settings.record_dependencies.add(source)
277 return 1
278 if os.path.isfile(dest):
279 return 1
281 def depart_document(self, node):
282 self.head_prefix.extend([self.doctype,
283 self.head_prefix_template %
284 {'lang': self.settings.language_code}])
285 self.html_prolog.append(self.doctype)
286 self.meta.insert(0, self.content_type % self.settings.output_encoding)
287 self.head.insert(0, self.content_type % self.settings.output_encoding)
289 header = ''.join(self.s5_header)
290 footer = ''.join(self.s5_footer)
291 title = ''.join(self.html_title).replace('<h1 class="title">', '<h1>')
292 layout = self.layout_template % {'header': header,
293 'title': title,
294 'footer': footer}
295 self.fragment.extend(self.body)
296 self.body_prefix.extend(layout)
297 self.body_prefix.append('<div class="presentation">\n')
298 self.body_prefix.append(
299 self.starttag({'classes': ['slide'], 'ids': ['slide0']}, 'div'))
300 if not self.section_count:
301 self.body.append('</div>\n')
302 self.body_suffix.insert(0, '</div>\n')
303 # skip content-type meta tag with interpolated charset value:
304 self.html_head.extend(self.head[1:])
305 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
306 + self.docinfo + self.body
307 + self.body_suffix[:-1])
309 def depart_footer(self, node):
310 start = self.context.pop()
311 self.s5_footer.append('<h2>')
312 self.s5_footer.extend(self.body[start:])
313 self.s5_footer.append('</h2>')
314 del self.body[start:]
316 def depart_header(self, node):
317 start = self.context.pop()
318 header = ['<div id="header">\n']
319 header.extend(self.body[start:])
320 header.append('\n</div>\n')
321 del self.body[start:]
322 self.s5_header.extend(header)
324 def visit_section(self, node):
325 if not self.section_count:
326 self.body.append('\n</div>\n')
327 self.section_count += 1
328 self.section_level += 1
329 if self.section_level > 1:
330 # dummy for matching div's
331 self.body.append(self.starttag(node, 'div', CLASS='section'))
332 else:
333 self.body.append(self.starttag(node, 'div', CLASS='slide'))
335 def visit_subtitle(self, node):
336 if isinstance(node.parent, nodes.section):
337 level = self.section_level + self.initial_header_level - 1
338 if level == 1:
339 level = 2
340 tag = 'h%s' % level
341 self.body.append(self.starttag(node, tag, ''))
342 self.context.append('</%s>\n' % tag)
343 else:
344 html4css1.HTMLTranslator.visit_subtitle(self, node)
346 def visit_title(self, node):
347 html4css1.HTMLTranslator.visit_title(self, node)