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