##// END OF EJS Templates
encode image_tag as utf8 in [x]html export...
MinRK -
Show More
@@ -1,235 +1,255 b''
1 """ Defines classes and functions for working with Qt's rich text system.
1 """ Defines classes and functions for working with Qt's rich text system.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Imports
4 # Imports
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6
6
7 # Standard library imports.
7 # Standard library imports.
8 import os
8 import os
9 import re
9 import re
10
10
11 # System library imports.
11 # System library imports.
12 from IPython.external.qt import QtGui
12 from IPython.external.qt import QtGui
13
13
14 # IPython imports
15 from IPython.utils import py3compat
16
14 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
15 # Constants
18 # Constants
16 #-----------------------------------------------------------------------------
19 #-----------------------------------------------------------------------------
17
20
18 # A regular expression for an HTML paragraph with no content.
21 # A regular expression for an HTML paragraph with no content.
19 EMPTY_P_RE = re.compile(r'<p[^/>]*>\s*</p>')
22 EMPTY_P_RE = re.compile(r'<p[^/>]*>\s*</p>')
20
23
21 # A regular expression for matching images in rich text HTML.
24 # A regular expression for matching images in rich text HTML.
22 # Note that this is overly restrictive, but Qt's output is predictable...
25 # Note that this is overly restrictive, but Qt's output is predictable...
23 IMG_RE = re.compile(r'<img src="(?P<name>[\d]+)" />')
26 IMG_RE = re.compile(r'<img src="(?P<name>[\d]+)" />')
24
27
25 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
26 # Classes
29 # Classes
27 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
28
31
29 class HtmlExporter(object):
32 class HtmlExporter(object):
30 """ A stateful HTML exporter for a Q(Plain)TextEdit.
33 """ A stateful HTML exporter for a Q(Plain)TextEdit.
31
34
32 This class is designed for convenient user interaction.
35 This class is designed for convenient user interaction.
33 """
36 """
34
37
35 def __init__(self, control):
38 def __init__(self, control):
36 """ Creates an HtmlExporter for the given Q(Plain)TextEdit.
39 """ Creates an HtmlExporter for the given Q(Plain)TextEdit.
37 """
40 """
38 assert isinstance(control, (QtGui.QPlainTextEdit, QtGui.QTextEdit))
41 assert isinstance(control, (QtGui.QPlainTextEdit, QtGui.QTextEdit))
39 self.control = control
42 self.control = control
40 self.filename = 'ipython.html'
43 self.filename = 'ipython.html'
41 self.image_tag = None
44 self.image_tag = None
42 self.inline_png = None
45 self.inline_png = None
43
46
44 def export(self):
47 def export(self):
45 """ Displays a dialog for exporting HTML generated by Qt's rich text
48 """ Displays a dialog for exporting HTML generated by Qt's rich text
46 system.
49 system.
47
50
48 Returns
51 Returns
49 -------
52 -------
50 The name of the file that was saved, or None if no file was saved.
53 The name of the file that was saved, or None if no file was saved.
51 """
54 """
52 parent = self.control.window()
55 parent = self.control.window()
53 dialog = QtGui.QFileDialog(parent, 'Save as...')
56 dialog = QtGui.QFileDialog(parent, 'Save as...')
54 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
57 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
55 filters = [
58 filters = [
56 'HTML with PNG figures (*.html *.htm)',
59 'HTML with PNG figures (*.html *.htm)',
57 'XHTML with inline SVG figures (*.xhtml *.xml)'
60 'XHTML with inline SVG figures (*.xhtml *.xml)'
58 ]
61 ]
59 dialog.setNameFilters(filters)
62 dialog.setNameFilters(filters)
60 if self.filename:
63 if self.filename:
61 dialog.selectFile(self.filename)
64 dialog.selectFile(self.filename)
62 root,ext = os.path.splitext(self.filename)
65 root,ext = os.path.splitext(self.filename)
63 if ext.lower() in ('.xml', '.xhtml'):
66 if ext.lower() in ('.xml', '.xhtml'):
64 dialog.selectNameFilter(filters[-1])
67 dialog.selectNameFilter(filters[-1])
65
68
66 if dialog.exec_():
69 if dialog.exec_():
67 self.filename = dialog.selectedFiles()[0]
70 self.filename = dialog.selectedFiles()[0]
68 choice = dialog.selectedNameFilter()
71 choice = dialog.selectedNameFilter()
69 html = self.control.document().toHtml().encode('utf-8')
72 html = self.control.document().toHtml().encode('utf-8')
70
73
71 # Configure the exporter.
74 # Configure the exporter.
72 if choice.startswith('XHTML'):
75 if choice.startswith('XHTML'):
73 exporter = export_xhtml
76 exporter = export_xhtml
74 else:
77 else:
75 # If there are PNGs, decide how to export them.
78 # If there are PNGs, decide how to export them.
76 inline = self.inline_png
79 inline = self.inline_png
77 if inline is None and IMG_RE.search(html):
80 if inline is None and IMG_RE.search(html):
78 dialog = QtGui.QDialog(parent)
81 dialog = QtGui.QDialog(parent)
79 dialog.setWindowTitle('Save as...')
82 dialog.setWindowTitle('Save as...')
80 layout = QtGui.QVBoxLayout(dialog)
83 layout = QtGui.QVBoxLayout(dialog)
81 msg = "Exporting HTML with PNGs"
84 msg = "Exporting HTML with PNGs"
82 info = "Would you like inline PNGs (single large html " \
85 info = "Would you like inline PNGs (single large html " \
83 "file) or external image files?"
86 "file) or external image files?"
84 checkbox = QtGui.QCheckBox("&Don't ask again")
87 checkbox = QtGui.QCheckBox("&Don't ask again")
85 checkbox.setShortcut('D')
88 checkbox.setShortcut('D')
86 ib = QtGui.QPushButton("&Inline")
89 ib = QtGui.QPushButton("&Inline")
87 ib.setShortcut('I')
90 ib.setShortcut('I')
88 eb = QtGui.QPushButton("&External")
91 eb = QtGui.QPushButton("&External")
89 eb.setShortcut('E')
92 eb.setShortcut('E')
90 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
93 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
91 dialog.windowTitle(), msg)
94 dialog.windowTitle(), msg)
92 box.setInformativeText(info)
95 box.setInformativeText(info)
93 box.addButton(ib, QtGui.QMessageBox.NoRole)
96 box.addButton(ib, QtGui.QMessageBox.NoRole)
94 box.addButton(eb, QtGui.QMessageBox.YesRole)
97 box.addButton(eb, QtGui.QMessageBox.YesRole)
95 box.setDefaultButton(ib)
96 layout.setSpacing(0)
98 layout.setSpacing(0)
97 layout.addWidget(box)
99 layout.addWidget(box)
98 layout.addWidget(checkbox)
100 layout.addWidget(checkbox)
99 dialog.setLayout(layout)
101 dialog.setLayout(layout)
100 dialog.show()
102 dialog.show()
101 reply = box.exec_()
103 reply = box.exec_()
102 dialog.hide()
104 dialog.hide()
103 inline = (reply == 0)
105 inline = (reply == 0)
104 if checkbox.checkState():
106 if checkbox.checkState():
105 # Don't ask anymore; always use this choice.
107 # Don't ask anymore; always use this choice.
106 self.inline_png = inline
108 self.inline_png = inline
107 exporter = lambda h, f, i: export_html(h, f, i, inline)
109 exporter = lambda h, f, i: export_html(h, f, i, inline)
108
110
109 # Perform the export!
111 # Perform the export!
110 try:
112 try:
111 return exporter(html, self.filename, self.image_tag)
113 return exporter(html, self.filename, self.image_tag)
112 except Exception, e:
114 except Exception, e:
113 msg = "Error exporting HTML to %s\n" % self.filename + str(e)
115 msg = "Error exporting HTML to %s\n" % self.filename + str(e)
114 reply = QtGui.QMessageBox.warning(parent, 'Error', msg,
116 reply = QtGui.QMessageBox.warning(parent, 'Error', msg,
115 QtGui.QMessageBox.Ok, QtGui.QMessageBox.Ok)
117 QtGui.QMessageBox.Ok, QtGui.QMessageBox.Ok)
116
118
117 return None
119 return None
118
120
119 #-----------------------------------------------------------------------------
121 #-----------------------------------------------------------------------------
120 # Functions
122 # Functions
121 #-----------------------------------------------------------------------------
123 #-----------------------------------------------------------------------------
122
124
123 def export_html(html, filename, image_tag = None, inline = True):
125 def export_html(html, filename, image_tag = None, inline = True):
124 """ Export the contents of the ConsoleWidget as HTML.
126 """ Export the contents of the ConsoleWidget as HTML.
125
127
126 Parameters:
128 Parameters:
127 -----------
129 -----------
128 html : str,
130 html : str,
129 A utf-8 encoded Python string containing the Qt HTML to export.
131 A utf-8 encoded Python string containing the Qt HTML to export.
130
132
131 filename : str
133 filename : str
132 The file to be saved.
134 The file to be saved.
133
135
134 image_tag : callable, optional (default None)
136 image_tag : callable, optional (default None)
135 Used to convert images. See ``default_image_tag()`` for information.
137 Used to convert images. See ``default_image_tag()`` for information.
136
138
137 inline : bool, optional [default True]
139 inline : bool, optional [default True]
138 If True, include images as inline PNGs. Otherwise, include them as
140 If True, include images as inline PNGs. Otherwise, include them as
139 links to external PNG files, mimicking web browsers' "Web Page,
141 links to external PNG files, mimicking web browsers' "Web Page,
140 Complete" behavior.
142 Complete" behavior.
141 """
143 """
142 if image_tag is None:
144 if image_tag is None:
143 image_tag = default_image_tag
145 image_tag = default_image_tag
146 else:
147 image_tag = ensure_utf8(image_tag)
144
148
145 if inline:
149 if inline:
146 path = None
150 path = None
147 else:
151 else:
148 root,ext = os.path.splitext(filename)
152 root,ext = os.path.splitext(filename)
149 path = root + "_files"
153 path = root + "_files"
150 if os.path.isfile(path):
154 if os.path.isfile(path):
151 raise OSError("%s exists, but is not a directory." % path)
155 raise OSError("%s exists, but is not a directory." % path)
152
156
153 with open(filename, 'w') as f:
157 with open(filename, 'w') as f:
154 html = fix_html(html)
158 html = fix_html(html)
155 f.write(IMG_RE.sub(lambda x: image_tag(x, path = path, format = "png"),
159 f.write(IMG_RE.sub(lambda x: image_tag(x, path = path, format = "png"),
156 html))
160 html))
157
161
158
162
159 def export_xhtml(html, filename, image_tag=None):
163 def export_xhtml(html, filename, image_tag=None):
160 """ Export the contents of the ConsoleWidget as XHTML with inline SVGs.
164 """ Export the contents of the ConsoleWidget as XHTML with inline SVGs.
161
165
162 Parameters:
166 Parameters:
163 -----------
167 -----------
164 html : str,
168 html : str,
165 A utf-8 encoded Python string containing the Qt HTML to export.
169 A utf-8 encoded Python string containing the Qt HTML to export.
166
170
167 filename : str
171 filename : str
168 The file to be saved.
172 The file to be saved.
169
173
170 image_tag : callable, optional (default None)
174 image_tag : callable, optional (default None)
171 Used to convert images. See ``default_image_tag()`` for information.
175 Used to convert images. See ``default_image_tag()`` for information.
172 """
176 """
173 if image_tag is None:
177 if image_tag is None:
174 image_tag = default_image_tag
178 image_tag = default_image_tag
179 else:
180 image_tag = ensure_utf8(image_tag)
175
181
176 with open(filename, 'w') as f:
182 with open(filename, 'w') as f:
177 # Hack to make xhtml header -- note that we are not doing any check for
183 # Hack to make xhtml header -- note that we are not doing any check for
178 # valid XML.
184 # valid XML.
179 offset = html.find("<html>")
185 offset = html.find("<html>")
180 assert offset > -1, 'Invalid HTML string: no <html> tag.'
186 assert offset > -1, 'Invalid HTML string: no <html> tag.'
181 html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+
187 html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+
182 html[offset+6:])
188 html[offset+6:])
183
189
184 html = fix_html(html)
190 html = fix_html(html)
185 f.write(IMG_RE.sub(lambda x: image_tag(x, path = None, format = "svg"),
191 f.write(IMG_RE.sub(lambda x: image_tag(x, path = None, format = "svg"),
186 html))
192 html))
187
193
188
194
189 def default_image_tag(match, path = None, format = "png"):
195 def default_image_tag(match, path = None, format = "png"):
190 """ Return (X)HTML mark-up for the image-tag given by match.
196 """ Return (X)HTML mark-up for the image-tag given by match.
191
197
192 This default implementation merely removes the image, and exists mostly
198 This default implementation merely removes the image, and exists mostly
193 for documentation purposes. More information than is present in the Qt
199 for documentation purposes. More information than is present in the Qt
194 HTML is required to supply the images.
200 HTML is required to supply the images.
195
201
196 Parameters
202 Parameters
197 ----------
203 ----------
198 match : re.SRE_Match
204 match : re.SRE_Match
199 A match to an HTML image tag as exported by Qt, with match.group("Name")
205 A match to an HTML image tag as exported by Qt, with match.group("Name")
200 containing the matched image ID.
206 containing the matched image ID.
201
207
202 path : string|None, optional [default None]
208 path : string|None, optional [default None]
203 If not None, specifies a path to which supporting files may be written
209 If not None, specifies a path to which supporting files may be written
204 (e.g., for linked images). If None, all images are to be included
210 (e.g., for linked images). If None, all images are to be included
205 inline.
211 inline.
206
212
207 format : "png"|"svg", optional [default "png"]
213 format : "png"|"svg", optional [default "png"]
208 Format for returned or referenced images.
214 Format for returned or referenced images.
209 """
215 """
210 return ''
216 return ''
211
217
212
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
231
232
213 def fix_html(html):
233 def fix_html(html):
214 """ Transforms a Qt-generated HTML string into a standards-compliant one.
234 """ Transforms a Qt-generated HTML string into a standards-compliant one.
215
235
216 Parameters:
236 Parameters:
217 -----------
237 -----------
218 html : str,
238 html : str,
219 A utf-8 encoded Python string containing the Qt HTML.
239 A utf-8 encoded Python string containing the Qt HTML.
220 """
240 """
221 # A UTF-8 declaration is needed for proper rendering of some characters
241 # A UTF-8 declaration is needed for proper rendering of some characters
222 # (e.g., indented commands) when viewing exported HTML on a local system
242 # (e.g., indented commands) when viewing exported HTML on a local system
223 # (i.e., without seeing an encoding declaration in an HTTP header).
243 # (i.e., without seeing an encoding declaration in an HTTP header).
224 # C.f. http://www.w3.org/International/O-charset for details.
244 # C.f. http://www.w3.org/International/O-charset for details.
225 offset = html.find('<head>')
245 offset = html.find('<head>')
226 if offset > -1:
246 if offset > -1:
227 html = (html[:offset+6]+
247 html = (html[:offset+6]+
228 '\n<meta http-equiv="Content-Type" '+
248 '\n<meta http-equiv="Content-Type" '+
229 'content="text/html; charset=utf-8" />\n'+
249 'content="text/html; charset=utf-8" />\n'+
230 html[offset+6:])
250 html[offset+6:])
231
251
232 # Replace empty paragraphs tags with line breaks.
252 # Replace empty paragraphs tags with line breaks.
233 html = re.sub(EMPTY_P_RE, '<br/>', html)
253 html = re.sub(EMPTY_P_RE, '<br/>', html)
234
254
235 return html
255 return html
General Comments 0
You need to be logged in to leave comments. Login now