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