eric7/UI/Previewers/PreviewerHTML.py

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

eric ide

mercurial