##// END OF EJS Templates
Backport PR #4054: use unicode for HTML export...
MinRK -
Show More
@@ -1,339 +1,341
1 1 #-----------------------------------------------------------------------------
2 2 # Copyright (c) 2010, IPython Development Team.
3 3 #
4 4 # Distributed under the terms of the Modified BSD License.
5 5 #
6 6 # The full license is in the file COPYING.txt, distributed with this software.
7 7 #-----------------------------------------------------------------------------
8 8
9 9 # Standard libary imports.
10 10 from base64 import decodestring
11 11 import os
12 12 import re
13 13
14 14 # System libary imports.
15 15 from IPython.external.qt import QtCore, QtGui
16 16
17 17 # Local imports
18 18 from IPython.utils.traitlets import Bool
19 19 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
20 20 from ipython_widget import IPythonWidget
21 21
22 22
23 23 class RichIPythonWidget(IPythonWidget):
24 24 """ An IPythonWidget that supports rich text, including lists, images, and
25 25 tables. Note that raw performance will be reduced compared to the plain
26 26 text version.
27 27 """
28 28
29 29 # RichIPythonWidget protected class variables.
30 30 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
31 31 _jpg_supported = Bool(False)
32 32
33 33 # Used to determine whether a given html export attempt has already
34 34 # displayed a warning about being unable to convert a png to svg.
35 35 _svg_warning_displayed = False
36 36
37 37 #---------------------------------------------------------------------------
38 38 # 'object' interface
39 39 #---------------------------------------------------------------------------
40 40
41 41 def __init__(self, *args, **kw):
42 42 """ Create a RichIPythonWidget.
43 43 """
44 44 kw['kind'] = 'rich'
45 45 super(RichIPythonWidget, self).__init__(*args, **kw)
46 46
47 47 # Configure the ConsoleWidget HTML exporter for our formats.
48 48 self._html_exporter.image_tag = self._get_image_tag
49 49
50 50 # Dictionary for resolving document resource names to SVG data.
51 51 self._name_to_svg_map = {}
52 52
53 53 # Do we support jpg ?
54 54 # it seems that sometime jpg support is a plugin of QT, so try to assume
55 55 # it is not always supported.
56 56 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
57 57 self._jpg_supported = 'jpeg' in _supported_format
58 58
59 59
60 60 #---------------------------------------------------------------------------
61 61 # 'ConsoleWidget' public interface overides
62 62 #---------------------------------------------------------------------------
63 63
64 64 def export_html(self):
65 65 """ Shows a dialog to export HTML/XML in various formats.
66 66
67 67 Overridden in order to reset the _svg_warning_displayed flag prior
68 68 to the export running.
69 69 """
70 70 self._svg_warning_displayed = False
71 71 super(RichIPythonWidget, self).export_html()
72 72
73 73
74 74 #---------------------------------------------------------------------------
75 75 # 'ConsoleWidget' protected interface
76 76 #---------------------------------------------------------------------------
77 77
78 78 def _context_menu_make(self, pos):
79 79 """ Reimplemented to return a custom context menu for images.
80 80 """
81 81 format = self._control.cursorForPosition(pos).charFormat()
82 82 name = format.stringProperty(QtGui.QTextFormat.ImageName)
83 83 if name:
84 84 menu = QtGui.QMenu()
85 85
86 86 menu.addAction('Copy Image', lambda: self._copy_image(name))
87 87 menu.addAction('Save Image As...', lambda: self._save_image(name))
88 88 menu.addSeparator()
89 89
90 90 svg = self._name_to_svg_map.get(name, None)
91 91 if svg is not None:
92 92 menu.addSeparator()
93 93 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
94 94 menu.addAction('Save SVG As...',
95 95 lambda: save_svg(svg, self._control))
96 96 else:
97 97 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
98 98 return menu
99 99
100 100 #---------------------------------------------------------------------------
101 101 # 'BaseFrontendMixin' abstract interface
102 102 #---------------------------------------------------------------------------
103 103 def _pre_image_append(self, msg, prompt_number):
104 104 """ Append the Out[] prompt and make the output nicer
105 105
106 106 Shared code for some the following if statement
107 107 """
108 108 self.log.debug("pyout: %s", msg.get('content', ''))
109 109 self._append_plain_text(self.output_sep, True)
110 110 self._append_html(self._make_out_prompt(prompt_number), True)
111 111 self._append_plain_text('\n', True)
112 112
113 113 def _handle_pyout(self, msg):
114 114 """ Overridden to handle rich data types, like SVG.
115 115 """
116 116 if not self._hidden and self._is_from_this_session(msg):
117 117 content = msg['content']
118 118 prompt_number = content.get('execution_count', 0)
119 119 data = content['data']
120 120 metadata = msg['content']['metadata']
121 121 if 'image/svg+xml' in data:
122 122 self._pre_image_append(msg, prompt_number)
123 123 self._append_svg(data['image/svg+xml'], True)
124 124 self._append_html(self.output_sep2, True)
125 125 elif 'image/png' in data:
126 126 self._pre_image_append(msg, prompt_number)
127 127 png = decodestring(data['image/png'].encode('ascii'))
128 128 self._append_png(png, True, metadata=metadata.get('image/png', None))
129 129 self._append_html(self.output_sep2, True)
130 130 elif 'image/jpeg' in data and self._jpg_supported:
131 131 self._pre_image_append(msg, prompt_number)
132 132 jpg = decodestring(data['image/jpeg'].encode('ascii'))
133 133 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
134 134 self._append_html(self.output_sep2, True)
135 135 else:
136 136 # Default back to the plain text representation.
137 137 return super(RichIPythonWidget, self)._handle_pyout(msg)
138 138
139 139 def _handle_display_data(self, msg):
140 140 """ Overridden to handle rich data types, like SVG.
141 141 """
142 142 if not self._hidden and self._is_from_this_session(msg):
143 143 source = msg['content']['source']
144 144 data = msg['content']['data']
145 145 metadata = msg['content']['metadata']
146 146 # Try to use the svg or html representations.
147 147 # FIXME: Is this the right ordering of things to try?
148 148 if 'image/svg+xml' in data:
149 149 self.log.debug("display: %s", msg.get('content', ''))
150 150 svg = data['image/svg+xml']
151 151 self._append_svg(svg, True)
152 152 elif 'image/png' in data:
153 153 self.log.debug("display: %s", msg.get('content', ''))
154 154 # PNG data is base64 encoded as it passes over the network
155 155 # in a JSON structure so we decode it.
156 156 png = decodestring(data['image/png'].encode('ascii'))
157 157 self._append_png(png, True, metadata=metadata.get('image/png', None))
158 158 elif 'image/jpeg' in data and self._jpg_supported:
159 159 self.log.debug("display: %s", msg.get('content', ''))
160 160 jpg = decodestring(data['image/jpeg'].encode('ascii'))
161 161 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
162 162 else:
163 163 # Default back to the plain text representation.
164 164 return super(RichIPythonWidget, self)._handle_display_data(msg)
165 165
166 166 #---------------------------------------------------------------------------
167 167 # 'RichIPythonWidget' protected interface
168 168 #---------------------------------------------------------------------------
169 169
170 170 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
171 171 """ Append raw JPG data to the widget."""
172 172 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
173 173
174 174 def _append_png(self, png, before_prompt=False, metadata=None):
175 175 """ Append raw PNG data to the widget.
176 176 """
177 177 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
178 178
179 179 def _append_svg(self, svg, before_prompt=False):
180 180 """ Append raw SVG data to the widget.
181 181 """
182 182 self._append_custom(self._insert_svg, svg, before_prompt)
183 183
184 184 def _add_image(self, image):
185 185 """ Adds the specified QImage to the document and returns a
186 186 QTextImageFormat that references it.
187 187 """
188 188 document = self._control.document()
189 189 name = str(image.cacheKey())
190 190 document.addResource(QtGui.QTextDocument.ImageResource,
191 191 QtCore.QUrl(name), image)
192 192 format = QtGui.QTextImageFormat()
193 193 format.setName(name)
194 194 return format
195 195
196 196 def _copy_image(self, name):
197 197 """ Copies the ImageResource with 'name' to the clipboard.
198 198 """
199 199 image = self._get_image(name)
200 200 QtGui.QApplication.clipboard().setImage(image)
201 201
202 202 def _get_image(self, name):
203 203 """ Returns the QImage stored as the ImageResource with 'name'.
204 204 """
205 205 document = self._control.document()
206 206 image = document.resource(QtGui.QTextDocument.ImageResource,
207 207 QtCore.QUrl(name))
208 208 return image
209 209
210 210 def _get_image_tag(self, match, path = None, format = "png"):
211 211 """ Return (X)HTML mark-up for the image-tag given by match.
212 212
213 213 Parameters
214 214 ----------
215 215 match : re.SRE_Match
216 216 A match to an HTML image tag as exported by Qt, with
217 217 match.group("Name") containing the matched image ID.
218 218
219 219 path : string|None, optional [default None]
220 220 If not None, specifies a path to which supporting files may be
221 221 written (e.g., for linked images). If None, all images are to be
222 222 included inline.
223 223
224 224 format : "png"|"svg"|"jpg", optional [default "png"]
225 225 Format for returned or referenced images.
226 226 """
227 227 if format in ("png","jpg"):
228 228 try:
229 229 image = self._get_image(match.group("name"))
230 230 except KeyError:
231 231 return "<b>Couldn't find image %s</b>" % match.group("name")
232 232
233 233 if path is not None:
234 234 if not os.path.exists(path):
235 235 os.mkdir(path)
236 236 relpath = os.path.basename(path)
237 237 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
238 238 "PNG"):
239 239 return '<img src="%s/qt_img%s.%s">' % (relpath,
240 240 match.group("name"),format)
241 241 else:
242 242 return "<b>Couldn't save image!</b>"
243 243 else:
244 244 ba = QtCore.QByteArray()
245 245 buffer_ = QtCore.QBuffer(ba)
246 246 buffer_.open(QtCore.QIODevice.WriteOnly)
247 247 image.save(buffer_, format.upper())
248 248 buffer_.close()
249 249 return '<img src="data:image/%s;base64,\n%s\n" />' % (
250 250 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
251 251
252 252 elif format == "svg":
253 253 try:
254 254 svg = str(self._name_to_svg_map[match.group("name")])
255 255 except KeyError:
256 256 if not self._svg_warning_displayed:
257 257 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
258 'Cannot convert a PNG to SVG. To fix this, add this '
258 'Cannot convert PNG images to SVG, export with PNG figures instead. '
259 'If you want to export matplotlib figures as SVG, add '
259 260 'to your ipython config:\n\n'
260 '\tc.InlineBackendConfig.figure_format = \'svg\'\n\n'
261 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
261 262 'And regenerate the figures.',
262 263 QtGui.QMessageBox.Ok)
263 264 self._svg_warning_displayed = True
264 return ("<b>Cannot convert a PNG to SVG.</b> "
265 "To fix this, add this to your config: "
266 "<span>c.InlineBackendConfig.figure_format = 'svg'</span> "
265 return ("<b>Cannot convert PNG images to SVG.</b> "
266 "You must export this session with PNG images. "
267 "If you want to export matplotlib figures as SVG, add to your config "
268 "<span>c.InlineBackend.figure_format = 'svg'</span> "
267 269 "and regenerate the figures.")
268 270
269 271 # Not currently checking path, because it's tricky to find a
270 272 # cross-browser way to embed external SVG images (e.g., via
271 273 # object or embed tags).
272 274
273 275 # Chop stand-alone header from matplotlib SVG
274 276 offset = svg.find("<svg")
275 277 assert(offset > -1)
276 278
277 279 return svg[offset:]
278 280
279 281 else:
280 282 return '<b>Unrecognized image format</b>'
281 283
282 284 def _insert_jpg(self, cursor, jpg, metadata=None):
283 285 """ Insert raw PNG data into the widget."""
284 286 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
285 287
286 288 def _insert_png(self, cursor, png, metadata=None):
287 289 """ Insert raw PNG data into the widget.
288 290 """
289 291 self._insert_img(cursor, png, 'png', metadata=metadata)
290 292
291 293 def _insert_img(self, cursor, img, fmt, metadata=None):
292 294 """ insert a raw image, jpg or png """
293 295 if metadata:
294 296 width = metadata.get('width', None)
295 297 height = metadata.get('height', None)
296 298 else:
297 299 width = height = None
298 300 try:
299 301 image = QtGui.QImage()
300 302 image.loadFromData(img, fmt.upper())
301 303 if width and height:
302 304 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
303 305 elif width and not height:
304 306 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
305 307 elif height and not width:
306 308 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
307 309 except ValueError:
308 310 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
309 311 else:
310 312 format = self._add_image(image)
311 313 cursor.insertBlock()
312 314 cursor.insertImage(format)
313 315 cursor.insertBlock()
314 316
315 317 def _insert_svg(self, cursor, svg):
316 318 """ Insert raw SVG data into the widet.
317 319 """
318 320 try:
319 321 image = svg_to_image(svg)
320 322 except ValueError:
321 323 self._insert_plain_text(cursor, 'Received invalid SVG data.')
322 324 else:
323 325 format = self._add_image(image)
324 326 self._name_to_svg_map[format.name()] = svg
325 327 cursor.insertBlock()
326 328 cursor.insertImage(format)
327 329 cursor.insertBlock()
328 330
329 331 def _save_image(self, name, format='PNG'):
330 332 """ Shows a save dialog for the ImageResource with 'name'.
331 333 """
332 334 dialog = QtGui.QFileDialog(self._control, 'Save Image')
333 335 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
334 336 dialog.setDefaultSuffix(format.lower())
335 337 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
336 338 if dialog.exec_():
337 339 filename = dialog.selectedFiles()[0]
338 340 image = self._get_image(name)
339 341 image.save(filename, format)
@@ -1,255 +1,238
1 1 """ Defines classes and functions for working with Qt's rich text system.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 # Standard library imports.
7 # Standard library imports
8 import io
8 9 import os
9 10 import re
10 11
11 # System library imports.
12 # System library imports
12 13 from IPython.external.qt import QtGui
13 14
14 15 # IPython imports
15 16 from IPython.utils import py3compat
16 17
17 18 #-----------------------------------------------------------------------------
18 19 # Constants
19 20 #-----------------------------------------------------------------------------
20 21
21 22 # A regular expression for an HTML paragraph with no content.
22 23 EMPTY_P_RE = re.compile(r'<p[^/>]*>\s*</p>')
23 24
24 25 # A regular expression for matching images in rich text HTML.
25 26 # Note that this is overly restrictive, but Qt's output is predictable...
26 27 IMG_RE = re.compile(r'<img src="(?P<name>[\d]+)" />')
27 28
28 29 #-----------------------------------------------------------------------------
29 30 # Classes
30 31 #-----------------------------------------------------------------------------
31 32
32 33 class HtmlExporter(object):
33 34 """ A stateful HTML exporter for a Q(Plain)TextEdit.
34 35
35 36 This class is designed for convenient user interaction.
36 37 """
37 38
38 39 def __init__(self, control):
39 40 """ Creates an HtmlExporter for the given Q(Plain)TextEdit.
40 41 """
41 42 assert isinstance(control, (QtGui.QPlainTextEdit, QtGui.QTextEdit))
42 43 self.control = control
43 44 self.filename = 'ipython.html'
44 45 self.image_tag = None
45 46 self.inline_png = None
46 47
47 48 def export(self):
48 49 """ Displays a dialog for exporting HTML generated by Qt's rich text
49 50 system.
50 51
51 52 Returns
52 53 -------
53 54 The name of the file that was saved, or None if no file was saved.
54 55 """
55 56 parent = self.control.window()
56 57 dialog = QtGui.QFileDialog(parent, 'Save as...')
57 58 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
58 59 filters = [
59 60 'HTML with PNG figures (*.html *.htm)',
60 61 'XHTML with inline SVG figures (*.xhtml *.xml)'
61 62 ]
62 63 dialog.setNameFilters(filters)
63 64 if self.filename:
64 65 dialog.selectFile(self.filename)
65 66 root,ext = os.path.splitext(self.filename)
66 67 if ext.lower() in ('.xml', '.xhtml'):
67 68 dialog.selectNameFilter(filters[-1])
68 69
69 70 if dialog.exec_():
70 71 self.filename = dialog.selectedFiles()[0]
71 72 choice = dialog.selectedNameFilter()
72 html = self.control.document().toHtml().encode('utf-8')
73 html = py3compat.cast_unicode(self.control.document().toHtml())
73 74
74 75 # Configure the exporter.
75 76 if choice.startswith('XHTML'):
76 77 exporter = export_xhtml
77 78 else:
78 79 # If there are PNGs, decide how to export them.
79 80 inline = self.inline_png
80 81 if inline is None and IMG_RE.search(html):
81 82 dialog = QtGui.QDialog(parent)
82 83 dialog.setWindowTitle('Save as...')
83 84 layout = QtGui.QVBoxLayout(dialog)
84 85 msg = "Exporting HTML with PNGs"
85 86 info = "Would you like inline PNGs (single large html " \
86 87 "file) or external image files?"
87 88 checkbox = QtGui.QCheckBox("&Don't ask again")
88 89 checkbox.setShortcut('D')
89 90 ib = QtGui.QPushButton("&Inline")
90 91 ib.setShortcut('I')
91 92 eb = QtGui.QPushButton("&External")
92 93 eb.setShortcut('E')
93 94 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
94 95 dialog.windowTitle(), msg)
95 96 box.setInformativeText(info)
96 97 box.addButton(ib, QtGui.QMessageBox.NoRole)
97 98 box.addButton(eb, QtGui.QMessageBox.YesRole)
98 99 layout.setSpacing(0)
99 100 layout.addWidget(box)
100 101 layout.addWidget(checkbox)
101 102 dialog.setLayout(layout)
102 103 dialog.show()
103 104 reply = box.exec_()
104 105 dialog.hide()
105 106 inline = (reply == 0)
106 107 if checkbox.checkState():
107 108 # Don't ask anymore; always use this choice.
108 109 self.inline_png = inline
109 110 exporter = lambda h, f, i: export_html(h, f, i, inline)
110 111
111 112 # Perform the export!
112 113 try:
113 114 return exporter(html, self.filename, self.image_tag)
114 115 except Exception as e:
115 116 msg = "Error exporting HTML to %s\n" % self.filename + str(e)
116 117 reply = QtGui.QMessageBox.warning(parent, 'Error', msg,
117 118 QtGui.QMessageBox.Ok, QtGui.QMessageBox.Ok)
118 119
119 120 return None
120 121
121 122 #-----------------------------------------------------------------------------
122 123 # Functions
123 124 #-----------------------------------------------------------------------------
124 125
125 126 def export_html(html, filename, image_tag = None, inline = True):
126 127 """ Export the contents of the ConsoleWidget as HTML.
127 128
128 129 Parameters:
129 130 -----------
130 html : str,
131 A utf-8 encoded Python string containing the Qt HTML to export.
131 html : unicode,
132 A Python unicode string containing the Qt HTML to export.
132 133
133 134 filename : str
134 135 The file to be saved.
135 136
136 137 image_tag : callable, optional (default None)
137 138 Used to convert images. See ``default_image_tag()`` for information.
138 139
139 140 inline : bool, optional [default True]
140 141 If True, include images as inline PNGs. Otherwise, include them as
141 142 links to external PNG files, mimicking web browsers' "Web Page,
142 143 Complete" behavior.
143 144 """
144 145 if image_tag is None:
145 146 image_tag = default_image_tag
146 else:
147 image_tag = ensure_utf8(image_tag)
148 147
149 148 if inline:
150 149 path = None
151 150 else:
152 151 root,ext = os.path.splitext(filename)
153 152 path = root + "_files"
154 153 if os.path.isfile(path):
155 154 raise OSError("%s exists, but is not a directory." % path)
156 155
157 with open(filename, 'w') as f:
156 with io.open(filename, 'w', encoding='utf-8') as f:
158 157 html = fix_html(html)
159 158 f.write(IMG_RE.sub(lambda x: image_tag(x, path = path, format = "png"),
160 159 html))
161 160
162 161
163 162 def export_xhtml(html, filename, image_tag=None):
164 163 """ Export the contents of the ConsoleWidget as XHTML with inline SVGs.
165 164
166 165 Parameters:
167 166 -----------
168 html : str,
169 A utf-8 encoded Python string containing the Qt HTML to export.
167 html : unicode,
168 A Python unicode string containing the Qt HTML to export.
170 169
171 170 filename : str
172 171 The file to be saved.
173 172
174 173 image_tag : callable, optional (default None)
175 174 Used to convert images. See ``default_image_tag()`` for information.
176 175 """
177 176 if image_tag is None:
178 177 image_tag = default_image_tag
179 else:
180 image_tag = ensure_utf8(image_tag)
181 178
182 with open(filename, 'w') as f:
179 with io.open(filename, 'w', encoding='utf-8') as f:
183 180 # Hack to make xhtml header -- note that we are not doing any check for
184 181 # valid XML.
185 182 offset = html.find("<html>")
186 183 assert offset > -1, 'Invalid HTML string: no <html> tag.'
187 html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+
184 html = (u'<html xmlns="http://www.w3.org/1999/xhtml">\n'+
188 185 html[offset+6:])
189 186
190 187 html = fix_html(html)
191 188 f.write(IMG_RE.sub(lambda x: image_tag(x, path = None, format = "svg"),
192 189 html))
193 190
194 191
195 192 def default_image_tag(match, path = None, format = "png"):
196 193 """ Return (X)HTML mark-up for the image-tag given by match.
197 194
198 195 This default implementation merely removes the image, and exists mostly
199 196 for documentation purposes. More information than is present in the Qt
200 197 HTML is required to supply the images.
201 198
202 199 Parameters
203 200 ----------
204 201 match : re.SRE_Match
205 202 A match to an HTML image tag as exported by Qt, with match.group("Name")
206 203 containing the matched image ID.
207 204
208 205 path : string|None, optional [default None]
209 206 If not None, specifies a path to which supporting files may be written
210 207 (e.g., for linked images). If None, all images are to be included
211 208 inline.
212 209
213 210 format : "png"|"svg", optional [default "png"]
214 211 Format for returned or referenced images.
215 212 """
216 return ''
217
218
219 def ensure_utf8(image_tag):
220 """wrapper for ensuring image_tag returns utf8-encoded str on Python 2"""
221 if py3compat.PY3:
222 # nothing to do on Python 3
223 return image_tag
224
225 def utf8_image_tag(*args, **kwargs):
226 s = image_tag(*args, **kwargs)
227 if isinstance(s, unicode):
228 s = s.encode('utf8')
229 return s
230 return utf8_image_tag
213 return u''
231 214
232 215
233 216 def fix_html(html):
234 217 """ Transforms a Qt-generated HTML string into a standards-compliant one.
235 218
236 219 Parameters:
237 220 -----------
238 html : str,
239 A utf-8 encoded Python string containing the Qt HTML.
221 html : unicode,
222 A Python unicode string containing the Qt HTML.
240 223 """
241 224 # A UTF-8 declaration is needed for proper rendering of some characters
242 225 # (e.g., indented commands) when viewing exported HTML on a local system
243 226 # (i.e., without seeing an encoding declaration in an HTTP header).
244 227 # C.f. http://www.w3.org/International/O-charset for details.
245 228 offset = html.find('<head>')
246 229 if offset > -1:
247 230 html = (html[:offset+6]+
248 231 '\n<meta http-equiv="Content-Type" '+
249 232 'content="text/html; charset=utf-8" />\n'+
250 233 html[offset+6:])
251 234
252 235 # Replace empty paragraphs tags with line breaks.
253 236 html = re.sub(EMPTY_P_RE, '<br/>', html)
254 237
255 238 return html
General Comments 0
You need to be logged in to leave comments. Login now