4

How can I render a markdown file in my PyQt5 application?

Here I read that I should use a QWebEngineView instead of a QTextEdit because the QTextEdit can't render external images.

In a comment someone references this example. It is however a complete markdown texteditor and additionally written in c++. I tried to translate the needed parts into Python but I don't quite get how what works. I just need a minimal example.

What I have right now is the following:

from PyQt5.QtWebChannel import QWebChannel
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QUrl
import sys

app = QApplication(sys.argv)

web_widget = QWebEngineView()  
webChannel = QWebChannel()    # ?
page = QWebEnginePage()       # ?
web_widget.setPage(page)      # ? 
my_url = QUrl("/index.html")
web_widget.load(my_url)

# now somehow replace the placeholder in the loaded html page with file contents?

file_url = QUrl("file.md")

# help 


web_widget.show()
app.exec_()
Legatio
  • 259
  • 3
  • 9
  • Qt provides support to Markdown since 5.14. Use [`setMarkdown()`](https://doc.qt.io/qt-5/qtextedit.html#markdown-prop) – musicamante Feb 05 '21 at 15:38
  • I didn't find `QWebEngineView::setMarkDown()`. What docs are you using? Maybe take a look at the first provided link so you understand my problem. I don't just look for a solution for local files but also external files. – Legatio Feb 05 '21 at 16:15
  • Sorry, I was referring to QTextEdit and missed the first link (btw, it *can* load external sources, but it's a bit tricky and requires QNetworkAccessManager). Maybe you could use `QTextDocument.setMarkDown()` and then `QTextDocument.toHtml()`, and then set that html for the web view, assuming that the img resource addresses are maintained. Otherwise, you'll need to use external modules that allow markdown parsing. – musicamante Feb 05 '21 at 16:56

1 Answers1

9

QTextEdit since Qt 5.14 can render markdown but as the OP points out it has a limitation: it cannot render remote images. So an alternative is to use QWebEngineView + js libraries like markdown.js and marked.js as the official example shows. You can also use QNetworkAccessManager to download remote .md files.

import os.path
import sys

from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTextCodec, QUrl
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PyQt5.QtWebChannel import QWebChannel
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
from PyQt5.QtWidgets import QApplication

CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))


class Document(QObject):
    textChanged = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.m_text = ""

    def get_text(self):
        return self.m_text

    def set_text(self, text):
        if self.m_text == text:
            return
        self.m_text = text
        self.textChanged.emit(self.m_text)

    text = pyqtProperty(str, fget=get_text, fset=set_text, notify=textChanged)


class DownloadManager(QObject):
    finished = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)

        self._manager = QNetworkAccessManager()
        self.manager.finished.connect(self.handle_finished)

    @property
    def manager(self):
        return self._manager

    def start_download(self, url):
        self.manager.get(QNetworkRequest(url))

    def handle_finished(self, reply):
        if reply.error() != QNetworkReply.NoError:
            print("error: ", reply.errorString())
            return
        codec = QTextCodec.codecForName("UTF-8")
        raw_data = codec.toUnicode(reply.readAll())
        self.finished.emit(raw_data)


def main():

    app = QApplication(sys.argv)

    filename = os.path.join(CURRENT_DIR, "index.html")

    document = Document()
    download_manager = DownloadManager()

    channel = QWebChannel()
    channel.registerObject("content", document)

    # remote file
    markdown_url = QUrl.fromUserInput(
        "https://raw.githubusercontent.com/eyllanesc/stackoverflow/master/README.md"
    )
    # local file
    # markdown_url = QUrl.fromUserInput(/path/of/markdown.md)

    download_manager.finished.connect(document.set_text)
    download_manager.start_download(markdown_url)

    view = QWebEngineView()
    view.page().setWebChannel(channel)
    url = QUrl.fromLocalFile(filename)
    view.load(url)
    view.resize(640, 480)
    view.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

index.html

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<head>
  <link rel="stylesheet" type="text/css" href="3rdparty/markdown.css">
  <script src="3rdparty/marked.js"></script>
  <script src="qrc:/qtwebchannel/qwebchannel.js"></script>
</head>
<body>
  <div id="placeholder"></div>
  <script>
  'use strict';

  var placeholder = document.getElementById('placeholder');

  var updateText = function(text) {
      placeholder.innerHTML = marked(text);
  }

  new QWebChannel(qt.webChannelTransport,
    function(channel) {
      var content = channel.objects.content;
      updateText(content.text);
      content.textChanged.connect(updateText);
    }
  );
  </script>
</body>
</html>
├── 3rdparty
│   ├── markdown.css
│   ├── MARKDOWN-LICENSE.txt
│   ├── marked.js
│   ├── MARKED-LICENSE.txt
│   └── qt_attribution.json
├── index.html
└── main.py

enter image description here

Note: the files in the 3rdparty folder are in the official Qt repository.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241