Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/docutils/writers/s5_html/__init__.py: 4%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

178 statements  

1# $Id: __init__.py 9542 2024-02-17 10:37:23Z milde $ 

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

7S5/HTML Slideshow Writer. 

8""" 

9 

10__docformat__ = 'reStructuredText' 

11 

12import sys 

13import os 

14import re 

15import docutils 

16from docutils import frontend, nodes, utils 

17from docutils.writers import html4css1 

18 

19themes_dir_path = utils.relative_path( 

20 os.path.join(os.getcwd(), 'dummy'), 

21 os.path.join(os.path.dirname(__file__), 'themes')) 

22 

23 

24def find_theme(name): 

25 # Where else to look for a theme? 

26 # Check working dir? Destination dir? Config dir? Plugins dir? 

27 path = os.path.join(themes_dir_path, name) 

28 if not os.path.isdir(path): 

29 raise docutils.ApplicationError( 

30 'Theme directory not found: %r (path: %r)' % (name, path)) 

31 return path 

32 

33 

34class Writer(html4css1.Writer): 

35 

36 settings_spec = html4css1.Writer.settings_spec + ( 

37 'S5 Slideshow Specific Options', 

38 'For the S5/HTML writer, the --no-toc-backlinks option ' 

39 '(defined in General Docutils Options above) is the default, ' 

40 'and should not be changed.', 

41 (('Specify an installed S5 theme by name. Overrides --theme-url. ' 

42 'The default theme name is "default". The theme files will be ' 

43 'copied into a "ui/<theme>" directory, in the same directory as the ' 

44 'destination file (output HTML). Note that existing theme files ' 

45 'will not be overwritten (unless --overwrite-theme-files is used).', 

46 ['--theme'], 

47 {'default': 'default', 'metavar': '<name>', 

48 'overrides': 'theme_url'}), 

49 ('Specify an S5 theme URL. The destination file (output HTML) will ' 

50 'link to this theme; nothing will be copied. Overrides --theme.', 

51 ['--theme-url'], 

52 {'metavar': '<URL>', 'overrides': 'theme'}), 

53 ('Allow existing theme files in the ``ui/<theme>`` directory to be ' 

54 'overwritten. The default is not to overwrite theme files.', 

55 ['--overwrite-theme-files'], 

56 {'action': 'store_true', 'validator': frontend.validate_boolean}), 

57 ('Keep existing theme files in the ``ui/<theme>`` directory; do not ' 

58 'overwrite any. This is the default.', 

59 ['--keep-theme-files'], 

60 {'dest': 'overwrite_theme_files', 'action': 'store_false'}), 

61 ('Set the initial view mode to "slideshow" [default] or "outline".', 

62 ['--view-mode'], 

63 {'choices': ['slideshow', 'outline'], 'default': 'slideshow', 

64 'metavar': '<mode>'}), 

65 ('Normally hide the presentation controls in slideshow mode. ' 

66 'This is the default.', 

67 ['--hidden-controls'], 

68 {'action': 'store_true', 'default': True, 

69 'validator': frontend.validate_boolean}), 

70 ('Always show the presentation controls in slideshow mode. ' 

71 'The default is to hide the controls.', 

72 ['--visible-controls'], 

73 {'dest': 'hidden_controls', 'action': 'store_false'}), 

74 ('Enable the current slide indicator ("1 / 15"). ' 

75 'The default is to disable it.', 

76 ['--current-slide'], 

77 {'action': 'store_true', 'validator': frontend.validate_boolean}), 

78 ('Disable the current slide indicator. This is the default.', 

79 ['--no-current-slide'], 

80 {'dest': 'current_slide', 'action': 'store_false'}),)) 

81 

82 settings_default_overrides = {'toc_backlinks': 0} 

83 

84 config_section = 's5_html writer' 

85 config_section_dependencies = ('writers', 'html writers', 

86 'html4css1 writer') 

87 

88 def __init__(self): 

89 html4css1.Writer.__init__(self) 

90 self.translator_class = S5HTMLTranslator 

91 

92 

93class 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') 

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

157 self.setup_theme() 

158 except docutils.ApplicationError as e: 

159 self.document.reporter.warning(e) 

160 view_mode = self.document.settings.view_mode 

161 control_visibility = ('visible', 'hidden')[self.document.settings 

162 .hidden_controls] 

163 self.stylesheet.append(self.s5_stylesheet_template 

164 % {'path': self.theme_file_path, 

165 'view_mode': view_mode, 

166 'control_visibility': control_visibility}) 

167 if not self.document.settings.current_slide: 

168 self.stylesheet.append(self.disable_current_slide) 

169 self.meta.append('<meta name="version" content="S5 1.1" />\n') 

170 self.s5_footer = [] 

171 self.s5_header = [] 

172 self.section_count = 0 

173 self.theme_files_copied = None 

174 

175 def setup_theme(self): 

176 if self.document.settings.theme: 

177 self.copy_theme() 

178 elif self.document.settings.theme_url: 

179 self.theme_file_path = self.document.settings.theme_url 

180 else: 

181 raise docutils.ApplicationError( 

182 'No theme specified for S5/HTML writer.') 

183 

184 def copy_theme(self): 

185 """ 

186 Locate & copy theme files. 

187 

188 A theme may be explicitly based on another theme via a '__base__' 

189 file. The default base theme is 'default'. Files are accumulated 

190 from the specified theme, any base themes, and 'default'. 

191 """ 

192 settings = self.document.settings 

193 path = find_theme(settings.theme) 

194 theme_paths = [path] 

195 self.theme_files_copied = {} 

196 required_files_copied = {} 

197 # This is a link (URL) in HTML, so we use "/", not os.sep: 

198 self.theme_file_path = 'ui/%s' % settings.theme 

199 if not settings.output: 

200 raise docutils.ApplicationError( 

201 'Output path not specified, you may need to copy' 

202 ' the S5 theme files "by hand" or set the "--output" option.') 

203 dest = os.path.join( 

204 os.path.dirname(settings.output), 'ui', settings.theme) 

205 if not os.path.isdir(dest): 

206 os.makedirs(dest) 

207 default = False 

208 while path: 

209 for f in os.listdir(path): # copy all files from each theme 

210 if f == self.base_theme_file: 

211 continue # ... except the "__base__" file 

212 if (self.copy_file(f, path, dest) 

213 and f in self.required_theme_files): 

214 required_files_copied[f] = True 

215 if default: 

216 break # "default" theme has no base theme 

217 # Find the "__base__" file in theme directory: 

218 base_theme_file = os.path.join(path, self.base_theme_file) 

219 # If it exists, read it and record the theme path: 

220 if os.path.isfile(base_theme_file): 

221 with open(base_theme_file, encoding='utf-8') as f: 

222 lines = f.readlines() 

223 for line in lines: 

224 line = line.strip() 

225 if line and not line.startswith('#'): 

226 path = find_theme(line) 

227 if path in theme_paths: # check for duplicates/cycles 

228 path = None # if found, use default base 

229 else: 

230 theme_paths.append(path) 

231 break 

232 else: # no theme name found 

233 path = None # use default base 

234 else: # no base theme file found 

235 path = None # use default base 

236 if not path: 

237 path = find_theme(self.default_theme) 

238 theme_paths.append(path) 

239 default = True 

240 if len(required_files_copied) != len(self.required_theme_files): 

241 # Some required files weren't found & couldn't be copied. 

242 required = list(self.required_theme_files) 

243 for f in required_files_copied.keys(): 

244 required.remove(f) 

245 raise docutils.ApplicationError( 

246 'Theme files not found: %s' 

247 % ', '.join('%r' % f for f in required)) 

248 

249 files_to_skip_pattern = re.compile(r'~$|\.bak$|#$|\.cvsignore$') 

250 

251 def copy_file(self, name, source_dir, dest_dir): 

252 """ 

253 Copy file `name` from `source_dir` to `dest_dir`. 

254 Return True if the file exists in either `source_dir` or `dest_dir`. 

255 """ 

256 source = os.path.join(source_dir, name) 

257 dest = os.path.join(dest_dir, name) 

258 if dest in self.theme_files_copied: 

259 return True 

260 else: 

261 self.theme_files_copied[dest] = True 

262 if os.path.isfile(source): 

263 if self.files_to_skip_pattern.search(source): 

264 return None 

265 settings = self.document.settings 

266 if os.path.exists(dest) and not settings.overwrite_theme_files: 

267 settings.record_dependencies.add(dest) 

268 else: 

269 with open(source, 'rb') as src_file: 

270 src_data = src_file.read() 

271 with open(dest, 'wb') as dest_file: 

272 dest_dir = dest_dir.replace(os.sep, '/') 

273 dest_file.write(src_data.replace( 

274 b'ui/default', 

275 dest_dir[dest_dir.rfind('ui/'):].encode( 

276 sys.getfilesystemencoding()))) 

277 settings.record_dependencies.add(source) 

278 return True 

279 if os.path.isfile(dest): 

280 return True 

281 

282 def depart_document(self, node): 

283 self.head_prefix.extend([self.doctype, 

284 self.head_prefix_template % 

285 {'lang': self.settings.language_code}]) 

286 self.html_prolog.append(self.doctype) 

287 self.head = self.meta[:] + self.head 

288 if self.math_header: 

289 if self.math_output == 'mathjax': 

290 self.head.extend(self.math_header) 

291 else: 

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, 

301 'title': title, 

302 'footer': footer} 

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

309 # 

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

314 

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

321 

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) 

329 

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

338 else: 

339 self.body.append(self.starttag(node, 'div', CLASS='slide')) 

340 

341 def visit_subtitle(self, node): 

342 if isinstance(node.parent, nodes.section): 

343 level = self.section_level + self.initial_header_level - 1 

344 if level == 1: 

345 level = 2 

346 tag = 'h%s' % level 

347 self.body.append(self.starttag(node, tag, '')) 

348 self.context.append('</%s>\n' % tag) 

349 else: 

350 html4css1.HTMLTranslator.visit_subtitle(self, node) 

351 

352 def visit_title(self, node): 

353 html4css1.HTMLTranslator.visit_title(self, node)