2 # Authors: Chris Liechti <cliechti@gmx.net>;
3 # David Goodger <goodger@python.org>
4 # Copyright: This module has been placed in the public domain.
7 S5/HTML Slideshow Writer.
10 __docformat__
= 'reStructuredText'
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'))
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
))
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).',
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.',
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".',
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.',
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')
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
114 disable_current_slide
= """
115 <style type="text/css">
116 #currentSlide {display: none;}
119 layout_template
= """\
121 <div id="controls"></div>
122 <div id="currentSlide"></div>
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
158 view_mode
= self
.document
.settings
.view_mode
159 control_visibility
= ('visible', 'hidden')[self
.document
.settings
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')
170 self
.section_count
= 0
171 self
.theme_files_copied
= None
173 def setup_theme(self
):
174 if self
.document
.settings
.theme
:
176 elif self
.document
.settings
.theme_url
:
177 self
.theme_file_path
= self
.document
.settings
.theme_url
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
)
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
:
199 os
.path
.dirname(settings
._destination
), 'ui', settings
.theme
)
200 if not os
.path
.isdir(dest
):
203 # no destination, so we can't copy the theme
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
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()
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
227 theme_paths
.append(path
)
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
234 path
= find_theme(self
.default_theme
)
235 theme_paths
.append(path
)
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():
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
:
258 self
.theme_files_copied
[dest
] = 1
259 if os
.path
.isfile(source
):
260 if self
.files_to_skip_pattern
.search(source
):
262 settings
= self
.document
.settings
263 if os
.path
.exists(dest
) and not settings
.overwrite_theme_files
:
264 settings
.record_dependencies
.add(dest
)
266 src_file
= open(source
, 'rb')
267 src_data
= src_file
.read()
269 dest_file
= open(dest
, 'wb')
270 dest_dir
= dest_dir
.replace(os
.sep
, '/')
271 dest_file
.write(src_data
.replace(
273 dest_dir
[dest_dir
.rfind('ui/'):].encode(
274 sys
.getfilesystemencoding())))
276 settings
.record_dependencies
.add(source
)
278 if os
.path
.isfile(dest
):
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 if self
.math_output
== 'mathjax':
290 self
.head
.extend(self
.math_header
)
292 self
.stylesheet
.extend(self
.math_header
)
293 # skip content-type meta tag with interpolated charset value:
294 self
.html_head
.extend(self
.head
[1:])
295 self
.fragment
.extend(self
.body
)
296 # special S5 code up to the next comment line
297 header
= ''.join(self
.s5_header
)
298 footer
= ''.join(self
.s5_footer
)
299 title
= ''.join(self
.html_title
).replace('<h1 class="title">', '<h1>')
300 layout
= self
.layout_template
% {'header': header
,
303 self
.body_prefix
.extend(layout
)
304 self
.body_prefix
.append('<div class="presentation">\n')
305 self
.body_prefix
.append(
306 self
.starttag({'classes': ['slide'], 'ids': ['slide0']}, 'div'))
307 if not self
.section_count
:
308 self
.body
.append('</div>\n')
310 self
.body_suffix
.insert(0, '</div>\n')
311 self
.html_body
.extend(self
.body_prefix
[1:] + self
.body_pre_docinfo
312 + self
.docinfo
+ self
.body
313 + self
.body_suffix
[:-1])
315 def depart_footer(self
, node
):
316 start
= self
.context
.pop()
317 self
.s5_footer
.append('<h2>')
318 self
.s5_footer
.extend(self
.body
[start
:])
319 self
.s5_footer
.append('</h2>')
320 del self
.body
[start
:]
322 def depart_header(self
, node
):
323 start
= self
.context
.pop()
324 header
= ['<div id="header">\n']
325 header
.extend(self
.body
[start
:])
326 header
.append('\n</div>\n')
327 del self
.body
[start
:]
328 self
.s5_header
.extend(header
)
330 def visit_section(self
, node
):
331 if not self
.section_count
:
332 self
.body
.append('\n</div>\n')
333 self
.section_count
+= 1
334 self
.section_level
+= 1
335 if self
.section_level
> 1:
336 # dummy for matching div's
337 self
.body
.append(self
.starttag(node
, 'div', CLASS
='section'))
339 self
.body
.append(self
.starttag(node
, 'div', CLASS
='slide'))
341 def visit_subtitle(self
, node
):
342 if isinstance(node
.parent
, nodes
.section
):
343 level
= self
.section_level
+ self
.initial_header_level
- 1
347 self
.body
.append(self
.starttag(node
, tag
, ''))
348 self
.context
.append('</%s>\n' % tag
)
350 html4css1
.HTMLTranslator
.visit_subtitle(self
, node
)
352 def visit_title(self
, node
):
353 html4css1
.HTMLTranslator
.visit_title(self
, node
)