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