src/eric7/UI/Previewers/PreviewerHTML.py

branch
eric7
changeset 9221
bf71ee032bb4
parent 9209
b99e7fd55fd3
child 9268
0d414630a28e
equal deleted inserted replaced
9220:e9e7eca7efee 9221:bf71ee032bb4
17 import contextlib 17 import contextlib
18 18
19 from PyQt6.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QThread 19 from PyQt6.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QThread
20 from PyQt6.QtGui import QCursor 20 from PyQt6.QtGui import QCursor
21 from PyQt6.QtWidgets import ( 21 from PyQt6.QtWidgets import (
22 QWidget, QVBoxLayout, QLabel, QCheckBox, QSizePolicy, QToolTip 22 QWidget,
23 QVBoxLayout,
24 QLabel,
25 QCheckBox,
26 QSizePolicy,
27 QToolTip,
23 ) 28 )
24 29
25 from EricWidgets.EricApplication import ericApp 30 from EricWidgets.EricApplication import ericApp
26 31
27 import Utilities 32 import Utilities
30 35
31 class PreviewerHTML(QWidget): 36 class PreviewerHTML(QWidget):
32 """ 37 """
33 Class implementing a previewer widget for HTML, Markdown and ReST files. 38 Class implementing a previewer widget for HTML, Markdown and ReST files.
34 """ 39 """
40
35 def __init__(self, parent=None): 41 def __init__(self, parent=None):
36 """ 42 """
37 Constructor 43 Constructor
38 44
39 @param parent reference to the parent widget (QWidget) 45 @param parent reference to the parent widget (QWidget)
40 """ 46 """
41 super().__init__(parent) 47 super().__init__(parent)
42 48
43 self.__layout = QVBoxLayout(self) 49 self.__layout = QVBoxLayout(self)
44 50
45 self.titleLabel = QLabel(self) 51 self.titleLabel = QLabel(self)
46 self.titleLabel.setWordWrap(True) 52 self.titleLabel.setWordWrap(True)
47 self.titleLabel.setTextInteractionFlags( 53 self.titleLabel.setTextInteractionFlags(
48 Qt.TextInteractionFlag.NoTextInteraction) 54 Qt.TextInteractionFlag.NoTextInteraction
55 )
49 self.__layout.addWidget(self.titleLabel) 56 self.__layout.addWidget(self.titleLabel)
50 57
51 self.__previewAvailable = True 58 self.__previewAvailable = True
52 59
53 try: 60 try:
54 from PyQt6.QtWebEngineWidgets import QWebEngineView 61 from PyQt6.QtWebEngineWidgets import QWebEngineView
62
55 self.previewView = QWebEngineView(self) 63 self.previewView = QWebEngineView(self)
56 self.previewView.page().linkHovered.connect(self.__showLink) 64 self.previewView.page().linkHovered.connect(self.__showLink)
57 except ImportError: 65 except ImportError:
58 self.__previewAvailable = False 66 self.__previewAvailable = False
59 self.titleLabel.setText(self.tr( 67 self.titleLabel.setText(
60 "<b>HTML Preview is not available!<br/>" 68 self.tr(
61 "Install PyQt6-WebEngine.</b>")) 69 "<b>HTML Preview is not available!<br/>"
70 "Install PyQt6-WebEngine.</b>"
71 )
72 )
62 self.titleLabel.setAlignment(Qt.AlignmentFlag.AlignHCenter) 73 self.titleLabel.setAlignment(Qt.AlignmentFlag.AlignHCenter)
63 self.__layout.addStretch() 74 self.__layout.addStretch()
64 return 75 return
65 76
66 sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, 77 sizePolicy = QSizePolicy(
67 QSizePolicy.Policy.Expanding) 78 QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding
79 )
68 sizePolicy.setHorizontalStretch(0) 80 sizePolicy.setHorizontalStretch(0)
69 sizePolicy.setVerticalStretch(0) 81 sizePolicy.setVerticalStretch(0)
70 sizePolicy.setHeightForWidth( 82 sizePolicy.setHeightForWidth(self.previewView.sizePolicy().hasHeightForWidth())
71 self.previewView.sizePolicy().hasHeightForWidth())
72 self.previewView.setSizePolicy(sizePolicy) 83 self.previewView.setSizePolicy(sizePolicy)
73 self.previewView.setContextMenuPolicy( 84 self.previewView.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
74 Qt.ContextMenuPolicy.NoContextMenu)
75 self.previewView.setUrl(QUrl("about:blank")) 85 self.previewView.setUrl(QUrl("about:blank"))
76 self.__layout.addWidget(self.previewView) 86 self.__layout.addWidget(self.previewView)
77 87
78 self.jsCheckBox = QCheckBox(self.tr("Enable JavaScript"), self) 88 self.jsCheckBox = QCheckBox(self.tr("Enable JavaScript"), self)
79 self.jsCheckBox.setToolTip(self.tr( 89 self.jsCheckBox.setToolTip(
80 "Select to enable JavaScript for HTML previews")) 90 self.tr("Select to enable JavaScript for HTML previews")
91 )
81 self.__layout.addWidget(self.jsCheckBox) 92 self.__layout.addWidget(self.jsCheckBox)
82 93
83 self.ssiCheckBox = QCheckBox(self.tr("Enable Server Side Includes"), 94 self.ssiCheckBox = QCheckBox(self.tr("Enable Server Side Includes"), self)
84 self) 95 self.ssiCheckBox.setToolTip(
85 self.ssiCheckBox.setToolTip(self.tr( 96 self.tr("Select to enable support for Server Side Includes")
86 "Select to enable support for Server Side Includes")) 97 )
87 self.__layout.addWidget(self.ssiCheckBox) 98 self.__layout.addWidget(self.ssiCheckBox)
88 99
89 self.jsCheckBox.clicked[bool].connect(self.on_jsCheckBox_clicked) 100 self.jsCheckBox.clicked[bool].connect(self.on_jsCheckBox_clicked)
90 self.ssiCheckBox.clicked[bool].connect(self.on_ssiCheckBox_clicked) 101 self.ssiCheckBox.clicked[bool].connect(self.on_ssiCheckBox_clicked)
91 self.previewView.titleChanged.connect(self.on_previewView_titleChanged) 102 self.previewView.titleChanged.connect(self.on_previewView_titleChanged)
92 103
93 self.jsCheckBox.setChecked( 104 self.jsCheckBox.setChecked(Preferences.getUI("ShowFilePreviewJS"))
94 Preferences.getUI("ShowFilePreviewJS")) 105 self.ssiCheckBox.setChecked(Preferences.getUI("ShowFilePreviewSSI"))
95 self.ssiCheckBox.setChecked( 106
96 Preferences.getUI("ShowFilePreviewSSI"))
97
98 self.__scrollBarPositions = {} 107 self.__scrollBarPositions = {}
99 self.__vScrollBarAtEnd = {} 108 self.__vScrollBarAtEnd = {}
100 self.__hScrollBarAtEnd = {} 109 self.__hScrollBarAtEnd = {}
101 110
102 self.__processingThread = PreviewProcessingThread() 111 self.__processingThread = PreviewProcessingThread()
103 self.__processingThread.htmlReady.connect(self.__setHtml) 112 self.__processingThread.htmlReady.connect(self.__setHtml)
104 113
105 self.__previewedPath = None 114 self.__previewedPath = None
106 self.__previewedEditor = None 115 self.__previewedEditor = None
107 116
108 def shutdown(self): 117 def shutdown(self):
109 """ 118 """
110 Public method to perform shutdown actions. 119 Public method to perform shutdown actions.
111 """ 120 """
112 if self.__previewAvailable: 121 if self.__previewAvailable:
113 self.__processingThread.wait() 122 self.__processingThread.wait()
114 123
115 @pyqtSlot(bool) 124 @pyqtSlot(bool)
116 def on_jsCheckBox_clicked(self, checked): 125 def on_jsCheckBox_clicked(self, checked):
117 """ 126 """
118 Private slot to enable/disable JavaScript. 127 Private slot to enable/disable JavaScript.
119 128
120 @param checked state of the checkbox (boolean) 129 @param checked state of the checkbox (boolean)
121 """ 130 """
122 Preferences.setUI("ShowFilePreviewJS", checked) 131 Preferences.setUI("ShowFilePreviewJS", checked)
123 self.__setJavaScriptEnabled(checked) 132 self.__setJavaScriptEnabled(checked)
124 133
125 def __setJavaScriptEnabled(self, enable): 134 def __setJavaScriptEnabled(self, enable):
126 """ 135 """
127 Private method to enable/disable JavaScript. 136 Private method to enable/disable JavaScript.
128 137
129 @param enable flag indicating the enable state (boolean) 138 @param enable flag indicating the enable state (boolean)
130 """ 139 """
131 self.jsCheckBox.setChecked(enable) 140 self.jsCheckBox.setChecked(enable)
132 141
133 settings = self.previewView.settings() 142 settings = self.previewView.settings()
134 settings.setAttribute(settings.JavascriptEnabled, enable) 143 settings.setAttribute(settings.JavascriptEnabled, enable)
135 144
136 self.processEditor() 145 self.processEditor()
137 146
138 @pyqtSlot(bool) 147 @pyqtSlot(bool)
139 def on_ssiCheckBox_clicked(self, checked): 148 def on_ssiCheckBox_clicked(self, checked):
140 """ 149 """
141 Private slot to enable/disable SSI. 150 Private slot to enable/disable SSI.
142 151
143 @param checked state of the checkbox (boolean) 152 @param checked state of the checkbox (boolean)
144 """ 153 """
145 Preferences.setUI("ShowFilePreviewSSI", checked) 154 Preferences.setUI("ShowFilePreviewSSI", checked)
146 self.processEditor() 155 self.processEditor()
147 156
148 @pyqtSlot(str) 157 @pyqtSlot(str)
149 def __showLink(self, urlStr): 158 def __showLink(self, urlStr):
150 """ 159 """
151 Private slot to show the hovered link in a tooltip. 160 Private slot to show the hovered link in a tooltip.
152 161
153 @param urlStr hovered URL 162 @param urlStr hovered URL
154 @type str 163 @type str
155 """ 164 """
156 QToolTip.showText(QCursor.pos(), urlStr, self.previewView) 165 QToolTip.showText(QCursor.pos(), urlStr, self.previewView)
157 166
158 def processEditor(self, editor=None): 167 def processEditor(self, editor=None):
159 """ 168 """
160 Public slot to process an editor's text. 169 Public slot to process an editor's text.
161 170
162 @param editor editor to be processed (Editor) 171 @param editor editor to be processed (Editor)
163 """ 172 """
164 if not self.__previewAvailable: 173 if not self.__previewAvailable:
165 return 174 return
166 175
167 if editor is None: 176 if editor is None:
168 editor = self.__previewedEditor 177 editor = self.__previewedEditor
169 else: 178 else:
170 self.__previewedEditor = editor 179 self.__previewedEditor = editor
171 180
172 if editor is not None: 181 if editor is not None:
173 fn = editor.getFileName() 182 fn = editor.getFileName()
174 183
175 if fn: 184 if fn:
176 extension = os.path.normcase(os.path.splitext(fn)[1][1:]) 185 extension = os.path.normcase(os.path.splitext(fn)[1][1:])
177 else: 186 else:
178 extension = "" 187 extension = ""
179 if ( 188 if (
180 extension in Preferences.getEditor( 189 extension in Preferences.getEditor("PreviewHtmlFileNameExtensions")
181 "PreviewHtmlFileNameExtensions") or 190 or editor.getLanguage() == "HTML"
182 editor.getLanguage() == "HTML"
183 ): 191 ):
184 language = "HTML" 192 language = "HTML"
185 elif ( 193 elif (
186 extension in Preferences.getEditor( 194 extension in Preferences.getEditor("PreviewMarkdownFileNameExtensions")
187 "PreviewMarkdownFileNameExtensions") or 195 or editor.getLanguage().lower() == "markdown"
188 editor.getLanguage().lower() == "markdown"
189 ): 196 ):
190 language = "Markdown" 197 language = "Markdown"
191 elif ( 198 elif (
192 extension in Preferences.getEditor( 199 extension in Preferences.getEditor("PreviewRestFileNameExtensions")
193 "PreviewRestFileNameExtensions") or 200 or editor.getLanguage().lower() == "restructuredtext"
194 editor.getLanguage().lower() == "restructuredtext"
195 ): 201 ):
196 language = "ReST" 202 language = "ReST"
197 else: 203 else:
198 self.__setHtml(fn, self.tr( 204 self.__setHtml(
199 "<p>No preview available for this type of file.</p>")) 205 fn, self.tr("<p>No preview available for this type of file.</p>")
206 )
200 return 207 return
201 208
202 if fn: 209 if fn:
203 rootPath = os.path.dirname(os.path.abspath(fn)) 210 rootPath = os.path.dirname(os.path.abspath(fn))
204 else: 211 else:
205 rootPath = "" 212 rootPath = ""
206 213
207 if bool(editor.text()): 214 if bool(editor.text()):
208 self.__processingThread.process( 215 self.__processingThread.process(
209 fn, language, editor.text(), 216 fn,
210 self.ssiCheckBox.isChecked(), rootPath, 217 language,
218 editor.text(),
219 self.ssiCheckBox.isChecked(),
220 rootPath,
211 Preferences.getEditor("PreviewRestUseSphinx"), 221 Preferences.getEditor("PreviewRestUseSphinx"),
212 Preferences.getEditor("PreviewMarkdownNLtoBR"), 222 Preferences.getEditor("PreviewMarkdownNLtoBR"),
213 Preferences.getEditor( 223 Preferences.getEditor("PreviewMarkdownUsePyMdownExtensions"),
214 "PreviewMarkdownUsePyMdownExtensions"),
215 Preferences.getEditor("PreviewMarkdownHTMLFormat"), 224 Preferences.getEditor("PreviewMarkdownHTMLFormat"),
216 Preferences.getEditor("PreviewRestDocutilsHTMLFormat")) 225 Preferences.getEditor("PreviewRestDocutilsHTMLFormat"),
226 )
217 227
218 def __setHtml(self, filePath, html, rootPath): 228 def __setHtml(self, filePath, html, rootPath):
219 """ 229 """
220 Private method to set the HTML to the view and restore the scroll bars 230 Private method to set the HTML to the view and restore the scroll bars
221 positions. 231 positions.
222 232
223 @param filePath file path of the previewed editor 233 @param filePath file path of the previewed editor
224 @type str 234 @type str
225 @param html processed HTML text ready to be shown 235 @param html processed HTML text ready to be shown
226 @type str 236 @type str
227 @param rootPath path of the web site root 237 @param rootPath path of the web site root
228 @type str 238 @type str
229 """ 239 """
230 self.__previewedPath = Utilities.normcasepath( 240 self.__previewedPath = Utilities.normcasepath(
231 Utilities.fromNativeSeparators(filePath)) 241 Utilities.fromNativeSeparators(filePath)
242 )
232 self.__saveScrollBarPositions() 243 self.__saveScrollBarPositions()
233 self.previewView.page().loadFinished.connect( 244 self.previewView.page().loadFinished.connect(self.__restoreScrollBarPositions)
234 self.__restoreScrollBarPositions)
235 if not filePath: 245 if not filePath:
236 filePath = "/" 246 filePath = "/"
237 baseUrl = ( 247 baseUrl = (
238 QUrl.fromLocalFile(rootPath + "/index.html") 248 QUrl.fromLocalFile(rootPath + "/index.html")
239 if rootPath else 249 if rootPath
240 QUrl.fromLocalFile(filePath) 250 else QUrl.fromLocalFile(filePath)
241 ) 251 )
242 self.previewView.setHtml(html, baseUrl=baseUrl) 252 self.previewView.setHtml(html, baseUrl=baseUrl)
243 if self.__previewedEditor: 253 if self.__previewedEditor:
244 self.__previewedEditor.setFocus() 254 self.__previewedEditor.setFocus()
245 255
246 @pyqtSlot(str) 256 @pyqtSlot(str)
247 def on_previewView_titleChanged(self, title): 257 def on_previewView_titleChanged(self, title):
248 """ 258 """
249 Private slot to handle a change of the title. 259 Private slot to handle a change of the title.
250 260
251 @param title new title (string) 261 @param title new title (string)
252 """ 262 """
253 if title: 263 if title:
254 self.titleLabel.setText(self.tr("Preview - {0}").format(title)) 264 self.titleLabel.setText(self.tr("Preview - {0}").format(title))
255 else: 265 else:
256 self.titleLabel.setText(self.tr("Preview")) 266 self.titleLabel.setText(self.tr("Preview"))
257 267
258 def __saveScrollBarPositions(self): 268 def __saveScrollBarPositions(self):
259 """ 269 """
260 Private method to save scroll bar positions for a previewed editor. 270 Private method to save scroll bar positions for a previewed editor.
261 """ 271 """
262 from PyQt6.QtCore import QPoint 272 from PyQt6.QtCore import QPoint
273
263 try: 274 try:
264 pos = self.previewView.scrollPosition() 275 pos = self.previewView.scrollPosition()
265 except AttributeError: 276 except AttributeError:
266 pos = self.__execJavaScript( 277 pos = self.__execJavaScript(
267 "(function() {" 278 "(function() {"
283 """ 294 """
284 Private method to restore scroll bar positions for a previewed editor. 295 Private method to restore scroll bar positions for a previewed editor.
285 """ 296 """
286 if self.__previewedPath not in self.__scrollBarPositions: 297 if self.__previewedPath not in self.__scrollBarPositions:
287 return 298 return
288 299
289 pos = self.__scrollBarPositions[self.__previewedPath] 300 pos = self.__scrollBarPositions[self.__previewedPath]
290 self.previewView.page().runJavaScript( 301 self.previewView.page().runJavaScript(
291 "window.scrollTo({0}, {1});".format(pos.x(), pos.y())) 302 "window.scrollTo({0}, {1});".format(pos.x(), pos.y())
292 303 )
304
293 def __execJavaScript(self, script): 305 def __execJavaScript(self, script):
294 """ 306 """
295 Private function to execute a JavaScript function Synchroneously. 307 Private function to execute a JavaScript function Synchroneously.
296 308
297 @param script JavaScript script source to be executed 309 @param script JavaScript script source to be executed
298 @type str 310 @type str
299 @return result of the script 311 @return result of the script
300 @rtype depending upon script result 312 @rtype depending upon script result
301 """ 313 """
302 from PyQt6.QtCore import QEventLoop 314 from PyQt6.QtCore import QEventLoop
315
303 loop = QEventLoop() 316 loop = QEventLoop()
304 resultDict = {"res": None} 317 resultDict = {"res": None}
305 318
306 def resultCallback(res, resDict=resultDict): 319 def resultCallback(res, resDict=resultDict):
307 if loop and loop.isRunning(): 320 if loop and loop.isRunning():
308 resDict["res"] = res 321 resDict["res"] = res
309 loop.quit() 322 loop.quit()
310 323
311 self.previewView.page().runJavaScript( 324 self.previewView.page().runJavaScript(script, resultCallback)
312 script, resultCallback) 325
313
314 loop.exec() 326 loop.exec()
315 return resultDict["res"] 327 return resultDict["res"]
316 328
317 329
318 class PreviewProcessingThread(QThread): 330 class PreviewProcessingThread(QThread):
319 """ 331 """
320 Class implementing a thread to process some text into HTML usable by the 332 Class implementing a thread to process some text into HTML usable by the
321 previewer view. 333 previewer view.
322 334
323 @signal htmlReady(str, str, str) emitted with the file name, the processed 335 @signal htmlReady(str, str, str) emitted with the file name, the processed
324 HTML and the web site root path to signal the availability of the 336 HTML and the web site root path to signal the availability of the
325 processed HTML 337 processed HTML
326 """ 338 """
339
327 htmlReady = pyqtSignal(str, str, str) 340 htmlReady = pyqtSignal(str, str, str)
328 341
329 def __init__(self, parent=None): 342 def __init__(self, parent=None):
330 """ 343 """
331 Constructor 344 Constructor
332 345
333 @param parent reference to the parent object (QObject) 346 @param parent reference to the parent object (QObject)
334 """ 347 """
335 super().__init__() 348 super().__init__()
336 349
337 self.__lock = threading.Lock() 350 self.__lock = threading.Lock()
338 351
339 def process(self, filePath, language, text, ssiEnabled, rootPath, 352 def process(
340 useSphinx, convertNewLineToBreak, usePyMdownExtensions, 353 self,
341 markdownHtmlFormat, restDocutilsHtmlFormat): 354 filePath,
355 language,
356 text,
357 ssiEnabled,
358 rootPath,
359 useSphinx,
360 convertNewLineToBreak,
361 usePyMdownExtensions,
362 markdownHtmlFormat,
363 restDocutilsHtmlFormat,
364 ):
342 """ 365 """
343 Public method to convert the given text to HTML. 366 Public method to convert the given text to HTML.
344 367
345 @param filePath file path of the text 368 @param filePath file path of the text
346 @type str 369 @type str
347 @param language language of the text 370 @param language language of the text
348 @type str 371 @type str
349 @param text text to be processed 372 @param text text to be processed
378 self.__usePyMdownExtensions = usePyMdownExtensions 401 self.__usePyMdownExtensions = usePyMdownExtensions
379 self.__markdownHtmlFormat = markdownHtmlFormat 402 self.__markdownHtmlFormat = markdownHtmlFormat
380 self.__restDocutilsHtmlFormat = restDocutilsHtmlFormat 403 self.__restDocutilsHtmlFormat = restDocutilsHtmlFormat
381 if not self.isRunning(): 404 if not self.isRunning():
382 self.start(QThread.Priority.LowPriority) 405 self.start(QThread.Priority.LowPriority)
383 406
384 def run(self): 407 def run(self):
385 """ 408 """
386 Public thread method to convert the stored data. 409 Public thread method to convert the stored data.
387 """ 410 """
388 while True: 411 while True:
396 useSphinx = self.__useSphinx 419 useSphinx = self.__useSphinx
397 convertNewLineToBreak = self.__convertNewLineToBreak 420 convertNewLineToBreak = self.__convertNewLineToBreak
398 usePyMdownExtensions = self.__usePyMdownExtensions 421 usePyMdownExtensions = self.__usePyMdownExtensions
399 markdownHtmlFormat = self.__markdownHtmlFormat 422 markdownHtmlFormat = self.__markdownHtmlFormat
400 restDocutilsHtmlFormat = self.__restDocutilsHtmlFormat 423 restDocutilsHtmlFormat = self.__restDocutilsHtmlFormat
401 424
402 self.__haveData = False 425 self.__haveData = False
403 426
404 html = self.__getHtml(language, text, ssiEnabled, filePath, 427 html = self.__getHtml(
405 rootPath, useSphinx, convertNewLineToBreak, 428 language,
406 usePyMdownExtensions, markdownHtmlFormat, 429 text,
407 restDocutilsHtmlFormat) 430 ssiEnabled,
408 431 filePath,
432 rootPath,
433 useSphinx,
434 convertNewLineToBreak,
435 usePyMdownExtensions,
436 markdownHtmlFormat,
437 restDocutilsHtmlFormat,
438 )
439
409 with self.__lock: 440 with self.__lock:
410 if not self.__haveData: 441 if not self.__haveData:
411 self.htmlReady.emit(filePath, html, rootPath) 442 self.htmlReady.emit(filePath, html, rootPath)
412 break 443 break
413 # else - next iteration 444 # else - next iteration
414 445
415 def __getHtml(self, language, text, ssiEnabled, filePath, rootPath, 446 def __getHtml(
416 useSphinx, convertNewLineToBreak, usePyMdownExtensions, 447 self,
417 markdownHtmlFormat, restDocutilsHtmlFormat): 448 language,
449 text,
450 ssiEnabled,
451 filePath,
452 rootPath,
453 useSphinx,
454 convertNewLineToBreak,
455 usePyMdownExtensions,
456 markdownHtmlFormat,
457 restDocutilsHtmlFormat,
458 ):
418 """ 459 """
419 Private method to process the given text depending upon the given 460 Private method to process the given text depending upon the given
420 language. 461 language.
421 462
422 @param language language of the text 463 @param language language of the text
423 @type str 464 @type str
424 @param text to be processed 465 @param text to be processed
425 @type str 466 @type str
426 @param ssiEnabled flag indicating to do some (limited) SSI processing 467 @param ssiEnabled flag indicating to do some (limited) SSI processing
451 else: 492 else:
452 html = text 493 html = text
453 return self.__processRootPath(html, rootPath) 494 return self.__processRootPath(html, rootPath)
454 elif language == "Markdown": 495 elif language == "Markdown":
455 return self.__convertMarkdown( 496 return self.__convertMarkdown(
456 text, convertNewLineToBreak, usePyMdownExtensions, 497 text, convertNewLineToBreak, usePyMdownExtensions, markdownHtmlFormat
457 markdownHtmlFormat) 498 )
458 elif language == "ReST": 499 elif language == "ReST":
459 return self.__convertReST(text, useSphinx, restDocutilsHtmlFormat) 500 return self.__convertReST(text, useSphinx, restDocutilsHtmlFormat)
460 else: 501 else:
461 return self.tr( 502 return self.tr("<p>No preview available for this type of file.</p>")
462 "<p>No preview available for this type of file.</p>") 503
463
464 def __processSSI(self, txt, filename, root): 504 def __processSSI(self, txt, filename, root):
465 """ 505 """
466 Private method to process the given text for SSI statements. 506 Private method to process the given text for SSI statements.
467 507
468 Note: Only a limited subset of SSI statements are supported. 508 Note: Only a limited subset of SSI statements are supported.
469 509
470 @param txt text to be processed (string) 510 @param txt text to be processed (string)
471 @param filename name of the file associated with the given text 511 @param filename name of the file associated with the given text
472 (string) 512 (string)
473 @param root directory of the document root (string) 513 @param root directory of the document root (string)
474 @return processed HTML (string) 514 @return processed HTML (string)
475 """ 515 """
476 if not filename: 516 if not filename:
477 return txt 517 return txt
478 518
479 # SSI include 519 # SSI include
480 incRe = re.compile( 520 incRe = re.compile(
481 r"""<!--#include[ \t]+(virtual|file)=[\"']([^\"']+)[\"']\s*-->""", 521 r"""<!--#include[ \t]+(virtual|file)=[\"']([^\"']+)[\"']\s*-->""",
482 re.IGNORECASE) 522 re.IGNORECASE,
523 )
483 baseDir = os.path.dirname(os.path.abspath(filename)) 524 baseDir = os.path.dirname(os.path.abspath(filename))
484 docRoot = root if root != "" else baseDir 525 docRoot = root if root != "" else baseDir
485 while True: 526 while True:
486 incMatch = incRe.search(txt) 527 incMatch = incRe.search(txt)
487 if incMatch is None: 528 if incMatch is None:
488 break 529 break
489 530
490 if incMatch.group(1) == "virtual": 531 if incMatch.group(1) == "virtual":
491 incFile = Utilities.normjoinpath(docRoot, incMatch.group(2)) 532 incFile = Utilities.normjoinpath(docRoot, incMatch.group(2))
492 elif incMatch.group(1) == "file": 533 elif incMatch.group(1) == "file":
493 incFile = Utilities.normjoinpath(baseDir, incMatch.group(2)) 534 incFile = Utilities.normjoinpath(baseDir, incMatch.group(2))
494 else: 535 else:
501 # remove SSI include 542 # remove SSI include
502 incTxt = "" 543 incTxt = ""
503 else: 544 else:
504 # remove SSI include 545 # remove SSI include
505 incTxt = "" 546 incTxt = ""
506 txt = txt[:incMatch.start(0)] + incTxt + txt[incMatch.end(0):] 547 txt = txt[: incMatch.start(0)] + incTxt + txt[incMatch.end(0) :]
507 548
508 return txt 549 return txt
509 550
510 def __processRootPath(self, txt, root): 551 def __processRootPath(self, txt, root):
511 """ 552 """
512 Private method to adjust absolute references to the given root path. 553 Private method to adjust absolute references to the given root path.
513 554
514 @param txt text to be processed 555 @param txt text to be processed
515 @type str 556 @type str
516 @param root directory of the document root 557 @param root directory of the document root
517 @type str 558 @type str
518 @return processed HTML 559 @return processed HTML
519 @rtype str 560 @rtype str
520 """ 561 """
521 if not root: 562 if not root:
522 return txt 563 return txt
523 564
524 root = Utilities.fromNativeSeparators(root) 565 root = Utilities.fromNativeSeparators(root)
525 if not root.endswith("/"): 566 if not root.endswith("/"):
526 root += "/" 567 root += "/"
527 rootLen = len(root) 568 rootLen = len(root)
528 569
529 refRe = re.compile( 570 refRe = re.compile(r"""(href|src)=[\\"']/([^\\"']+)[\\"']""", re.IGNORECASE)
530 r"""(href|src)=[\\"']/([^\\"']+)[\\"']""",
531 re.IGNORECASE)
532 pos = 0 571 pos = 0
533 while True: 572 while True:
534 refMatch = refRe.search(txt, pos) 573 refMatch = refRe.search(txt, pos)
535 if refMatch is None: 574 if refMatch is None:
536 break 575 break
537 576
538 txt = (txt[:refMatch.start(0)] + refMatch.group(1) + '="' + root + 577 txt = (
539 refMatch.group(2) + '"' + txt[refMatch.end(0):]) 578 txt[: refMatch.start(0)]
579 + refMatch.group(1)
580 + '="'
581 + root
582 + refMatch.group(2)
583 + '"'
584 + txt[refMatch.end(0) :]
585 )
540 pos = refMatch.end(0) + rootLen 586 pos = refMatch.end(0) + rootLen
541 587
542 return txt 588 return txt
543 589
544 def __convertReST(self, text, useSphinx, restDocutilsHtmlFormat): 590 def __convertReST(self, text, useSphinx, restDocutilsHtmlFormat):
545 """ 591 """
546 Private method to convert ReST text into HTML. 592 Private method to convert ReST text into HTML.
547 593
548 @param text text to be processed (string) 594 @param text text to be processed (string)
549 @param useSphinx flag indicating to use Sphinx to generate the 595 @param useSphinx flag indicating to use Sphinx to generate the
550 ReST preview (boolean) 596 ReST preview (boolean)
551 @param restDocutilsHtmlFormat HTML format to be generated by docutils 597 @param restDocutilsHtmlFormat HTML format to be generated by docutils
552 (string) 598 (string)
554 """ 600 """
555 if useSphinx: 601 if useSphinx:
556 return self.__convertReSTSphinx(text) 602 return self.__convertReSTSphinx(text)
557 else: 603 else:
558 return self.__convertReSTDocutils(text, restDocutilsHtmlFormat) 604 return self.__convertReSTDocutils(text, restDocutilsHtmlFormat)
559 605
560 def __convertReSTSphinx(self, text): 606 def __convertReSTSphinx(self, text):
561 """ 607 """
562 Private method to convert ReST text into HTML using 'sphinx'. 608 Private method to convert ReST text into HTML using 'sphinx'.
563 609
564 @param text text to be processed (string) 610 @param text text to be processed (string)
565 @return processed HTML (string) 611 @return processed HTML (string)
566 """ 612 """
567 try: 613 try:
568 from sphinx.application import Sphinx # __IGNORE_EXCEPTION__ 614 from sphinx.application import Sphinx # __IGNORE_EXCEPTION__
569 except ImportError: 615 except ImportError:
570 return self.tr( 616 return self.tr(
571 """<p>ReStructuredText preview requires the""" 617 """<p>ReStructuredText preview requires the"""
572 """ <b>sphinx</b> package.<br/>Install it with""" 618 """ <b>sphinx</b> package.<br/>Install it with"""
573 """ your package manager,'pip install Sphinx' or see""" 619 """ your package manager,'pip install Sphinx' or see"""
574 """ <a href="http://pypi.python.org/pypi/Sphinx">""" 620 """ <a href="http://pypi.python.org/pypi/Sphinx">"""
575 """this page.</a></p>""" 621 """this page.</a></p>"""
576 """<p>Alternatively you may disable Sphinx usage""" 622 """<p>Alternatively you may disable Sphinx usage"""
577 """ on the Editor, Filehandling configuration page.</p>""") 623 """ on the Editor, Filehandling configuration page.</p>"""
578 624 )
625
579 srcTempDir = tempfile.mkdtemp(prefix="eric-rest-src-") 626 srcTempDir = tempfile.mkdtemp(prefix="eric-rest-src-")
580 outTempDir = tempfile.mkdtemp(prefix="eric-rest-out-") 627 outTempDir = tempfile.mkdtemp(prefix="eric-rest-out-")
581 doctreeTempDir = tempfile.mkdtemp(prefix="eric-rest-doctree-") 628 doctreeTempDir = tempfile.mkdtemp(prefix="eric-rest-doctree-")
582 try: 629 try:
583 filename = 'sphinx_preview' 630 filename = "sphinx_preview"
584 basePath = os.path.join(srcTempDir, filename) 631 basePath = os.path.join(srcTempDir, filename)
585 with open(basePath + '.rst', 'w', encoding='utf-8') as fh: 632 with open(basePath + ".rst", "w", encoding="utf-8") as fh:
586 fh.write(text) 633 fh.write(text)
587 634
588 overrides = {'html_add_permalinks': False, 635 overrides = {
589 'html_copy_source': False, 636 "html_add_permalinks": False,
590 'html_title': 'Sphinx preview', 637 "html_copy_source": False,
591 'html_use_index': False, 638 "html_title": "Sphinx preview",
592 'html_use_modindex': False, 639 "html_use_index": False,
593 'html_use_smartypants': True, 640 "html_use_modindex": False,
594 'master_doc': filename} 641 "html_use_smartypants": True,
595 app = Sphinx(srcdir=srcTempDir, confdir=None, outdir=outTempDir, 642 "master_doc": filename,
596 doctreedir=doctreeTempDir, buildername='html', 643 }
597 confoverrides=overrides, status=None, 644 app = Sphinx(
598 warning=io.StringIO()) 645 srcdir=srcTempDir,
646 confdir=None,
647 outdir=outTempDir,
648 doctreedir=doctreeTempDir,
649 buildername="html",
650 confoverrides=overrides,
651 status=None,
652 warning=io.StringIO(),
653 )
599 app.build(force_all=True, filenames=None) 654 app.build(force_all=True, filenames=None)
600 655
601 basePath = os.path.join(outTempDir, filename) 656 basePath = os.path.join(outTempDir, filename)
602 with open(basePath + '.html', 'r', encoding='utf-8') as fh: 657 with open(basePath + ".html", "r", encoding="utf-8") as fh:
603 html = fh.read() 658 html = fh.read()
604 finally: 659 finally:
605 shutil.rmtree(srcTempDir) 660 shutil.rmtree(srcTempDir)
606 shutil.rmtree(outTempDir) 661 shutil.rmtree(outTempDir)
607 shutil.rmtree(doctreeTempDir) 662 shutil.rmtree(doctreeTempDir)
608 663
609 return html 664 return html
610 665
611 def __convertReSTDocutils(self, text, htmlFormat): 666 def __convertReSTDocutils(self, text, htmlFormat):
612 """ 667 """
613 Private method to convert ReST text into HTML using 'docutils'. 668 Private method to convert ReST text into HTML using 'docutils'.
614 669
615 @param text text to be processed (string) 670 @param text text to be processed (string)
616 @param htmlFormat HTML format to be generated (string) 671 @param htmlFormat HTML format to be generated (string)
617 @return processed HTML (string) 672 @return processed HTML (string)
618 """ 673 """
619 if 'sphinx' in sys.modules: 674 if "sphinx" in sys.modules:
620 # Make sure any Sphinx polution of docutils has been removed. 675 # Make sure any Sphinx polution of docutils has been removed.
621 unloadKeys = [k for k in sys.modules.keys() 676 unloadKeys = [
622 if k.startswith(('docutils', 'sphinx'))] 677 k for k in sys.modules.keys() if k.startswith(("docutils", "sphinx"))
678 ]
623 for key in unloadKeys: 679 for key in unloadKeys:
624 sys.modules.pop(key) 680 sys.modules.pop(key)
625 681
626 try: 682 try:
627 import docutils.core # __IGNORE_EXCEPTION__ 683 import docutils.core # __IGNORE_EXCEPTION__
628 import docutils.utils # __IGNORE_EXCEPTION__ 684 import docutils.utils # __IGNORE_EXCEPTION__
629 except ImportError: 685 except ImportError:
630 return self.tr( 686 return self.tr(
631 """<p>ReStructuredText preview requires the""" 687 """<p>ReStructuredText preview requires the"""
632 """ <b>python-docutils</b> package.<br/>Install it with""" 688 """ <b>python-docutils</b> package.<br/>Install it with"""
633 """ your package manager, 'pip install docutils' or see""" 689 """ your package manager, 'pip install docutils' or see"""
634 """ <a href="http://pypi.python.org/pypi/docutils">""" 690 """ <a href="http://pypi.python.org/pypi/docutils">"""
635 """this page.</a></p>""") 691 """this page.</a></p>"""
636 692 )
693
637 # redirect sys.stderr because we are not interested in it here 694 # redirect sys.stderr because we are not interested in it here
638 origStderr = sys.stderr 695 origStderr = sys.stderr
639 sys.stderr = io.StringIO() 696 sys.stderr = io.StringIO()
640 try: 697 try:
641 html = docutils.core.publish_string( 698 html = docutils.core.publish_string(
642 text, writer_name=htmlFormat.lower()).decode("utf-8") 699 text, writer_name=htmlFormat.lower()
700 ).decode("utf-8")
643 except docutils.utils.SystemMessage as err: 701 except docutils.utils.SystemMessage as err:
644 errStr = str(err).split(":")[-1].replace("\n", "<br/>") 702 errStr = str(err).split(":")[-1].replace("\n", "<br/>")
645 return self.tr( 703 return self.tr("""<p>Docutils returned an error:</p><p>{0}</p>""").format(
646 """<p>Docutils returned an error:</p><p>{0}</p>""" 704 errStr
647 ).format(errStr) 705 )
648 706
649 sys.stderr = origStderr 707 sys.stderr = origStderr
650 return html 708 return html
651 709
652 def __convertMarkdown(self, text, convertNewLineToBreak, 710 def __convertMarkdown(
653 usePyMdownExtensions, htmlFormat): 711 self, text, convertNewLineToBreak, usePyMdownExtensions, htmlFormat
712 ):
654 """ 713 """
655 Private method to convert Markdown text into HTML. 714 Private method to convert Markdown text into HTML.
656 715
657 @param text text to be processed 716 @param text text to be processed
658 @type str 717 @type str
659 @param convertNewLineToBreak flag indicating to convert new lines 718 @param convertNewLineToBreak flag indicating to convert new lines
660 to HTML break (Markdown only) 719 to HTML break (Markdown only)
661 @type bool 720 @type bool
666 @type str 725 @type str
667 @return processed HTML 726 @return processed HTML
668 @rtype str 727 @rtype str
669 """ 728 """
670 try: 729 try:
671 import markdown # __IGNORE_EXCEPTION__ 730 import markdown # __IGNORE_EXCEPTION__
672 except ImportError: 731 except ImportError:
673 return self.tr( 732 return self.tr(
674 """<p>Markdown preview requires the <b>Markdown</b> """ 733 """<p>Markdown preview requires the <b>Markdown</b> """
675 """package.<br/>Install it with your package manager,""" 734 """package.<br/>Install it with your package manager,"""
676 """ 'pip install Markdown' or see """ 735 """ 'pip install Markdown' or see """
677 """<a href="http://pythonhosted.org/Markdown/install.html">""" 736 """<a href="http://pythonhosted.org/Markdown/install.html">"""
678 """installation instructions.</a></p>""") 737 """installation instructions.</a></p>"""
679 738 )
739
680 from . import PreviewerHTMLStyles 740 from . import PreviewerHTMLStyles
681 from . import MarkdownExtensions 741 from . import MarkdownExtensions
682 742
683 extensions = [] 743 extensions = []
684 744
685 mermaidNeeded = False 745 mermaidNeeded = False
686 if ( 746 if Preferences.getEditor(
687 Preferences.getEditor("PreviewMarkdownMermaid") and 747 "PreviewMarkdownMermaid"
688 MarkdownExtensions.MermaidRegexFullText.search(text) 748 ) and MarkdownExtensions.MermaidRegexFullText.search(text):
689 ):
690 extensions.append(MarkdownExtensions.MermaidExtension()) 749 extensions.append(MarkdownExtensions.MermaidExtension())
691 mermaidNeeded = True 750 mermaidNeeded = True
692 751
693 if convertNewLineToBreak: 752 if convertNewLineToBreak:
694 extensions.append('nl2br') 753 extensions.append("nl2br")
695 754
696 pyMdown = False 755 pyMdown = False
697 if usePyMdownExtensions: 756 if usePyMdownExtensions:
698 with contextlib.suppress(ImportError): 757 with contextlib.suppress(ImportError):
699 import pymdownx # __IGNORE_EXCEPTION__ __IGNORE_WARNING__ 758 import pymdownx # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
759
700 # PyPI package is 'pymdown-extensions' 760 # PyPI package is 'pymdown-extensions'
701 761
702 extensions.extend([ 762 extensions.extend(
703 'toc', 763 [
704 'pymdownx.extra', 'pymdownx.caret', 'pymdownx.emoji', 764 "toc",
705 'pymdownx.mark', 'pymdownx.tilde', 'pymdownx.keys', 765 "pymdownx.extra",
706 'pymdownx.tasklist', 'pymdownx.smartsymbols', 766 "pymdownx.caret",
707 ]) 767 "pymdownx.emoji",
768 "pymdownx.mark",
769 "pymdownx.tilde",
770 "pymdownx.keys",
771 "pymdownx.tasklist",
772 "pymdownx.smartsymbols",
773 ]
774 )
708 pyMdown = True 775 pyMdown = True
709 776
710 if not pyMdown: 777 if not pyMdown:
711 extensions.extend(['extra', 'toc']) 778 extensions.extend(["extra", "toc"])
712 779
713 # version 2.0 supports only extension names, not instances 780 # version 2.0 supports only extension names, not instances
714 if ( 781 if markdown.version_info[0] > 2 or (
715 markdown.version_info[0] > 2 or 782 markdown.version_info[0] == 2 and markdown.version_info[1] > 0
716 (markdown.version_info[0] == 2 and
717 markdown.version_info[1] > 0)
718 ): 783 ):
719 extensions.append(MarkdownExtensions.SimplePatternExtension()) 784 extensions.append(MarkdownExtensions.SimplePatternExtension())
720 785
721 if Preferences.getEditor("PreviewMarkdownMathJax"): 786 if Preferences.getEditor("PreviewMarkdownMathJax"):
722 mathjax = ( 787 mathjax = (
725 "tex-chtml.js'>\n" 790 "tex-chtml.js'>\n"
726 "</script>\n" 791 "</script>\n"
727 ) 792 )
728 # prepare text for mathjax 793 # prepare text for mathjax
729 text = ( 794 text = (
730 text 795 text.replace(r"\(", r"\\(")
731 .replace(r"\(", r"\\(")
732 .replace(r"\)", r"\\)") 796 .replace(r"\)", r"\\)")
733 .replace(r"\[", r"\\[") 797 .replace(r"\[", r"\\[")
734 .replace(r"\]", r"\\]") 798 .replace(r"\]", r"\\]")
735 ) 799 )
736 else: 800 else:
737 mathjax = "" 801 mathjax = ""
738 802
739 if mermaidNeeded: 803 if mermaidNeeded:
740 mermaid = ( 804 mermaid = (
741 "<script type='text/javascript' id='Mermaid-script'" 805 "<script type='text/javascript' id='Mermaid-script'"
742 " src='https://unpkg.com/mermaid@8/dist/mermaid.min.js'>\n" 806 " src='https://unpkg.com/mermaid@8/dist/mermaid.min.js'>\n"
743 "</script>\n" 807 "</script>\n"
757 "});</script>" 821 "});</script>"
758 ) 822 )
759 else: 823 else:
760 mermaid = "" 824 mermaid = ""
761 mermaid_initialize = "" 825 mermaid_initialize = ""
762 826
763 htmlFormat = Preferences.getEditor("PreviewMarkdownHTMLFormat").lower() 827 htmlFormat = Preferences.getEditor("PreviewMarkdownHTMLFormat").lower()
764 body = markdown.markdown(text, extensions=extensions, 828 body = markdown.markdown(
765 output_format=htmlFormat.lower()) 829 text, extensions=extensions, output_format=htmlFormat.lower()
830 )
766 style = ( 831 style = (
767 (PreviewerHTMLStyles.css_markdown_dark + 832 (
768 PreviewerHTMLStyles.css_pygments_dark) 833 PreviewerHTMLStyles.css_markdown_dark
769 if ericApp().usesDarkPalette() else 834 + PreviewerHTMLStyles.css_pygments_dark
770 (PreviewerHTMLStyles.css_markdown_light + 835 )
771 PreviewerHTMLStyles.css_pygments_light) 836 if ericApp().usesDarkPalette()
772 ) 837 else (
773 838 PreviewerHTMLStyles.css_markdown_light
839 + PreviewerHTMLStyles.css_pygments_light
840 )
841 )
842
774 if htmlFormat == "xhtml1": 843 if htmlFormat == "xhtml1":
775 head = ( 844 head = (
776 '''<!DOCTYPE html PUBLIC "-//W3C//DTD''' 845 """<!DOCTYPE html PUBLIC "-//W3C//DTD"""
777 ''' XHTML 1.0 Transitional//EN"\n''' 846 """ XHTML 1.0 Transitional//EN"\n"""
778 ''' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional''' 847 """ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional"""
779 '''.dtd">\n''' 848 """.dtd">\n"""
780 '''<html xmlns="http://www.w3.org/1999/xhtml">\n''' 849 """<html xmlns="http://www.w3.org/1999/xhtml">\n"""
781 ) 850 )
782 elif htmlFormat == "html5": 851 elif htmlFormat == "html5":
783 head = ( 852 head = """<!DOCTYPE html>\n""" """<html lang="EN">\n"""
784 '''<!DOCTYPE html>\n'''
785 '''<html lang="EN">\n'''
786 )
787 else: 853 else:
788 head = '<html lang="EN">\n' 854 head = '<html lang="EN">\n'
789 head += '''<head>\n''' 855 head += """<head>\n"""
790 head += ( 856 head += (
791 '''<meta name="Generator" content="eric" />\n''' 857 """<meta name="Generator" content="eric" />\n"""
792 '''<meta http-equiv="Content-Type" ''' 858 """<meta http-equiv="Content-Type" """
793 '''content="text/html; charset=utf-8" />\n''' 859 """content="text/html; charset=utf-8" />\n"""
794 '''{0}''' 860 """{0}"""
795 '''{1}''' 861 """{1}"""
796 '''<style type="text/css">''' 862 """<style type="text/css">"""
797 '''{2}''' 863 """{2}"""
798 '''</style>\n''' 864 """</style>\n"""
799 '''</head>\n''' 865 """</head>\n"""
800 '''<body>\n''' 866 """<body>\n"""
801 ).format(mathjax, mermaid, style) 867 ).format(mathjax, mermaid, style)
802 868
803 foot = '''\n</body>\n</html>\n''' 869 foot = """\n</body>\n</html>\n"""
804 870
805 return head + body + mermaid_initialize + foot 871 return head + body + mermaid_initialize + foot

eric ide

mercurial