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